Swift-MVVM 簡單演練(一)

Swift-MVVM 簡單演練(二)javascript

Swift-MVVM 簡單演練(三)html

Swift-MVVM 簡單演練(四)java

前言

最近在學習swiftMVVM架構模式,目的只是將本身的學習筆記記錄下來,方便本身往後查找,僅此而已!!!nginx

若是有任何問題,歡迎和我一塊兒討論。固然若是有什麼存在的問題,歡迎批評指正,我會積極改造的!git


這篇文章都寫啥

  • 自定義NavgationBar
  • 抽取便利構造函數
  • 初步的下拉刷新/上拉加載的簡單處理
  • 未登陸邏輯的處理
  • 蘋果原生布局NSLayoutConstraint
  • 如何用VFL佈局(VisualFormatLanguage)
  • 模擬網絡加載應用程序的一些配置tabBar的標題和圖片樣式
  • 簡單的網絡工具單例的封裝
  • 隔離項目中的網絡請求方法
  • 初步的視圖模型的體驗
  • 以及一些遇到的語法問題的簡單探究

GitHub 上建立項目

若有須要,請移步下面兩篇文章github


項目配置

  • 刪除ViewController.swiftMain.storyboardLaunchScreen.storyboard
  • 設置APPIconLaunchImage
  • 設置項目目錄結構
    • HQMainViewController繼承自UITabBarController
    • HQNavigationController繼承自UINavigationController
    • HQBaseViewController繼承自UIViewController(基類控制器)

設置子控制器

HQMainViewController中設置四個子控制器objective-c

  • extension將代碼拆分
  • 經過反射機制,獲取子控制器類名,建立子控制器
  • 設置每一個子控制的tabBar圖片及標題

HQMainViewController中代碼以下所示json

class HQMainViewController: UITabBarController {

    override func viewDidLoad() {
        super.viewDidLoad()

        setupChildControllers()
    }
}

/* extension 相似於 OC 中的分類,在 Swift 中還能夠用來切分代碼塊 能夠把功能相近的函數,放在一個extension中 */
extension HQMainViewController {

    /// 設置全部子控制器
    fileprivate func setupChildControllers() {

        let array = [
            ["className": "HQAViewController", "title": "首頁", "imageName": "a"],
            ["className": "HQBViewController", "title": "消息", "imageName": "b"],
            ["className": "HQCViewController", "title": "發現", "imageName": "c"],
            ["className": "HQDViewController", "title": "我", "imageName": "d"]
        ]
        var arrayM = [UIViewController]()
        for dict in array {
            arrayM.append(controller(dict: dict))
        }
        viewControllers = arrayM
    }
    /* ## 關於 fileprvita 和 private - 在`swift 3.0`,新增長了一個`fileprivate`,這個元素的訪問權限爲文件內私有 - 過去的`private`至關於如今的`fileprivate` - 如今的`private`是真正的私有,離開了這個類或者結構體的做用域外面就沒法訪問了 */

    /// 使用字典建立一個子控制器
    ///
    /// - Parameter dict: 信息字典[className, title, imageName]
    /// - Returns: 子控制器
    private func controller(dict: [String: String]) -> UIViewController {

        // 1. 獲取字典內容
        guard let className = dict["className"],
            let title = dict["title"],
            let imageName = dict["imageName"],
            let cls = NSClassFromString(Bundle.main.namespace + "." + className) as? UIViewController.Type else {

                return UIViewController()
        }

        // 2. 建立視圖控制器
        let vc = cls.init()
        vc.title = title

        // 3. 設置圖像
        vc.tabBarItem.image = UIImage(named: "tabbar_" + imageName)
        vc.tabBarItem.selectedImage = UIImage(named: "tabbar_" + imageName + "_selected")?.withRenderingMode(.alwaysOriginal)
        // 設置`tabBar`標題顏色
        vc.tabBarItem.setTitleTextAttributes(
            [NSForegroundColorAttributeName: UIColor.orange],
            for: .selected)
        // 設置`tabBar`標題字體大小,系統默認是`12`號字
        vc.tabBarItem.setTitleTextAttributes(
            [NSFontAttributeName: UIFont.systemFont(ofSize: 12)],
            for: .normal)

        let nav = HQNavigationController(rootViewController: vc)
        return nav
    }
}複製代碼

設置中間加號按鈕

  • 經過增長tabBarItem的方式,給中間留出一個+按鈕的位置
  • 自定義一個UIButton的分類HQButton+Extension,封裝快速建立自定義按鈕的方法

HQButton.swiftswift

extension UIButton {

    /// 便利構造函數
    ///
    /// - Parameters:
    /// - imageName: 圖像名稱
    /// - backImageName: 背景圖像名稱
    convenience init(hq_imageName: String, backImageName: String?) {
        self.init()

        setImage(UIImage(named: hq_imageName), for: .normal)
        setImage(UIImage(named: hq_imageName + "_highlighted"), for: .highlighted)

        if let backImageName = backImageName {
            setBackgroundImage(UIImage(named: backImageName), for: .normal)
            setBackgroundImage(UIImage(named: backImageName + "_highlighted"), for: .highlighted)
        }

        // 根據背景圖片大小調整尺寸
        sizeToFit()
    }
}複製代碼

HQMainViewController.swiftapi

