Swift-MVVM 簡單演練(四)

Swift-MVVM 簡單演練(一)git

Swift-MVVM 簡單演練(二)github

Swift-MVVM 簡單演練(三)web

前言

這一篇主要寫微博的首頁佈局,及MVVM模式的體會。像微博這種自定義的Cell佈局略顯複雜一些,咱們最好將其拆分出來各個不一樣的模塊來處理比較好一些。不要像以前那樣,全部的控件都寫在一個cell裏面,那樣很差處理。雖說整體上來講,是學習MVVM模式,可是架構都是基於項目而設立的。脫離業務談什麼模式自己就不是很好。凡事有法,但法無定式。依我的習慣去延伸就好。不必非得說誰的代碼就必定是錯的。這樣真的不太好。json


搭界面、展現微博正文文字

凡事先揀簡單的東西去實現。沒有一蹴而就的事情。先看下接下來咱們要實現的目標,見下圖swift

主要就是將頭部的視圖(頭像、暱稱、會員圖標、時間、來源、認證圖標)微博正文先顯示出來再說。api

並且,這裏不是全部的控件都直接寫在cell裏面的,那樣太複雜,也很差處理業務邏輯。所以,將每個cell大體分爲四個模塊:數組

  • 頂部視圖(頭像、暱稱、會員圖標、時間、來源、認證圖標)
  • 微博正文
  • 配圖視圖
  • 底部視圖(評論、轉發點贊)

佈局頂部視圖HQACellTopView

class HQACellTopView: UIView {

    fileprivate lazy var carveView: UIView = {
        let view = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.hq_screenWidth(), height: 8))
        view.backgroundColor = UIColor.hq_color(withHex: 0xF2F2F2)
        return view
    }()
    /// 頭像
    fileprivate lazy var avatarImageView: UIImageView = UIImageView(hq_imageName: "avatar_default_big")
    /// 姓名
    fileprivate lazy var nameLabel: UILabel = UILabel(hq_title: "吳彥祖", fontSize: 14, color: UIColor.hq_color(withHex: 0xFC3E00))
    /// 會員
    fileprivate lazy var memberIconView: UIImageView = UIImageView(hq_imageName: "common_icon_membership_level1")
    /// 時間
    fileprivate lazy var timeLabel: UILabel = UILabel(hq_title: "如今", fontSize: 11, color: UIColor.hq_color(withHex: 0xFF6C00))
    /// 來源
    fileprivate lazy var sourceLabel: UILabel = UILabel(hq_title: "來源", fontSize: 11, color: UIColor.hq_color(withHex: 0x828282))
    /// 認證
    fileprivate lazy var vipIconImageView: UIImageView = UIImageView(hq_imageName: "avatar_vip")

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

        setupUI()
    }

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

    fileprivate func setupUI() {

        addSubview(carveView)
        addSubview(avatarImageView)
        addSubview(nameLabel)
        addSubview(memberIconView)
        addSubview(timeLabel)
        addSubview(sourceLabel)
        addSubview(vipIconImageView)

        avatarImageView.snp.makeConstraints { (make) in
            make.top.equalTo(carveView.snp.bottom).offset(margin)
            make.left.equalTo(self).offset(margin)
            make.width.equalTo(AvatarImageViewWidth)
            make.height.equalTo(AvatarImageViewWidth)
        }
        nameLabel.snp.makeConstraints { (make) in
            make.top.equalTo(avatarImageView).offset(4)
            make.left.equalTo(avatarImageView.snp.right).offset(margin - 4)
        }
        memberIconView.snp.makeConstraints { (make) in
            make.left.equalTo(nameLabel.snp.right).offset(margin / 2)
            make.centerY.equalTo(nameLabel)
        }
        timeLabel.snp.makeConstraints { (make) in
            make.left.equalTo(nameLabel)
            make.bottom.equalTo(avatarImageView)
        }
        sourceLabel.snp.makeConstraints { (make) in
            make.left.equalTo(timeLabel.snp.right).offset(margin / 2)
            make.centerY.equalTo(timeLabel)
        }
        vipIconImageView.snp.makeConstraints { (make) in
            make.centerX.equalTo(avatarImageView.snp.right)
            make.centerY.equalTo(avatarImageView.snp.bottom)
        }
    }
}複製代碼

