前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Swift-MVVM 简单演练(四)

Swift-MVVM 简单演练(四)

作者头像
用户1890628
发布2018-05-10 17:40:39
2.2K0
发布2018-05-10 17:40:39
举报
文章被收录于专栏:Objective-CObjective-C

Swift-MVVM 简单演练(一)

Swift-MVVM 简单演练(二)

Swift-MVVM 简单演练(三)

前言

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


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

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

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

而且,这里不是所有的控件都直接写在cell里面的,那样太复杂,也不好处理业务逻辑。因此,将每一个cell大致分为四个模块:

  • 顶部视图(头像、昵称、会员图标、时间、来源、认证图标)
  • 微博正文
  • 配图视图
  • 底部视图(评论、转发点赞)
布局顶部视图HQACellTopView
代码语言:javascript
复制
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")
    }
}
代码语言:javascript
复制
// 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
代码语言:javascript
复制
/// 头像的宽度
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")
    }
}
代码语言:javascript
复制
// 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赋值
代码语言:javascript
复制
// 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()
    }

之前加载数据的代码

代码语言:javascript
复制
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的数据源方法里面赋值

代码语言:javascript
复制
// 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

代码语言:javascript
复制
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的属性

代码语言:javascript
复制
/// 用户属性信息
var user: HQUser?

到此为止,我们就可以拿到我们需要的信息了,虽然突然了一点,但是这都是基于YYModel的功劳。不管我们的数据嵌套多少层,都可以一句代码搞定。

yy_modelArray(with: AnyClass, json: Any)这句代码的功劳

代码语言:javascript
复制
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打印的信息

代码语言:javascript
复制
<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
代码语言:javascript
复制
class HQStatusViewModel {
    
    var status: HQStatus
    
    init(model: HQStatus) {
        self.status = model
    }
}
调整HQStatusListViewModel中代码

主要目的就是使HQStatusListViewModelHQStatus分离,通过HQStatusViewModel来联系之间的关系。

代码语言:javascript
复制
/// 微博数据列表视图模型
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的视图模型数组,如下

代码语言:javascript
复制
[
HQSwiftMVVM.HQStatusViewModel,
HQSwiftMVVM.HQStatusViewModel,
。
。
。
HQSwiftMVVM.HQStatusViewModel,
HQSwiftMVVM.HQStatusViewModel
]

代码对比

由于控制台输出上面的格式,非常不便于我们调试,这里再拓展一个小技巧。

如果一个类没有任何父类,在开发时需要输出调试信息,需要遵守如下规则:

  • 遵守CustomStringConvertible协议
  • 实现description方法
代码语言:javascript
复制
class HQStatusViewModel: CustomStringConvertible {
    
    var status: HQStatus
    
    init(model: HQStatus) {
        self.status = model
    }
    
    var description: String {
        return status.description
    }
}

此时再次运行程序,刚才的打印输出,就变成如下内容

代码语言:javascript
复制
[
。
。
。
<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中的代码并没有很大的改动。仅仅是下面赋值的时候稍微改动了一点点而已。

代码语言:javascript
复制
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方法里赋值。

代码语言:javascript
复制
class HQACell: UITableViewCell {

    var viewModel: HQStatusViewModel? {
        didSet {
            
            contentLabel.text = viewModel?.status.text
            topView.viewModel = viewModel
        }
    }

因为之前说过,我们是将自定义cell拆分成几个部分。那么昵称和头像这类的赋值就不能直接在cell中完成,我们只需要将viewModel传给topView,然后在topView中赋值就好了。

代码语言:javascript
复制
class HQACellTopView: UIView {

    var viewModel: HQStatusViewModel? {
        didSet {
            nameLabel.text = viewModel?.status.user?.screen_name
        }
    }

接下来,我们要做的就是在控制器中将viewModel传到cell中就可以了。

代码语言:javascript
复制
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方法中去写判断,大概代码是这样的

代码语言:javascript
复制
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里定义一个会员图标的属性,并且在视图模型里面处理不同等级显示不同图标的业务逻辑

代码语言:javascript
复制
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中设置会员图标

代码语言:javascript
复制
class HQACellTopView: UIView {

    var viewModel: HQStatusViewModel? {
        didSet {
            memberIconView.image = viewModel?.memberIcon
        }
    }

而且HQACell中的代码我们一点都没有改动,还是原来的样子

代码语言:javascript
复制
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中定义一个认证图标的图片属性
代码语言:javascript
复制
class HQStatusViewModel: CustomStringConvertible {
    
    /// 认证图标(-1:没有认证, 0:认证用户, 2,3,5:企业认证, 220:达人)
    var vipIcon: UIImage?
  • HQStatusViewModel中根据返回数据verified_type类型来设置vipIcon该显示哪张图标
代码语言:javascript
复制
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设置图像
代码语言:javascript
复制
class HQACellTopView: UIView {

    var viewModel: HQStatusViewModel? {
        didSet {
            vipIconImageView.image = viewModel?.vipIcon
        }
    }

这样设置的时候,就不用再像之前那样,好多的逻辑判断都放在viewviewModeldidSet方法里面去判断了。我们设置的时候,只需要将视图模型的属性直接赋值到相应的控件就好。是不是方便了很多。简化了代码。


隔离SDWebImage,设置头像

隔离SDWebImage

在项目中,我们经常会用到各种第三方框架,除了一些比较知名的框架以外,其它框架都存在这不稳定的因素,就算是知名的框架,也是总在更新的。为了以防万一,我们最好是能将第三方框架隔离出来。这样日后更换的时候也会省了不少的麻烦。

创建一个UIImageViewExtension,即HQImageView

SDWebImage的设置图像的方法封装起来

代码语言:javascript
复制
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)
            }
        }
    }
}

设置头像

代码语言:javascript
复制
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等级图标那样处理的话,认证图标周围四个角,会有白色的背景显示,会遮挡头像,效果非常不好,而我暂时也并没有太好的办法去处理,暂时就不对其做处理了。

如果用代码处理是这样的

代码语言:javascript
复制
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,下面这句代码

代码语言:javascript
复制
memberIconView.image = viewModel?.memberIcon?.hq_rectImage(size: memberIconView.bounds.size)

虽然控制台打印输出error,但是并没有影响程序的运行。报错如下

代码语言:javascript
复制
<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)