/// 設置撰寫按鈕
fileprivate func setupComposeButton() {
    tabBar.addSubview(composeButton)

    // 設置按鈕的位置
    let count = CGFloat(childViewControllers.count)
    // 減`1`是爲了是按鈕變寬,覆蓋住系統的容錯點
    let w = tabBar.bounds.size.width / count - 1
    composeButton.frame = tabBar.bounds.insetBy(dx: w * 2, dy: 0)

    composeButton.addTarget(self, action: #selector(composeStatus), for: .touchUpInside)
}複製代碼
// MARK: - 監聽方法
// @objc 容許這個函數在運行時經過`OC`消息的消息機制被調用
@objc fileprivate func composeStatus() {
    print("點擊加號按鈕")
}

// MARK: - 撰寫按鈕
fileprivate lazy var composeButton = UIButton(hq_imageName: "tabbar_compose_icon_add",
                                          backImageName: "tabbar_compose_button")複製代碼

自定義頂部導航欄

  • 系統自己的絕大多數狀況下不能知足咱們的平常需求
  • 有一些系統的樣式自己處理的很差,好比側滑返回的時候,系統的會出現漸溶的效果,這種用戶體驗不太好
  • 須要解決push出一個控制器後,底部TabBar隱藏/顯示問題

Push 出控制器後,底部 TabBar 隱藏/顯示問題

  • 在導航控制器的基類裏面重寫一下push方法
  • 判斷若是不是根控制器,那麼push的時候就隱藏BottomBar
  • 注意調用super.pushViewController要在重寫方法以後

HQNavigationController.swift

override func pushViewController(_ viewController: UIViewController, animated: Bool) {

    if childViewControllers.count > 0 {
        viewController.hidesBottomBarWhenPushed = true
    }
    super.pushViewController(viewController, animated: true)
}複製代碼

抽取 BarButtonItem 便利構造函數

  • 系統的UIBarButtonItem方法不能方便的知足咱們建立所需的leftBarButtonItemrightBarButtonItem
  • 若是自定義建立須要些好幾行代碼
  • 而這些代碼又可能在不少地方用到,因此儘可能抽取個便利構造函數

通常自定義ftBarButtonItem時候可能會寫以下代碼

  • 最討厭的就是btn.sizeToFit()這句,若是不加,rightBarButtonItem就顯示不出來
  • 若是封裝起來,就不再用考慮這問題了
let btn = UIButton()
btn.setTitle("下一個", for: .normal)
btn.setTitleColor(UIColor.lightGray, for: .normal)
btn.setTitleColor(UIColor.orange, for: .highlighted)
btn.addTarget(self, action: #selector(showNext), for: .touchUpInside)
// 最討厭的就是這句,若是不加,`rightBarButtonItem`就顯示不出來
btn.sizeToFit()
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: btn)複製代碼

若是抽取一個便利構造函數,代碼可能會簡化成以下

  • 一行代碼搞定,簡單了許多
navigationItem.rightBarButtonItem = UIBarButtonItem(hq_title: "下一個", target: self, action: #selector(showNext))複製代碼

便利構造函數的做用:簡化控件的建立


解決導航欄側滑返回過程當中,按鈕及標題的融合問題

  • 由於側滑返回的時候,leftBarButtonItemtitle的字體有漸融的問題,咱們又想解決這樣的問題。
  • 因而乎就要自定義NavigationBar
  • 要想實現這些功能,必定儘可能要少動不少控制器的代碼。若是在某一個地方就能夠寫好,對其它控制器的代碼入侵的越少越好,這是一個程序好的架構的原則

首先,在HQNavigationController中隱藏系統的navigationBar

override func viewDidLoad() {
    super.viewDidLoad()

    navigationBar.isHidden = true
}複製代碼

其次,在基類控制器HQBaseViewController裏自定義

class HQBaseViewController: UIViewController {

    /// 自定義導航條
    lazy var navigationBar = UINavigationBar(frame: CGRect(x: 0, y: 0, width: UIScreen.hq_screenWidth(), height: 64))
    /// 自定義導航條目 - 之後設置導航欄內容,統一使用`navItem`
    lazy var navItem = UINavigationItem()

    override func viewDidLoad() {
        super.viewDidLoad()

        setupUI()
    }

    override var title: String? {
        didSet {
            navItem.title = title
        }
    }
}

// MARK: - 設置界面
extension HQBaseViewController {

    func setupUI() {

        view.backgroundColor = UIColor.hq_randomColor()
        view.addSubview(navigationBar)
        navigationBar.items = [navItem]
    }
}複製代碼

注意:這裏有一個小bug

  • push出下一個控制器的時候,導航欄右側會有一段白色的樣式出現
  • 緣由是:系統默認的導航欄的透明度過高,自定義設置一個顏色就行了

HQBaseViewController.swift

// 設置`navigationBar`的渲染顏色
navigationBar.barTintColor = UIColor.hq_color(withHex: 0xF6F6F6)複製代碼

設置左側 leftBarButtonItem

  • 左側都是返回(第二級頁面如下)
  • 或者是上一級title的名稱(只在第二級頁面這樣顯示)

在重寫pushViewController的方法裏面去判斷,若是子控制器的個數childViewControllers.count == 1的時候,就設置返回按鈕文字爲根控制器的title

override func pushViewController(_ viewController: UIViewController, animated: Bool) {

    if childViewControllers.count > 0 {
        viewController.hidesBottomBarWhenPushed = true

        /* 判斷控制器的類型 - 若是是第一級頁面,不顯示`leftBarButtonItem` - 只有第二級頁面之後才顯示`leftBarButtonItem` */
        if let vc = viewController as? HQBaseViewController {

            var title = "返回"

            if childViewControllers.count == 1 {
                title = childViewControllers.first?.title ?? "返回"
            }

            vc.navItem.leftBarButtonItem = UIBarButtonItem(hq_title: title, target: self, action: #selector(popToParent))
        }
    }

    super.pushViewController(viewController, animated: true)
}複製代碼

給 leftBarButtonItem 加上 icon

仍是以前的原則,當改動某一處的代碼時候,儘可能對原有代碼作儘量小的改動

  • 以前咱們已經設置好leftbarButtonItem文字顯示的狀態問題
  • 咱們的需求又是在此基礎上直接加一個返回的icon而已
  • 所以,咱們若是對自定義快速建立leftBarButtonItem這裏若是能直接改好了就最好

小技巧:

  • 當你想查看某一個方法都在哪一個文件內被哪些方法調用的時候
  • 你能夠在這個方法的方法明上右鍵->Find Call Hierarchy
    Hierarchy : 層級

UIBarButtonItem的自定義快速建立leftbarButtonItem的方法擴展一下,增長一個參數isBack,默認值是false

/// 字體+target+action
///
/// - Parameters:
/// - hq_title: title
/// - fontSize: fontSize
/// - target: target
/// - action: action
/// - isBack: 是不是返回按鈕,若是是就加上箭頭的`icon`
convenience init(hq_title: String, fontSize: CGFloat = 16, target: Any?, action: Selector, isBack: Bool = false) {

    let btn = UIButton(hq_title: hq_title, fontSize: fontSize, normalColor: UIColor.darkGray, highlightedColor: UIColor.orange)

    if isBack {
        let imageName = "nav_back"
        btn.setImage(UIImage.init(named: imageName), for: .normal)
        btn.setImage(UIImage.init(named: imageName + "_highlighted"), for: .highlighted)
        btn.sizeToFit()
    }

    btn.addTarget(target, action: action, for: .touchUpInside)
    // self.init 實例化 UIBarButtonItem
    self.init(customView: btn)
}複製代碼

在以前判斷返回按鈕顯示文字的地方從新設置一下,只須要增長一個參數isBack: true

vc.navItem.leftBarButtonItem = UIBarButtonItem(hq_title: title, target: self, action: #selector(popToParent), isBack: true)複製代碼

通過這樣的演進,我忽然發現swift在這裏是比objective-c友好不少的,若是你給參數設置了一個默認值。那麼,就能夠不對原方法形成侵害,不影響原方法的調用。

可是,objective-c就沒有這麼友好,若是在原方法上增長參數,那麼以前調用過此方法的地方,就會所有報錯。若是不想對原方法有改動,那麼就要從新寫一個徹底同樣的只是最後面增長了這個須要的參數而已的一個新的方法。

你看swift是否是真的簡潔了許多。

設置 navigationBar 的 title 的顏色

navigationBar.tintColor = UIColor.red這樣是不對的,由於tintColor不是設置標題顏色的。

barTintColor是管理整個導航條的背景色

tintColor是管理導航條上item文字的顏色

titleTextAttributes是設置導航欄title的顏色

若是你找不到設置的方法,最好去UINavigationItem的頭文件裏面去找一下,你能夠control + 6快速搜索color關鍵字,若是沒有的話,建議你搜索attribute試試,由於通常設置屬性的方法均可以解決多數你想解決的問題的。

// 設置`navigationBar`的渲染顏色
navigationBar.barTintColor = UIColor.hq_color(withHex: 0xF6F6F6)
// 設置導航欄`title`的顏色
navigationBar.titleTextAttributes = [NSForegroundColorAttributeName : UIColor.darkGray]
// 設置系統`leftBarButtonItem`渲染顏色
navigationBar.tintColor = UIColor.orange複製代碼

設置設備方向

有些時候咱們的APP可能會在某個界面裏面須要支持橫屏可是其它的地方又但願它只支持豎屏,這就須要咱們用代碼去設置

override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
    return .portrait
}複製代碼

設置支持的方向以後,當前的控制器及子控制器都會遵照這個方向,所以寫在HQMainViewController裏面


利用 extension 隔離 TableView 數據源方法

在基類設置datasourcedelegate,這樣子類就能夠直接實現方法就能夠了,不用每一個tableView的頁面都去設置tableView?.dataSource = selftableView?.delegate = self了。

  • 基類只是實現方法,子類負責具體的實現
  • 子類的數據源方法不須要super
  • 返回UITableViewCell()只是爲了沒有語法錯誤

HQBaseViewController裏,實現以下代碼

extension HQBaseViewController: UITableViewDataSource, UITableViewDelegate {

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 0
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return UITableViewCell()
    }
}複製代碼

設置一個加載數據的方法loadData,在這裏並不去作任何事情,只是爲了方便子類重寫此方法加載數據就能夠了。

/// 加載數據,具體的實現由子類負責
func loadData() {

}複製代碼

綁定假數據測試

因爲HQBaseViewController裏面實現了tableViewtableViewDataSourcetableViewDelegate以及loadData(自定義加載數據的方法),下一步咱們就要在子控制器裏面測試一下效果了。

  • 製造一些假數據
fileprivate lazy var statusList = [String]()

/// 加載數據
override func loadData() {

    for i in 0..<10 {
        statusList.insert(i.description, at: 0)
    }
}複製代碼
  • 實現數據源方法
// MARK: - tableViewDataSource
extension HQAViewController {

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return statusList.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
        cell.textLabel?.text = statusList[indexPath.row]
        return cell
    }
}複製代碼

至此,界面上應該能夠顯示出數據了,以下所示

可是仔細觀察是存在問題的

  • 第一行應該是從9開始的,說明tableView的起始位置不對
  • 若是數據足夠多的狀況下(多到能夠超過一個屏幕的數據),能夠發現下面也是停在tabBar的後面,底部位置也有問題

解決 TableView 的位置問題

主要在HQBaseViewController裏,從新設置tableViewContentInsets

/* 取消自動縮進,當導航欄遇到`scrollView`的時候,通常都要設置這個屬性 默認是`true`,會使`scrollView`向下移動`20`個點 */
automaticallyAdjustsScrollViewInsets = false複製代碼
tableView?.contentInset = UIEdgeInsets(top: navigationBar.bounds.height,
                                       left: 0,
                                       bottom: tabBarController?.tabBar.bounds.height ?? 49,
                                       right: 0)複製代碼

由於通常的公司裏,頁面多數都是ViewController + TableView。因此,相似的需求,直接在基類控制器設置好就能夠了。


添加下拉刷新控件

  • 在基類控制器中定義下拉刷新控件,這樣就不用每一個子控制器頁面單獨設置了
  • refreshControl添加監聽方法,監聽refreshControlvalueChange事件
  • 當值改變的時候,從新執行loadData方法
  • 子類會重寫基類的loadData方法,所以不用在去子類重寫此方法
// 設置刷新控件
refreshControl = UIRefreshControl()
tableView?.addSubview(refreshControl!)
refreshControl?.addTarget(self, action: #selector(loadData), for: .valueChanged)複製代碼

模擬延時加載數據

  • 通常網絡請求都會有延時,爲了模擬的逼真一點,這裏咱們也作了模擬延時加載數據。
  • 而且對比一下swiftobjective-c的延遲加載異同點

模擬延遲加載數據

/// 加載數據
override func loadData() {

    // 模擬`延時`加載數據
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {

        for i in 0..<15 {
            self.statusList.insert(i.description, at: 0)
        }
        self.refreshControl?.endRefreshing()
        self.tableView?.reloadData()
    }
}複製代碼

swift 延遲加載

// 模擬`延時`加載數據
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) {

    print("5 秒後,執行閉包內的代碼")
}複製代碼

objective-c 延遲加載

/*
 dispatch_time_t when,      從如今開始,通過多少納秒(delayInSeconds * 1000000000)
 dispatch_queue_t queue,    由隊列調度任務執行
 dispatch_block_t block     執行任務的 block
 */
dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC));