HQACellTopView添加到HQACell

/// 頭像的寬度
let AvatarImageViewWidth: CGFloat = 35

class HQACell: UITableViewCell {

    /// 頂部視圖
    fileprivate lazy var topView: HQACellTopView = HQACellTopView()
    /// 正文
    lazy var contentLabel: UILabel = UILabel(hq_title: "正文", fontSize: 15, color: UIColor.darkGray)

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        setupUI()
    }

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

    fileprivate func setupUI() {

        addSubview(topView)
        addSubview(contentLabel)

        topView.snp.makeConstraints { (make) in
            make.top.equalTo(self)
            make.left.equalTo(self)
            make.right.equalTo(self)
            make.height.equalTo(margin * 2 + AvatarImageViewWidth)
        }
        contentLabel.snp.makeConstraints { (make) in
            make.top.equalTo(topView.snp.bottom).offset(margin / 2)
            make.left.equalTo(self).offset(margin)
            make.right.equalTo(self).offset(0)
            make.bottom.equalTo(self).offset(-margin / 2)
        }
    }
}複製代碼

在控制器中給微博正文Label賦值

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

    /// 重寫父類的方法
    override func setupTableView() {
        super.setupTableView()

        navItem.leftBarButtonItem = UIBarButtonItem(hq_title: "好友", target: self, action: #selector(showFriends))
        tableView?.register(HQACell.classForCoder(), forCellReuseIdentifier: HQACellId)
        tableView?.rowHeight = UITableViewAutomaticDimension
        tableView?.estimatedRowHeight = 400
        tableView?.separatorStyle = .none

        setupNavTitle()
    }複製代碼

以前加載數據的代碼微信

class HQAViewController: HQBaseViewController {

    fileprivate lazy var listViewModel = HQStatusListViewModel()

    /// 加載數據
    override func loadData() {
        listViewModel.loadStatus(pullup: self.isPullup) { (isSuccess, shouldRefresh) in
            print("最後一條微博數據是 \(self.listViewModel.statusList.last?.text ?? "")")

            self.refreshControl?.endRefreshing()
            self.isPullup = false

            if shouldRefresh {
                self.tableView?.reloadData()
            }
        }
    }複製代碼

tableView的數據源方法裏面賦值網絡

// MARK: - tableViewDataSource
extension HQAViewController {

    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: HQACellId, for: indexPath) as! HQACell
        cell.contentLabel.text = listViewModel.statusList[indexPath.row].text
        return cell
    }
}複製代碼

至此,咱們的第一個小目標就完成了。看着有幾分神似了。架構

完善微博數據模型

好友的頭像、暱稱等信息是存儲於每條微博數據的一個user屬性當中的。

咱們就須要再建立一個專門存儲用戶相關數據的模型HQUser

class HQUser: NSObject {

    // 基本數據類型設置成`Optional` 和 private類型修飾的 不能使用`KVC`設置
    var id: Int64 = 0
    /// 用戶暱稱
    var screen_name: String?
    /// 用戶頭像地址(中圖),50×50像素
    var profile_image_url: String?
    /// 認證類型,-1:沒有認證,0,認證用戶,2,3,5: 企業認證,220: 達人
    var verified_type: Int = 0
    /// 會員等級 0-6
    var mbrank: Int = 0

    override var description: String {
        return yy_modelDescription()
    }
}複製代碼

而後在以前的HQStatus模型中增長一個user的屬性

/// 用戶屬性信息
var user: HQUser?複製代碼

到此爲止,咱們就能夠拿到咱們須要的信息了,雖然忽然了一點,可是這都是基於YYModel的功勞。無論咱們的數據嵌套多少層,均可以一句代碼搞定。

yy_modelArray(with: AnyClass, json: Any)這句代碼的功勞

HQNetWorkManager.shared.statusList(since_id: since_id, max_id: max_id) { (list, isSuccess) in

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

        completion(isSuccess, false)

        return
    }
    print("刷新到 \(array.count) 條數據 \(array)")複製代碼

