Swift-MVVM 簡單演練(三)

Swift-MVVM 簡單演練(一)git

Swift-MVVM 簡單演練(二)github

Swift-MVVM 簡單演練(四)json

優化一些小細節

設置SVProgressHUD最小提示時間

在咱們用SVProgressHUD的時候,它默認的顯示時長可能會不符合你的使用規則。咱們能夠更改它顯示的最小時間(setMinimumDismissTimeInterval)swift

像這種全局都能用到的東西,咱們最好是設置在一個方便管理的地方,這裏以在AppDelegate中設置api

extension AppDelegate {

    fileprivate func setupAddtions() {

        // 設置`SVProgressHUD`最小解除時間
        SVProgressHUD.setMinimumDismissTimeInterval(1)
    }
}複製代碼

設置AFN指示器

不少好的應用程序是很是人性化的,若是有網絡請求的時候,會在狀態欄的位置有一個Loading的很小的標誌,這是蘋果自帶的標誌,其實咱們應該把它在應該顯示的時候顯示出來的。幸運的是,咱們遇上了一個好的時代。AFN這個框架已經幫咱們實現了。網絡

extension AppDelegate {

    fileprivate func setupAddtions() {

        // 設置網絡加載指示器
        AFNetworkActivityIndicatorManager.shared().isEnabled = true
    }
}複製代碼

這裏須要強調一下,如今不管是移動網絡仍是無線網絡,網速愈來愈快了(咱們遇上了一個好的時代)。若是網速很快的時候,即便是設置了這個,通常也是看不到的。可是網速很差的時候,它就起做用了。app

將詢問發送通知受權的代碼也抽取出來

swiftextension是能夠無限多個寫的,咱們若是能將更多的零碎的方法抽取出來,放到extension中去。代碼會清晰不少,也會方便管理不少。框架

extension AppDelegate {

    fileprivate func setupNotification() {

        if #available(iOS 10.0, *) {
            UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound, .carPlay]) { (sucess, error) in
// print("受權" + (sucess ? "成功" : "失敗"))
            }
        } else {
            // Fallback on earlier versions
            let notificationSettings = UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
            UIApplication.shared.registerUserNotificationSettings(notificationSettings)
        }
    }
}複製代碼

值得注意的是,以前下面這段代碼原本是這樣的ssh

} else {
    // Fallback on earlier versions
    let notificationSettings = UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
    application.registerUserNotificationSettings(notificationSettings)
}複製代碼

若是放到extensionapplication是須要當作參數傳遞過去的,而咱們本着省事的原則,直接使用UIApplication.shared就能夠了,UIApplication是單例,只要用的時候直接取出它就能夠了。async


處理登陸相關通知

Tokennil時測試

全部的網絡請求都是基於token的,若是沒有token的話(雖然實際程序中幾乎不可能出現token = nil的狀況),咱們應該使程序在當token = nil而且用戶又一次進行了網絡請求的時候將提示用戶,而且將登陸控制器展示出來。

HQNetWorkManager中,發送登陸通知

/// 帶`token`的網絡請求方法
func tokenRequest(method: HQHTTPMethod = .GET, URLString: String, parameters: [String: AnyObject]?, completion: @escaping (_ json: Any?, _ isSuccess: Bool)->()) {

    // 判斷`token`是否爲`nil`,爲`nil`直接返回,程序執行過程當中,通常`token`不會爲`nil`
    guard let token = userAccount.token else {

        // 發送通知,提示用戶登陸
        print("沒有 token 須要從新登陸")
        NotificationCenter.default.post(
            name: NSNotification.Name(rawValue: HQUserShouldLoginNotification),
            object: nil)
        completion(nil, false)
        return
    }複製代碼

寫的任何代碼都要測試,隨便找一個控制器的viewDidLoad方法裏面。將token置爲nil

class HQDViewController: HQBaseViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        HQNetWorkManager.shared.userAccount.token = nil
    }複製代碼

接下來再回到首頁,下拉刷新。因爲又進行了網絡請求,並且咱們判斷了當tokennil時的判斷,所以會發送一個登陸的通知。在HQMainViewController中,以前咱們添加了監聽的方法

class HQMainViewController: UITabBarController {