dispatch_after(when, dispatch_get_main_queue(), ^{
    // code to be executed after a specified delay
    NSLog(@"5 秒後,執行 Block 內的代碼");
});複製代碼

雖然都是一句話,可是swift語法的可讀性明顯比objective-c要好一些。


上拉刷新

如今多數APP作無縫的上拉刷新,就是當tableView滾動到最後一行cell的時候,自動刷新加載數據。

用一個屬性來記錄是不是上拉加載數據

/// 上拉刷新標記
var isPullup = false複製代碼

滾動到最後一行 cell 的時候加載數據

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {

    let row = indexPath.row
    let section = tableView.numberOfSections - 1

    if row < 0 || section < 0 {
        return
    }

    let count = tableView.numberOfRows(inSection: section)

    if row == (count - 1) && !isPullup {

        isPullup = true
        loadData()
    }
}複製代碼

在首頁控制器裏面模擬加載數據的時候,根據屬性isPullup判斷是上拉加載,仍是下拉刷新

/// 加載數據
override func loadData() {

    // 模擬`延時`加載數據
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {

        for i in 0..<15 {

            if self.isPullup {
                self.statusList.append("上拉 \(i)")
            } else {
                self.statusList.insert(i.description, at: 0)
            }
        }
        self.refreshControl?.endRefreshing()
        self.isPullup = false
        self.tableView?.reloadData()
    }
}複製代碼

未登陸視圖顯示(訪客視圖)

現實中常常會遇到一些臨時增長的需求,好比登陸後顯示的是一種視圖,未登陸又顯示另一種視圖,若是你的公司是面向公司內部的APP,那麼你可能會面對更多的用戶角色。這裏咱們暫時只討論已登陸未登陸兩種狀態下的狀況。