array打印的信息

 
  
  { id = 4146112736022810; text = "【男子將老人拖行至路邊,只因嫌其走路慢?】8月20日,俄羅斯媒體報道,一名男子因喝醉酒,嫌棄老人過馬路走太慢,竟將其拖行至路邊,遭到網友譴責。不過,也有網友看完視頻後替該男子說話,認爲對向車道的汽車沒有要停下的意思,他應該是擔憂發生危險,出於好意才上前拉住老人,事件仍在調查中。@微丟...全文: http://m.weibo.cn/1887344341/4146112736022810"; user = 
 
  
    { id = 1887344341; mbrank = 5; profile_image_url = "http://tva1.sinaimg.cn/crop.0.0.599.599.50/707e96d5gw1f88661z1prj20go0goabq.jpg"; screen_name = "觀察者網"; verified_type = 5 } } 
   

 複製代碼

視圖模型的體會

如今咱們的代碼裏面結構

  • HQAViewController首頁控制器
  • HQStatusListViewModel負責加載數據的視圖模型
  • HQStatus數據模型

控制器HQAViewController經過加載數據的視圖模型HQStatusListViewModel取得數據,可是HQStatusListViewModel加載的仍是HQStatus數據模型。

HQStatusListViewModel是引用着HQStatus的,而HQStatusListViewModel又是被HQAViewController引用的。至關於控制器仍是在直接使用模型。

爲了解決上面的問題,須要將加載數據的視圖模型HQStatusListViewModelHQStatus之間的相互引用打斷。所以,才引入了視圖模型(在這裏指單條微博的視圖模型),用於處理單條微博的全部的業務邏輯。至關於把以前寫在View和部分寫在Controller中的代碼抽取到這裏,達到ControllerView瘦身的做用。

添加單條微博視圖模型HQStatusViewModel

class HQStatusViewModel {

    var status: HQStatus

    init(model: HQStatus) {
        self.status = model
    }
}複製代碼

調整HQStatusListViewModel中代碼

主要目的就是使HQStatusListViewModelHQStatus分離,經過HQStatusViewModel來聯繫之間的關係。

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

    /// 微博視圖模型的懶加載
    lazy var statusList = [HQStatusViewModel]()

    /// 上拉刷新錯誤次數
    fileprivate var pullupErrorTimes = 0

    /// 加載微博數據字典數組
    ///
    /// - Parameters:
    /// - completion: 完成回調,微博字典數組/是否成功
    func loadStatus(pullup: Bool, completion: @escaping (_ isSuccess: Bool, _ shouldRefresh: Bool)->()) {

        if pullup && pullupErrorTimes > maxPullupTryTimes {

            completion(true, false)
            print("超出3次 再也不走網絡請求方法")
            return
        }

        // 取出微博中已經加載的第一條微博(最新的一條微博)的`since_id`進行比較,對下拉刷新作處理
        let since_id = pullup ? 0 : (statusList.first?.status.id ?? 0)
        // 上拉刷新,取出數組的最後一條微博`id`
        let max_id = !pullup ? 0 : (statusList.last?.status.id ?? 0)

        HQNetWorkManager.shared.statusList(since_id: since_id, max_id: max_id) { (list, isSuccess) in

            // 若是網絡請求失敗,直接執行完成回調
            if !isSuccess {

                completion(false, false)
                return
            }

            /* 遍歷字典數組,字典轉模型 模型->視圖模型 將視圖模型添加到數組 */
            var arrayM = [HQStatusViewModel]()

            for dict in list ?? [] {

                // 建立微博模型
                let status = HQStatus()

                // 字典轉模型
                status.yy_modelSet(with: dict)

                // 使用`HQStatus`建立`HQStatusViewModel`
                let viewModel = HQStatusViewModel(model: status)

                // 添加到數組
                arrayM.append(viewModel)
            }

            print(arrayM)
        }
    }
}複製代碼

至此,打印輸出arrayMHQStatusViewModel的視圖模型數組,以下

[
HQSwiftMVVM.HQStatusViewModel,
HQSwiftMVVM.HQStatusViewModel,
。
。
。
HQSwiftMVVM.HQStatusViewModel,
HQSwiftMVVM.HQStatusViewModel
]複製代碼

代碼對比

因爲控制檯輸出上面的格式,很是不便於咱們調試,這裏再拓展一個小技巧。

若是一個類沒有任何父類,在開發時須要輸出調試信息,須要遵照以下規則:

  • 遵照CustomStringConvertible協議
  • 實現description方法