代码语言:javascript
复制
class HQACellTopView: UIView {

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

输出结果

代码语言:javascript
复制
memberIconView.bounds.size = (0.0, 0.0)

解决办法

目前我还没有想到什么比较好的解决办法,只是设置size的时候,给定了固定一个值

代码语言:javascript
复制
memberIconView.image = viewModel?.memberIcon?.hq_rectImage(size: CGSize(width: 17, height: 17))

这样控制台就不会再输出error

布局底部视图

按照之前的逻辑,将底部视图HQACellBottomView也拆分出来,方便逻辑的处理。

我先根据需要自定义封装了一个快速创建ButtonExtension

代码语言:javascript
复制
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()
    }

然后进行布局

代码语言:javascript
复制
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的上

代码语言:javascript
复制
class HQACell: UITableViewCell {

    /// 底部视图
    fileprivate lazy var bottomView: HQACellBottomView = HQACellBottomView()
代码语言:javascript
复制
// 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上面都是如果有转发评论都是显示对应的数量,否则只显示汉字。

先扩展模型,增加相应字段

代码语言:javascript
复制
/// 微博数据模型
class HQStatus: NSObject {
    
    /// 转发数
    var reposts_count: Int = 0
    /// 评论数
    var comments_count: Int = 0
    /// 表态数
    var attitudes_count: Int = 0

bottomView中赋值

代码语言:javascript
复制
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

代码语言:javascript
复制
class HQACell: UITableViewCell {

    var viewModel: HQStatusViewModel? {
        didSet {
            
            bottomView.viewModel = viewModel
        }
    }

效果如下所示

因为这里需要对返回数据进行处理,并且不同情况有不同的显示情况

  • 如果数量 == 0, 显示默认标题
  • 如果数量 >= 10000,显示 x.xx 万
  • 如果数量 < 10000, 显示实际数字

而这些逻辑当然都要交给ViewModel来处理了

首先定义对应的字符串变量

代码语言:javascript
复制
class HQStatusViewModel: CustomStringConvertible {
    
    /// 转发
    var retweetString: String?
    /// 评论
    var commentString: String?
    /// 赞
    var likeSting: String?

接下来,自定义一个方法,根据返回的数据,及我们的需求创建出不同字符串的方法

代码语言:javascript
复制
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)
    }

然后在视图模型的构造方法里面设置值

代码语言:javascript
复制
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中赋值

代码语言:javascript
复制
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做尽可能少的侵害。

代码语言:javascript
复制
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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017.09.06 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 搭界面、展示微博正文文字
    • 布局顶部视图HQACellTopView
      • 将HQACellTopView添加到HQACell中
        • 在控制器中给微博正文Label赋值
        • 完善微博数据模型
        • 视图模型的体会
          • 添加单条微博视图模型HQStatusViewModel
            • 调整HQStatusListViewModel中代码
              • 给表格控件赋值
                • 设置会员图标
                  • 关于性能的一点探讨
                    • 设置认证图标
                    • 隔离SDWebImage,设置头像
                      • 隔离SDWebImage
                        • 布局底部视图
                          • 给Cell的BottomView赋值
                          • 测试
                          • 小结
                            • 视图模型的作用
                            领券
                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档