    override func viewDidLoad() {
        super.viewDidLoad()

        NotificationCenter.default.addObserver(self, selector: #selector(login), name: NSNotification.Name(rawValue: HQUserShouldLoginNotification), object: nil)複製代碼

所以,監聽到通知,就會走login的方法,彈出登陸界面了。

// MARK: - Targrt Action
extension HQMainViewController {

    // MARK: - 登陸監聽方法
    @objc fileprivate func login(n: Notification) {

        print("用戶登陸通知 \(n)")

        SVProgressHUD.setDefaultMaskType(.clear)
        let nav = UINavigationController(rootViewController: HQLoginController())
        self.present(nav, animated: true, completion: nil)
    }複製代碼

Token的過時處理

HQNetWorkManager內目前就兩個方法,並且仍是有關聯的,因此處理完第一個方法的時候,咱們理應看下第二個方法。若是token不爲nil,咱們該在什麼地方作何處理呢?

這裏根據請求失敗的返回碼處理一下,當statusCode == 403時,咱們再次發送用戶登陸的通知

/// 封裝 AFN 的 GET/POST 請求
///
/// - Parameters:
/// - method: GET/POST
/// - URLString: URLString
/// - parameters: parameters
/// - completion: 完成回調(json, isSuccess)
func request(method: HQHTTPMethod = .GET, URLString: String, parameters: [String: AnyObject]?, completion: @escaping (_ json: Any?, _ isSuccess: Bool)->()) {

    let success = { (task: URLSessionDataTask, json: Any?)->() in
        completion(json, true)
    }

    let failure = { (task: URLSessionDataTask?, error: Error)->() in

        if (task?.response as? HTTPURLResponse)?.statusCode == 403 {
            print("token 過時了")

            // 發送通知,提示用戶再次登陸
            NotificationCenter.default.post(
                name: NSNotification.Name(rawValue: HQUserShouldLoginNotification),
                object: "bad token")
        }

        print("網絡請求錯誤 \(error)")
        completion(nil, false)
    }複製代碼

任何狀況都要進行測試,再次回到以前的測試控制器裏面,給token賦值一個非空的值測試

class HQDViewController: HQBaseViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        HQNetWorkManager.shared.userAccount.token = "bad token"
    }複製代碼

若是咱們再次回到首頁控制器,進行網絡請求,就會再次彈出登陸界面。

處理彈出登陸界面的一些UI細節

若是咱們不作一些提示,或者動畫過分一下的話,直接就硬生生彈出登陸控制器,邏輯上沒有問題,可是交互老是感受不那麼好。所以咱們最好作一點小提示。

可是在哪裏作提示比較好呢。建議仍是放在接收到登陸通知的監聽方法裏面處理比較好。

首先,咱們發送登陸通知的時候,附帶一個自定義的object(這裏是字符串"bad token")過去。

// 發送通知,提示用戶再次登陸
NotificationCenter.default.post(
    name: NSNotification.Name(rawValue: HQUserShouldLoginNotification),
    object: "bad token")複製代碼

而後在處理監聽登陸通知的方法裏處理交互顯示的問題,僅僅是增長一點點提示的UI而已,有了下面的代碼,交互就會感受好了不少了。這裏主要學習的是若是忽然增長需求,咱們如何在合適的位置處理問題。

// MARK: - Targrt Action
extension HQMainViewController {

    // MARK: - 登陸監聽方法
    @objc fileprivate func login(n: Notification) {

        print("用戶登陸通知 \(n)")

        if n.object != nil {
            SVProgressHUD.setDefaultMaskType(.gradient)
            SVProgressHUD.showInfo(withStatus: "登陸超時,請從新登陸")
        }

        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {

            SVProgressHUD.setDefaultMaskType(.clear)
            let nav = UINavigationController(rootViewController: HQLoginController())
            self.present(nav, animated: true, completion: nil)
        }
    }複製代碼

看看本身爲了完成某一需求而改的代碼,有沒有影響到其它地方

時刻提醒本身,當咱們興高采烈的爲完成了某一處的改動而沾沾自喜的時候。要在對其它有可能會被影響的地方測試一下。否則,往後遺留的問題可能會讓你百思不得其解。

這不就,咱們剛爲了處理token過時而設置的延遲兩秒鐘再彈出登陸界面,果真就影響到了其它的登陸地方。

好比,一開始沒有登陸的時候,運行程序,會出現登陸註冊的按鈕。當咱們點擊登陸的按鈕的時候,咱們指望馬上彈出登陸控制器。

可是咱們剛纔寫的代碼,真的有影響到這裏了。點擊登陸也是延遲2秒鐘才彈出登陸界面,給人的感受老是怪怪的。

下面咱們想辦法測試一下

將存儲用戶帳戶相關的文件刪除

而後運行程序,就直接到登陸界面,而後點擊登陸按鈕發現老是須要等待2秒鐘,咱們找到以前延遲兩秒鐘的地方處理一下。

增長一個時間變量,若是token過時了,就將時間增減2秒,不然不增長。

// MARK: - Targrt Action
extension HQMainViewController {

    // MARK: - 登陸監聽方法
    @objc fileprivate func login(n: Notification) {

        print("用戶登陸通知 \(n)")

        var when = DispatchTime.now()

        if n.object != nil {
            SVProgressHUD.setDefaultMaskType(.gradient)
            SVProgressHUD.showInfo(withStatus: "登陸超時,請從新登陸")

            // 修改延遲時間
            when = DispatchTime.now() + 2
        }

        DispatchQueue.main.asyncAfter(deadline: when) {

            SVProgressHUD.setDefaultMaskType(.clear)
            let nav = UINavigationController(rootViewController: HQLoginController())
            self.present(nav, animated: true, completion: nil)
        }
    }複製代碼

這樣就能夠解決普通登陸狀態下的展示登陸界面的延遲問題了。


加載用戶我的信息

獲取用戶我的信息數據

接口地址

/// 我的信息
let HQUserInfoUrlString = "https://api.weibo.com/2/users/show.json"複製代碼

HQNetWorkManager+Extension中增長用戶我的信息獲取的網絡請求方法

// MARK: - 用戶信息
extension HQNetWorkManager {

    /// 加載用戶信息
    func loadUserInfo(completion: @escaping (_ dict: [String: AnyObject]) -> ()) {

        guard let uid = userAccount.uid else {
            return
        }
        let params = ["uid": uid]

        tokenRequest(URLString: HQUserInfoUrlString, parameters: params as [String : AnyObject]) { (json, isSuccess) in

            // 完成回調
            completion(json as? [String : AnyObject] ?? [:])
        }
    }
}複製代碼

那麼問題來了,此方法在哪裏調用比較合適呢?

由於,咱們須要拿到這個在首頁就展現暱稱或者頭像。因此在登陸成功可是沒有執行完成回調的時候去執行該方法獲取用戶我的信息是比較理想的位置。

下面我這裏並無作網絡請求交互獲取token,只是模擬了一下而已。

// MARK: - 請求`Token`
extension HQNetWorkManager {

    /// 根據`賬號`和`密碼`獲取`Token`
    ///
    /// - Parameters:
    /// - account: account
    /// - password: password
    /// - completion: 完成回調
    func loadAccessToken(account: String, password: String, completion: @escaping (_ isSuccess: Bool)->()) {

        // 從`bundle`加載`data`
        let path = Bundle.main.path(forResource: "userAccount.json", ofType: nil)
        let data = NSData(contentsOfFile: path!)

        // 從`Bundle`加載配置的`userAccount.json`
        guard let dict = try? JSONSerialization.jsonObject(with: data! as Data, options: []) as? [String: AnyObject]
            else {
                return
        }

        // 直接用字典設置`userAccount`的屬性
        self.userAccount.yy_modelSet(with: dict ?? [:])

        self.userAccount.saveAccount()

        // 加載用戶信息
        self.loadUserInfo { (dict) in
            print(dict)
            // 用戶信息加載完成再執行,首頁數據加載的完成回調
            completion(true)
        }

    }
}複製代碼

保存所須要的我的信息(暱稱、頭像地址)

獲取到我的信息以後,這種我的信息可能會在不少地方須要用到,咱們最好將其像保存token那樣將其保存起來。

所以,擴展一下我的信息模型,增長兩個屬性

/// 用戶暱稱
var screen_name: String?
/// 用戶頭像地址(大圖),180x180
var avatar_large: String?複製代碼

HQNetWorkManager+Extension中的請求token的方法裏保存,以前只是保存了tokenuidexpires_in(過時時間),如今須要將新獲取到的screen_nameavatar_large(頭像地址)也保存到此

func loadAccessToken(account: String, password: String, completion: @escaping (_ isSuccess: Bool)->()) {

    // 從`bundle`加載`data`
    let path = Bundle.main.path(forResource: "userAccount.json", ofType: nil)
    let data = NSData(contentsOfFile: path!)

    // 從`Bundle`加載配置的`userAccount.json`
    guard let dict = try? JSONSerialization.jsonObject(with: data! as Data, options: []) as? [String: AnyObject]
        else {
            return
    }

    // 直接用字典設置`userAccount`的屬性
    self.userAccount.yy_modelSet(with: dict ?? [:])

    // 加載用戶信息
    self.loadUserInfo { (dict) in

        self.userAccount.yy_modelSet(with: dict)
        self.userAccount.saveAccount()

        // 用戶信息加載完成再執行,首頁數據加載的完成回調
        completion(true)
    }複製代碼

和以前的對比一下,應該會看的更清楚


更改導航欄標題顯示樣式

以前微博的版本和如今多少有點區別,在首頁的導航欄的標題位置僅僅是顯示本身的暱稱,而且可下拉展開。這裏不去作那麼複雜,只是表達一下,更改導航欄標題顯示樣式和Button的文字圖片左右對調,以前我也寫過Objective-C的相關方法iOS-自定義 UIButton-文字在左、圖片在右(一)iOS-自定義 UIButton-文字在左、圖片在右(二)

將導航欄標題設置成自定義Button

這個沒什麼技術含量,直接上代碼了。

/// 設置導航欄標題演示
    fileprivate func setupNavTitle() {

        let btn = UIButton(hq_title: "王紅慶", fontSize: 17, normalColor: UIColor.darkGray, highlightedColor: UIColor.red)
        btn.setImage(UIImage(named: "nav_arrow_down"), for: .normal)
        btn.setImage(UIImage(named: "nav_arrow_up"), for: .selected)
        navItem.titleView = btn

        btn.addTarget(self, action: #selector(clickTitleButton), for: .touchUpInside)
    }

    @objc fileprivate func clickTitleButton(btn: UIButton) {

        btn.isSelected = !btn.isSelected
    }複製代碼

抽取建立相似標題按鈕的邏輯

相似這種需求可能一個項目中不止一個地方會用到,即使是目前就這一個地方會用到,咱們也應該儘可能將其抽取出來。由於要設置圖像和文字,而且顛倒其位置的這些代碼,應該封裝起來的。只留給使用者(包括咱們本身)一個快速建立此按鈕的方法就能夠了。

我選擇在ButtonExtension中搞定這個。

/// 文字在左、圖片在右的 Button
class HQTitleButton: UIButton {

    /// 重載構造函數
    ///
    /// - Parameter title: title 若是是 nil,就顯示首頁
    /// - Parameter title: title 若是不是 nil,顯示 title 和 箭頭
    init(title: String?) {
        super.init(frame: CGRect())

        if title == nil {
            setTitle("首頁", for: .normal)
        } else {
            setTitle(title!, for: .normal)
            setImage(UIImage(named: "nav_arrow_down"), for: .normal)
            setImage(UIImage(named: "nav_arrow_up"), for: .selected)
        }

        titleLabel?.font = UIFont.boldSystemFont(ofSize: 17)
        setTitleColor(UIColor.darkGray, for: .normal)

        // 設置大小
        sizeToFit()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}複製代碼

這樣咱們設置的時候就能夠簡化不少,目前尚未實現將文字和圖片顛倒

/// 設置導航欄標題演示
fileprivate func setupNavTitle() {

    let title = HQNetWorkManager.shared.userAccount.screen_name

    let btn = HQTitleButton(title: title)

    navItem.titleView = btn

    btn.addTarget(self, action: #selector(clickTitleButton), for: .touchUpInside)
}

@objc fileprivate func clickTitleButton(btn: UIButton) {

    btn.isSelected = !btn.isSelected
}複製代碼

利用layoutSubViews方法從新調整按鈕文字和圖像的位置

在調用override func layoutSubviews()方法的時候,必定要調用super.layoutSubviews(),若是不調用,就會出現顯示不出來的狀況。

/// 文字在左、圖片在右的 Button
class HQTitleButton: UIButton {

    /// 重載構造函數
    ///
    /// - Parameter title: title 若是是 nil,就顯示首頁
    /// - Parameter title: title 若是不是 nil,顯示 title 和 箭頭
    init(title: String?) {
        super.init(frame: CGRect())

        if title == nil {
            setTitle("首頁", for: .normal)
        } else {
            setTitle(title! + " ", for: .normal)
            setImage(UIImage(named: "nav_arrow_down"), for: .normal)
            setImage(UIImage(named: "nav_arrow_up"), for: .selected)
        }

        titleLabel?.font = UIFont.boldSystemFont(ofSize: 17)
        setTitleColor(UIColor.darkGray, for: .normal)

        // 設置大小
        sizeToFit()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    /// 從新佈局子視圖
    override func layoutSubviews() {
        super.layoutSubviews()

        // 判斷`label`和`imageView`是否同時存在
        guard let titleLabel = titleLabel,
            let imageView = imageView
            else {
                return
        }

        // 將`titleLabel`的`x`向左移動`imageView`的`width`,值得注意的是,這裏咱們須要將`width / 2` 
        titleEdgeInsets = UIEdgeInsetsMake(0, -imageView.bounds.width, 0, imageView.bounds.width)
        // 將`imageView`的`x`向右移動`titleLabel`的`width`,值得注意的是,這裏咱們須要將`width / 2`
        imageEdgeInsets = UIEdgeInsetsMake(0, titleLabel.bounds.width, 0, -titleLabel.bounds.width)
        /********** 下面這種作法不推薦 **********/
        // 會有問題
// titleLabel.frame = titleLabel.frame.offsetBy(dx: -imageView.bounds.width, dy: 0)
// imageView.frame = imageView.frame.offsetBy(dx: titleLabel.bounds.width, dy: 0)

    }
}複製代碼

這裏我要多寫點東西。由於最開始,我是設置ButtontitleLabelimageViewframe屬性的offSet的。

/********** 下面這種作法不推薦 **********/
// 會有問題
titleLabel.frame = titleLabel.frame.offsetBy(dx: -imageView.bounds.width, dy: 0)
imageView.frame = imageView.frame.offsetBy(dx: titleLabel.bounds.width, dy: 0)複製代碼

若是按照道理上講的話,應該是沒有什麼問題的,titleLabel左移imageView的寬度。imageView右移titleLabel的寬度。但實際上仍是出了問題。運行程序的時候你會發現,箭頭圖標不見了。

而後我就試着把偏移的距離縮小一倍

竟然就行了,我就很開心。雖然我內心也一直納悶,爲何會是一半的距離!就在我百思不得其解時候,我不當心點擊了一下按鈕。結果又是令我很是意外

仔細看,箭頭圖片在文字中央的位置,再屢次點擊的話,都是在這個位置切換圖片。在這個位置我是能夠理解的,由於點擊按鈕就會執行layoutSubviews方法,就會將titleLabelimageView按照代碼裏面的偏移量移動,而偏移量又是咱們以前設置的各個寬度的二分之一。

因而我就想到了,若是不設置偏移量是各個寬度的一半的話,最開始顯示雖然有問題,可是是否是,點擊就正常了呢。果不其然。

因而我測試了強行layoutIfNeeded這種方法也無濟於事,我只好參照本身以前用Objctive-C的方法,經過設置titleEdgeInsetsimageEdgeInsets來搞定。

// 將`titleLabel`的`x`向左移動`imageView`的`width`,值得注意的是,這裏咱們須要將`width / 2` 
titleEdgeInsets = UIEdgeInsetsMake(0, -imageView.bounds.width, 0, imageView.bounds.width)
// 將`imageView`的`x`向右移動`titleLabel`的`width`,值得注意的是,這裏咱們須要將`width / 2`
imageEdgeInsets = UIEdgeInsetsMake(0, titleLabel.bounds.width, 0, -titleLabel.bounds.width)複製代碼

這裏還有一點我要強調的是,若是隻是按照我那樣將titleLabelimageView的順序顛倒的話,titleLabelimageView也是牢牢的挨在一塊兒的。大概是下面這個樣子

而比較理想的狀態應該是,文字與圖片之間有必定的間距,這樣看起來比較舒服。

若是想達到這種狀態,咱們可能會延續上面的思惟,將偏移量增大一點。這種操做表面上看着沒什麼問題,可是實際上imageView其實已經超出了Button的右側邊界了,顯然是不太好的。

// 將`titleLabel`的`x`向左移動`imageView`的`width`,值得注意的是,這裏咱們須要將`width / 2`
titleEdgeInsets = UIEdgeInsetsMake(0, -imageView.bounds.width, 0, imageView.bounds.width)
// 將`imageView`的`x`向右移動`titleLabel`的`width`,值得注意的是,這裏咱們須要將`width / 2`
imageEdgeInsets = UIEdgeInsetsMake(0, titleLabel.bounds.width + 20, 0, -titleLabel.bounds.width - 20)複製代碼

爲此,咱們能夠嘗試轉換一種解決思路。給title的文字追加一個空格。

/// 文字在左、圖片在右的 Button
class HQTitleButton: UIButton {

    /// 重載構造函數
    ///
    /// - Parameter title: title 若是是 nil,就顯示首頁
    /// - Parameter title: title 若是不是 nil,顯示 title 和 箭頭
    init(title: String?) {
        super.init(frame: CGRect())

        if title == nil {
            setTitle("首頁", for: .normal)
        } else {
            setTitle(title! + " ", for: .normal)複製代碼

這種看起來就比較合適了。


新特性

每次有新的版本的時候,都會出現的一個界面,目的是介紹APP新增的功能之類的。

關於版本號的簡單介紹:

  • APP Store每次升級應用程序,版本號都要增長
  • 版本號通常由x.x.x組成,分別對應主版本號.次版本號.修訂版本號
  • 主版本號:意味着大的修改,使用者也須要作大的適應,好比Xcode每一年會更新一個主版本號8.3.3
  • 次版本號:意味着小的修改,某些函數和方法的使用或者參數有變化,對應APP多是主功能不變,可是新增了附加的一些新功能
  • 修訂版本號:程序內部bug的修訂,一些功能的緊急修復,通常不會對APP使用者有任何影響
// MARK: - 新特性
extension HQMainViewController {

    fileprivate func setupNewFeatureView() {

        // 若是用戶沒有登陸,則不顯示新特性界面,直接返回
        if !HQNetWorkManager.shared.userLogon {
            return
        }

        let v = isNewVersion ? HQNewFeatureView() : HQWelcomeView()

        v.frame = view.bounds

        view.addSubview(v)
    }

    /// 計算型屬性,不佔用存儲空間
    fileprivate var isNewVersion: Bool {

        // 獲取當前版本號
        let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""

        // 拼接保存到沙盒的路徑
        let path = String.hq_appendDocmentDirectory(fileName: "version") ?? ""
        let savedVersion = (try? String(contentsOfFile: path)) ?? ""

        // 將當前版本保存到沙盒路徑下
        try? currentVersion.write(toFile: path, atomically: true, encoding: .utf8)

        // 比較兩個版本是否相同
        return currentVersion != savedVersion
    }
}複製代碼

判斷新版本這裏,可能會有用將版本號轉換成數字,而後去逐個對比的作法,我的感受其實不用那麼複雜。由於提交到App Store的版本必定是遞增的,那麼只要比較當前版本和咱們本身保存的版本就徹底能夠比對出來的。

給頭像作動畫處理

準備代碼

class HQWelcomeView: UIView {

    fileprivate lazy var backImageView: UIImageView = UIImageView(hq_imageName: "ad_background")
    /// 頭像
    fileprivate lazy var avatarImageView: UIImageView = {

        let iv = UIImageView(hq_imageName: "avatar_default_big")
        iv.layer.cornerRadius = 45
        iv.layer.masksToBounds = true
        return iv
    }()
    fileprivate lazy var welcomeLabel: UILabel = {

        let label = UILabel(hq_title: "歡迎歸來", fontSize: 18, color: UIColor.hq_titleTextColor)
        label.alpha = 0
        return label
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.frame = UIScreen.main.bounds

        setupUI()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}複製代碼
// MARK: - UI
extension HQWelcomeView {

    fileprivate func setupUI() {

        addSubview(backImageView)
        addSubview(avatarImageView)
        addSubview(welcomeLabel)

        backImageView.frame = self.bounds
        avatarImageView.snp.makeConstraints { (make) in
            make.bottom.equalTo(self).offset(-200)
            make.centerX.equalTo(self)
            make.width.equalTo(90)
            make.height.equalTo(90)
        }
        welcomeLabel.snp.makeConstraints { (make) in
            make.top.equalTo(avatarImageView.snp.bottom).offset(16)
            make.centerX.equalTo(avatarImageView)
        }
    }
}複製代碼

若是這是一個控制器的話,咱們能夠選擇在viewDidAppear方法裏來處理。這裏有一個關於自動佈局開發的使用原則:

  • 全部使用約束設置位置的控件,不要再設置 frame
    • 緣由:自動佈局系統會根據設置的約束,自動計算控件的frame
    • layoutSubviews函數中設置frame
    • 若是咱們主動修改frame,會引發 自動佈局系統計算錯誤!

工做原理:

  • 當有一個運行循環啓動,自動佈局系統,會收集全部的約束變化
  • 在運行循環結束前,調用layoutSubviews函數統一設置frame
  • 若是但願某些約束提早更新!使用layoutIfNeeded 函數讓自動佈局系統,提早更新當前收集到的約束變化

可是咱們這裏不是控制器,只是一個View,裏面並無viewDidAppear方法。咱們就要找到一個相似的辦法。系統提供了一個方法didMoveToWindow,字面上咱們直接能夠翻譯出它的意思,就是視圖被添加到window,表示視圖已經顯示,和Controller裏面的viewDidAppear方法相似。

// MARK: - Animation
extension HQWelcomeView {

    /// 視圖被添加到`window`上,表示視圖已經顯示
    override func didMoveToWindow() {
        super.didMoveToWindow()

        avatarImageView.snp.updateConstraints { (make) in
            make.bottom.equalTo(self).offset(-bounds.size.height + 200)
        }

        UIView.animate(withDuration: 4.0,
                       delay: 0,
                       options: [],
                       animations: { 
                        self.layoutIfNeeded()
        }) { (_) in

        }
    }
}複製代碼

通過測試咱們發現,確實能夠出現動畫了,可是出現的方式有點和咱們所想的不同,咱們是但願控件已經被建立到咱們以前代碼寫好的位置,而後再經過動畫,移動到下圖中最終的位置。該如何處理呢?

上面說自動佈局工做原理的時候提到過

  • 若是但願某些約束提早更新!使用layoutIfNeeded 函數讓自動佈局系統,提早更新當前收集到的約束變化

所以,咱們手動調用一下layoutIfNeeded方法,將代碼佈局的約束都建立好,並顯示出來,而後再進行更新約束的動畫。

// MARK: - Animation
extension HQWelcomeView {

    /// 視圖被添加到`window`上,表示視圖已經顯示
    override func didMoveToWindow() {
        super.didMoveToWindow()

        // 將代碼佈局的約束都建立好並顯示出來,而後再進行下一步的更新動畫
        layoutIfNeeded()

        avatarImageView.snp.updateConstraints { (make) in
            make.bottom.equalTo(self).offset(-bounds.size.height + 200)
        }

        UIView.animate(withDuration: 2.0,
                       delay: 0,
                       usingSpringWithDamping: 0.7,
                       initialSpringVelocity: 0,
                       options: [],
                       animations: { 
                        self.layoutIfNeeded()
        }) { (_) in

            UIView.animate(withDuration: 1.0,
                           animations: { 
                            self.welcomeLabel.alpha = 1
            }, completion: { (_) in
                self.removeFromSuperview()
            })
        }
    }
}複製代碼

設置頭像

UI佈局完畢之後,就剩下將頭像設置到上面了,通常來說這些都是沒什麼技術含量的。可是這裏我仍是想簡單介紹一下。

我這裏仍是將設置頭像的代碼放在了didMoveToWindowlayoutIfNeeded方法後面去執行,

這裏須要提醒的是,若是是純代碼開發,不會走這個方法,即使是這段話仍然須要加上,可是若是你在init?(coder aDecoder: NSCoder)中寫代碼,會提示你Will never be executed

並且即使是xib開發,這裏也僅僅是將xib的二進制文件將視圖數據加載完成,尚未和代碼連線創建起關係,因此開發時,不能在這個方法裏面處理UI,並且若是是xib開發的話,你打印視圖的話,結果都是nil的。

/// 設置頭像
fileprivate func setAvatar() {

    guard let urlString = HQNetWorkManager.shared.userAccount.avatar_large else {
        return
    }
    avatarImageView.hq_setImage(urlString: urlString, placeholderImage: UIImage(named: "avatar_default_big"))
}複製代碼

新特性界面

因爲咱們以前在HQMainViewController中作好了判斷是顯示新特性界面仍是顯示歡迎界面。所以,咱們處理好歡迎界面之後,就仿照相似的方法建立新特性界面就行了。

// MARK: - 新特性
extension HQMainViewController {

    fileprivate func setupNewFeatureView() {

        // 若是用戶沒有登陸,則不顯示新特性界面,直接返回
        if !HQNetWorkManager.shared.userLogon {
            return
        }

        let v = isNewVersion ? HQNewFeatureView() : HQWelcomeView()複製代碼

HQNewFeatureView中,進行佈局,我寫UI佈局套路都比較單一,懶加載控件,在extensionsetupUI,若是有按鈕的監聽方法,再將按鈕的監聽方法抽取到extension中,只是暫時我本身習慣這樣寫而已。

class HQNewFeatureView: UIView {

    /// 開始體驗按鈕
    fileprivate lazy var startButton: UIButton = UIButton(hq_title: "開始體驗", color: UIColor.white, backImageName: "new_feature_finish_button")
    /// pageControl
    fileprivate lazy var pageControl: UIPageControl = {
        let pageControl = UIPageControl()
        pageControl.numberOfPages = 4
        pageControl.currentPageIndicatorTintColor = UIColor.orange
        pageControl.pageIndicatorTintColor = UIColor.black
        return pageControl
    }()
    fileprivate lazy var scrollView: UIScrollView = {
        let scrollView = UIScrollView(frame: UIScreen.main.bounds)
        return scrollView
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.frame = UIScreen.main.bounds

        setupUI()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}複製代碼
// MARK: - UI
extension HQNewFeatureView {

    /// setupUI
    fileprivate func setupUI() {

        addSubview(scrollView)
        addSubview(startButton)
        addSubview(pageControl)

        startButton.isHidden = true
        startButton.addTarget(self, action: #selector(enter), for: .touchUpInside)

        setupScrollView()

        startButton.snp.makeConstraints { (make) in
            make.centerX.equalTo(self)
            make.bottom.equalTo(self).multipliedBy(0.7)
        }
        pageControl.snp.makeConstraints { (make) in
            make.centerX.equalTo(startButton)
            make.top.equalTo(startButton.snp.bottom).offset(16)
        }
    }

    /// setupImageViewFrame
    fileprivate func setupScrollView() {

        let count = 4
        let rect = UIScreen.main.bounds

        for i in 0..<count {

            let imageName = "new_feature_\(i + 1)"
            let iv = UIImageView(hq_imageName: imageName)

            iv.frame = rect.offsetBy(dx: CGFloat(i) * rect.width, dy: 0)
            scrollView.addSubview(iv)
        }

        /// 設置`scrollView`的屬性
        // 這裏加`1`是爲了讓`scrollView`能夠多滾動一屏
        scrollView.contentSize = CGSize(width: CGFloat(count + 1) * rect.width, height: rect.height)
        scrollView.bounces = false
        scrollView.isPagingEnabled = true
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.showsVerticalScrollIndicator = false

    }
}複製代碼
// MARK: - Target Action
extension HQNewFeatureView {

    @objc fileprivate func enter() {
        print("enter")
    }
}複製代碼

界面佈局完畢之後,剩下的就是完善其它的業務邏輯了。主要還得靠scrollViewdelegate去實現

// MARK: - UIScrollViewDelegate
extension HQNewFeatureView: UIScrollViewDelegate {

    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {

        // 滾動到最後一個空白頁面,將新特性頁面從父視圖移除
        let page = Int(scrollView.contentOffset.x / scrollView.bounds.width)

        if page == scrollView.subviews.count {
            removeFromSuperview()
        }
        // 若是不是倒數第二頁,那麼就隱藏`startButton`按鈕
        startButton.isHidden = (page != scrollView.subviews.count - 1)
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {

        // 一旦滾動,隱藏按鈕
        startButton.isHidden = true

        // 設置當前的偏移量,+0.5是爲了處理`scrollView`滾動超過屏幕一半的時候,`pageControl`也滾動到下一頁
        let page = Int(scrollView.contentOffset.x / scrollView.bounds.width + 0.5)

        // 設置分頁控件
        pageControl.currentPage = page

        // 分頁控件的隱藏,滾動到最後一頁的時候
        pageControl.isHidden = (page == scrollView.subviews.count)
    }
}複製代碼
// MARK: - Target Action
extension HQNewFeatureView {

    @objc fileprivate func enter() {
        removeFromSuperview()
    }
}複製代碼

效果以下圖所示

至此爲止,總體框架基本搭建完畢,下一篇介紹自定義微博的cell及體會MVVM的好處。

DEMO傳送門:HQSwiftMVVM

歡迎來個人簡書看看:紅鯉魚與綠鯉魚與驢___

相關文章
相關標籤/搜索