仍是以前的原則,無論作什麼新功能,增長什麼臨時的需求,咱們要作的都是想辦法對原來的代碼及架構作最小的調整,特別是對原來的Controller裏面的代碼入侵的越小越好。

在基類控制器的setupUI(設置界面)的方法裏面,咱們直接建立了tableView,那麼咱們若是有一個標記,能根據這個標記來選擇是建立普通視圖,仍是建立訪客視圖。就能夠很好的解決此類問題了。

  • 增長一個用戶登陸標記
/// 用戶登陸標記
var userLogon = false複製代碼
  • 根據標記判斷視圖顯示
userLogon ? setupTableView() : setupVistorView()複製代碼
  • 建立訪客視圖的代碼
/// 設置訪客視圖
fileprivate func setupVistorView() {

    let vistorView = UIView(frame: view.bounds)
    vistorView.backgroundColor = UIColor.hq_randomColor()
    view.insertSubview(vistorView, belowSubview: navigationBar)
}複製代碼

自定義一個 View,繼承自UIView,在裏面設置訪客視圖的界面

class HQVistorView: UIView {

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

        setupUI()
    }

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

// MARK: - 設置訪客視圖界面
extension HQVistorView {

    func setupUI() {
        backgroundColor = UIColor.white
    }
}複製代碼

利用原生布局系統定義訪客視圖界面

在自定義訪客視圖HQVistorView中佈局各個子控件

  • 懶加載控件
/// 圖像視圖
fileprivate lazy var iconImageView: UIImageView = UIImageView(hq_imageName: "visitordiscover_feed_image_smallicon")
/// 遮罩視圖
fileprivate lazy var maskImageView: UIImageView = UIImageView(hq_imageName: "visitordiscover_feed_mask_smallicon")
/// 小房子
fileprivate lazy var houseImageView: UIImageView = UIImageView(hq_imageName: "visitordiscover_feed_image_house")
/// 提示標籤
fileprivate lazy var tipLabel: UILabel = UILabel(hq_title: "關注一些人,回這裏看看有什麼驚喜關注一些人,回這裏看看有什麼驚喜")
/// 註冊按鈕
fileprivate lazy var registerButton: UIButton = UIButton(hq_title: "註冊", color: UIColor.orange, backImageName: "common_button_white_disable")
/// 登陸按鈕
fileprivate lazy var loginButton: UIButton = UIButton(hq_title: "登陸", color: UIColor.darkGray, backImageName: "common_button_white_disable")複製代碼
  • 添加視圖
addSubview(iconImageView)
addSubview(maskImageView)
addSubview(houseImageView)
addSubview(tipLabel)
addSubview(registerButton)
addSubview(loginButton)

// 取消 autoresizing
for v in subviews {
    v.translatesAutoresizingMaskIntoConstraints = false
}複製代碼
  • 原生布局

自動佈局本質公式 : A控件的屬性a = B控件的屬性b * 常數 + 約束

firstItem.firstAttribute {==,<=,>=} secondItem.secondAttribute * multiplier + constant複製代碼
let margin: CGFloat = 20.0

/// 圖像視圖
addConstraint(NSLayoutConstraint(item: iconImageView,
                                 attribute: .centerX,
                                 relatedBy: .equal,
                                 toItem: self,
                                 attribute: .centerX,
                                 multiplier: 1.0,
                                 constant: 0))
addConstraint(NSLayoutConstraint(item: iconImageView,
                                 attribute: .centerY,
                                 relatedBy: .equal,
                                 toItem: self,
                                 attribute: .centerY,
                                 multiplier: 1.0,
                                 constant: -60))
/// 小房子
addConstraint(NSLayoutConstraint(item: houseImageView,
                                 attribute: .centerX,
                                 relatedBy: .equal,
                                 toItem: iconImageView,
                                 attribute: .centerX,
                                 multiplier: 1.0,
                                 constant: 0))
addConstraint(NSLayoutConstraint(item: houseImageView,
                                 attribute: .centerY,
                                 relatedBy: .equal,
                                 toItem: iconImageView,
                                 attribute: .centerY,
                                 multiplier: 1.0,
                                 constant: 0))
/// 提示標籤
addConstraint(NSLayoutConstraint(item: tipLabel,
                                 attribute: .centerX,
                                 relatedBy: .equal,
                                 toItem: iconImageView,
                                 attribute: .centerX,
                                 multiplier: 1.0,
                                 constant: 0))
addConstraint(NSLayoutConstraint(item: tipLabel,
                                 attribute: .top,
                                 relatedBy: .equal,
                                 toItem: iconImageView,
                                 attribute: .bottom,
                                 multiplier: 1.0,
                                 constant: margin))
addConstraint(NSLayoutConstraint(item: tipLabel,
                                 attribute: .width,
                                 relatedBy: .equal,
                                 toItem: nil,
                                 attribute: .notAnAttribute,
                                 multiplier: 1.0,
                                 constant: 236))
/// 註冊按鈕
addConstraint(NSLayoutConstraint(item: registerButton,
                                 attribute: .left,
                                 relatedBy: .equal,
                                 toItem: tipLabel,
                                 attribute: .left,
                                 multiplier: 1.0,
                                 constant: 0))
addConstraint(NSLayoutConstraint(item: registerButton,
                                 attribute: .top,
                                 relatedBy: .equal,
                                 toItem: tipLabel,
                                 attribute: .bottom,
                                 multiplier: 1.0,
                                 constant: margin))
addConstraint(NSLayoutConstraint(item: registerButton,
                                 attribute: .width,
                                 relatedBy: .equal,
                                 toItem: nil,
                                 attribute: .notAnAttribute,
                                 multiplier: 1.0,
                                 constant: 100))
/// 登陸按鈕
addConstraint(NSLayoutConstraint(item: loginButton,
                                 attribute: .right,
                                 relatedBy: .equal,
                                 toItem: tipLabel,
                                 attribute: .right,
                                 multiplier: 1.0,
                                 constant: 0))
addConstraint(NSLayoutConstraint(item: loginButton,
                                 attribute: .top,
                                 relatedBy: .equal,
                                 toItem: registerButton,
                                 attribute: .top,
                                 multiplier: 1.0,
                                 constant: 0))
addConstraint(NSLayoutConstraint(item: loginButton,
                                 attribute: .width,
                                 relatedBy: .equal,
                                 toItem: registerButton,
                                 attribute: .width,
                                 multiplier: 1.0,
                                 constant: 0))複製代碼

採用 VFL 佈局子控件

  • VFL 可視化語言,多用於連續參照關係,如遇到居中對其,一般多使用參照
  • H水平方向
  • V豎直方向
  • |邊界
  • []包含控件的名稱字符串,對應關係在views字典中定義
  • ()定義控件的寬/高,能夠在metrics中指定