class HQStatusViewModel: CustomStringConvertible {

    var status: HQStatus

    init(model: HQStatus) {
        self.status = model
    }

    var description: String {
        return status.description
    }
}複製代碼

此時再次運行程序,剛纔的打印輸出,就變成以下內容

[
。
。
。
<HQSwiftMVVM.HQStatus: 0x608000272140> {
    id = 4146549921682611;
    text = "【零難度照燒雞腿便當!】開學了,你可別輸在「起跑飯」上@罐頭視頻http://t.cn/RN2e2EF";
    user = <HQSwiftMVVM.HQUser: 0x6080002c3790> {
        id = 1977460817;
        mbrank = 4;
        profile_image_url = "http://tva4.sinaimg.cn/crop.6.5.171.171.50/75dda851jw8ev8xowav75j2050050aa5.jpg";
        screen_name = "網絡新聞聯播";
        verified_type = 3
    }
}
]複製代碼

這樣就很是直觀了,咱們就能夠愉快的繼續玩耍了。

雖然增長了HQStatusViewModel這個單條微博的視圖模型,而且對負責加載數據的HQStatusListViewModel視圖模型進行了調整,使其和HQStatus直接分離。可是實際上咱們在HQAViewController中的代碼並無很大的改動。僅僅是下面賦值的時候稍微改動了一點點而已。

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

    let cell = tableView.dequeueReusableCell(withIdentifier: HQACellId, for: indexPath) as! HQACell

    let viewModel = listViewModel.statusList[indexPath.row]

    cell.contentLabel.text = viewModel.status.text

    return cell複製代碼

給表格控件賦值

之前咱們的套路是,在自定義cellmodel屬性的set方法裏賦值。如今仍然延續以前的套路。

在自定義cellviewModel屬性的didSet方法裏賦值。

class HQACell: UITableViewCell {

    var viewModel: HQStatusViewModel? {
        didSet {

            contentLabel.text = viewModel?.status.text
            topView.viewModel = viewModel
        }
    }複製代碼

由於以前說過,咱們是將自定義cell拆分紅幾個部分。那麼暱稱和頭像這類的賦值就不能直接在cell中完成,咱們只須要將viewModel傳給topView,而後在topView中賦值就行了。

class HQACellTopView: UIView {

    var viewModel: HQStatusViewModel? {
        didSet {
            nameLabel.text = viewModel?.status.user?.screen_name
        }
    }複製代碼

接下來,咱們要作的就是在控制器中將viewModel傳到cell中就能夠了。

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

    let cell = tableView.dequeueReusableCell(withIdentifier: HQACellId, for: indexPath) as! HQACell

    let viewModel = listViewModel.statusList[indexPath.row]

    cell.viewModel = viewModel複製代碼

到此,咱們實現的效果是正文和暱稱能夠正常顯示了

到這裏其實就應該多多少少能體會到視圖模型的一點點好處了。

  • 有專門負責加載數據的視圖模型
  • 有專門處理業務邏輯的視圖模型
  • 控制器和模型之間能夠解除耦合
  • 視圖能夠進一步拆分,各處耦合性都不是很大,並且又比較容易處理邏輯問題

可是如今爲止,尚未徹底發揮出視圖模型的最大功能,繼續往下看!

設置會員圖標

這裏就能展現出視圖模型的優勢了,會員分不一樣的等級對應不一樣的圖標,咱們要根據返回的mbrank的值,來給會員圖標的ImageView設置圖像。若是是之前,咱們就須要在celldidSet方法中去寫判斷,大概代碼是這樣的

class HQACell: UITableViewCell {

    var viewModel: HQStatusViewModel? {
        didSet {

            contentLabel.text = viewModel?.status.text

            // 會員等級
            if (viewModel?.status.user?.mbrank)! > 0 && (viewModel?.status.user?.mbrank)! < 7 {
                let imageName = "common_icon_membership_level\(viewModel?.status.user?.mbrank ?? 1)"
                memberIconView.image = UIImage(named: imageName)
            }
        }
    }複製代碼

可能你會感受沒什麼,平時就這麼寫的啊。可是這麼小的一個控件都要這幾行代碼塞在這裏。每一條微博有那麼多控件,都在這裏一個一個判斷嗎?

並且這個控件的邏輯判斷算是簡單的,若是邏輯判斷複雜的就不是4行代碼的事情了。

試着把代碼這部分代碼放到viewModel中嘗試一下。

在單條視圖模型HQStatusViewModel裏定義一個會員圖標的屬性,而且在視圖模型裏面處理不一樣等級顯示不一樣圖標的業務邏輯

class HQStatusViewModel: CustomStringConvertible {

