新浪微博開發筆記
iPhone 項目目標
- 項目掌控能力
- 工具使用能力
- 開發技巧能力
課程提綱
新浪微博接口地址
微博開放平臺地址
http://open.weibo.com微博接口文檔地址
http://open.weibo.com/wiki/微博API
項目主題框架
走向工作崗位之後,一般會遇到兩種工作情況:
新項目開發
- 通常在項目開始之前,公司的產品經理會提供完整的產品原型圖,或功能設計文檔
- 通過對這些文檔的解讀,能夠梳理出目標項目的整體架構,從而協助項目框架的搭建
舊項目維護
- 很多老項目是缺乏文檔的,這種情況在一些小公司中表現的尤爲突出
- 要想快速上手一個老項目,首先運行項目,並且整理項目整體框架結構
- 然後用整理出的框架結構與代碼結構相互印證,無疑可以對了解項目的整體架構起到重要的輔助
綜上所述,無論是新項目,還是老項目,在開發之前確定項目的主體架構都是非常重要,也是十分必要的!
主體架構確認的好處
開發之前,明確項目的主體架構具有以下好處:
- 明確開發目標,項目一旦啓動,始終鎖定目標前進!
- 明確功能模塊的數量,方便工期覈算
- 根據開發進度,預判開發週期,及時與相關部門溝通、協調
- 根據主體架構搭建項目框架,方便團隊開發,各個功能模塊齊頭並進,提高開發效率!
- 確定項目開發中的重點難點,提前安排攻關能力強的同事進行技術攻關,待需要時能夠享受攻關成果,或者及時調整產品設計
- 新增或調整功能時,能夠高屋建瓴,在最合適的位置添加相關功能模塊
新浪微博
作爲中國移動互聯網的代表性產品之一,新浪微博涵蓋了大量的移動互聯網元素,通過對新浪微博的研究及模仿,可以:
- 對這些元素在實際產品中的應用有深入的瞭解和認識
- 知道如何在一個真實的項目中運用相關技術點
- 對大型項目的架構、開發及掌控有更全面的認識和理解
正如前文所述,在開始模仿之前,首先運行產品,掌握項目的整體架構,確定開發的主體功能非常重要!
新浪微博主體架構
對界面預覽之後,可以發現新浪微博符合經典應用程序架構設計:
- 主視圖控制器是一個
UITabbarController
- 包含四個
UINavigationController
,分別是
- 首頁
- 消息
- 發現
- 我
特殊之處:
- UITabbarController
中間有一個 “+” 按鈕,點擊該按鈕能夠 Modal 顯示微博類型選擇
界面,方便用戶選擇自己需要的微博類型
- 四個 UINavigationController
在用戶登錄前後顯示的界面格式是不一樣的
根原版新浪微博的區別
由於必須使用新浪微博官方的 API 才能夠正常開發,換言之,如果沒有登錄系統是無法使用新浪微博提供的接口的!
基於上述原因,在實際開發中對未登錄之前的界面設計進行簡化
開源中國社區
官方網站
- 開源中國社區成立於2008年8月,其目的是爲中國的IT技術人員提供一個全面的、快捷更新的用來檢索開源軟件以及交流使用開源經驗的平臺
- 目前國內有很多公司會將公司的項目部署在
OSChina
與 GitHUB
的對比
- 服務器在國內,速度更快
- 免費賬戶同樣可以建立
私有
項目,而GitHUB
上要建立私有項目必須付費
使用
註冊賬號
- 建議使用網易的郵箱,使用其他免費郵箱可能會收不到驗證郵件
添加 SSH 公鑰,進入終端,並輸入以下命令
- 開源中國幫助文檔地址:https://git.oschina.net/oschina/git-osc/wikis/幫助#ssh-keys
# 切換目錄,MAC中目錄的第一個字符如果是 `.` 表示改文件夾是隱藏文件夾
$ cd ~/.ssh
# 查看當前目錄文件
$ ls
# 生成 RSA 密鑰對
# 1> "" 中輸入個人郵箱
# 2> 提示輸入私鑰文件名稱,直接回車
# 3> 提示輸入密碼,可以隨便輸入,只要本次能夠記住即可
$ ssh-keygen -t rsa -C "[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.plist
中CFBundleName
對應的內容 - 注意不要超過6箇中文,否則會影響用戶體驗
- 此處修改的內容是
啓動程序
- 在
AppDelegate
的didFinishLaunchingWithOptions
函數中添加以下代碼:
window = UIWindow(frame: UIScreen.mainScreen().bounds)
window?.backgroundColor = UIColor.whiteColor()
window?.rootViewController = ViewController()
window?.makeKeyAndVisible()
運行測試
添加啓動圖片
- 提示
- 關於啓動圖片的設置,需要注意上課的操作細節
- 關於各個設備的實際屏幕尺寸,注意一下不同類型的啓動圖片即可
項目搭建
課程目標
- 熟悉 swift 語法
- 搭建系統主體框架結構
- 對比與 OC 開發的異同
- 純代碼搭建框架
創建文件
準備工作
刪除模板文件
- ViewController.swift
- Main.storyboard
- LaunchScreen.xib
創建項目結構
主目錄 Classes
二級目錄
目錄名 | 說明 |
---|---|
Module | 功能模塊 |
Model | 業務邏輯模型 |
Tools | 工具類 |
Module
子目錄
目錄名 | 說明 |
---|---|
Main | 主要 |
Home | 首頁 |
Message | 消息 |
Discover | 發現 |
Profile | 我 |
創建項目文件
Main
目錄 | Controller |
---|---|
Main | MainViewController.swift(:UITabBarController ) |
功能模塊
目錄 | Controller |
---|---|
Home | HomeTableViewController.swift |
Message | MessageTableViewController.swift |
Discover | DiscoverTableViewController.swift |
Profile | ProfileTableViewController.swift |
細節
- 每個 ViewController 繼承自
UITableViewController
- 搭建完成的文件結構圖如下:
- 修改
AppDelegate
中的didFinishLaunchingWithOptions
函數,設置啓動控制器
window?.rootViewController = MainViewController()
添加子控制器
功能需求
- 由於採用了多視圖控制器的設計方式,因此需要通過代碼的方式向主控制器中添加子控制器
文件準備
- 將素材文件夾中的
TabBar
拖拽到Images.xcassets
目錄下
代碼實現
添加第一個視圖控制器
override func viewDidLoad() {
super.viewDidLoad()
addChildViewController()
}
private func addChildViewController() {
tabBar.tintColor = UIColor.orangeColor()
let vc = HomeTableViewController()
vc.title = "首頁"
vc.tabBarItem.image = UIImage(named: "tabbar_home")
let nav = UINavigationController(rootViewController: vc)
addChildViewController(nav)
}
重構代碼抽取參數
/// 添加控制器
///
/// - parameter vc : 視圖控制器
/// - parameter title : 標題
/// - parameter imageName: 圖像名稱
private func addChildViewController(vc: UIViewController, title: String, imageName: String) {
tabBar.tintColor = UIColor.orangeColor()
let vc = HomeTableViewController()
vc.title = title
vc.tabBarItem.image = UIImage(named: imageName)
let nav = UINavigationController(rootViewController: vc)
addChildViewController(nav)
}
- 擴充調用函數,添加其他控制器
/// 添加所有子控制器
private func addChildViewControllers() {
addChildViewController(HomeTableViewController(), title: "首頁", imageName: "tabbar_home")
addChildViewController(MessageTableViewController(), title: "消息", imageName: "tabbar_message_center")
addChildViewController(DiscoverTableViewController(), title: "發現", imageName: "tabbar_discover")
addChildViewController(ProfileTableViewController(), title: "我", imageName: "tabbar_profile")
}
自定義 TabBar
功能需求
- 在 4 個控制器切換按鈕中間增加一個撰寫按鈕
- 點擊撰寫按鈕能夠彈出對話框撰寫微博
需求分析
- 自定義 TabBar
- 計算控制器按鈕位置,在中間添加一個
撰寫
按鈕
思路
- 加號按鈕的大小與其他
tabBarItem
的大小是一致的 - 如果不考慮 modal 的方式,其所在位置應該同樣有一個
tabBarItem
- 建立一個空的視圖控制器形成佔位
- 然後在該位置添加一個按鈕遮擋
代碼實現
- 添加空的視圖控制器
/// 添加所有子控制器
private func addChildViewControllers() {
// ...
addChildViewController(UIViewController())
// ...
}
注意 UIViewController() 的位置
- 添加按鈕
// MARK: - 懶加載
/// 撰寫按鈕
private lazy var composedButton: UIButton = {
let btn = UIButton()
btn.setImage(UIImage(named: "tabbar_compose_icon_add"), forState: UIControlState.Normal)
btn.setImage(UIImage(named: "tabbar_compose_icon_add_highlighted"), forState: UIControlState.Highlighted)
btn.setBackgroundImage(UIImage(named: "tabbar_compose_button"), forState: UIControlState.Normal)
btn.setBackgroundImage(UIImage(named: "tabbar_compose_button_highlighted"), forState: UIControlState.Highlighted)
self.tabBar.addSubview(btn)
return btn
}()
- 設置按鈕位置
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
setupComposeButton()
}
/// 設置撰寫按鈕位置
private func setupComposeButton() {
let w = tabBar.bounds.width / CGFloat(childViewControllers.count)
let rect = CGRect(x: 0, y: 0, 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
協議,並且重寫description
的getter
方法,在 Swift 2.0 中,description
屬性定義在CustomStringConvertible
協議中
override var description: String {
let dict = ["access_token", "expires_in", "uid"]
return "\(dictionaryWithValuesForKeys(dict))"
}
目前的版本需要先遵守
CustomStringConvertible
協議,重寫了description
屬性後,再刪除,相信後續版本中會得到改進
設置過期日期
過期日期
在新浪微博返回的數據中,過期日期是以當前系統時間加上秒數計算的,爲了方便後續使用,增加過期日期屬性
定義屬性
/// token過期日期
var expiresDate: NSDate?
- 修改構造函數
expiresDate = NSDate(timeIntervalSinceNow: expires_in)
- 修改
description
let properties = ["access_token", "expires_in", "expiresDate", "uid"]
歸檔 & 解檔
課程目標
- 對比 OC 的
歸檔 & 解檔
實現 利用
歸檔 & 解檔
保存用戶信息遵守協議
class UserAccount: NSObject, NSCoding
- 實現協議方法
// MARK: - NSCoding
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(access_token, forKey: "access_token")
aCoder.encodeDouble(expires_in, forKey: "expires_in")
aCoder.encodeObject(expiresDate, forKey: "expiresDate")
aCoder.encodeObject(uid, forKey: "uid")
}
required init?(coder aDecoder: NSCoder) {
access_token = aDecoder.decodeObjectForKey("access_token") as? String
expires_in = aDecoder.decodeDoubleForKey("expires_in")
expiresDate = aDecoder.decodeObjectForKey("expiresDate") as? NSDate
uid = aDecoder.decodeObjectForKey("uid") as? String
}
- 定義歸檔路徑
/// 歸檔保存路徑
private static let accountPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true).last!.stringByAppendingPathComponent("account.plist")
- 保存賬戶信息
/// 保存賬號
func saveAccount() {
NSKeyedArchiver.archiveRootObject(self, toFile: UserAccount.accountPath)
}
- 加載賬戶信息
/// 加載賬號
class func loadAccount() -> UserAccount? {
let account = NSKeyedUnarchiver.unarchiveObjectWithFile(accountPath) as? UserAccount
return account
}
- 調整
OAuthViewController.swift
中的loadAccessToken
函數
// 保存用戶賬號信息
UserAccount(dict: result!).saveAccount()
- 修改加載賬號函數
/// 用戶賬號
private static var userAccount: UserAccount?
/// 加載賬號
class func loadAccount() -> UserAccount? {
if userAccount == nil {
// 解檔用戶賬戶信息
userAccount = NSKeyedUnarchiver.unarchiveObjectWithFile(accountPath) as? UserAccount
}
// 如果用戶賬戶存在,判斷是否過期
if let date = userAccount?.expiresDate where date.compare(NSDate()) == NSComparisonResult.OrderedAscending {
userAccount = nil
}
return userAccount
}
由於後續所有網絡訪問都基於用戶賬戶中的
access_token
,因此定義一個全局變量,可以避免重複加載,而且能夠在每次調用 AccessToken 時都判斷是否過期
- 修改 BaseTableViewController 中的用戶是否登錄判斷
/// 用戶登錄標記
var userLogon = UserAccount.loadAccount() != nil
加載用戶信息
課程目標
- 通過
AccessToken
獲取新浪微博網絡數據
接口定義
文檔地址
http://open.weibo.com/wiki/2/users/show
接口地址
https://api.weibo.com/2/users/show.json
HTTP 請求方式
- GET
請求參數
參數 | 描述 |
---|---|
access_token | 採用OAuth授權方式爲必填參數,其他授權方式不需要此參數,OAuth授權後獲得 |
uid | 需要查詢的用戶ID |
返回數據
返回值字段 | 字段說明 |
---|---|
name | 友好顯示名稱 |
avatar_large | 用戶頭像地址(大圖),180×180像素 |
測試 URL
https://api.weibo.com/2/users/show.json?access_token=2.00ml8IrF0qLZ9W5bc20850c50w9hi9&uid=5365823342
代碼實現
- 在
NetworkTools
中封裝 GET 方法
/// 錯誤域
private let errorDomainName = "com.itheima.network.errorDomain"
// MARK: - 封裝網絡請求方法
/// 完成回調類型
typealias HMFinishedCallBack = (result: [String: AnyObject]?, error: NSError?) -> ()
/// GET 請求
///
/// - parameter urlString: URL 地址
/// - parameter params : 參數字典
/// - parameter finished : 完成回調
private func requestGET(urlString: String, params: [String: AnyObject], finished: HMFinishedCallBack) {
GET(urlString, parameters: params, success: { _, JSON in
if let result = JSON as? [String: AnyObject] {
finished(result: result, error: nil)
} else {
finished(result: nil, error: NSError(domain: errorDomainName, code: -10000, userInfo: ["error": "空數據"]))
}
}) { _, error in
finished(result: nil, error: error)
}
}
- 定義通知常量
/// AccessToken 不存在通知
let HMAccessTokenEmptyNotification = "HMAccessTokenEmptyNotification"
- 生成 Token 參數字典
/// 生成 Token 參數字典
private func tokenDict() -> [String: AnyObject]? {
if let token = UserAccount.loadAccount()?.access_token {
return ["access_token": token]
}
NSNotificationCenter.defaultCenter().postNotificationName(HMAccessTokenEmptyNotification, object: nil)
return nil
}
- 在
NetworkTools
中增加加載用戶信息函數
// MARK: - 加載用戶信息
func loadUserInfo(uid: Int, finished: (result: [String: AnyObject]?, error: NSError?) -> ()) {
let urlString = "2/users/show.json"
guard var params = tokenDict() else {
return
}
params["uid"] = uid
requestGET(urlString, params: params) { (result, error) -> () in
finished(result: result, error: error)
}
}
- 在
UserAccount
中增加加載用戶信息函數
func loadUserInfo() {
NetworkTools.sharedTools.loadUserInfo(uid!) { (result, error) -> () in
print(result)
}
}
- 測試加載用戶信息
UserAccount(dict: result!).loadUserInfo()
- 增加屬性定義
/// 友好顯示名稱
var name: String?
/// 用戶頭像地址(大圖),180×180像素
var avatar_large: String?
- 調整加載用戶信息函數
// MARK: - 加載用戶信息
func loadUserInfo(finished: (error: NSError?) -> ()) {
NetworkTools.sharedTools.loadUserInfo(uid!) { (result, error) -> () in
if let dict = result {
self.name = dict["name"] as? String
self.avatar_large = dict["avatar_large"] as? String
self.saveAccount()
}
finished(error: error)
}
}
- 修改
description
屬性
let properties = ["access_token", "expires_in", "uid", "expiresDate", "name", "avatar_large"]
- 修改歸檔&解檔函數,增加用戶名和圖像地址屬性
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(access_token, forKey: "access_token")
aCoder.encodeDouble(expires_in, forKey: "expires_in")
aCoder.encodeObject(expiresDate, forKey: "expiresDate")
aCoder.encodeObject(uid, forKey: "uid")
aCoder.encodeObject(name, forKey: "name")
aCoder.encodeObject(avatar_large, forKey: "avatar_large")
}
required init?(coder aDecoder: NSCoder) {
access_token = aDecoder.decodeObjectForKey("access_token") as? String
expires_in = aDecoder.decodeDoubleForKey("expires_in")
expiresDate = aDecoder.decodeObjectForKey("expiresDate") as? NSDate
uid = aDecoder.decodeObjectForKey("uid") as? String
name = aDecoder.decodeObjectForKey("name") as? String
avatar_large = aDecoder.decodeObjectForKey("avatar_large") as? String
}
- 修改
loadAccessToken
方法
/// 使用授權碼換取 AccessToken
private func loadAccessToken(code: String) {
NetworkTools.sharedTools.loadAccessToken(code) { (result, error) -> () in
if error != nil || result == nil {
self.loadError()
return
}
// 加載用戶賬號信息
UserAccount(dict: result!).loadUserInfo() { (error) -> () in
if error != nil {
self.loadError()
return
}
print(UserAccount.loadAccount())
}
}
}
/// 數據加載錯誤
private func loadError() {
SVProgressHUD.showInfoWithStatus("您的網絡不給力")
// 延時一段時間再關閉
let when = dispatch_time(DISPATCH_TIME_NOW, Int64(1 * NSEC_PER_SEC))
dispatch_after(when, dispatch_get_main_queue()) {
self.close()
}
}
每一個令牌授權一個
特定的網站
在特定的時段內
訪問特定的資源
調整網絡代碼
- 封裝 POST 請求方法
/// POST 請求
///
/// - parameter urlString: URL 地址
/// - parameter params : 參數字典
/// - parameter finished : 完成回調
private func requestPOST(urlString: String, params: [String: AnyObject], finished: HMFinishedCallBack) {
POST(urlString, parameters: params, success: { _, JSON in
if let result = JSON as? [String: AnyObject] {
finished(result: result, error: nil)
} else {
finished(result: nil, error: NSError(domain: errorDomainName, code: -10000, userInfo: ["error": "空數據"]))
}
}) { _, error in
print(error)
finished(result: nil, error: error)
}
}
- 修改加載 token 函數
/// 加載 Token
func loadAccessToken(code: String, finished: HMFinishedCallBack) {
let urlString = "https://api.weibo.com/oauth2/access_token"
let params = ["client_id": clientId,
"client_secret": appSecret,
"grant_type": "authorization_code",
"code": code,
"redirect_uri": redirectUri]
requestPOST(urlString, params: params) { (result, error) -> () in
finished(result: result, error: error)
}
}
新特性
- 新特性是現在很多應用程序中包含的功能,主要用於在系統升級後,用戶第一次進入系統時獲知新升級的功能
課程目標
- UICollectionView 使用
根視圖控制器
切換
新特性功能
準備文件
- 將新特性圖片素材拖拽到 Images.xcsets 中
- 在
Module
下建立NewFeature
目錄 - 新建
NewFeatureViewController.swift
繼承自UICollectionViewController
- 在
NewFeatureViewController.swift
的末尾添加如下代碼:
代碼實現
- 修改
AppDelegate
的根視圖控制器
window?.rootViewController = NewFeatureViewController()
運行測試,崩潰!
原因:實例化
CollectionViewController
時必須指定佈局參數實現
init()
簡化外部調用
/// 界面佈局
private let layout = UICollectionViewFlowLayout()
init() {
super.init(collectionViewLayout: layout)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
- 定義 NewFeatureCell
/// 新特性 Cell
class NewFeatureCell: UICollectionViewCell {
var imageIndex: Int = 0 {
didSet {
iconView.image = UIImage(named: "new_feature_\(imageIndex + 1)")
}
}
override init(frame: CGRect) {
super.init(frame: frame)
contentView.addSubview(iconView)
// 自動佈局
// 1> 圖片視圖
iconView.translatesAutoresizingMaskIntoConstraints = false
contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[subview]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": iconView]))
contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[subview]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": iconView]))
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// 懶加載控件
lazy var iconView: UIImageView = UIImageView()
}
- 註冊可重用 Cell
override func viewDidLoad() {
super.viewDidLoad()
// 註冊可重用 Cell
self.collectionView!.registerClass(NewFeatureCell.self, forCellWithReuseIdentifier: reuseIdentifier)
}
運行測試,需要設置佈局屬性
- 設置佈局屬性
/// 新特性佈局
private class NewFeatureLayout: UICollectionViewFlowLayout {
private override func prepareLayout() {
itemSize = collectionView!.bounds.size
minimumInteritemSpacing = 0
minimumLineSpacing = 0
scrollDirection = UICollectionViewScrollDirection.Horizontal
collectionView?.pagingEnabled = true
collectionView?.showsHorizontalScrollIndicator = false
collectionView?.bounces = false
}
}
在
prepareLayout
函數中定義 collectionView 的佈局屬性是最佳位置
- 修改佈局屬性
/// 界面佈局
private let layout = NewFeatureLayout()
- 定義按鈕
/// 按鈕
lazy var startButton: UIButton = {
let button = UIButton()
button.setBackgroundImage(UIImage(named: "new_feature_finish_button"), forState: UIControlState.Normal)
button.setBackgroundImage(UIImage(named: "new_feature_finish_button_highlighted"), forState: UIControlState.Highlighted)
button.setTitle("開始體驗", forState: UIControlState.Normal)
return button
}()
- 設置按鈕佈局
// 2> 開始按鈕
startButton.translatesAutoresizingMaskIntoConstraints = false
contentView.addConstraint(NSLayoutConstraint(item: startButton, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: contentView, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0))
contentView.addConstraint(NSLayoutConstraint(item: startButton, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: contentView, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: -160))
動畫顯示 開始體驗
按鈕
- 在
NewFeatureCell
中添加showStartButton
函數
/// 動畫顯示按鈕
func showStartButton() {
startButton.hidden = false
startButton.transform = CGAffineTransformMakeScale(0, 0)
startButton.userInteractionEnabled = false
UIView.animateWithDuration(1.2, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 10.0, options: UIViewAnimationOptions(rawValue: 0), animations: {
self.startButton.transform = CGAffineTransformIdentity
}) { _ in
self.startButton.userInteractionEnabled = true
}
}
- 在
collectionView
的完成顯示Cell
代理方法中添加以下代碼:
// 參數 cell, indexPath 是前一個 cell 和 indexPath
override func collectionView(collectionView: UICollectionView, didEndDisplayingCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
let indexPath = collectionView.indexPathsForVisibleItems().last!
if indexPath.item == imageCount - 1 {
(collectionView.cellForItemAtIndexPath(indexPath) as! NewFeatureCell).showStartButton()
}
}
注意:參數中的
cell
&indexPath
是之前消失的cell
,而不是當前顯示的cell
的
隱藏狀態欄
override func prefersStatusBarHidden() -> Bool {
return true
}
歡迎界面
- 在新浪微博中,如果用戶登錄成功會顯示一個歡迎界面
- 特例:如果用戶的系統剛剛升級或者第一次登錄,會顯示
新特性
界面,而不是歡迎
界面
準備文件
- 在
NewFeature
目錄下新建WelcomeViewController.swift
繼承自UIViewController
- 新建
Welcome.storyboard
,初始視圖控制器的自定義類爲WelcomeViewController
代碼實現
- 修改
AppDelegate
的根視圖控制器
window?.rootViewController = WelcomeViewController()
- 懶加載控件
// MARK: - 懶加載控件
/// 背景圖片
private lazy var backImageView: UIImageView = UIImageView(image: UIImage(named: "ad_background"))
/// 頭像視圖
private lazy var iconView: UIImageView = {
let iv = UIImageView(image: UIImage(named: "avatar_default_big"))
iv.layer.masksToBounds = true
iv.layer.cornerRadius = 45
return iv
}()
/// 文本標籤
private lazy var messageLabel: UILabel = {
let label = UILabel()
label.text = "歡迎歸來"
return label
}()
- 搭建界面
/// 頭像底部約束
private var iconBottomCons: NSLayoutConstraint?
override func viewDidLoad() {
super.viewDidLoad()
prepareUI()
}
/// 準備 UI
private func prepareUI() {
view.addSubview(backImageView)
view.addSubview(iconView)
view.addSubview(messageLabel)
// 自動佈局
// 1> 背景圖片
backImageView.translatesAutoresizingMaskIntoConstraints = false
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-0-[subview]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": backImageView]))
view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-0-[subview]-0-|", options: NSLayoutFormatOptions(rawValue: 0), metrics: nil, views: ["subview": backImageView]))
// 2> 頭像
iconView.translatesAutoresizingMaskIntoConstraints = false
view.addConstraint(NSLayoutConstraint(item: iconView, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: view, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0))
view.addConstraint(NSLayoutConstraint(item: view, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: iconView, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: 160))
iconBottomCons = view.constraints.last
// 3> 標籤
messageLabel.translatesAutoresizingMaskIntoConstraints = false
view.addConstraint(NSLayoutConstraint(item: messageLabel, attribute: NSLayoutAttribute.CenterX, relatedBy: NSLayoutRelation.Equal, toItem: iconView, attribute: NSLayoutAttribute.CenterX, multiplier: 1.0, constant: 0))
view.addConstraint(NSLayoutConstraint(item: messageLabel, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: iconView, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: 20))
}
- 界面動畫
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
iconBottomCons?.constant = UIScreen.mainScreen().bounds.height - 240
UIView.animateWithDuration(1.2, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 10.0, options: UIViewAnimationOptions(rawValue: 0), animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
參數說明
usingSpringWithDamping
的範圍爲0.0f
到1.0f
,數值越小彈簧
的振動效果越明顯initialSpringVelocity
則表示初始的速度,數值越大一開始移動越快,初始速度取值較高而時間較短時,會出現反彈情況
設置用戶頭像
if let urlString = UserAccount.loadAccount()?.avatar_large {
iconView.sd_setImageWithURL(NSURL(string: urlString)!)
}
- 添加圖像寬高約束
view.addConstraint(NSLayoutConstraint(item: iconView, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1.0, constant: 90))
view.addConstraint(NSLayoutConstraint(item: view, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: iconView, attribute: NSLayoutAttribute.Bottom, multiplier: 1.0, constant: 160))
代碼評審(Code Review)
通常在企業開發中,會定期
面對面
(face to face)對代碼進行評審
Code Review的意識
- 作爲一個
Developer
,不僅要提交可工作的代碼
(Deliver working code),更要提交可維護的代碼
(Deliver maintainable code) - 必要時進行重構,隨着項目的迭代,在計劃新增功能的同時,開發要主動計劃重構的工作項
- 開放的心態,虛心接受大家的
評審建議
(Review Comments)
代碼評審的方式
- 開 Code Review 會議
- 團隊內部會整理 Check List
- 團隊內部成員交換代碼
- 找出可優化方案
- 多問問題,例如:“這塊兒是怎麼工作的?”、“如果有XXX 情況,你這個怎麼處理?”
- 區分重點,優先抓住
設計
,可讀性
,健壯性
等重點問題 - 整理好的編碼實踐,用來作爲
Code Review
的參考
評審內容
架構/設計
- 單一職責原則
- 這是經常被違背的原則。一個類只能幹一個事情,一個方法最好也只幹一件事情。比較常見的違背是
一個類既幹UI的事情,又幹邏輯的事情
,這個在低質量的客戶端代碼裏很常見
- 這是經常被違背的原則。一個類只能幹一個事情,一個方法最好也只幹一件事情。比較常見的違背是
- 行爲是否統一,例如:
- 緩存是否統一
- 錯誤處理是否統一
- 錯誤提示是否統一
- 彈出框是否統一
- ……
- 代碼污染
- 代碼有沒有對其他模塊強耦合
- 重複代碼
- 開閉原則
- 面向接口編程
- 健壯性
- 是否考慮線程安全
- 數據訪問是否一致性
- 邊界處理是否完整
- 邏輯是否健壯
- 是否有內存泄漏
- 有沒有循環依賴
- 有沒有野指針
- ……
- 錯誤處理
- 改動是不是對代碼的提升
- 新的改動是打補丁,讓代碼質量繼續惡化,還是對代碼質量做了修復
- 效率/性能
- 關鍵算法的時間複雜度多少?有沒有可能有潛在的性能瓶頸
- 客戶端程序對頻繁消息和較大數據等耗時操作是否處理得當
代碼風格
- 可讀性
- 衡量可讀性的可以有很好實踐的標準,就是 Reviewer 能否非常容易的理解這個代碼。如果不是,那意味着代碼的可讀性要進行改進
- 命名
- 命名對可讀性非常重要
- 英語用詞儘量準確一點,必要時可以查字典
- 函數長度/類長度
- 函數太長的不好閱讀
- 類太長了,檢查是否違反的
單一職責
原則
- 註釋
- 恰到好處的註釋
- 參數個數
- 不要太多,一般不要超過 3 個
Review Your Own Code First
- 每次提交前整體把自己的代碼過一遍非常有幫助,尤其是看看有沒有犯低級錯誤
OAuthViewController
- 刪除多餘的 print
- 刪除 // TODO: 換取 TOKEN
- 修改
loadAccessToken
函數中的註釋
提示:在實際開發中,代碼中的註釋一定要及時調整!
UserAccount
知識點:類屬性
vs 類函數
- 都是
通過類名調用
- 類屬性作爲屬性一定有返回值
- 類函數不一定有返回值
- 類本質上只是對
對象
的描述,從面相對象的角度而言,類不應該有存儲功能
- 類屬性是隻讀的,可以返回一個函數計算結果
- 也可以返回一個私有靜態成員記錄的內容
- 通過類屬性,能夠提高代碼的可讀性
演練 & 體會
- 將
loadAccount()
類函數修改爲sharedUserAccount
類屬性
class var sharedUserAccount: UserAccount? {
// 1. 判斷賬戶是否存在
if userAccount == nil {
// 解檔 - 如果沒有保存過,解檔結果可能仍然是 nil
userAccount = NSKeyedUnarchiver.unarchiveObjectWithFile(accountPath) as? UserAccount
}
// 2. 判斷日期
if let date = userAccount?.expiresDate where date.compare(NSDate()) == NSComparisonResult.OrderedAscending {
// 如果已經過期,需要清空賬號記錄
userAccount = nil
}
return userAccount
}
- 利用編譯器提示修改出錯的代碼
對比前後兩種方式的代碼可讀性的提高
- 說明:類屬性是 Swift 特有的語法,僅供體會
NetworkTools
- 移動
HMNetFinishedCallBack
聲明的位置
定義網絡訪問錯誤枚舉
- 定義網絡訪問錯誤枚舉
/// 網絡訪問錯誤
private enum HMNetworkError: Int {
case emptyDataError = -1
case emptyTokenError = -2
private var description: String {
switch self {
case .emptyDataError:
return "空數據"
case .emptyTokenError:
return "AccessToken 錯誤"
}
}
private var error: NSError {
return NSError(domain: HMErrorDomainName, code: rawValue, userInfo: [HMErrorDomainName: description])
}
}
可以在 Playground 中測試枚舉類型
- 修改
requestGET
中的空數據錯誤
finished(result: nil, error: HMNetworkError.emptyDataError.error)
- 修改
loadUserInfo
中 token 爲空的檢測代碼,增加錯誤回調
// 判斷 token 是否存在
if UserAccount.sharedUserAccount?.access_token == nil {
let error = HMNetworkError.emptyTokenError.error
print(error)
finished(result: nil, error: error)
return
}
- 註釋
UserAccount
中爲全局賬號賦值的代碼,並且調試運行效果
封裝 AFN 的 POST 方法
- 複製 GET 代碼,並且修改部分單詞
/// POST 請求
///
/// :param: urlString URL 地址
/// :param: params 參數字典
/// :param: finished 完成回調
private func requestPOST(urlString: String, params: [String: AnyObject], finished: HMNetFinishedCallBack) {
POST(urlString, parameters: params, success: { (_, JSON) -> Void in
if let result = JSON as? [String: AnyObject] {
// 有結果的回調
finished(result: result, error: nil)
} else {
// 沒有錯誤,同時沒有結果
print("沒有數據 GET Request \(urlString)")
finished(result: nil, error: HMNetworkError.emptyDataError.error)
}
}) { (_, error) -> Void in
print(error)
finished(result: nil, error: error)
}
}
- 修改 函數並運行測試
/// 加載 Token
func loadAccessToken(code: String, finished: HMNetFinishedCallBack) {
let urlString = "https://api.weibo.com/oauth2/access_token"
let params = ["client_id": clientId,
"client_secret": appSecret,
"grant_type": "authorization_code",
"code": code,
"redirect_uri": redirectUri]
requestPOST(urlString, params: params, finished: finished)
}
整合網絡訪問方法
- 定義網絡方法枚舉
/// 網絡訪問方法
private enum HMNetworkMethod: String {
case GET = "GET"
case POST = "POST"
}
- 封裝網絡訪問方法
/// 網絡請求
///
/// - parameter method : 訪問方法
/// - parameter urlString: URL 地址
/// - parameter params : 參數自帶呢
/// - parameter finished : 完成回調
private func request(method: HMNetworkMethod, urlString: String, params: [String: AnyObject], finished: HMNetFinishedCallBack) {
let successCallBack: (NSURLSessionTask!, AnyObject!) -> Void = { _, JSON in
if let result = JSON as? [String: AnyObject] {
// 有結果的回調
finished(result: result, error: nil)
} else {
// 沒有錯誤,同時沒有結果
print("沒有數據 \(method) Request \(urlString)")
finished(result: nil, error: HMNetworkError.emptyDataError.error)
}
}
let failedCallBack: (NSURLSessionTask!, NSError!) -> Void = { _, error in
print(error)
finished(result: nil, error: error)
}
switch method {
case .GET:
GET(urlString, parameters: params, success: successCallBack, failure: failedCallBack)
case .POST:
POST(urlString, parameters: params, success: successCallBack, failure: failedCallBack)
}
}
運行測試
自動佈局框架
- 爲簡化純代碼佈局,抽取了常用的自動佈局代碼
將 UIView+AutoLayout 拖拽到項目中的
Tools
目錄下調整
NewFeatureCell
iconView.ff_Fill(contentView)
startButton.ff_AlignInner(type: ff_AlignType.BottomCenter, referView: contentView, size: nil, offset: CGPoint(x: 0, y: -160))
- 調整
WelcomeViewController
// 1> 背景圖片
backImageView.ff_Fill(view)
// 2> 頭像
let cons = iconView.ff_AlignInner(type: ff_AlignType.BottomCenter, referView: view, size: CGSize(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