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
引用的。相当于控制器还是在直接使用模型。
为了解决上面的问题,需要将加载数据的视图模型HQStatusListViewModel
和HQStatus
之间的相互引用打断。因此,才引入了视图模型(在这里指单条微博的视图模型)
,用于处理单条微博的所有的业务逻辑。相当于把之前写在View
和部分写在Controller
中的代码抽取到这里,达到Controller
和View
瘦身的作用。
添加单条微博视图模型HQStatusViewModel
class HQStatusViewModel {
var status: HQStatus
init(model: HQStatus) {
self.status = model
}
}
调整HQStatusListViewModel
中代码
主要目的就是使HQStatusListViewModel
和HQStatus
分离,通过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)
}
}
}
至此,打印输出arrayM
为HQStatusViewModel
的视图模型数组,如下
[
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
给表格控件赋值
以前我们的套路是,在自定义cell
的model
属性的set
方法里赋值。现在仍然延续之前的套路。
在自定义cell
的viewModel
属性的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
设置图像。如果是以前,我们就需要在cell
的didSet
方法中去写判断,大概代码是这样的
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
}
}
- 在
HQACellTopView
中viewModel
的didSet
方法中为vipIconImageView
设置图像
class HQACellTopView: UIView {
var viewModel: HQStatusViewModel? {
didSet {
vipIconImageView.image = viewModel?.vipIcon
}
}
这样设置的时候,就不用再像之前那样,好多的逻辑判断都放在view
的viewModel
的didSet
方法里面去判断了。我们设置的时候,只需要将视图模型的属性直接赋值到相应的控件就好。是不是方便了很多。简化了代码。
隔离SDWebImage
,设置头像
隔离SDWebImage
在项目中,我们经常会用到各种第三方框架,除了一些比较知名的框架以外,其它框架都存在这不稳定的因素,就算是知名的框架,也是总在更新的。为了以防万一,我们最好是能将第三方框架隔离出来。这样日后更换的时候也会省了不少的麻烦。
创建一个UIImageView
的Extension
,即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
也拆分出来,方便逻辑的处理。
我先根据需要自定义封装了一个快速创建Button
的Extension
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)
}
显示效果如下所示
给Cell
的BottomView
赋值
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
传到bottomView
的viewModel
中
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
的情况,所以我们需要自己造数据测试一下
因为是视图模型处理业务逻辑,因此,测试的时候,我们直接在视图模型里面处理就好。这样会对View
和Controller
做尽可能少的侵害。
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