    var status: HQStatus

    /// 會員圖標
    var memberIcon: UIImage?

    init(model: HQStatus) {
        self.status = model

        // 會員等級
        if (model.user?.mbrank)! > 0 && (model.user?.mbrank)! < 7 {
            let imageName = "common_icon_membership_level\(model.user?.mbrank ?? 1)"
            memberIcon = UIImage(named: imageName)
        }
    }複製代碼

而後再回到自定義的HQACellTopView中設置會員圖標

class HQACellTopView: UIView {

    var viewModel: HQStatusViewModel? {
        didSet {
            memberIconView.image = viewModel?.memberIcon
        }
    }複製代碼

並且HQACell中的代碼咱們一點都沒有改動,仍是原來的樣子

class HQACell: UITableViewCell {

    var viewModel: HQStatusViewModel? {
        didSet {

            contentLabel.text = viewModel?.status.text
            topView.viewModel = viewModel
        }
    }複製代碼

到這裏是否是有點感受了。漸漸的體會到視圖模型的好處了吧。不只是爲控制器瘦身,連View的代碼都比以前更少更清晰了。

關於性能的一點探討

以前在didSet方法中設置時,若是是表格,每次滾出屏幕再滾動回來的時候都要從新執行didSet方法,從新計算。不斷的消耗CPU。必定會多多少少影響一點性能的。

而在ViewModel中的咱們自定義的memberIcon是一個存儲型屬性,在init構造函數中,直接計算出該是哪一個會員圖標。計算好之後,下次就能夠直接使用,再也不須要計算了。這樣會比較耗內存,可是內存獲得警告的話,咱們能夠去釋放內存。可是CPU消耗的多了,就會直接形成表格的卡頓。

關於表格性能的優化:

  • 儘可能少計算,全部須要的素材提早計算好。
  • 控件上不要設置圓角半徑,全部圖像渲染的屬性都要注意。
  • 不要動態建立控件,全部須要的控件,都要提早建立好,根據須要來隱藏/顯示
  • 全部的目的都是爲了減小CPU的消耗,用內存來換CPU

設置認證圖標

按照設置會員圖標的思路來設置認證圖標

  • HQStatusViewModel中定義一個認證圖標的圖片屬性
class HQStatusViewModel: CustomStringConvertible {

    /// 認證圖標(-1:沒有認證, 0:認證用戶, 2,3,5:企業認證, 220:達人)
    var vipIcon: UIImage?複製代碼
  • HQStatusViewModel中根據返回數據verified_type類型來設置vipIcon該顯示哪張圖標
class HQStatusViewModel: CustomStringConvertible {

    init(model: HQStatus) {
        self.status = model

        // 認證圖標
        switch model.user?.verified_type ?? -1 {
        case 0:
            vipIcon = UIImage(named: "avatar_vip")
        case 2, 3, 5:
            vipIcon = UIImage(named: "avatar_enterprise_vip")
        case 220:
            vipIcon = UIImage(named: "avatar_grassroot")
        default:
            break
        }
    }複製代碼
  • HQACellTopViewviewModeldidSet方法中爲vipIconImageView設置圖像
class HQACellTopView: UIView {

    var viewModel: HQStatusViewModel? {
        didSet {
            vipIconImageView.image = viewModel?.vipIcon
        }
    }複製代碼

這樣設置的時候,就不用再像以前那樣,好多的邏輯判斷都放在viewviewModeldidSet方法裏面去判斷了。咱們設置的時候,只須要將視圖模型的屬性直接賦值到相應的控件就好。是否是方便了不少。簡化了代碼。


隔離SDWebImage,設置頭像

隔離SDWebImage

在項目中,咱們常常會用到各類第三方框架,除了一些比較知名的框架之外,其它框架都存在這不穩定的因素,就算是知名的框架,也是總在更新的。爲了以防萬一,咱們最好是能將第三方框架隔離出來。這樣往後更換的時候也會省了很多的麻煩。

建立一個UIImageViewExtension,即HQImageView

SDWebImage的設置圖像的方法封裝起來

import UIKit
import SDWebImage

// MARK: - 隔離`SDWebImage框架`
extension UIImageView {

