微博開放平臺地址
http://open.weibo.comjavascript
微博接口文檔地址
http://open.weibo.com/wiki/微博APIphp
走向工做崗位以後,通常會遇到兩種工做狀況:css
新項目開發java
舊項目維護ios
綜上所述,不管是新項目,仍是老項目,在開發以前肯定項目的主體架構都是很是重要,也是十分必要的!git
開發以前,明確項目的主體架構具備如下好處:程序員
做爲中國移動互聯網的表明性產品之一,新浪微博涵蓋了大量的移動互聯網元素,經過對新浪微博的研究及模仿,能夠:github
正如前文所述,在開始模仿以前,首先運行產品,掌握項目的總體架構,肯定開發的主體功能很是重要!web
對界面預覽以後,能夠發現新浪微博符合經典應用程序架構設計:算法
UITabbarController
UINavigationController
,分別是 特殊之處:
- UITabbarController
中間有一個 「+」 按鈕,點擊該按鈕可以 Modal 顯示微博類型選擇
界面,方便用戶選擇本身須要的微博類型
- 四個 UINavigationController
在用戶登陸先後顯示的界面格式是不同的
因爲必須使用新浪微博官方的 API 纔可以正常開發,換言之,若是沒有登陸系統是沒法使用新浪微博提供的接口的!
基於上述緣由,在實際開發中對未登陸以前的界面設計進行簡化
OSChina
GitHUB
的對比私有
項目,而 GitHUB
上要創建私有項目必須 付費
註冊帳號
添加 SSH 公鑰,進入終端,並輸入如下命令
# 切換目錄,MAC中目錄的第一個字符若是是 `.` 表示改文件夾是隱藏文件夾
$ cd ~/.ssh
# 查看當前目錄文件
$ ls
# 生成 RSA 密鑰對
# 1> "" 中輸入我的郵箱
# 2> 提示輸入私鑰文件名稱,直接回車
# 3> 提示輸入密碼,能夠隨便輸入,只要本次可以記住便可
$ ssh-keygen -t rsa -C "xxx@126.com"
# 查看公鑰內容
$ cat id_rsa.pub
將公鑰內容複製並粘貼至 https://git.oschina.net/profile/sshkeys
測試公鑰
# 測試 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+
的分辨率設計提示:如今大多數應用程序還適配 iOS 6,下載的 ipa 包可以拿到圖片素材,可是若是從此應用程序只支持 iOS 7+,解壓縮包以後,擇沒法再得到對應的圖片素材。
請妥善保管好一些優秀做品的 IPA 文件
Launch Screen File
& Main.storyboard
,而且設置啓動圖片
和應用方向
提示:iPhone 項目通常不須要支持橫屏,遊戲除外
Info.plist
中 CFBundleName
對應的內容AppDelegate
的 didFinishLaunchingWithOptions
函數中添加如下代碼:window = UIWindow(frame: UIScreen.mainScreen().bounds)
window?.backgroundColor = UIColor.whiteColor()
window?.rootViewController = ViewController()
window?.makeKeyAndVisible()
運行測試
Classes
目錄名 | 說明 |
---|---|
Module | 功能模塊 |
Model | 業務邏輯模型 |
Tools | 工具類 |
Module
子目錄目錄名 | 說明 |
---|---|
Main | 主要 |
Home | 首頁 |
Message | 消息 |
Discover | 發現 |
Profile | 我 |
目錄 | Controller |
---|---|
Main | MainViewController.swift(:UITabBarController ) |
目錄 | Controller |
---|---|
Home | HomeTableViewController.swift |
Message | MessageTableViewController.swift |
Discover | DiscoverTableViewController.swift |
Profile | ProfileTableViewController.swift |
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")
}
撰寫
按鈕tabBarItem
的大小是一致的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
let w = tabBar.bounds.width / CGFloat(childViewControllers.count)
Swift 中的懶加載本質上是一個閉包,所以引用當前控制器的對象時須要使用 self.
不但願暴露的方法,應該使用 private
修飾符
運行循環
監聽而且以消息機制
傳遞的,所以,按鈕監聽函數不能設置爲 private
AFNetworking
SDWebImage
SVProgressHUD
$ 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
NetworkTools
單例import AFNetworking
/// 網絡工具類
class NetworkTools: AFHTTPSessionManager {
// 全局訪問點
static let sharedNetworkTools: NetworkTools = {
let instance = NetworkTools(baseURL: NSURL(string: "https://api.weibo.com/")!)
return instance
}()
}
SVProgressHUD
是使用 OC 開發的指示器https://github.com/TransitApp/SVProgressHUD
MBProgressHUD
對比SVProgressHUD
ARC
MBProgressHUD
ARC
& MRC
import SVProgressHUD
SVProgressHUD.showInfoWithStatus("正在玩命加載中...", maskType: SVProgressHUDMaskType.Gradient)
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)
}
+ (instancetype)sharedManager {
static id instance;
static dispatch_once_t onceToken;
NSLog(@"%ld", onceToken);
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
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()
特定的網站
在 特定的時段內
訪問 特定的資源
微鏈接
- 移動應用
應用信息
- 高級信息
,設置回調地址,以下圖所示: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
https://api.weibo.com/oauth2/authorize?client_id=479651210&redirect_uri=http://itheima.com
注意:回調地址必須與註冊應用程序保持一致
OAuth
文件夾OAuthViewController.swift
繼承自 UIViewController
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))
}
// MARK: - UIWebView 代理方法
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
print(request)
return true
}
結果分析
修改代碼
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
func webViewDidStartLoad(webView: UIWebView) { SVProgressHUD.show() }
func webViewDidFinishLoad(webView: UIWebView) { SVProgressHUD.dismiss() }
/// 關閉
func close() {
SVProgressHUD.dismiss()
dismissViewControllerAnimated(true, completion: nil)
}
http://open.weibo.com/wiki/OAuth2/access_token
https://api.weibo.com/oauth2/access_token
參數 | 描述 |
---|---|
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 |
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)
}
}
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"]
歸檔 & 解檔
實現利用歸檔 & 解檔
保存用戶信息
遵照協議
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 時都判斷是否過時
/// 用戶登陸標記
var userLogon = UserAccount.loadAccount() != nil
AccessToken
獲取新浪微博網絡數據http://open.weibo.com/wiki/2/users/show
https://api.weibo.com/2/users/show.json
參數 | 描述 |
---|---|
access_token | 採用OAuth受權方式爲必填參數,其餘受權方式不須要此參數,OAuth受權後得到 |
uid | 須要查詢的用戶ID |
返回值字段 | 字段說明 |
---|---|
name | 友好顯示名稱 |
avatar_large | 用戶頭像地址(大圖),180×180像素 |
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 參數字典
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 請求
///
/// - 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
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)
}
}
根視圖控制器
切換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") }
/// 新特性 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()
}
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))
一般在企業開發中,會按期
面對面
(face to face)對代碼進行評審
Developer
,不只要提交可工做的代碼
(Deliver working code),更要提交可維護的代碼
(Deliver maintainable code)評審建議
(Review Comments)設計
,可讀性
,健壯性
等重點問題Code Review
的參考一個類既幹UI的事情,又幹邏輯的事情
,這個在低質量的客戶端代碼裏很常見單一職責
原則loadAccessToken
函數中的註釋提示:在實際開發中,代碼中的註釋必定要及時調整!
知識點:類屬性
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
}
對比先後兩種方式的代碼可讀性的提升
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
中爲全局帳號賦值的代碼,而且調試運行效果/// 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