VFL 參數的解釋 :

  • views: 定義 VFL 中控件名稱和實際名稱的映射關係
  • metrics: 定義 VFL 中 () 內指定的常數映射關係,防止在代碼中出現魔法數字
let viewDict: [String: Any] = ["maskImageView": maskImageView,
                "registerButton": registerButton]
let metrics = ["spacing": -35]

addConstraints(NSLayoutConstraint.constraints(
    withVisualFormat: "H:|-0-[maskImageView]-0-|",
    options: [],
    metrics: nil,
    views: viewDict))
addConstraints(NSLayoutConstraint.constraints(
    withVisualFormat: "V:|-0-[maskImageView]-(spacing)-[registerButton]",
    options: [],
    metrics: metrics,
    views: viewDict))複製代碼

處理每一個子控制器訪客視圖顯示問題

到目前爲止,雖然咱們只是在基類控制器裏面建立了訪客視圖setupVistorView,只有一個訪客視圖的HQVistorView,可是實際上當咱們點擊不一樣的子控制器的時候,每一個子控制器都會建立一個訪客視圖。點擊四個子控制器的時候,訪客視圖打印的地址都不同。

<HQSwiftMVVM.HQVistorView: 0x7fea6970ed30; frame = (0 0; 375 667); layer = <CALayer: 0x608000036ec0>>
<HQSwiftMVVM.HQVistorView: 0x7fea6940d3b0; frame = (0 0; 375 667); layer = <CALayer: 0x600000421e60>>
<HQSwiftMVVM.HQVistorView: 0x7fea6973cf60; frame = (0 0; 375 667); layer = <CALayer: 0x608000036a40>>
<HQSwiftMVVM.HQVistorView: 0x7fea6943d990; frame = (0 0; 375 667); layer = <CALayer: 0x600000423760>>複製代碼

定義一個屬性字典,把圖片名稱和提示標語傳入到HQVistorView中,經過重寫didSet方法設置

/// 設置訪客視圖信息字典[imageName / message]
var vistorInfo: [String: String]? {
    didSet {
        guard let imageName = vistorInfo?["imageName"],
            let message = vistorInfo?["message"]
        else {
            return
        }
        tipLabel.text = message
        if imageName == "" {
            return
        }
        iconImageView.image = UIImage(named: imageName)
    }
}複製代碼

HQBaseViewController定義一個一樣的訪客視圖信息字典,方便外界傳入。這樣作的目的是外界傳入到HQBaseViewController中信息字典,能夠經過setupVistorView方法傳到HQVistorView中,再重寫HQVistorView中的訪客視圖信息字典的didSet方法以達到設置的目的。

/// 設置訪客視圖信息字典
var visitorInfoDictionary: [String: String]?複製代碼
/// 設置訪客視圖
fileprivate func setupVistorView() {

    let vistorView = HQVistorView(frame: view.bounds)
    view.insertSubview(vistorView, belowSubview: navigationBar)
    vistorView.vistorInfo = visitorInfoDictionary
}複製代碼

下一步就是研究在哪裏給訪客視圖信息字典傳值的問題了。

修改設置子控制器的參數配置

  • 修改設置子控制器的配置
fileprivate func setupChildControllers() {

    let array: [[String: Any]] = [
        [
            "className": "HQAViewController",
            "title": "首頁",
            "imageName": "a",
            "visitorInfo": [
                "imageName": "",
                "message": "關注一些人,回這裏看看有什麼驚喜"
            ]
        ],
        [
            "className": "HQBViewController",
            "title": "消息",
            "imageName": "b",
            "visitorInfo": [
                "imageName": "visitordiscover_image_message",
                "message": "登陸後,別人評論你的微博,發給你的信息,都會在這裏收到通知"
            ]
        ],
        [
            "className": "UIViewController"
        ],
        [
            "className": "HQCViewController",
            "title": "發現",
            "imageName": "c",
            "visitorInfo": [
                "imageName": "visitordiscover_image_message",
                "message": "登陸後,最新、最熱微博盡在掌握,再也不會與時事潮流擦肩而過"
            ]
        ],
        [
            "className": "HQDViewController",
            "title": "我",
            "imageName": "d",
            "visitorInfo": [
                "imageName": "visitordiscover_image_profile",
                "message": "登陸後,你的微博、相冊,我的資料會顯示在這裏,顯示給別人"
            ]
        ]
    ]

    (array as NSArray).write(toFile: "/Users/wanghongqing/Desktop/demo.plist", atomically: true)

    var arrayM = [UIViewController]()
    for dict in array {
        arrayM.append(controller(dict: dict))
    }
    viewControllers = arrayM
}複製代碼
fileprivate func controller(dict: [String: Any]) -> UIViewController {

    // 1. 獲取字典內容
    guard let className = dict["className"] as? String,
        let title = dict["title"] as? String,
        let imageName = dict["imageName"] as? String,
        let cls = NSClassFromString(Bundle.main.namespace + "." + className) as? HQBaseViewController.Type,
        let vistorDict = dict["visitorInfo"] as? [String: String]

        else {

            return UIViewController()
    }

    // 2. 建立視圖控制器
    let vc = cls.init()
    vc.title = title
    vc.visitorInfoDictionary = vistorDict
}複製代碼

將數組寫入plist並保存到本地

swfit語法裏,並無直接將array經過write(toFile:)的方法。所以,這裏須要轉一下,方便查看數據格式。

(array as NSArray).write(toFile: "/Users/wanghongqing/Desktop/demo.plist", atomically: true)複製代碼

設置首頁動畫旋轉效果

有幾點須要注意的

  • 動畫旋轉須要一直保持,切換到其它控制器或者退到後臺再回來,要保證動畫仍然能繼續轉動
  • 設置動畫的旋轉週數tiValueM_PIswift 3.0之後已經不能再用了,須要用Double.pi替代
if imageName == "" {
    startAnimation()
    return
}複製代碼
/// 旋轉視圖動畫
fileprivate func startAnimation() {

    let anim = CABasicAnimation(keyPath: "transform.rotation")
    anim.toValue = 2 * Double.pi
    anim.repeatCount = MAXFLOAT
    anim.duration = 15

    // 設置動畫一直保持轉動,若是`iconImageView`被釋放,動畫會被一塊兒釋放
    anim.isRemovedOnCompletion = false
    // 將動畫添加到圖層
    iconImageView.layer.add(anim, forKey: nil)
}複製代碼

使用 json 配置文件設置界面控制器內容

將以前HQMainViewController寫好的配置內容(控制各個控制器標題等內容的數組)輸出main.json文件,並保存。

let data = try! JSONSerialization.data(withJSONObject: array, options: [.prettyPrinted])
(data as NSData).write(toFile: "/Users/wanghongqing/Desktop/main.json", atomically: true)複製代碼

main.json拖入到文件中,經過加載這個main.json配置界面控制器內容。