    /// 隔離`SDWebImage`設置圖像函數
    ///
    /// - Parameters:
    /// - urlString: urlString
    /// - placeholderImage: placeholderImage
    /// - isAvatar: 是不是頭像(圓角)
    func hq_setImage(urlString: String?, placeholderImage: UIImage?, isAvatar: Bool = false) {

        guard let urlString = urlString,
            let url = URL(string: urlString)
            else {

                image = placeholderImage
                return
        }

        sd_setImage(with: url, placeholderImage: placeholderImage, options: []) { [weak self] (image, _, _, _) in

            if isAvatar {
                self?.image = image?.hq_avatarImage(size: self?.bounds.size)
            } else {
                self?.image = image?.hq_rectImage(size: self?.bounds.size)
            }
        }
    }
}複製代碼

設置頭像

class HQACellTopView: UIView {

    var viewModel: HQStatusViewModel? {
        didSet {
            avatarImageView.hq_setImage(urlString: viewModel?.status.user?.profile_image_url, placeholderImage: UIImage(named: "avatar_default_big"), isAvatar: true)
            memberIconView.image = viewModel?.memberIcon?.hq_rectImage(size: memberIconView.bounds.size)
        }
    }複製代碼

Color Blended Layers效果以下

Color Misaligned Images效果以下

能夠看到,通過代碼設置之後,頭像vip等級圖標已經徹底沒有問題了。

可是,頭像右下角的認證圖標仍是存在問題的。而我並無去處理它,由於,若是像處理vip等級圖標那樣處理的話,認證圖標周圍四個角,會有白色的背景顯示,會遮擋頭像,效果很是很差,而我暫時也並無太好的辦法去處理,暫時就不對其作處理了。

若是用代碼處理是這樣的

class HQACellTopView: UIView {

    var viewModel: HQStatusViewModel? {
        didSet {
// vipIconImageView.image = viewModel?.vipIcon?.hq_rectImage(size: vipIconImageView.bounds.size)
            vipIconImageView.image = viewModel?.vipIcon?.hq_rectImage(size: CGSize(width: 30, height: 30))
        }
    }複製代碼

效果是這樣的

雖然在Color Blended Layers模式下,不會有紅色的問題,可是這裏真的不能那樣作

補充:

若是設置hq_rectImage控制檯會打印error,下面這句代碼

memberIconView.image = viewModel?.memberIcon?.hq_rectImage(size: memberIconView.bounds.size)複製代碼

雖然控制檯打印輸出error,可是並無影響程序的運行。報錯以下

 
  
  : CGContextSetFillColorWithColor: invalid context 0x0. If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable. 
 
  
    : CGContextGetCompositeOperation: invalid context 0x0. If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable. 
   
     : CGContextSetCompositeOperation: invalid context 0x0. If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable. 
    
      : CGContextFillRects: invalid context 0x0. If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable. 
     
    
   

 複製代碼

緣由是由於在cell佈局的時候,有時memberIconView.bounds.size的值爲(0.0, 0.0)

class HQACellTopView: UIView {

