微博開發筆記上(未完待續)

新浪微博開發筆記

iPhone 項目目標

  • 項目掌控能力
  • 工具使用能力
  • 開發技巧能力

課程提綱

新浪微博接口地址

項目主題框架

走向工做崗位以後,通常會遇到兩種工做狀況:css

  1. 新項目開發java

    • 一般在項目開始以前,公司的產品經理會提供完整的產品原型圖,或功能設計文檔
    • 經過對這些文檔的解讀,可以梳理出目標項目的總體架構,從而協助項目框架的搭建
  2. 舊項目維護ios

    • 不少老項目是缺少文檔的,這種狀況在一些小公司中表現的尤其突出
    • 要想快速上手一個老項目,首先運行項目,而且整理項目總體框架結構
    • 而後用整理出的框架結構與代碼結構相互印證,無疑能夠對了解項目的總體架構起到重要的輔助

綜上所述,不管是新項目,仍是老項目,在開發以前肯定項目的主體架構都是很是重要,也是十分必要的!git

主體架構確認的好處

開發以前,明確項目的主體架構具備如下好處:程序員

  1. 明確開發目標,項目一旦啓動,始終鎖定目標前進!
  2. 明確功能模塊的數量,方便工期覈算
  3. 根據開發進度,預判開發週期,及時與相關部門溝通、協調
  4. 根據主體架構搭建項目框架,方便團隊開發,各個功能模塊齊頭並進,提升開發效率!
  5. 肯定項目開發中的重點難點,提早安排攻關能力強的同事進行技術攻關,待須要時可以享受攻關成果,或者及時調整產品設計
  6. 新增或調整功能時,可以高屋建瓴,在最合適的位置添加相關功能模塊

新浪微博

做爲中國移動互聯網的表明性產品之一,新浪微博涵蓋了大量的移動互聯網元素,經過對新浪微博的研究及模仿,能夠:github

  • 對這些元素在實際產品中的應用有深刻的瞭解和認識
  • 知道如何在一個真實的項目中運用相關技術點
  • 對大型項目的架構、開發及掌控有更全面的認識和理解

正如前文所述,在開始模仿以前,首先運行產品,掌握項目的總體架構,肯定開發的主體功能很是重要!web

新浪微博主體架構

對界面預覽以後,能夠發現新浪微博符合經典應用程序架構設計:算法

  • 主視圖控制器是一個 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 文件以後,每次提交時不會將我的的項目設置信息(例如:末次打開的文件,調試斷點等)提交到服務器,在團隊開發中很是重要

圖片素材

素材對應的設備

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>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 協議,而且重寫 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
相關文章
相關標籤/搜索