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

微信

相关文章
相关标签/搜索