微博开发笔记上(未完待续)

新浪微博开发笔记

iPhone 项目目标

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

课程提纲

新浪微博接口地址

  • 微博开放平台地址
    http://open.weibo.com

  • 微博接口文档地址
    http://open.weibo.com/wiki/微博API

项目主题框架

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

  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 公钥,进入终端,并输入以下命令

    • 开源中国帮助文档地址:https://git.oschina.net/oschina/git-osc/wikis/帮助#ssh-keys
# 切换目录,MAC中目录的第一个字符如果是 `.` 表示改文件夹是隐藏文件夹
$ cd ~/.ssh
# 查看当前目录文件
$ ls

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

# 查看公钥内容
$ cat id_rsa.pub
  • 将公钥内容复制并粘贴至 https://git.oschina.net/profile/sshkeys

  • 测试公钥

# 测试 SSH 连接
$ ssh -T [email protected]

# 终端提示 `Welcome to Git@OSC, 刀哥!` 说明连接成功
  • 新建项目
  • 克隆项目
# 切换至项目目录
$ cd 项目目录

# 克隆项目,地址可以在项目首页复制
$ git clone [email protected]: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.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
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, 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_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>NSAppTransportSecuritykey>
<dict>
    <key>NSAllowsArbitraryLoadskey>
    <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
  • 增加闭包回调
/// 使用 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

转载于:https://www.cnblogs.com/jiahao89/p/5118265.html

你可能感兴趣的:(微博开发笔记上(未完待续))