Swift-MVVM 简单演练(四)

Swift-MVVM 简单演练(一)

Swift-MVVM 简单演练(二)

Swift-MVVM 简单演练(三)

前言

这一篇主要写微博的首页布局,及MVVM模式的体会。像微博这种自定义的Cell布局略显复杂一些,我们最好将其拆分出来各个不同的模块来处理比较好一些。不要像之前那样,所有的控件都写在一个cell里面,那样不好处理。虽然说总体上来说,是学习MVVM模式,但是架构都是基于项目而设立的。脱离业务谈什么模式本身就不是很好。凡事有法,但法无定式。依个人习惯去延伸就好。没必要非得说谁的代码就一定是错的。这样真的不太好。


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

凡事先拣简单的东西去实现。没有一蹴而就的事情。先看下接下来我们要实现的目标,见下图

主要就是将头部的视图(头像、昵称、会员图标、时间、来源、认证图标)微博正文先显示出来再说。

而且,这里不是所有的控件都直接写在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打印的信息

<HQSwiftMVVM.HQStatus: 0x60000027bd00> {
    id = 4146112736022810;
    text = "【男子将老人拖行至路边,只因嫌其走路慢?】8月20日,俄罗斯媒体报道,一名男子因喝醉酒,嫌弃老人过马路走太慢,竟将其拖行至路边,遭到网友谴责。不过,也有网友看完视频后替该男子说话,认为对向车道的汽车没有要停下的意思,他应该是担心发生危险,出于好意才上前拉住老人,事件仍在调查中。@微丢...全文: http://m.weibo.cn/1887344341/4146112736022810";
    user = <HQSwiftMVVM.HQUser: 0x6000000d5230> {
        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,但是并没有影响程序的运行。报错如下

<Error>: CGContextSetFillColorWithColor: invalid context 0x0. If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable.
<Error>: CGContextGetCompositeOperation: invalid context 0x0. If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable.
<Error>: CGContextSetCompositeOperation: invalid context 0x0. If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable.
<Error>: 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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏HT

HTML5五种客户端离线存储方案

最近折腾HTML5游戏需要离线存储功能,便把目前可用的几种HTML5存储方式研究了下,基于HT for Web写了个综合的实例,分别利用了Cookie、WebS...

25860
来自专栏小壮和前端

js手写俄罗斯方块

15210
来自专栏walterlv - 吕毅的博客

WPF 中使用附加属性,将任意 UI 元素或控件裁剪成圆形(椭圆)

发布于 2018-06-15 01:22 更新于 2018-09...

17230
来自专栏阮一峰的网络日志

关于Lorem ipsum

Wikipedia上的解释是,这只是一段用来测试排版效果的占位文字,没有实际的含义。据说,16世纪的时候就有人开始用了。当时的某个印刷工人,从古罗马政治家西塞罗...

18010
来自专栏菩提树下的杨过

WritableBitmapEx 一瞥

今天在蓝色上看到一篇介绍WritableBitmapEx的贴子(是开源项目),项目地址:http://writeablebitmapex.codeplex.co...

20580
来自专栏CDA数据分析师

Python 爬取北京二手房数据,分析北漂族买得起房吗? | 附完整源码

本文主要分为两部分:Python爬取赶集网北京二手房数据,R对爬取的二手房房价做线性回归分析,适合刚刚接触Python&R的同学们学习参考。

14720
来自专栏iOS开发随笔

iOS 固定UITableView的cell.imageView.image图片大小

22240
来自专栏君赏技术博客

想要漂亮的蒙版指引吗?跟着我手把手的教你写出来

既然接到了这个需求,那么就需要做出来,第一眼看上去确实有点难。当时安卓已经找到了对应的库,十分容易的做这个需求了。

13120
来自专栏Android点滴积累

Android高效内存之让你的图片省内存

Android高效内存之让你的图片省内存        在做内存优化的时候,我们发现除了解决内存泄露问题,剩下的就只有想办法减少真实的内存占用。而在App中,大...

230110
来自专栏清墨_iOS分享

iOS UIStepper实现数量递增递减

很多app都有这样的功能:(大家一看就能懂吧) ? DBF08F90-FB6B-424B-9240-AB893A576065.png 这个功能我们要实现的话,估...

32850

扫码关注云+社区

领取腾讯云代金券