/// 設置全部子控制器
fileprivate func setupChildControllers() {

    // 從`Bundle`加載配置的`json`
    guard let path = Bundle.main.path(forResource: "main.json", ofType: nil),
        let data = NSData(contentsOfFile: path),
    let array = try? JSONSerialization.jsonObject(with: data as Data, options: []) as? [[String: Any]]
        else {
        return
    }

    var arrayM = [UIViewController]()
    for dict in array! {
        arrayM.append(controller(dict: dict))
    }
    viewControllers = arrayM
}複製代碼

模擬網絡加載應用程序配置

如今不少應用程序都是帶有一個配置文件的.json文件,當應用程序啓動的時候去查看沙盒裏面有沒有該.json文件。

  • 若是沒有
    • 經過網絡請求加載默認的.json文件
  • 若是有
    • 直接使用沙盒裏面保存的.json文件
    • 網絡請求異步加載新的.json文件,等下一次用戶再次啓動APP的時候就能夠顯示比較新的配置文件了

AppDelegate中模擬加載數據

extension AppDelegate {

    fileprivate func loadAppInfo() {

        DispatchQueue.global().async {
            let url = Bundle.main.url(forResource: "main.json", withExtension: nil)
            let data = NSData(contentsOf: url!)
            let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
            let jsonPath = (path as NSString).appendingPathComponent("main.json")
            data?.write(toFile: jsonPath, atomically: true)
        }
    }
}複製代碼

HQMainViewController中設置

/// 設置全部子控制器
fileprivate func setupChildControllers() {

    /// 獲取沙盒`json`路徑
    let docPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
    let jsonPath = (docPath as NSString).appendingPathComponent("main.json")

    /// 加載 `data`
    var data = NSData(contentsOfFile: jsonPath)

    /// 若是`data`沒有內容,說明沙盒沒有內容
    if data == nil {
        // 從`bundle`加載`data`
        let path = Bundle.main.path(forResource: "main.json", ofType: nil)
        data = NSData(contentsOfFile: path!)
    }

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

    var arrayM = [UIViewController]()
    for dict in array! {
        arrayM.append(controller(dict: dict))
    }
    viewControllers = arrayM
}複製代碼

解釋一下 try

在以前的代碼中,json的反序列化的時候,咱們遇到了try,下面用幾個簡單的例子說明一下

推薦用法,弱 try->try?

let jsonString = "{\"name\": \"zhang\"}"
let data = jsonString.data(using: .utf8)

let json = try? JSONSerialization.jsonObject(with: data!, options: [])
print(json ?? "nil")

// 輸出結果
{
    name = zhang;
}複製代碼

若是jsonString的格式有問題的話,好比改爲下面這樣

let jsonString = "{\"name\": \"zhang\"]"複製代碼

則輸出

nil複製代碼

不推薦用法 強 try->try!

當咱們改爲強try!而且jsonString有問題的時候

let jsonString = "{\"name\": \"zhang\"]"
let data = jsonString.data(using: .utf8)

let json = try! JSONSerialization.jsonObject(with: data!, options: [])
print(json)複製代碼

則會直接崩潰,崩潰到try!的地方

Error Domain=NSCocoaErrorDomain Code=3840 "Badly formed object around character 16." UserInfo={NSDebugDescription=Badly formed object around character 16.}: file /Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-802.0.53/src/swift/stdlib/public/core/ErrorType.swift, line 182複製代碼

雖然會將錯誤信息完整的打印出來,可是程序崩潰對於用戶來講是很不友好的,所以不建議。

do...catch...

對於第二種狀況,咱們能夠採用do...catch...避免程序崩潰。

let jsonString = "{\"name\": \"zhang\"]"
let data = jsonString.data(using: .utf8)

do {
    let json = try JSONSerialization.jsonObject(with: data!, options: [])
    print(json)
} catch {
    print(error)
}複製代碼

程序能夠免於崩潰,可是會增長語法結構的複雜性,而且ARC開發中,編譯器自動添加retainreleaseautorelease,若是用do...catch...一旦不平衡,就會出現內存泄露的問題。因此若是當真用的時候要慎重!


監聽註冊和登陸按鈕的點擊事件

HQVistorView裏將兩個按鈕暴露出來,而後直接在HQBaseViewController中添加監聽方法便可。

