微博开发笔记上(未完待续)
新浪微博开发笔记
iPhone 项目目标
- 项目掌控能力
- 工具使用能力
- 开发技巧能力
课程提纲
新浪微博接口地址
微博开放平台地址
http://open.weibo.com微博接口文档地址
http://open.weibo.com/wiki/微博API
项目主题框架
走向工作岗位之后,一般会遇到两种工作情况:
新项目开发
- 通常在项目开始之前,公司的产品经理会提供完整的产品原型图,或功能设计文档
- 通过对这些文档的解读,能够梳理出目标项目的整体架构,从而协助项目框架的搭建
旧项目维护
- 很多老项目是缺乏文档的,这种情况在一些小公司中表现的尤为突出
- 要想快速上手一个老项目,首先运行项目,并且整理项目整体框架结构
- 然后用整理出的框架结构与代码结构相互印证,无疑可以对了解项目的整体架构起到重要的辅助
综上所述,无论是新项目,还是老项目,在开发之前确定项目的主体架构都是非常重要,也是十分必要的!
主体架构确认的好处
开发之前,明确项目的主体架构具有以下好处:
- 明确开发目标,项目一旦启动,始终锁定目标前进!
- 明确功能模块的数量,方便工期核算
- 根据开发进度,预判开发周期,及时与相关部门沟通、协调
- 根据主体架构搭建项目框架,方便团队开发,各个功能模块齐头并进,提高开发效率!
- 确定项目开发中的重点难点,提前安排攻关能力强的同事进行技术攻关,待需要时能够享受攻关成果,或者及时调整产品设计
- 新增或调整功能时,能够高屋建瓴,在最合适的位置添加相关功能模块
新浪微博
作为中国移动互联网的代表性产品之一,新浪微博涵盖了大量的移动互联网元素,通过对新浪微博的研究及模仿,可以:
- 对这些元素在实际产品中的应用有深入的了解和认识
- 知道如何在一个真实的项目中运用相关技术点
- 对大型项目的架构、开发及掌控有更全面的认识和理解
正如前文所述,在开始模仿之前,首先运行产品,掌握项目的整体架构,确定开发的主体功能非常重要!
新浪微博主体架构
对界面预览之后,可以发现新浪微博符合经典应用程序架构设计:
- 主视图控制器是一个
UITabbarController
- 包含四个
UINavigationController
,分别是- 首页
- 消息
- 发现
- 我
特殊之处:
- UITabbarController
中间有一个 “+” 按钮,点击该按钮能够 Modal 显示微博类型选择
界面,方便用户选择自己需要的微博类型
- 四个 UINavigationController
在用户登录前后显示的界面格式是不一样的
根原版新浪微博的区别
由于必须使用新浪微博官方的 API 才能够正常开发,换言之,如果没有登录系统是无法使用新浪微博提供的接口的!
基于上述原因,在实际开发中对未登录之前的界面设计进行简化
开源中国社区
官方网站
https://git.oschina.net/
- 开源中国社区成立于2008年8月,其目的是为中国的IT技术人员提供一个全面的、快捷更新的用来检索开源软件以及交流使用开源经验的平台
- 目前国内有很多公司会将公司的项目部署在
OSChina
与 GitHUB
的对比
- 服务器在国内,速度更快
- 免费账户同样可以建立
私有
项目,而GitHUB
上要建立私有项目必须付费
使用
注册账号
- 建议使用网易的邮箱,使用其他免费邮箱可能会收不到验证邮件
添加 SSH 公钥,进入终端,并输入以下命令
- 开源中国帮助文档地址:https://git.oschina.net/oschina/git-osc/wikis/帮助#ssh-keys
# 切换目录,MAC中目录的第一个字符如果是 `.` 表示改文件夹是隐藏文件夹
$ cd ~/.ssh
# 查看当前目录文件
$ ls# 生成 RSA 密钥对
# 1> "" 中输入个人邮箱
# 2> 提示输入私钥文件名称,直接回车
# 3> 提示输入密码,可以随便输入,只要本次能够记住即可
$ ssh-keygen -t rsa -C "xxx@126.com"# 查看公钥内容
$ cat id_rsa.pub
将公钥内容复制并粘贴至 https://git.oschina.net/profile/sshkeys
测试公钥
# 测试 SSH 连接
$ ssh -T git@git.oschina.net# 终端提示 `Welcome to Git@OSC, 刀哥!` 说明连接成功
- 新建项目
- 克隆项目
# 切换至项目目录
$ cd 项目目录# 克隆项目,地址可以在项目首页复制
$ git clone git@git.oschina.net:xxx/ProjectName.git
- 添加
gitignore
# ~/dev/github/gitignore/ 是保存 gitignore 的目录
$ cp ~/dev/github/gitignore/Swift.gitignore .gitignore
- 提示:
- 可以从
https://github.com/github/gitignore
获取最新版本的gitignore
文件 - 添加
.gitignore
文件之后,每次提交时不会将个人的项目设置信息(例如:末次打开的文件,调试断点等)提交到服务器,在团队开发中非常重要
- 可以从
图片素材
素材对应的设备
1x | 2x | 3x |
---|---|---|
大小对应开发中的点
|
宽高是 1x 的两倍
|
宽高时 1x 的三倍
|
iPhone 3GS,可以省略 |
iPhone 4 iPhone 4s iPhone 5 iPhone 5s iPhone 6 |
iPhone 6+ |
与美工的配合
- 让美工在设计原型图时,按照
iPhone 6+
的分辨率设计 - 然后切图的时候,切两套即可
- 一套以 @3x 结尾,供 iPhone 6+ 使用
- 一套缩小 2/3,以 @2x 结尾,供小屏视网膜手机使用
提示:现在大多数应用程序还适配 iOS 6,下载的 ipa 包能够拿到图片素材,但是如果今后应用程序只支持 iOS 7+,解压缩包之后,择无法再获得对应的图片素材。
请妥善保管好一些优秀作品的 IPA 文件
图标素材 & App 名称
图标素材
设置图标选项
- 如下图所示,删除
Launch Screen File
&Main.storyboard
,并且设置启动图片
和应用方向
提示:iPhone 项目一般不需要支持横屏,游戏除外
添加图标
App 名称
- 提示
- 此处修改的内容是
Info.plist
中CFBundleName
对应的内容 - 注意不要超过6个中文,否则会影响用户体验
- 此处修改的内容是
启动程序
- 在
AppDelegate
的didFinishLaunchingWithOptions
函数中添加以下代码:
window = UIWindow(frame: UIScreen.mainScreen().bounds)
window?.backgroundColor = UIColor.whiteColor()
window?.rootViewController = ViewController()window?.makeKeyAndVisible()
运行测试
添加启动图片
- 提示
- 关于启动图片的设置,需要注意上课的操作细节
- 关于各个设备的实际屏幕尺寸,注意一下不同类型的启动图片即可
项目搭建
课程目标
- 熟悉 swift 语法
- 搭建系统主体框架结构
- 对比与 OC 开发的异同
- 纯代码搭建框架
创建文件
准备工作
删除模板文件
- ViewController.swift
- Main.storyboard
- LaunchScreen.xib
创建项目结构
主目录 Classes
二级目录
目录名 | 说明 |
---|---|
Module | 功能模块 |
Model | 业务逻辑模型 |
Tools | 工具类 |
Module
子目录
目录名 | 说明 |
---|---|
Main | 主要 |
Home | 首页 |
Message | 消息 |
Discover | 发现 |
Profile | 我 |
创建项目文件
Main
目录 | Controller |
---|---|
Main |
MainViewController.swift(:UITabBarController )
|
功能模块
目录 | Controller |
---|---|
Home | HomeTableViewController.swift |
Message | MessageTableViewController.swift |
Discover | DiscoverTableViewController.swift |
Profile | ProfileTableViewController.swift |
细节
- 每个 ViewController 继承自
UITableViewController
- 搭建完成的文件结构图如下:
- 修改
AppDelegate
中的didFinishLaunchingWithOptions
函数,设置启动控制器
window?.rootViewController = MainViewController()
添加子控制器
功能需求
- 由于采用了多视图控制器的设计方式,因此需要通过代码的方式向主控制器中添加子控制器
文件准备
- 将素材文件夹中的
TabBar
拖拽到Images.xcassets
目录下
代码实现
添加第一个视图控制器
override func viewDidLoad() {super.viewDidLoad()addChildViewController()
}private func addChildViewController() {tabBar.tintColor = UIColor.orangeColor()let vc = HomeTableViewController()vc.title = "首页"vc.tabBarItem.image = UIImage(named: "tabbar_home")let nav = UINavigationController(rootViewController: vc)addChildViewController(nav)
}
重构代码抽取参数
/// 添加控制器
///
/// - parameter vc : 视图控制器
/// - parameter title : 标题
/// - parameter imageName: 图像名称
private func addChildViewController(vc: UIViewController, title: String, imageName: String) {tabBar.tintColor = UIColor.orangeColor()let vc = HomeTableViewController()vc.title = titlevc.tabBarItem.image = UIImage(named: imageName)let nav = UINavigationController(rootViewController: vc)addChildViewController(nav)
}
- 扩充调用函数,添加其他控制器
/// 添加所有子控制器
private func addChildViewControllers() {addChildViewController(HomeTableViewController(), title: "首页", imageName: "tabbar_home")addChildViewController(MessageTableViewController(), title: "消息", imageName: "tabbar_message_center")addChildViewController(DiscoverTableViewController(), title: "发现", imageName: "tabbar_discover")addChildViewController(ProfileTableViewController(), title: "我", imageName: "tabbar_profile")
}
自定义 TabBar
功能需求
- 在 4 个控制器切换按钮中间增加一个撰写按钮
- 点击撰写按钮能够弹出对话框撰写微博
需求分析
- 自定义 TabBar
- 计算控制器按钮位置,在中间添加一个
撰写
按钮
思路
- 加号按钮的大小与其他
tabBarItem
的大小是一致的 - 如果不考虑 modal 的方式,其所在位置应该同样有一个
tabBarItem
- 建立一个空的视图控制器形成占位
- 然后在该位置添加一个按钮遮挡
代码实现
- 添加空的视图控制器
/// 添加所有子控制器
private func addChildViewControllers() {// ...addChildViewController(UIViewController())// ...
}
注意 UIViewController() 的位置
- 添加按钮
// MARK: - 懒加载
/// 撰写按钮
private lazy var composedButton: UIButton = {let btn = UIButton()btn.setImage(UIImage(named: "tabbar_compose_icon_add"), forState: UIControlState.Normal)btn.setImage(UIImage(named: "tabbar_compose_icon_add_highlighted"), forState: UIControlState.Highlighted)btn.setBackgroundImage(UIImage(named: "tabbar_compose_button"), forState: UIControlState.Normal)btn.setBackgroundImage(UIImage(named: "tabbar_compose_button_highlighted"), forState: UIControlState.Highlighted)self.tabBar.addSubview(btn)return btn
}()
- 设置按钮位置
override func viewDidLayoutSubviews() {super.viewDidLayoutSubviews()setupComposeButton()
}/// 设置撰写按钮位置
private func setupComposeButton() {let w = tabBar.bounds.width / CGFloat(childViewControllers.count)let rect = CGRect(x: 0, y: 0, width: w, height: tabBar.bounds.height)composedButton.frame = CGRectOffset(rect, 2 * w, 0)
}
- 添加按钮监听方法
btn.addTarget(self, action: "clickComposeButton", forControlEvents: UIControlEvents.TouchUpInside)
- 按钮监听方法
/// 点击撰写按钮
func clickComposeButton() {print(__FUNCTION__)
}
注意:按钮的监听方法不能使用
private
阶段性小结
- 整体开发思路与使用 OC 几乎一致
- Swift 语法更加简洁
- Swift 对类型校验更加严格,不同类型的变量不允许直接计算
let w = tabBar.bounds.width / CGFloat(childViewControllers.count)
Swift 中的懒加载本质上是一个闭包,因此引用当前控制器的对象时需要使用 self.
不希望暴露的方法,应该使用
private
修饰符- 按钮点击事件的调用是由
运行循环
监听并且以消息机制
传递的,因此,按钮监听函数不能设置为private
第三方框架
项目中使用到以下第三方框架
AFNetworking
SDWebImage
SVProgressHUD
Pod 安装
- git 备份
- 打开终端
$ cd
进入项目目录- 输入以下终端命令建立或编辑
Podfile
$ vim Podfile
- 输入以下内容
use_frameworks!
platform :ios, '8.0'
pod 'AFNetworking'
pod 'SDWebImage'
pod 'SVProgressHUD'
:wq
保存退出输入以下命令安装第三方框架
$ pod install
- 如果第三方框架不能正常工作或者升级,可以输入以下命令更新
$ pod update
在 Swift 项目中,cocoapod 仅支持以 Framework 方式添加框架,因此需要在 Podfile 中添加
use_frameworks!
在终端提交添加的框架
# 将修改添加至暂存区
$ git add .# 提交修改并且添加备注信息
$ git commit -m "添加第三方框架"# 将修改推送到远程服务器
$ git push
修改项目版本
AFNetworking
- 建立
NetworkTools
单例
import AFNetworking/// 网络工具类
class NetworkTools: AFHTTPSessionManager {// 全局访问点static let sharedNetworkTools: NetworkTools = {let instance = NetworkTools(baseURL: NSURL(string: "https://api.weibo.com/")!)return instance}()
}
SDWebImage & SVProgressHUD
SVProgressHUD
SVProgressHUD
是使用 OC 开发的指示器- 使用非常广泛
框架地址
https://github.com/TransitApp/SVProgressHUD
与 MBProgressHUD
对比
SVProgressHUD
- 只支持
ARC
- 支持较新的苹果 API
- 提供有素材包
- 使用更简单
- 只支持
MBProgressHUD
- 支持
ARC
&MRC
- 没有素材包,程序员需要针对框架进行一定的定制才能使用
- 支持
使用
import SVProgressHUDSVProgressHUD.showInfoWithStatus("正在玩命加载中...", maskType: SVProgressHUDMaskType.Gradient)
SDWebImage
import SDWebImagelet url = NSURL(string: "http://img0.bdstatic.com/img/image/6446027056db8afa73b23eaf953dadde1410240902.jpg")!
SDWebImageManager.sharedManager().downloadImageWithURL(url, options: SDWebImageOptions.allZeros, progress: nil) { (image, _, _, _, _) inlet data = UIImagePNGRepresentation(image)data.writeToFile("/Users/liufan/Desktop/123.jpg", atomically: true)
}
单例
单例的目标
- 内存中只有一个对象实例
- 提供一个全局访问点
OC 中的单例
+ (instancetype)sharedManager {static id instance;static dispatch_once_t onceToken;NSLog(@"%ld", onceToken);dispatch_once(&onceToken, ^{instance = [[self alloc] init];});return instance;
}
Swift 中的单例
static var instance: NetworkTools?
static var token: dispatch_once_t = 0/// 在 swift 中类变量不能是存储型变量
class func sharedSoundTools() -> SoundTools {dispatch_once(&token) { () -> Void ininstance = SoundTools()}return instance!
}
不过!在 Swift 中
let
本身就是线程安全的
- 改进过的单例代码
private static let instance = NetworkTools()
/// 在 swift 中类变量不能是存储型变量
class var sharedNetworkTools: NetworkTools {return instance
}
- 单例其实还可以更简单
static let sharedSoundTools = SoundTools()
OAuth
基本概念
- OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准
- OAuth 的授权不会使第三方触及到用户的帐号信息
- OAuth 允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据
- 每一个令牌授权一个
特定的网站
在特定的时段内
访问特定的资源
OAuth 授权流程图
注册应用程序
注册应用程序
- 注册新浪微博账号
- 访问 http://open.weibo.com
- 点击
微连接
-移动应用
- 填写基本信息,如下图所示:
- 点击
应用信息
-高级信息
,设置回调地址,如下图所示:
应用程序信息
Key | 值 |
---|---|
client_id | 113773579 |
client_secret | a34f52ecaad5571bfed41e6df78299f6 |
redirect_uri | http://www.baidu.com |
access_token | 2.00ml8IrF0jh4hHe09f471dc4C_L3nC |
注意:授权回调地址一定要完全一致
加载授权页面
功能需求
- 通过浏览器访问新浪授权页面,获取授权码
接口文档
http://open.weibo.com/wiki/Oauth2/authorize
- 测试授权 URL
https://api.weibo.com/oauth2/authorize?client_id=479651210&redirect_uri=http://itheima.com
注意:回调地址必须与注册应用程序保持一致
功能实现
准备工作
- 新建
OAuth
文件夹 - 新建
OAuthViewController.swift
继承自UIViewController
加载 OAuth 视图控制器
- 修改
BaseTableViewController
中用户登录部分代码
/// 用户登录
func visitorLoginViewWillLogin() {let nav = UINavigationController(rootViewController: OAuthViewController())presentViewController(nav, animated: true, completion: nil)
}
- 在
OAuthViewController
中添加以下代码
lazy var webView: UIWebView = {return UIWebView()
}()override func loadView() {view = webViewtitle = "新浪微博"navigationItem.rightBarButtonItem = UIBarButtonItem(title: "关闭", style: UIBarButtonItemStyle.Plain, target: self, action: "close")
}/// 关闭
func close() {dismissViewControllerAnimated(true, completion: nil)
}
运行测试
加载授权页面
- 在
NetworkTools
中定义应用程序授权相关信息
// MARK: - 应用程序信息
private var clientId = "113773579"
private var clientSecret = "a34f52ecaad5571bfed41e6df78299f6"
var redirectUri = "http://www.baidu.com"/// 授权 URL
var oauthURL: NSURL {return NSURL(string: "https://api.weibo.com/oauth2/authorize?client_id=\(clientId)&redirect_uri=\(redirectUri)")!
}
- 在
info.plist
中增加ATS
设置
<key>NSAppTransportSecurity</key>
<dict><key>NSAllowsArbitraryLoads</key><true/>
</dict>
- 加载授权页面
override func viewDidAppear(animated: Bool) {super.viewDidAppear(animated)webView.loadRequest(NSURLRequest(URL: NetworkTools.sharedNetworkTools.oauthURL))
}
- 实现代理方法,跟踪重定向 URL
// MARK: - UIWebView 代理方法
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {print(request)return true
}
结果分析
- 如果 URL 以回调地址开始,需要检查查询参数
- 其他 URL 均加载
修改代码
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {// 判断请求的 URL 中是否包含回调地址let urlString = request.URL!.absoluteStringif !urlString.hasPrefix(NetworkTools.sharedNetworkTools.redirectUri) {return true}guard let query = request.URL?.query where query.hasPrefix("code=") else {print("取消授权")close()return false}let code = query.substringFromIndex(advance(query.startIndex, "code=".characters.count))print("授权成功 \(code)")NetworkTools.sharedNetworkTools.loadAccessToken(code)return false
}
加载指示器
- 导入
SVProgressHUD
import SVProgressHUD
- WebView 代理方法
func webViewDidStartLoad(webView: UIWebView) {SVProgressHUD.show()
}func webViewDidFinishLoad(webView: UIWebView) {SVProgressHUD.dismiss()
}
- 关闭
/// 关闭
func close() {SVProgressHUD.dismiss()dismissViewControllerAnimated(true, completion: nil)
}
AccessToken
课程目标
- 自定义对象
- 构造函数
- 归档 & 接档
接口定义
文档地址
http://open.weibo.com/wiki/OAuth2/access_token
接口地址
https://api.weibo.com/oauth2/access_token
HTTP 请求方式
- POST
请求参数
参数 | 描述 |
---|---|
client_id | 申请应用时分配的AppKey |
client_secret | 申请应用时分配的AppSecret |
grant_type |
请求的类型,填写 authorization_code
|
code | 调用authorize获得的code值 |
redirect_uri | 回调地址,需需与注册应用里的回调地址一致 |
返回数据
返回值字段 | 字段说明 |
---|---|
access_token | 用于调用access_token,接口获取授权后的access token |
expires_in | access_token的生命周期,单位是秒数 |
remind_in | access_token的生命周期(该参数即将废弃,开发者请使用expires_in) |
uid | 当前授权用户的UID |
UserAccount 模型
加载 AccessToken
- 在
NetworkTools
中增加函数加载AccessToken
/// 使用 code 获取 accessToken
///
/// - parameter code: 请求码
func loadAccessToken(code: String) {let urlString = "https://api.weibo.com/oauth2/access_token"let parames = ["client_id": clientId,"client_secret": clientSecret,"grant_type": "authorization_code","code": code,"redirect_uri": redirectUri]POST(urlString, parameters: parames, success: { (_, JSON) -> Void inprint(JSON)}) { (_, error) -> Void inprint(error)}
}
- 在
OAuthViewController
中获取授权码成功后调用网络方法
NetworkTools.sharedNetworkTools.loadAccessToken(code)
运行测试
- 返回错误信息
Error Domain=com.alamofire.error.serialization.response Code=-1016 "Request failed: unacceptable content-type: text/plain"
- 在
NetworkTools
中增加反序列化数据格式
// 设置反序列化数据格式集合
instance.responseSerializer.acceptableContentTypes = NSSet(objects: "application/json", "text/json", "text/javascript", "text/plain") as Set<NSObject>
- 增加闭包回调
/// 使用 code 获取 accessToken
///
/// - parameter code: 请求码
func loadAccessToken(code: String, finished: (result: [String: AnyObject]?, error: NSError?)->()) {let urlString = "https://api.weibo.com/oauth2/access_token"let parames = ["client_id": clientId,"client_secret": clientSecret,"grant_type": "authorization_code","code": code,"redirect_uri": redirectUri]POST(urlString, parameters: parames, success: { (_, JSON) infinished(result: JSON as? [String: AnyObject], error: nil)}) { (_, error) infinished(result: nil, error: error)}
}
- 修改调用代码
private func loadAccessToken(code: String) {NetworkTools.sharedNetworkTools.loadAccessToken(code) { (result, error) -> () inif error != nil result == nil {SVProgressHUD.showInfoWithStatus("网络不给力")dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * Int64(NSEC_PER_SEC)), dispatch_get_main_queue()) {self.close()}return}print(result)}
}
定义 UserAcount 模型
- 在
Model
目录下添加UserAccount
类 - 定义模型属性
/// 用于调用access_token,接口获取授权后的access token
var access_token: String?
/// access_token的生命周期,单位是秒数
var expires_in: String?
/// 当前授权用户的UID
var uid: String?init(dict: [String: AnyObject]) {super.init()self.setValuesForKeysWithDictionary(dict)
}override func setValue(value: AnyObject?, forUndefinedKey key: String) {}
- 字典转模型
let account = UserAccount(dict: result!)
print(account)
- 运行测试程序会崩溃!
因为从新浪服务器返回的
expires_in
是整数而不是字符串
- 调整代码,验证
expires_in
数据类型
responseSerializer = AFHTTPResponseSerializer()
POST(urlString, parameters: parames, success: { (_, JSON) inprint(NSString(data: JSON as! NSData, encoding: NSUTF8StringEncoding))finished(result: JSON as? [String: AnyObject], error: nil)}) { (_, error) infinished(result: nil, error: error)
}
再次运行测试
调试模型信息
与 OC 不同,如果要在 Swift 1.2 中调试模型信息,需要遵守
Printable
协议,并且重写description
的getter
方法,在 Swift 2.0 中,description
属性定义在CustomStringConvertible
协议中
override var description: String {let dict = ["access_token", "expires_in", "uid"]return "\(dictionaryWithValuesForKeys(dict))"
}
目前的版本需要先遵守
CustomStringConvertible
协议,重写了description
属性后,再删除,相信后续版本中会得到改进
设置过期日期
过期日期
在新浪微博返回的数据中,过期日期是以当前系统时间加上秒数计算的,为了方便后续使用,增加过期日期属性
定义属性
/// token过期日期
var expiresDate: NSDate?
- 修改构造函数
expiresDate = NSDate(timeIntervalSinceNow: expires_in)
- 修改
description
let properties = ["access_token", "expires_in", "expiresDate", "uid"]
归档 & 解档
课程目标
- 对比 OC 的
归档 & 解档
实现 利用
归档 & 解档
保存用户信息遵守协议
class UserAccount: NSObject, NSCoding
- 实现协议方法
// MARK: - NSCoding
func encodeWithCoder(aCoder: NSCoder) {aCoder.encodeObject(access_token, forKey: "access_token")aCoder.encodeDouble(expires_in, forKey: "expires_in")aCoder.encodeObject(expiresDate, forKey: "expiresDate")aCoder.encodeObject(uid, forKey: "uid")
}required init?(coder aDecoder: NSCoder) {access_token = aDecoder.decodeObjectForKey("access_token") as? Stringexpires_in = aDecoder.decodeDoubleForKey("expires_in")expiresDate = aDecoder.decodeObjectForKey("expiresDate") as? NSDateuid = aDecoder.decodeObjectForKey("uid") as? String
}
- 定义归档路径
/// 归档保存路径
private static let accountPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true).last!.stringByAppendingPathComponent("account.plist")
- 保存账户信息
/// 保存账号
func saveAccount() {NSKeyedArchiver.archiveRootObject(self, toFile: UserAccount.accountPath)
}
- 加载账户信息
/// 加载账号
class func loadAccount() -> UserAccount? {let account = NSKeyedUnarchiver.unarchiveObjectWithFile(accountPath) as? UserAccountreturn account
}
- 调整
OAuthViewController.swift
中的loadAccessToken
函数
// 保存用户账号信息
UserAccount(dict: result!).saveAccount()
- 修改加载账号函数
/// 用户账号
private static var userAccount: UserAccount?/// 加载账号
class func loadAccount() -> UserAccount? {if userAccount == nil {// 解档用户账户信息userAccount = NSKeyedUnarchiver.unarchiveObjectWithFile(accountPath) as? UserAccount}// 如果用户账户存在,判断是否过期if let date = userAccount?.expiresDate where date.compare(NSDate()) == NSComparisonResult.OrderedAscending {userAccount = nil}return userAccount
}
由于后续所有网络访问都基于用户账户中的
access_token
,因此定义一个全局变量,可以避免重复加载,而且能够在每次调用 AccessToken 时都判断是否过期
- 修改 BaseTableViewController 中的用户是否登录判断
/// 用户登录标记
var userLogon = UserAccount.loadAccount() != nil
加载用户信息
课程目标
- 通过
AccessToken
获取新浪微博网络数据
接口定义
文档地址
http://open.weibo.com/wiki/2/users/show
接口地址
https://api.weibo.com/2/users/show.json
HTTP 请求方式
- GET
请求参数
参数 | 描述 |
---|---|
access_token | 采用OAuth授权方式为必填参数,其他授权方式不需要此参数,OAuth授权后获得 |
uid | 需要查询的用户ID |
返回数据
返回值字段 | 字段说明 |
---|---|
name | 友好显示名称 |
avatar_large | 用户头像地址(大图),180×180像素 |
测试 URL
https://api.weibo.com/2/users/show.json?access_token=2.00ml8IrF0qLZ9W5bc20850c50w9hi9&uid=5365823342
代码实现
- 在
NetworkTools
中封装 GET 方法
/// 错误域
private let errorDomainName = "com.itheima.network.errorDomain"// MARK: - 封装网络请求方法
/// 完成回调类型
typealias HMFinishedCallBack = (result: [String: AnyObject]?, error: NSError?) -> ()/// GET 请求
///
/// - parameter urlString: URL 地址
/// - parameter params : 参数字典
/// - parameter finished : 完成回调
private func requestGET(urlString: String, params: [String: AnyObject], finished: HMFinishedCallBack) {GET(urlString, parameters: params, success: { _, JSON inif let result = JSON as? [String: AnyObject] {finished(result: result, error: nil)} else {finished(result: nil, error: NSError(domain: errorDomainName, code: -10000, userInfo: ["error": "空数据"]))}}) { _, error infinished(result: nil, error: error)}
}
- 定义通知常量
/// AccessToken 不存在通知
let HMAccessTokenEmptyNotification = "HMAccessTokenEmptyNotification"
- 生成 Token 参数字典
/// 生成 Token 参数字典
private func tokenDict() -> [String: AnyObject]? {if let token = UserAccount.loadAccount()?.access_token {return ["access_token": token]}NSNotificationCenter.defaultCenter().postNotificationName(HMAccessTokenEmptyNotification, object: nil)return nil
}
- 在
NetworkTools
中增加加载用户信息函数
// MARK: - 加载用户信息
func loadUserInfo(uid: Int, finished: (result: [String: AnyObject]?, error: NSError?) -> ()) {let urlString = "2/users/show.json"guard var params = tokenDict() else {return}params["uid"] = uidrequestGET(urlString, params: params) { (result, error) -> () infinished(result: result, error: error)}
}
- 在
UserAccount
中增加加载用户信息函数
func loadUserInfo() {NetworkTools.sharedTools.loadUserInfo(uid!) { (result, error) -> () inprint(result)}
}
- 测试加载用户信息
UserAccount(dict: result!).loadUserInfo()
- 增加属性定义
/// 友好显示名称
var name: String?
/// 用户头像地址(大图),180×180像素
var avatar_large: String?
- 调整加载用户信息函数
// MARK: - 加载用户信息
func loadUserInfo(finished: (error: NSError?) -> ()) {NetworkTools.sharedTools.loadUserInfo(uid!) { (result, error) -> () inif let dict = result {self.name = dict["name"] as? Stringself.avatar_large = dict["avatar_large"] as? Stringself.saveAccount()}finished(error: error)}
}
- 修改
description
属性
let properties = ["access_token", "expires_in", "uid", "expiresDate", "name", "avatar_large"]
- 修改归档&解档函数,增加用户名和图像地址属性
func encodeWithCoder(aCoder: NSCoder) {aCoder.encodeObject(access_token, forKey: "access_token")aCoder.encodeDouble(expires_in, forKey: "expires_in")aCoder.encodeObject(expiresDate, forKey: "expiresDate")aCoder.encodeObject(uid, forKey: "uid")aCoder.encodeObject(name, forKey: "name")aCoder.encodeObject(avatar_large, forKey: "avatar_large")
}required init?(coder aDecoder: NSCoder) {access_token = aDecoder.decodeObjectForKey("access_token") as? Stringexpires_in = aDecoder.decodeDoubleForKey("expires_in")expiresDate = aDecoder.decodeObjectForKey("expiresDate") as? NSDateuid = aDecoder.decodeObjectForKey("uid") as? Stringname = aDecoder.decodeObjectForKey("name") as? Stringavatar_large = aDecoder.decodeObjectForKey("avatar_large") as? String
}
- 修改
loadAccessToken
方法
/// 使用授权码换取 AccessToken
private func loadAccessToken(code: String) {NetworkTools.sharedTools.loadAccessToken(code) { (result, error) -> () inif error != nil || result == nil {self.loadError()return}// 加载用户账号信息UserAccount(dict: result!).loadUserInfo() { (error) -> () inif error != nil {self.loadError()return}print(UserAccount.loadAccount())}}
}/// 数据加载错误
private func loadError() {SVProgressHUD.showInfoWithStatus("您的网络不给力")// 延时一段时间再关闭let when = dispatch_time(DISPATCH_TIME_NOW, Int64(1 * NSEC_PER_SEC))dispatch_after(when, dispatch_get_main_queue()) {self.close()}
}
每一个令牌授权一个
特定的网站
在特定的时段内
访问特定的资源
调整网络代码
- 封装 POST 请求方法
/// POST 请求
///
/// - parameter urlString: URL 地址
/// - parameter params : 参数字典
/// - parameter finished : 完成回调
private func requestPOST(urlString: String, params: [String: AnyObject], finished: HMFinishedCallBack) {POST(urlString, parameters: params, success: { _, JSON inif let result = JSON as? [String: AnyObject] {finished(result: result, error: nil)} else {finished(result: nil, error: NSError(domain: errorDomainName, code: -10000, userInfo: ["error": "空数据"]))}}) { _, error inprint(error)finished(result: nil, error: error)}
}
- 修改加载 token 函数
/// 加载 Token
func loadAccessToken(code: String, finished: HMFinishedCallBack) {let urlString = "https://api.weibo.com/oauth2/access_token"let params = ["client_id": clientId,"client_secret": appSecret,"grant_type": "authorization_code","code": code,"redirect_uri": redirectUri]requestPOST(urlString, params: params) { (result, error) -> () infinished(result: result, error: error)}
}
新特性
- 新特性是现在很多应用程序中包含的功能,主要用于在系统升级后,用户第一次进入系统时获知新升级的功能
课程目标
- UICollectionView 使用
根视图控制器
切换
新特性功能
准备文件
- 将新特性图片素材拖拽到 Images.xcsets 中
- 在
Module
下建立NewFeature
目录 - 新建
NewFeatureViewController.swift
继承自UICollectionViewController
- 在
NewFeatureViewController.swift
的末尾添加如下代码:
代码实现
- 修改
AppDelegate
的根视图控制器
window?.rootViewController = NewFeatureViewController()
运行测试,崩溃!
原因:实例化
CollectionViewController
时必须指定布局参数实现
init()
简化外部调用
/// 界面布局
private let layout = UICollectionViewFlowLayout()init() {super.init(collectionViewLayout: layout)
}required init?(coder aDecoder: NSCoder) {fatalError("init(coder:) has not been implemented")
}
- 定义 NewFeatureCell
/// 新特性 Cell
class NewFeatureCell: UICollectionViewCell {var imageIndex: Int = 0 {didSet {iconView.image = UIImage(named: "new_feature_\(imageIndex + 1)")}}override init(frame: CGRect) {super.init(frame: frame)contentView.addSubview(iconView)// 自动布局// 1> 图片视图iconView.translatesAutoresizingMaskIntoConstraints = falsecontentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[subview]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": iconView]))contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[subview]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": iconView]))}required init?(coder aDecoder: NSCoder) {fatalError("init(coder:) has not been implemented")}// 懒加载控件lazy var iconView: UIImageView = UIImageView()
}
- 注册可重用 Cell
override func viewDidLoad() {super.viewDidLoad()// 注册可重用 Cellself.collectionView!.registerClass(NewFeatureCell.self, forCellWithReuseIdentifier: reuseIdentifier)
}
运行测试,需要设置布局属性
- 设置布局属性
/// 新特性布局
private class NewFeatureLayout: UICollectionViewFlowLayout {private override func prepareLayout() {itemSize = collectionView!.bounds.sizeminimumInteritemSpacing = 0minimumLineSpacing = 0scrollDirection = UICollectionViewScrollDirection.HorizontalcollectionView?.pagingEnabled = truecollectionView?.showsHorizontalScrollIndicator = falsecollectionView?.bounces = false}
}
在
prepareLayout
函数中定义 collectionView 的布局属性是最佳位置
- 修改布局属性
/// 界面布局
private let layout = NewFeatureLayout()
- 定义按钮
/// 按钮
lazy var startButton: UIButton = {let button = UIButton()button.setBackgroundImage(UIImage(named: "new_feature_finish_button"), forState: UIControlState.Normal)button.setBackgroundImage(UIImage(named: "new_feature_finish_button_highlighted"), forState: UIControlState.Highlighted)button.setTitle("开始体验", forState: UIControlState.Normal)return button
}()
- 设置按钮布局
// 2> 开始按钮
startButton.translatesAutoresizingMaskIntoConstraints = false
contentView.addConstraint(NSLayoutConstraint(item: startButton, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: contentView, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0))
contentView.addConstraint(NSLayoutConstraint(item: startButton, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: contentView, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: -160))
动画显示 开始体验
按钮
- 在
NewFeatureCell
中添加showStartButton
函数
/// 动画显示按钮
func showStartButton() {startButton.hidden = falsestartButton.transform = CGAffineTransformMakeScale(0, 0)startButton.userInteractionEnabled = falseUIView.animateWithDuration(1.2, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 10.0, options: UIViewAnimationOptions(rawValue: 0), animations: {self.startButton.transform = CGAffineTransformIdentity}) { _ inself.startButton.userInteractionEnabled = true}
}
- 在
collectionView
的完成显示Cell
代理方法中添加以下代码:
// 参数 cell, indexPath 是前一个 cell 和 indexPath
override func collectionView(collectionView: UICollectionView, didEndDisplayingCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {let indexPath = collectionView.indexPathsForVisibleItems().last!if indexPath.item == imageCount - 1 {(collectionView.cellForItemAtIndexPath(indexPath) as! NewFeatureCell).showStartButton()}
}
注意:参数中的
cell
&indexPath
是之前消失的cell
,而不是当前显示的cell
的
隐藏状态栏
override func prefersStatusBarHidden() -> Bool {return true
}
欢迎界面
- 在新浪微博中,如果用户登录成功会显示一个欢迎界面
- 特例:如果用户的系统刚刚升级或者第一次登录,会显示
新特性
界面,而不是欢迎
界面
准备文件
- 在
NewFeature
目录下新建WelcomeViewController.swift
继承自UIViewController
- 新建
Welcome.storyboard
,初始视图控制器的自定义类为WelcomeViewController
代码实现
- 修改
AppDelegate
的根视图控制器
window?.rootViewController = WelcomeViewController()
- 懒加载控件
// MARK: - 懒加载控件
/// 背景图片
private lazy var backImageView: UIImageView = UIImageView(image: UIImage(named: "ad_background"))
/// 头像视图
private lazy var iconView: UIImageView = {let iv = UIImageView(image: UIImage(named: "avatar_default_big"))iv.layer.masksToBounds = trueiv.layer.cornerRadius = 45return iv
}()
/// 文本标签
private lazy var messageLabel: UILabel = {let label = UILabel()label.text = "欢迎归来"return label
}()
- 搭建界面
/// 头像底部约束
private var iconBottomCons: NSLayoutConstraint?override func viewDidLoad() {super.viewDidLoad()prepareUI()
}/// 准备 UI
private func prepareUI() {view.addSubview(backImageView)view.addSubview(iconView)view.addSubview(messageLabel)// 自动布局// 1> 背景图片backImageView.translatesAutoresizingMaskIntoConstraints = falseview.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[subview]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": backImageView]))view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[subview]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": backImageView]))// 2> 头像iconView.translatesAutoresizingMaskIntoConstraints = falseview.addConstraint(NSLayoutConstraint(item: iconView, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0))view.addConstraint(NSLayoutConstraint(item: view, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: iconView, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: 160))iconBottomCons = view.constraints.last// 3> 标签messageLabel.translatesAutoresizingMaskIntoConstraints = falseview.addConstraint(NSLayoutConstraint(item: messageLabel, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: iconView, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0))view.addConstraint(NSLayoutConstraint(item: messageLabel, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: iconView, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: 20))
}
- 界面动画
override func viewDidAppear(animated: Bool) {super.viewDidAppear(animated)iconBottomCons?.constant = UIScreen.mainScreen().bounds.height - 240UIView.animateWithDuration(1.2, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 10.0, options: UIViewAnimationOptions(rawValue: 0), animations: {self.view.layoutIfNeeded()}, completion: nil)
}
参数说明
usingSpringWithDamping
的范围为0.0f
到1.0f
,数值越小弹簧
的振动效果越明显initialSpringVelocity
则表示初始的速度,数值越大一开始移动越快,初始速度取值较高而时间较短时,会出现反弹情况
设置用户头像
if let urlString = UserAccount.loadAccount()?.avatar_large {iconView.sd_setImageWithURL(NSURL(string: urlString)!)
}
- 添加图像宽高约束
view.addConstraint(NSLayoutConstraint(item: iconView, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1.0, constant: 90))
view.addConstraint(NSLayoutConstraint(item: view, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: iconView, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: 160))
代码评审(Code Review)
通常在企业开发中,会定期
面对面
(face to face)对代码进行评审
Code Review的意识
- 作为一个
Developer
,不仅要提交可工作的代码
(Deliver working code),更要提交可维护的代码
(Deliver maintainable code) - 必要时进行重构,随着项目的迭代,在计划新增功能的同时,开发要主动计划重构的工作项
- 开放的心态,虚心接受大家的
评审建议
(Review Comments)
代码评审的方式
- 开 Code Review 会议
- 团队内部会整理 Check List
- 团队内部成员交换代码
- 找出可优化方案
- 多问问题,例如:“这块儿是怎么工作的?”、“如果有XXX 情况,你这个怎么处理?”
- 区分重点,优先抓住
设计
,可读性
,健壮性
等重点问题 - 整理好的编码实践,用来作为
Code Review
的参考
评审内容
架构/设计
- 单一职责原则
- 这是经常被违背的原则。一个类只能干一个事情,一个方法最好也只干一件事情。比较常见的违背是
一个类既干UI的事情,又干逻辑的事情
,这个在低质量的客户端代码里很常见
- 这是经常被违背的原则。一个类只能干一个事情,一个方法最好也只干一件事情。比较常见的违背是
- 行为是否统一,例如:
- 缓存是否统一
- 错误处理是否统一
- 错误提示是否统一
- 弹出框是否统一
- ……
- 代码污染
- 代码有没有对其他模块强耦合
- 重复代码
- 开闭原则
- 面向接口编程
- 健壮性
- 是否考虑线程安全
- 数据访问是否一致性
- 边界处理是否完整
- 逻辑是否健壮
- 是否有内存泄漏
- 有没有循环依赖
- 有没有野指针
- ……
- 错误处理
- 改动是不是对代码的提升
- 新的改动是打补丁,让代码质量继续恶化,还是对代码质量做了修复
- 效率/性能
- 关键算法的时间复杂度多少?有没有可能有潜在的性能瓶颈
- 客户端程序对频繁消息和较大数据等耗时操作是否处理得当
代码风格
- 可读性
- 衡量可读性的可以有很好实践的标准,就是 Reviewer 能否非常容易的理解这个代码。如果不是,那意味着代码的可读性要进行改进
- 命名
- 命名对可读性非常重要
- 英语用词尽量准确一点,必要时可以查字典
- 函数长度/类长度
- 函数太长的不好阅读
- 类太长了,检查是否违反的
单一职责
原则
- 注释
- 恰到好处的注释
- 参数个数
- 不要太多,一般不要超过 3 个
Review Your Own Code First
- 每次提交前整体把自己的代码过一遍非常有帮助,尤其是看看有没有犯低级错误
OAuthViewController
- 删除多余的 print
- 删除 // TODO: 换取 TOKEN
- 修改
loadAccessToken
函数中的注释
提示:在实际开发中,代码中的注释一定要及时调整!
UserAccount
知识点:类属性
vs 类函数
- 都是
通过类名调用
- 类属性作为属性一定有返回值
- 类函数不一定有返回值
- 类本质上只是对
对象
的描述,从面相对象的角度而言,类不应该有存储功能- 类属性是只读的,可以返回一个函数计算结果
- 也可以返回一个私有静态成员记录的内容
- 通过类属性,能够提高代码的可读性
演练 & 体会
- 将
loadAccount()
类函数修改为sharedUserAccount
类属性
class var sharedUserAccount: UserAccount? {// 1. 判断账户是否存在if userAccount == nil {// 解档 - 如果没有保存过,解档结果可能仍然是 niluserAccount = NSKeyedUnarchiver.unarchiveObjectWithFile(accountPath) as? UserAccount}// 2. 判断日期if let date = userAccount?.expiresDate where date.compare(NSDate()) == NSComparisonResult.OrderedAscending {// 如果已经过期,需要清空账号记录userAccount = nil}return userAccount
}
- 利用编译器提示修改出错的代码
对比前后两种方式的代码可读性的提高
- 说明:类属性是 Swift 特有的语法,仅供体会
NetworkTools
- 移动
HMNetFinishedCallBack
声明的位置
定义网络访问错误枚举
- 定义网络访问错误枚举
/// 网络访问错误
private enum HMNetworkError: Int {case emptyDataError = -1case emptyTokenError = -2private var description: String {switch self {case .emptyDataError:return "空数据"case .emptyTokenError:return "AccessToken 错误"}}private var error: NSError {return NSError(domain: HMErrorDomainName, code: rawValue, userInfo: [HMErrorDomainName: description])}
}
可以在 Playground 中测试枚举类型
- 修改
requestGET
中的空数据错误
finished(result: nil, error: HMNetworkError.emptyDataError.error)
- 修改
loadUserInfo
中 token 为空的检测代码,增加错误回调
// 判断 token 是否存在
if UserAccount.sharedUserAccount?.access_token == nil {let error = HMNetworkError.emptyTokenError.errorprint(error)finished(result: nil, error: error)return
}
- 注释
UserAccount
中为全局账号赋值的代码,并且调试运行效果
封装 AFN 的 POST 方法
- 复制 GET 代码,并且修改部分单词
/// POST 请求
///
/// :param: urlString URL 地址
/// :param: params 参数字典
/// :param: finished 完成回调
private func requestPOST(urlString: String, params: [String: AnyObject], finished: HMNetFinishedCallBack) {POST(urlString, parameters: params, success: { (_, JSON) -> Void inif let result = JSON as? [String: AnyObject] {// 有结果的回调finished(result: result, error: nil)} else {// 没有错误,同时没有结果print("没有数据 GET Request \(urlString)")finished(result: nil, error: HMNetworkError.emptyDataError.error)}}) { (_, error) -> Void inprint(error)finished(result: nil, error: error)}
}
- 修改 函数并运行测试
/// 加载 Token
func loadAccessToken(code: String, finished: HMNetFinishedCallBack) {let urlString = "https://api.weibo.com/oauth2/access_token"let params = ["client_id": clientId,"client_secret": appSecret,"grant_type": "authorization_code","code": code,"redirect_uri": redirectUri]requestPOST(urlString, params: params, finished: finished)
}
整合网络访问方法
- 定义网络方法枚举
/// 网络访问方法
private enum HMNetworkMethod: String {case GET = "GET"case POST = "POST"
}
- 封装网络访问方法
/// 网络请求
///
/// - parameter method : 访问方法
/// - parameter urlString: URL 地址
/// - parameter params : 参数自带呢
/// - parameter finished : 完成回调
private func request(method: HMNetworkMethod, urlString: String, params: [String: AnyObject], finished: HMNetFinishedCallBack) {let successCallBack: (NSURLSessionTask!, AnyObject!) -> Void = { _, JSON inif let result = JSON as? [String: AnyObject] {// 有结果的回调finished(result: result, error: nil)} else {// 没有错误,同时没有结果print("没有数据 \(method) Request \(urlString)")finished(result: nil, error: HMNetworkError.emptyDataError.error)}}let failedCallBack: (NSURLSessionTask!, NSError!) -> Void = { _, error inprint(error)finished(result: nil, error: error)}switch method {case .GET:GET(urlString, parameters: params, success: successCallBack, failure: failedCallBack)case .POST:POST(urlString, parameters: params, success: successCallBack, failure: failedCallBack)}
}
运行测试
自动布局框架
- 为简化纯代码布局,抽取了常用的自动布局代码
将 UIView+AutoLayout 拖拽到项目中的
Tools
目录下调整
NewFeatureCell
iconView.ff_Fill(contentView)
startButton.ff_AlignInner(type: ff_AlignType.BottomCenter, referView: contentView, size: nil, offset: CGPoint(x: 0, y: -160))
- 调整
WelcomeViewController
// 1> 背景图片
backImageView.ff_Fill(view)
// 2> 头像
let cons = iconView.ff_AlignInner(type: ff_AlignType.BottomCenter, referView: view, size: CGSize(width: 90, height: 90), offset: CGPoint(x: 0, y: -160))
// 记录底边约束
iconBottomCons = iconView.ff_Constraint(cons, attribute: NSLayoutAttribute.Bottom)// 3> 标签
label.ff_AlignVertical(type: ff_AlignType.BottomCenter, referView: iconView, size: nil, offset: CGPoint(x: 0, y: 16))
- 修改动画方法中的约束数值
iconBottomCons?.constant = -UIScreen.mainScreen().bounds.height - iconBottomCons!.constant
转载于:https://www.cnblogs.com/jiahao89/p/5118265.html
微博开发笔记上(未完待续)相关推荐
- 嵌入式Linux驱动开发笔记(未完待续。。。)
零.嵌入式Linux驱动编程思想 1.面向对象(把一个事件抽象成一个结构体) 2.分层 3.分离 一.Git仓库用法 1.linu终端输入下面命令安装 git clone https://e.codi ...
- 《今日简史》读书笔记(未完待续)
<今日简史>读书笔记(未完待续) 这本书是尤瓦尔·赫拉利的简史三部曲的最后一本,前2本书是<未来简史>和<人类简史>.根据豆瓣上网友的评价,这本书是尤瓦尔·赫拉利写 ...
- 二叉树学习笔记(未完待续)
摘要 二叉树学习笔记(未完待续). 博客 IT老兵驿站. 前言 昨天(2019-11-07)复习红黑树,发现红黑树和二叉树密不可分,所以这里再复习一下二叉树. 在大学的时候,这块我很认真地学习了一遍. ...
- 《图解 HTTP》读书笔记(未完待续)
ARP 协议(Address Resolution Protocol)一种以解析地址的协议,根据通信双方的 IP 地址就可以查出对应的 MAC 地址. MAC( Media Access Contro ...
- # Python基础笔记(未完待续)
写在前面:小白闲来无事,参考小甲鱼视频重温Python,所及笔记,仅供参考.第一次写长笔记,格式较乱,请谅解 一.数据类型 1.输入路径 >>>print("D:\thre ...
- springboot学习笔记(未完待续)
微服务阶段 javase: oop mysql:持久化 html + css + js + jquery + 框架 javaweb ssm 微服务:springboot springcloud 程序 ...
- 系统开发小结【未完待续】
最近在搞OA类的开发,主要开发了两个模块,一个是值班管理模块,另一个类似于文件管理的模块.像这两个模块中的一些主要功能是我们进行开发中常要开发的功能.OA系统也算是MIS系统,是MIS发展的初级阶段, ...
- 尚学堂java SE学习笔记(未完待续)
1.关于递归,一定要注意函数调用顺序! 图1 如上图:在执行f(n-1)+f(n-2)的过程中,先执行f(n-1)一直到f(n-1)有返回值才执行f(n-2). 2. 图2 注意成员变量和局部变量的 ...
- 组合数学学习笔记(未完待续
这学期学了不少组合数学,期末给他补完. 算法竞赛考得很多的部分啊 这个还是很重要的 在目前的算法竞赛中有三大计数考点 1)组合计数 2)线性计数 3)群论计数 其中群论计数比较困难,我又不知道什么是线 ...
- 计算机网络谢希仁第七版笔记(未完待续)
一.概述 1.1 计算机网络在信息时代的作用 1.2 互联网概述 Internet(互联网,或因特网)是一个专用名词,指当前全球最大的.开放的.由众多网络相互连接而成的特定互连网 internet是一 ...
最新文章
- 为什么Eureka比ZooKeeper更适合做注册中心?
- SpringMVC怎么获取前台传来的数组
- 进程的用户栈和内核栈
- 计算机组装维护的概念,实用计算机组装与维护库及概念.doc
- urllib基本使用-Handler和自定义的opener()
- BZOJ[1009] [HNOI2008]GT考试
- oracle for函数,oracle分区表述的FOR语句(一)
- Mybaits-plus实战(二)
- python超时处理_Python如何实现让一个函数超时退出?
- treeview wpf代码设置选中_C# WPF过渡效果实现(Transitions)
- [ 转载 ]微信小程序font-family
- matlab meshlab,MeshLab(网格模型处理软件)下载-MeshLab官方版下载[电脑版]-PC下载网
- java一元抢购,拼多多1元抢购的步骤是什么?
- ubuntu安装docker + 配置国内源和加速器
- 重磅:一台电脑两个macOS系统,macOS Big Sur和macOS Monterey切换使用
- ML - 线性回归(Linear Regression)
- 互联网巨头常用词汇大全 每一个词都在改变世界
- 从 PC 卸载 Office
- 解决VirtualBox增强功能异常
- 移动端车牌识别:新能源车牌识别上线
热门文章
- suse 查看java版本_如何查看当前Linux的版本
- python小论文范文3000字_完整的论文范文3000字 [论文的名字 ]
- 安装activex手机控件_86/BRZ 免“油饼”安装 Defi 机油压力表
- lisp用entmake生产圆柱体_液态基酒生产
- astype()函数
- 计算机网络与通信pdf谢希仁_考情分析|2020年同济大学计算机考研考情分析
- 机器学习- 吴恩达Andrew Ng Week8 知识总结 Clustering
- Kubernetes可以代替Docker,可笑
- FP-growth发现频繁项集
- 结构体嵌套时的sizeof运算