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

新浪微博開發筆記

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 "[email protected]"

# 查看公鑰內容
$ cat id_rsa.pub
# 測試 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>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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章