前言
這一篇主要寫微博的首頁佈局,及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
列印的資訊
{
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
引用的。相當於控制器還是在直接使用模型。
為了解決上面的問題,需要將載入資料的檢視模型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
,但是並沒有影響程式的執行。報錯如下
: 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
也拆分出來,方便邏輯的處理。
我先根據需要自定義封裝了一個快速建立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
歡迎來我的簡書看看:紅鯉魚與綠鯉魚與驢___
最後,發個求職廣告。小弟最近在求職,現工作在北京,準備去杭州發展,有願意幫忙推薦、介紹、或者丟擲橄欖枝的,在下感激不盡!
聯絡方式
郵箱:
- 13120010341@163.com
微信