新浪微博开发笔记
iPhone 项目目标
- 项目掌控能力
- 工具使用能力
- 开发技巧能力
课程提纲
新浪微博接口地址
微博开放平台地址
http://open.weibo.com微博接口文档地址
http://open.weibo.com/wiki/微博API
项目主题框架
走向工作岗位之后,一般会遇到两种工作情况:
新项目开发
- 通常在项目开始之前,公司的产品经理会提供完整的产品原型图,或功能设计文档
- 通过对这些文档的解读,能够梳理出目标项目的整体架构,从而协助项目框架的搭建
旧项目维护
- 很多老项目是缺乏文档的,这种情况在一些小公司中表现的尤为突出
- 要想快速上手一个老项目,首先运行项目,并且整理项目整体框架结构
- 然后用整理出的框架结构与代码结构相互印证,无疑可以对了解项目的整体架构起到重要的辅助
综上所述,无论是新项目,还是老项目,在开发之前确定项目的主体架构都是非常重要,也是十分必要的!
主体架构确认的好处
开发之前,明确项目的主体架构具有以下好处:
- 明确开发目标,项目一旦启动,始终锁定目标前进!
- 明确功能模块的数量,方便工期核算
- 根据开发进度,预判开发周期,及时与相关部门沟通、协调
- 根据主体架构搭建项目框架,方便团队开发,各个功能模块齐头并进,提高开发效率!
- 确定项目开发中的重点难点,提前安排攻关能力强的同事进行技术攻关,待需要时能够享受攻关成果,或者及时调整产品设计
- 新增或调整功能时,能够高屋建瓴,在最合适的位置添加相关功能模块
新浪微博
作为中国移动互联网的代表性产品之一,新浪微博涵盖了大量的移动互联网元素,通过对新浪微博的研究及模仿,可以:
- 对这些元素在实际产品中的应用有深入的了解和认识
- 知道如何在一个真实的项目中运用相关技术点
- 对大型项目的架构、开发及掌控有更全面的认识和理解
正如前文所述,在开始模仿之前,首先运行产品,掌握项目的整体架构,确定开发的主体功能非常重要!
新浪微博主体架构
对界面预览之后,可以发现新浪微博符合经典应用程序架构设计:
- 主视图控制器是一个
UITabbarController
- 包含四个
UINavigationController
,分别是
- 首页
- 消息
- 发现
- 我
特殊之处:
- UITabbarController
中间有一个 “+” 按钮,点击该按钮能够 Modal 显示微博类型选择
界面,方便用户选择自己需要的微博类型
- 四个 UINavigationController
在用户登录前后显示的界面格式是不一样的
根原版新浪微博的区别
由于必须使用新浪微博官方的 API 才能够正常开发,换言之,如果没有登录系统是无法使用新浪微博提供的接口的!
基于上述原因,在实际开发中对未登录之前的界面设计进行简化
开源中国社区
官方网站
- 开源中国社区成立于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 = title
vc.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, 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 SVProgressHUD
SVProgressHUD.showInfoWithStatus("正在玩命加载中...", maskType: SVProgressHUDMaskType.Gradient)
SDWebImage
import SDWebImage
let url = NSURL(string: "http://img0.bdstatic.com/img/image/6446027056db8afa73b23eaf953dadde1410240902.jpg")!
SDWebImageManager.sharedManager().downloadImageWithURL(url, options: SDWebImageOptions.allZeros, progress: nil) { (image, _, _, _, _) in
let 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 in
instance = 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 = webView
title = "新浪微博"
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!.absoluteString
if !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 in
print(JSON)
}) { (_, error) -> Void in
print(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) in
finished(result: JSON as? [String: AnyObject], error: nil)
}) { (_, error) in
finished(result: nil, error: error)
}
}
- 修改调用代码
private func loadAccessToken(code: String) {
NetworkTools.sharedNetworkTools.loadAccessToken(code) { (result, error) -> () in
if 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) in
print(NSString(data: JSON as! NSData, encoding: NSUTF8StringEncoding))
finished(result: JSON as? [String: AnyObject], error: nil)
}) { (_, error) in
finished(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? String
expires_in = aDecoder.decodeDoubleForKey("expires_in")
expiresDate = aDecoder.decodeObjectForKey("expiresDate") as? NSDate
uid = 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? UserAccount
return 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 in
if let result = JSON as? [String: AnyObject] {
finished(result: result, error: nil)
} else {
finished(result: nil, error: NSError(domain: errorDomainName, code: -10000, userInfo: ["error": "空数据"]))
}
}) { _, error in
finished(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"] = uid
requestGET(urlString, params: params) { (result, error) -> () in
finished(result: result, error: error)
}
}
- 在
UserAccount
中增加加载用户信息函数
func loadUserInfo() {
NetworkTools.sharedTools.loadUserInfo(uid!) { (result, error) -> () in
print(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) -> () in
if let dict = result {
self.name = dict["name"] as? String
self.avatar_large = dict["avatar_large"] as? String
self.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? String
expires_in = aDecoder.decodeDoubleForKey("expires_in")
expiresDate = aDecoder.decodeObjectForKey("expiresDate") as? NSDate
uid = aDecoder.decodeObjectForKey("uid") as? String
name = aDecoder.decodeObjectForKey("name") as? String
avatar_large = aDecoder.decodeObjectForKey("avatar_large") as? String
}
- 修改
loadAccessToken
方法
/// 使用授权码换取 AccessToken
private func loadAccessToken(code: String) {
NetworkTools.sharedTools.loadAccessToken(code) { (result, error) -> () in
if error != nil || result == nil {
self.loadError()
return
}
// 加载用户账号信息
UserAccount(dict: result!).loadUserInfo() { (error) -> () in
if 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 in
if let result = JSON as? [String: AnyObject] {
finished(result: result, error: nil)
} else {
finished(result: nil, error: NSError(domain: errorDomainName, code: -10000, userInfo: ["error": "空数据"]))
}
}) { _, error in
print(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) -> () in
finished(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 = false
contentView.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()
// 注册可重用 Cell
self.collectionView!.registerClass(NewFeatureCell.self, forCellWithReuseIdentifier: reuseIdentifier)
}
运行测试,需要设置布局属性
- 设置布局属性
/// 新特性布局
private class NewFeatureLayout: UICollectionViewFlowLayout {
private override func prepareLayout() {
itemSize = collectionView!.bounds.size
minimumInteritemSpacing = 0
minimumLineSpacing = 0
scrollDirection = UICollectionViewScrollDirection.Horizontal
collectionView?.pagingEnabled = true
collectionView?.showsHorizontalScrollIndicator = false
collectionView?.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 = false
startButton.transform = CGAffineTransformMakeScale(0, 0)
startButton.userInteractionEnabled = false
UIView.animateWithDuration(1.2, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 10.0, options: UIViewAnimationOptions(rawValue: 0), animations: {
self.startButton.transform = CGAffineTransformIdentity
}) { _ in
self.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 = true
iv.layer.cornerRadius = 45
return 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 = false
view.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 = false
view.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 = false
view.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 - 240
UIView.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 {
// 解档 - 如果没有保存过,解档结果可能仍然是 nil
userAccount = 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 = -1
case emptyTokenError = -2
private 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.error
print(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 in
if 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 in
print(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 in
if 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 in
print(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( 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