新浪微博开发笔记

iPhone 项目目标

  • 项目掌控能力
  • 工具使用能力
  • 开发技巧能力

课程提纲

新浪微博接口地址

项目主题框架

走向工作岗位之后,一般会遇到两种工作情况:

  1. 新项目开发

    • 通常在项目开始之前,公司的产品经理会提供完整的产品原型图,或功能设计文档
    • 通过对这些文档的解读,能够梳理出目标项目的整体架构,从而协助项目框架的搭建
  2. 旧项目维护

    • 很多老项目是缺乏文档的,这种情况在一些小公司中表现的尤为突出
    • 要想快速上手一个老项目,首先运行项目,并且整理项目整体框架结构
    • 然后用整理出的框架结构与代码结构相互印证,无疑可以对了解项目的整体架构起到重要的辅助

综上所述,无论是新项目,还是老项目,在开发之前确定项目的主体架构都是非常重要,也是十分必要的!

主体架构确认的好处

开发之前,明确项目的主体架构具有以下好处:

  1. 明确开发目标,项目一旦启动,始终锁定目标前进!
  2. 明确功能模块的数量,方便工期核算
  3. 根据开发进度,预判开发周期,及时与相关部门沟通、协调
  4. 根据主体架构搭建项目框架,方便团队开发,各个功能模块齐头并进,提高开发效率!
  5. 确定项目开发中的重点难点,提前安排攻关能力强的同事进行技术攻关,待需要时能够享受攻关成果,或者及时调整产品设计
  6. 新增或调整功能时,能够高屋建瓴,在最合适的位置添加相关功能模块

新浪微博

作为中国移动互联网的代表性产品之一,新浪微博涵盖了大量的移动互联网元素,通过对新浪微博的研究及模仿,可以:

  • 对这些元素在实际产品中的应用有深入的了解和认识
  • 知道如何在一个真实的项目中运用相关技术点
  • 对大型项目的架构、开发及掌控有更全面的认识和理解

正如前文所述,在开始模仿之前,首先运行产品,掌握项目的整体架构,确定开发的主体功能非常重要!

新浪微博主体架构

对界面预览之后,可以发现新浪微博符合经典应用程序架构设计:

  • 主视图控制器是一个 UITabbarController
  • 包含四个 UINavigationController,分别是
    • 首页
    • 消息
    • 发现

特殊之处:
- UITabbarController 中间有一个 “+” 按钮,点击该按钮能够 Modal 显示微博类型选择界面,方便用户选择自己需要的微博类型
- 四个 UINavigationController 在用户登录前后显示的界面格式是不一样的

根原版新浪微博的区别

由于必须使用新浪微博官方的 API 才能够正常开发,换言之,如果没有登录系统是无法使用新浪微博提供的接口的!

基于上述原因,在实际开发中对未登录之前的界面设计进行简化

开源中国社区

官方网站

https://git.oschina.net/

  • 开源中国社区成立于2008年8月,其目的是为中国的IT技术人员提供一个全面的、快捷更新的用来检索开源软件以及交流使用开源经验的平台
  • 目前国内有很多公司会将公司的项目部署在 OSChina

GitHUB 的对比

  1. 服务器在国内,速度更快
  2. 免费账户同样可以建立 私有 项目,而 GitHUB 上要建立私有项目必须 付费

使用

  • 注册账号

    • 建议使用网易的邮箱,使用其他免费邮箱可能会收不到验证邮件
  • 添加 SSH 公钥,进入终端,并输入以下命令

# 切换目录,MAC中目录的第一个字符如果是 `.` 表示改文件夹是隐藏文件夹
$ cd ~/.ssh
# 查看当前目录文件
$ ls

# 生成 RSA 密钥对
# 1> "" 中输入个人邮箱
# 2> 提示输入私钥文件名称,直接回车
# 3> 提示输入密码,可以随便输入,只要本次能够记住即可
$ ssh-keygen -t rsa -C "xxx@126.com"

# 查看公钥内容
$ cat id_rsa.pub
# 测试 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 文件之后,每次提交时不会将个人的项目设置信息(例如:末次打开的文件,调试断点等)提交到服务器,在团队开发中非常重要

图片素材

素材对应的设备

1x2x3x
大小对应开发中的宽高是 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.plistCFBundleName 对应的内容
    • 注意不要超过6个中文,否则会影响用户体验

启动程序

  • AppDelegatedidFinishLaunchingWithOptions 函数中添加以下代码:
window = UIWindow(frame: UIScreen.mainScreen().bounds)
window?.backgroundColor = UIColor.whiteColor()
window?.rootViewController = ViewController()

window?.makeKeyAndVisible()

运行测试

添加启动图片

  • 提示
    • 关于启动图片的设置,需要注意上课的操作细节
    • 关于各个设备的实际屏幕尺寸,注意一下不同类型的启动图片即可

项目搭建

课程目标

  1. 熟悉 swift 语法
  2. 搭建系统主体框架结构
  3. 对比与 OC 开发的异同
  4. 纯代码搭建框架

创建文件

准备工作

删除模板文件

  • ViewController.swift
  • Main.storyboard
  • LaunchScreen.xib

创建项目结构

主目录 Classes
二级目录
目录名说明
Module功能模块
Model业务逻辑模型
Tools工具类
Module 子目录
目录名说明
Main主要
Home首页
Message消息
Discover发现
Profile

创建项目文件

Main

目录Controller
MainMainViewController.swift(:UITabBarController)

功能模块

目录Controller
HomeHomeTableViewController.swift
MessageMessageTableViewController.swift
DiscoverDiscoverTableViewController.swift
ProfileProfileTableViewController.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, 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 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_id113773579
client_secreta34f52ecaad5571bfed41e6df78299f6
redirect_urihttp://www.baidu.com
access_token2.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_inaccess_token的生命周期,单位是秒数
remind_inaccess_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 协议,并且重写 descriptiongetter 方法,在 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.0f1.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(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
Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