/// 註冊按鈕
lazy var registerButton: UIButton = UIButton(hq_title: "註冊", color: UIColor.orange, backImageName: "common_button_white_disable")
/// 登陸按鈕
lazy var loginButton: UIButton = UIButton(hq_title: "登陸", color: UIColor.darkGray, backImageName: "common_button_white_disable")複製代碼
vistorView.loginButton.addTarget(self, action: #selector(login), for: .touchUpInside)
vistorView.registerButton.addTarget(self, action: #selector(register), for: .touchUpInside)複製代碼
// MARK: - 註冊/登陸 點擊事件
extension HQBaseViewController {

    @objc fileprivate func login() {
        print(#function)
    }
    @objc fileprivate func register() {
        print("bbb")
    }
}複製代碼

這裏之因此選擇直接addTarget方法,是由於這樣最簡單,若是用代理 / 閉包等方式會增長不少代碼。代理的合核心是解耦,當一個控件能夠不停的被複用的時候就選擇代理,好比TableViewDelegate中的didSelectRowAt indexPath:該方法是能夠在任何地方只要建立TableView均可能被用到的方法。所以,設置成Delegate

在這裏HQVistorViewHQBaseViewController是緊耦合的關係,HQVistorView能夠當作是從屬於HQBaseViewController。基本不會被在其它地方被用到。雖然是緊耦合,可是添加監聽方法特別簡單。是否須要解耦須要根據實際狀況判斷,不必爲了解耦而解耦,爲了模式而模式。

總結

  • 使用代理傳遞消息是爲了在控制器和視圖之間解耦,讓視圖可以被多個控制器複用,如TableView
  • 可是,若是視圖僅僅是爲了封裝代碼,而從控制器中剝離出來的,而且可以確認該視圖不會被其它控制器引用,則能夠直接經過addTarget的方式爲該視圖中的按鈕添加監聽方法
  • 這樣作的代價是耦合度高,控制器和視圖綁定在一塊兒,可是省略部分冗餘代碼

調整未登陸時導航按鈕

若是單純的在setupVistorView中設置leftBarButtonItemrightBarButtonItem,那麼在首頁就會出現左側的leftBarButtonItem變成了好友了,再點擊好友按鈕push出來的控制器的全部的返回按鈕都變成了註冊

而在未登陸狀態下,導航欄上面的按鈕都是顯示註冊登陸。登陸以後才顯示別的,所以,咱們能夠將HQBaseViewController中的setupUI方法設置成fileprivate不讓外界訪問到,而且將setupTableView設置成外界能夠訪問,若是須要在登陸後的控制器裏面顯示所需的樣式,只須要在各子類重寫setupTableView的方法裏從新設置leftBarButtonItem就能夠了。

/// 設置訪客視圖
fileprivate func setupVistorView() {

    navItem.leftBarButtonItem = UIBarButtonItem(title: "註冊", style: .plain, target: self, action: #selector(register))
    navItem.rightBarButtonItem = UIBarButtonItem(title: "登陸", style: .plain, target: self, action: #selector(login))
}複製代碼

使用CocoaPods管理一些咱們須要用到的第三方工具,這裏跳過。


封裝網絡工具單例

swift單例寫法

static let shared = HQNetWorkManager()複製代碼

objective-c單例寫法

+ (instancetype)sharedTools {

    static HQNetworkTools *tools;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        NSURL *baseURL = [NSURL URLWithString:HQBaseURL];
        tools = [[self alloc] initWithBaseURL:baseURL];

        tools.requestSerializer = [AFJSONRequestSerializer serializer];
        tools.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", @"text/html", @"text/plain", nil];
    });
    return tools;
}複製代碼

到此,咱們不要急於包裝網絡請求方法,應該先測試一下網絡請求通不通,實際中咱們也是同樣,先把要實現的主要目標先完成,而後再進行深層次的探究。

HQAViewController中加載數據測試

/// 加載數據
override func loadData() {

    let urlString = "https://api.weibo.com/2/statuses/home_timeline.json"
    let para = ["access_token": "2.00It5tsGQ6eDJE4ecbf2d825DCpbBD"]

    HQNetWorkManager.shared.get(urlString, parameters: para, progress: nil, success: { (_, json) in
        print(json ?? "")
    }) { (_, error) in
        print(error)
    }複製代碼

請求到的數據

{
    ad =     (
    );
    advertises =     (
    );
    "has_unread" = 0;
    hasvisible = 0;
    interval = 2000;
    "max_id" = 4130532835237793;
    "next_cursor" = 4130532835237793;
    "previous_cursor" = 0;
    "since_id" = 4130540976425281;
    statuses =     (
                {
            "attitudes_count" = 0;
            "biz_feature" = 0;
            "bmiddle_pic" = "http://wx3.sinaimg.cn/bmiddle/9603cdd7ly1fhmz6ui42tj20l414a0w7.jpg";
            "comment_manage_info" =             {
                "comment_permission_type" = "-1";
            };
            "comments_count" = 0;
            "created_at" = "Mon Jul 17 16:46:13 +0800 2017";複製代碼

封裝AFNetworkingGETPOST請求

注意:

若是你的閉包是這樣的寫法

func request(method: HQHTTPMethod = .GET, URLString: String, parameters: [String: Any], completion: (json: Any?, isSuccess: Bool)->()) {複製代碼

那麼在你調用completion這個閉包的時候,你可能會遇到下面的錯誤

Closure use of non-escaping parameter 'completion' may allow it to escape複製代碼

解決辦法直接按照Xcode的提示就能夠改正了,應該是下面的樣子

func request(method: HQHTTPMethod = .GET, URLString: String, parameters: [String: Any], completion: @escaping (_ json: Any?, _ isSuccess: Bool)->()) {複製代碼

From the Apple Developer docs

A closure is said to escape a function when the closure is passed as an argument to the function, but is called after the function returns. When you declare a function that takes a closure as one of its parameters, you can write @escaping before the parameter’s type to indicate that the closure is allowed to escape.

簡單總結:

由於該函數中的網絡請求方法,有一個參數completion: (json: Any?, isSuccess: Bool)->()是閉包。是在網絡請求方法執行完之後的完成回調。即閉包在函數執行完之後被調用了,調用的地方超過了request函數的範圍,這種閉包叫作逃逸閉包

swift 3.0中對閉包作了改變,默認請款下都是非逃逸閉包,再也不須要@noescape修飾。而若是你的閉包是在函數執行完之後再調用的,好比我舉例子的網絡請求完成回調,這種逃逸閉包,就須要用@escaping修飾。

若是你先仔細瞭解這方便的問題請閱讀Swift 3必看:@noescape走了, @escaping來了

網絡工具類HQNetWorkManager中的代碼

enum HQHTTPMethod {
    case GET
    case POST
}

class HQNetWorkManager: AFHTTPSessionManager {

    static let shared = HQNetWorkManager()

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

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

        let failure = { (task: URLSessionDataTask?, error: Error)->() in
            print("網絡請求錯誤 \(error)")
            completion(nil, false)
        }

        if method == .GET {
            get(URLString, parameters: parameters, progress: nil, success: success, failure: failure)
        } else {
            post(URLString, parameters: parameters, progress: nil, success: success, failure: failure)
        }

    }
}複製代碼

調整後的HQAViewController中加載數據的代碼

let urlString = "https://api.weibo.com/2/statuses/home_timeline.json"
let para = ["access_token": "2.00It5tsGQ6eDJE4ecbf2d825DCpbBD"]

// HQNetWorkManager.shared.get(urlString, parameters: para, progress: nil, success: { (_, json) in
// print(json ?? "")
// }) { (_, error) in
// print(error)
// }
HQNetWorkManager.shared.request(URLString: urlString, parameters: para) { (json, isSuccess) in
    print(json ?? "")
}複製代碼

利用extension封裝項目中網絡請求方法

HQAViewController中的網絡請求方法雖然進行了一些封裝,可是仍是要在控制器中填寫urlStringpara,若是能把這些也直接封裝到一個便於管理的地方,就更好了。這樣,當咱們偶一個網絡接口的url或者para有變化的話,咱們不用花費很長的時間去苦苦尋找究竟是在那個Controller中。

還有就是,返回的數據格式是這樣的

{
    ad =     (
    );
    advertises =     (
    );
    "has_unread" = 0;
    hasvisible = 0;
    interval = 2000;
    "max_id" = 4130532835237793;
    "next_cursor" = 4130532835237793;
    "previous_cursor" = 0;
    "since_id" = 4130540976425281;
    statuses =     (
                {
            "attitudes_count" = 0;
            "biz_feature" = 0;
            "bmiddle_pic" = "http://wx3.sinaimg.cn/bmiddle/9603cdd7ly1fhmz6ui42tj20l414a0w7.jpg";
            "comment_manage_info" =             {
                "comment_permission_type" = "-1";
            };
            "comments_count" = 0;
            "created_at" = "Mon Jul 17 16:46:13 +0800 2017";複製代碼

其實,只有statuses對應的數組纔是咱們須要的微博數據,其它的對於咱們來講,暫時都是沒有用的。通常的公司開發中,也返回相似的格式,只不過沒有微博這麼複雜罷了。

所以,若是能直接給控制器提供statuses的數據就最好了,controller直接拿到最有用的數據,並且包裝又少了一層。字典轉模型也方便一層。

extension HQNetWorkManager {

    /// 微博數據字典數組
    ///
    /// - Parameter completion: 微博字典數組/是否成功
    func statusList(completion: @escaping (_ list: [[String: AnyObject]]?, _ isSuccess: Bool)->()) {

        let urlString = "https://api.weibo.com/2/statuses/home_timeline.json"
        let para = ["access_token": "2.00It5tsGQ6eDJE4ecbf2d825DCpbBD"]

        request(URLString: urlString, parameters: para) { (json, isSuccess) in
            /* 從`json`中獲取`statuses`字典數組 若是`as?`失敗,`result = nil` */
            let result = (json as AnyObject)["statuses"] as? [[String: AnyObject]]
            completion(result, isSuccess)
        }
    }
}複製代碼

注意:

若是你下面這句話這樣寫,像objective-c那樣寫json["statuses"]就會報錯的。

let result = json["statuses"] as? [[String: AnyObject]]複製代碼

報以下錯誤:

Type 'Any?' has no subscript members複製代碼

須要改爲這樣

let result = (json as AnyObject)["statuses"] as? [[String: AnyObject]]複製代碼

接下來,控制器中HQAViewController的代碼就能夠簡化成這樣

HQNetWorkManager.shared.statusList { (list, isSuccess) in
    print(list ?? "")
}複製代碼

至此,HQAViewController中拿到的就是最有用的數組數據,下一步就直接字典轉模型就能夠了。和以前把網絡請求urlpara都放在controller相比,是否是,控制器輕鬆了一點呢!

封裝Token

項目中,全部的網絡請求,除了登錄之外,基本都須要token,所以,若是咱們能將token封裝起來,之後傳參數的時候,不用再考慮token相關的問題就最好了。

HQNetWorkManager中新建一個tokenRequest方法,該方法只是把以前的request方法調用一下,同時把token增長到該方法裏。使得在專門處理網絡請求的方法裏HQNetWorkManager+Extension不用再去考慮token相關的問題了。

/// token
var accessToken: String? = "2.00It5tsGQ6eDJE4ecbf2d825DCpbBD"

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

    guard let token = accessToken else {
        print("沒有 token 須要從新登陸")
        completion(nil, false)
        return
    }

    var parameters = parameters

    if parameters == nil {
        parameters = [String: AnyObject]()
    }

    parameters!["access_token"] = token as AnyObject

    request(URLString: URLString, parameters: parameters, completion: completion)
}複製代碼

這樣封裝之後,在HQNetWorkManager+Extension中再也不須要考慮token相關的問題,而且對controller代碼無侵害。

token 過時處理

由於token存在時效性,所以咱們須要對其判斷是否有效,若是token過時須要讓用戶從新登陸,或者進行其它頁面的跳轉等操做。

假如token過時,咱們仍然向服務器請求數據,那麼就會報錯

Error Domain=com.alamofire.error.serialization.response Code=-1011 
"Request failed: forbidden (403)"
UserInfo={
    com.alamofire.serialization.response.error.response=
  
  
  

 
  
  { URL: https://api.weibo.com/2/statuses/home_timeline.json?access_token=2.00It5tsGQ6eDJE4ecbf2d825DCpbBD111 } { status code: 403, headers { "Content-Encoding" = gzip; "Content-Type" = "application/json;charset=UTF-8"; Date = "Tue, 18 Jul 2017 07:54:51 GMT"; Server = "nginx/1.6.1"; Vary = "Accept-Encoding"; } }, NSErrorFailingURLKey=https://api.weibo.com/2/statuses/home_timeline.json?access_token=2.00It5tsGQ6eDJE4ecbf2d825DCpbBD111, com.alamofire.serialization.response.error.data=<7b226572 22657272="" 63657373="" 65737422="" 72657175="" 74757365="" 726f7222="" 3a22696e="" 76616c69="" 645f6163="" 5f746f6b="" 656e222c="" 6f725f63="" 6f646522="" 3a323133="" 33322c22="" 3a222f32="" 2f737461="" 732f686f="" 6d655f74="" 696d656c="" 696e652e="" 6a736f6e="" 227d="">, NSLocalizedDescription=Request failed: forbidden (403)} 
 
   

 複製代碼

咱們須要在網絡請求失敗的時候作個處理

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

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

        // FIXME: 發送通知,提示用戶再次登陸
    }

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

創建微博數據模型

HQStatus.swift中簡單定義兩個屬性

import YYModel

/// 微博數據模型
class HQStatus: NSObject {

    /* `Int`類型,在`64`位的機器是`64`位,在`32`位的機器是`32`位 若是不寫明`Int 64`在 iPad 2 / iPhone 5/5c/4s/4 都沒法正常運行 */
    /// 微博ID
    var id: Int64 = 0

    /// 微博信息內容
    var text: String?

    override var description: String {

        return yy_modelDescription()
    }
}複製代碼

創建視圖模型,封裝加載微博數據方法

viewModel的使命

  • 字典轉模型邏輯
  • 上拉 / 下拉數據處理邏輯
  • 下拉刷新數據數量
  • 本地緩存數據處理

初體驗

由於MVVMswift中都是沒有父類的,因此先說下關於父類的選擇問題

  • 若是分類須要使用KVC或者字典轉模型框架設置對象時,類就須要繼承自NSObject
  • 若是類只是包裝一些代碼邏輯(寫了一些函數),能夠不用繼承任何父類,好處: 更加輕量級

HQStatusListViewModel.swift不繼承任何父類

/// 微博數據列表視圖模型
class HQStatusListViewModel {

    lazy var statusList = [HQStatus]()

    func loadStatus(completion: @escaping (_ isSuccess: Bool)->()) {

        HQNetWorkManager.shared.statusList { (list, isSuccess) in

            guard let array = NSArray.yy_modelArray(with: HQStatus.classForCoder(), json: list ?? []) as? [HQStatus] else {

                completion(isSuccess)

                return
            }

            self.statusList += array

            completion(isSuccess)
        }
    }
}複製代碼

而後HQAViewController中加載數據的代碼就能夠簡化成這樣

fileprivate lazy var listViewModel = HQStatusListViewModel()

/// 加載數據
override func loadData() {

    listViewModel.loadStatus { (isSuccess) in
        self.refreshControl?.endRefreshing()
        self.isPullup = false
        self.tableView?.reloadData()
    }
}複製代碼

tableViewDataSource中直接調用HQStatusListViewModel中數據便可

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return listViewModel.statusList.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
    cell.textLabel?.text = listViewModel.statusList[indexPath.row].text
    return cell
}複製代碼

接下來運行程序應該能看到這樣的界面,目前因爲沒有處理下拉/下拉加載處理,所以只能看到20條微博數據。

DEMO傳送門:HQSwiftMVVM

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

參考:

  1. Swift 3 :Closure use of non-escaping parameter may allow it to escape
  2. Swift 3必看:@noescape走了, @escaping來了
相關文章
相關標籤/搜索