    var viewModel: HQStatusViewModel? {
        didSet {
            print("memberIconView.bounds.size = \(memberIconView.bounds.size)")
            memberIconView.image = viewModel?.memberIcon?.hq_rectImage(size: memberIconView.bounds.size)複製代碼

輸出結果

memberIconView.bounds.size = (0.0, 0.0)複製代碼

解決辦法

目前我尚未想到什麼比較好的解決辦法,只是設置size的時候,給定了固定一個值

memberIconView.image = viewModel?.memberIcon?.hq_rectImage(size: CGSize(width: 17, height: 17))複製代碼

這樣控制檯就不會再輸出error

佈局底部視圖

按照以前的邏輯,將底部視圖HQACellBottomView也拆分出來,方便邏輯的處理。

我先根據須要自定義封裝了一個快速建立ButtonExtension

extension UIButton {

    /// 標題 + 字號 + 文字顏色 + 圖片 + 背景圖片
    ///
    /// - Parameters:
    /// - hq_title: title
    /// - fontSize: fontSize
    /// - color: color
    /// - imageName: 圖片
    /// - backImage: 背景圖片
    /// - titleEdge: 圖片和文字間距
    convenience init(hq_title: String, fontSize: CGFloat, color: UIColor, imageName: String, backImage: String, titleEdge: CGFloat) {
        self.init()

        setTitle(hq_title, for: .normal)
        titleLabel?.font = UIFont.systemFont(ofSize: fontSize)
        setTitleColor(color, for: .normal)
        setImage(UIImage(named: imageName), for: .normal)

        setBackgroundImage(UIImage(named: backImage), for: .normal)

        titleEdgeInsets = UIEdgeInsetsMake(0, titleEdge, 0, -titleEdge)

        sizeToFit()
    }複製代碼

而後進行佈局

class HQACellBottomView: UIView {

    /// 轉發
    fileprivate lazy var retweetedButton: UIButton = UIButton(hq_title: " 轉發", fontSize: 12, color: UIColor.darkGray, imageName: "timeline_icon_retweet", backImage: "timeline_card_bottom_background", titleEdge: 5)
    /// 評論
    fileprivate lazy var commentButton: UIButton = UIButton(hq_title: " 評論", fontSize: 12, color: UIColor.darkGray, imageName: "timeline_icon_comment", backImage: "timeline_card_bottom_background", titleEdge: 5)
    /// 贊
    fileprivate lazy var likeButton: UIButton = UIButton(hq_title: " 贊", fontSize: 12, color: UIColor.darkGray, imageName: "timeline_icon_unlike", backImage: "timeline_card_bottom_background", titleEdge: 5)
    /// 分割線
    fileprivate lazy var sepView01: UIImageView = UIImageView(hq_imageName: "timeline_card_bottom_line_highlighted")
    /// 分割線
    fileprivate lazy var sepView02: UIImageView = UIImageView(hq_imageName: "timeline_card_bottom_line_highlighted")

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

        setupUI()
    }

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

// MARK: - UI
extension HQACellBottomView {

    fileprivate func setupUI() {

        backgroundColor = UIColor(white: 0.9, alpha: 1.0)

        addSubview(retweetedButton)
        addSubview(commentButton)
        addSubview(likeButton)
        addSubview(sepView01)
        addSubview(sepView02)

        retweetedButton.snp.makeConstraints { (make) in
            make.top.equalTo(self)
            make.left.equalTo(self)
            make.bottom.equalTo(self)
        }
        commentButton.snp.makeConstraints { (make) in
            make.top.equalTo(retweetedButton)
            make.left.equalTo(retweetedButton.snp.right)
            make.width.equalTo(retweetedButton)
            make.height.equalTo(retweetedButton)
        }
        likeButton.snp.makeConstraints { (make) in
            make.top.equalTo(commentButton)
            make.left.equalTo(commentButton.snp.right)
            make.width.equalTo(commentButton)
            make.height.equalTo(commentButton)
            make.right.equalTo(self)
        }
        sepView01.snp.makeConstraints { (make) in
            make.right.equalTo(retweetedButton)
            make.centerY.equalTo(retweetedButton)
        }
        sepView02.snp.makeConstraints { (make) in
            make.right.equalTo(commentButton)
            make.centerY.equalTo(commentButton)
        }
    }
}複製代碼

而後將bottomView添加到cell的上

class HQACell: UITableViewCell {

    /// 底部視圖
    fileprivate lazy var bottomView: HQACellBottomView = HQACellBottomView()複製代碼
// MARK: - UI
extension HQACell {

    fileprivate func setupUI() {

        addSubview(bottomView)

        bottomView.snp.makeConstraints { (make) in
            make.top.equalTo(contentLabel.snp.bottom).offset(margin)
            make.left.equalTo(self)
            make.right.equalTo(self)
            make.height.equalTo(44)
            make.bottom.equalTo(self)
        }複製代碼

顯示效果以下所示

CellBottomView賦值

bottomView的每一個Button上面都是若是有轉發評論都是顯示對應的數量,不然只顯示漢字。

先擴展模型,增長相應字段

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

    /// 轉發數
    var reposts_count: Int = 0
    /// 評論數
    var comments_count: Int = 0
    /// 表態數
    var attitudes_count: Int = 0複製代碼

bottomView中賦值

class HQACellBottomView: UIView {

    var viewModel: HQStatusViewModel? {
        didSet {
            retweetedButton.setTitle("\(viewModel?.status.reposts_count)", for: .normal)
            commentButton.setTitle("\(viewModel?.status.comments_count)", for: .normal)
            likeButton.setTitle("\(viewModel?.status.attitudes_count)", for: .normal)
        }
    }複製代碼

viewModel傳到bottomViewviewModel

class HQACell: UITableViewCell {

    var viewModel: HQStatusViewModel? {
        didSet {

            bottomView.viewModel = viewModel
        }
    }複製代碼

效果以下所示

由於這裏須要對返回數據進行處理,而且不一樣狀況有不一樣的顯示狀況

  • 若是數量 == 0, 顯示默認標題
  • 若是數量 >= 10000,顯示 x.xx 萬
  • 若是數量 < 10000, 顯示實際數字

而這些邏輯固然都要交給ViewModel來處理了

首先定義對應的字符串變量

class HQStatusViewModel: CustomStringConvertible {

    /// 轉發
    var retweetString: String?
    /// 評論
    var commentString: String?
    /// 贊
    var likeSting: String?複製代碼

接下來,自定義一個方法,根據返回的數據,及咱們的需求建立出不一樣字符串的方法

class HQStatusViewModel: CustomStringConvertible {

    /// 給定一個數字,返回對應的描述結果
    ///
    /// - Parameters:
    /// - count: 數字
    /// - defaultString: 默認字符串(轉發、評論、贊)
    fileprivate func countString(count: Int, defaultString: String) -> String {

        if count == 0 {
            return defaultString
        }

        if count < 10000 {
            return count.description
        }

        return String(format: "%0.2f 萬", CGFloat(count)  / 10000)
    }複製代碼

而後在視圖模型的構造方法裏面設置值

class HQStatusViewModel: CustomStringConvertible {

    init(model: HQStatus) {

        // 轉發、評論、贊
        retweetString = countString(count: model.reposts_count, defaultString: "轉發")
        commentString = countString(count: model.comments_count, defaultString: "評論")
        likeSting = countString(count: model.attitudes_count, defaultString: "贊")複製代碼

最後一步,在HQACellBottomView中賦值

class HQACellBottomView: UIView {

    var viewModel: HQStatusViewModel? {
        didSet {
            retweetedButton.setTitle(viewModel?.retweetString, for: .normal)
            commentButton.setTitle(viewModel?.commentString, for: .normal)
            likeButton.setTitle(viewModel?.likeSting, for: .normal)
        }
    }複製代碼

效果以下


測試

開發中,任何一個可能的狀況咱們都要儘量 的測試到,不然過了好久之後再發現問題,極可能就找不到有問題的地方了。

這裏,咱們還缺乏數量超過10000的狀況,因此咱們須要本身造數據測試一下

由於是視圖模型處理業務邏輯,所以,測試的時候,咱們直接在視圖模型裏面處理就好。這樣會對ViewController作儘量少的侵害。

class HQStatusViewModel: CustomStringConvertible {

    init(model: HQStatus) {
        self.status = model

        // 測試數量超過`10000`的狀況
        model.reposts_count = Int(arc4random_uniform(100000))
        // 轉發、評論、贊
        retweetString = countString(count: model.reposts_count, defaultString: "轉發")
        commentString = countString(count: model.comments_count, defaultString: "評論")
        likeSting = countString(count: model.attitudes_count, defaultString: "贊")複製代碼

效果以下


小結

視圖模型的做用

  • 把要計算的業務邏輯所有抽取出去
  • 在視圖中,須要什麼,直接去視圖模型中取相關的屬性
  • 視圖裏面再也不須要考慮計算相關的問題

DEMO傳送門:HQSwiftMVVM

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


最後,發個求職廣告。小弟最近在求職,現工做在北京,準備去杭州發展,有願意幫忙推薦、介紹、或者拋出橄欖枝的,在下感激涕零!

聯繫方式

郵箱:

  • 13120010341@163.com

微信

相關文章
相關標籤/搜索