概述
DCFrame是一个swift组合界面框架,在线上迭代了2年,目前已经比较稳定,使用该框架可以实现:
- 轻松组合管理复杂UI模块;
- 零成本迁移和重用UI模块;
- 模块间无耦合通信。
这篇文档会使用3个例子由浅如深介绍怎么使用DCFrame构建iOS界面,并且轻松实现模块间的通信。
简单列表
第一个例子我们来学习怎样使用DCFrame来创建一个单一cell的简单列表,如下图所示:
simple_list_1.png要创建这样一个简单列表,需要如下三步:
- 定义一个 CellModel 和 Cell 类型;
- 创建一个 ContainerModel 来包装 CellModel;
- 使用 ContainerTableView 去加载 ContainerModel。
创建 CellModel 和 Cell
定义一个 CellModel 类型需要满足如下几个条件:
- 需要继承自
DCCellModel
; - 定义Cell所需要的数据类型;
- 设置对应Cell的类型和高度。
在这个例子里面,Cell只需要一个字符串数据,所以我可以用如下方式定义 CellModel:
class SimpleLabelModel: DCCellModel {
var text: String = ""
required init() {
super.init()
cellClass = SimpleLabelCell.self
cellHeight = 50
}
}
定义一个 Cell 类型也需要3步:
- 需要继承自
DCCell
; - 定义UI元素,在
setupUI()
方法中进行布局; - 在
cellModelDidUpdate()
方法中更新界面数据。
在这个简单列表中,只需要一个 Label 界面元素和一个分割线,所以我们可以这样来定义 Cell:
class SimpleLabelCell: DCCell<SimpleLabelModel> {
let label: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 17)
return label
}()
let separateLine: CALayer = {
let layer = CALayer()
layer.backgroundColor = UIColor.lightGray.cgColor
return layer
}()
override func setupUI() {
super.setupUI()
contentView.addSubview(label)
contentView.layer.addSublayer(separateLine)
}
override func layoutSubviews() {
super.layoutSubviews()
let bounds = contentView.bounds
let left: CGFloat = 15
let height: CGFloat = 1.0 / UIScreen.main.scale
label.frame = bounds.inset(by: UIEdgeInsets(top: 8, left: left, bottom: 8, right: 15))
separateLine.frame = CGRect(x: left, y: bounds.height - height, width: bounds.width - left, height: height)
}
override func cellModelDidUpdate() {
super.cellModelDidUpdate()
label.text = cellModel.text
}
}
注意: Cell UI 界面的赋值操作建议放在 cellModelDidUpdate()
方法中, 因为在列表中 Cell 通常会被重用,cellModelDidUpdate()
方法会在 Cell 重用前被回调。
创建一个 ContainerModel
对于列表中单一的元素需要定义 Cell 和对应的 CellModel,如果要将这些单一元素组装成列表就需要另外一个角色:ContainerModel。定义一个ContainerModel需要满足如下条件:
- 需要继承自
DCContainerModel
; - 一些初始化逻辑可以放在
cmDidLoad()
中,比如:这个例子中创建列表操作; - 使用
addSubmodel()
方法,来组合 CellModel。
在上面例子中,初始化逻辑是组装一个列表,所以我们可以这样来定义 ContainerModel:
class SimpleListContainerModel: DCContainerModel {
override func cmDidLoad() {
super.cmDidLoad()
for num in 0...100 {
let model = SimpleLabelModel()
model.text = "\(num)"
addSubmodel(model)
}
}
}
提示:对于一个简单列表,可以直接创建一个 DCContainerModel
去组装列表,不需要再额外定义。
加载 ContainerModel
完成 ContainerModel 的定义后,对于列表 UI 数据和逻辑部分就完成了。现在可以使用 DCFrame 提供的 DCContainerTableView
来加载 ContainerModel,如下所示:
class SimpleListViewController: UIViewController {
let dcTableView = DCContainerTableView()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(dcTableView)
let simpleListCM = SimpleListContainerModel()
dcTableView.loadCM(simpleListCM)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
dcTableView.frame = view.bounds
}
}
复杂列表
上面学习了怎样使用 DCFrame 创建一个简单列表,现在我们来创建一个帖子的列表,如下所示:
post_list_1.png使用 DCFrame 也可以很容易的创建一个复杂列表,仍然需要三个步骤:
- 定义界面列表出现的所有 CellModel 和 Cell 类型;
- 创建 ContainerModel 去组合这些 CellModel;
- 使用 DCContainerTableView 去加载一个根的 ContainerModel。
创建界面中的 Cell 和 CellModel
这个列表中我们有 4 个不同的 Cell 类型,如下图所示:
post_list_2.pngUserCell and UserModel:
在这个例子中,UserCell 包含一个 UILabel
来显示名字,对应的 UserModel 也只需要包含一个 name 字符串类型数据。所以我们可以定义 UserInfoCell
和 UserInfoCellModel
类来表示:
class UserInfoCellModel: DCCellModel {
var name: String!
required init() {
super.init()
cellHeight = 41
cellClass = UserInfoCell.self
}
}
class UserInfoCell: DCCell<UserInfoCellModel> {
// The detailed code can be found in the project example
override func cellModelDidUpdate() {
super.cellModelDidUpdate()
nameLabel.text = cellModel.name
}
}
PhotCell and PhotoModel:
为了能简单展示例子,这里我们定义 PhotoCell 只涉及背景颜色的变更,所以在 PhotoModel 中我们只需要定义一个颜色数据属性,如下所示:
class PhotoCellModel: DCCellModel {
var color: UIColor = UIColor(red: 4/255.0, green: 170/255.0, blue: 166/255.0, alpha: 1.0)
required init() {
super.init()
cellClass = PhotoCell.self
cellHeight = 375
}
}
class PhotoCell: DCCell<PhotoCellModel> {
// The detailed code can be found in the project example
override func cellModelDidUpdate() {
super.cellModelDidUpdate()
contentView.backgroundColor = cellModel.color
}
}
InteractiveCell and InteractiveModel:
在这个例子中,InteractiveCell 会固定显示三个按钮(为简单起见代码中定义按钮和布局代码省略,可以在DCFrame例子中查看),所以无需定义界面数据,如下代码所示:
class InteractiveCellModel: DCCellModel {
required init() {
super.init()
cellClass = InteractiveCell.self
cellHeight = 41
}
}
class InteractiveCell: DCCell<InteractiveCellModel> {
// The detailed code can be found in the project example
}
CommentCell and CommentModel:
对于 CommentCell 来说只有一个显示评论的 Label,所以 CommentModel 中需要定一个评论数据的字符串,如下代码所示:
class CommentCellModel: DCCellModel {
var comment: String!
required init() {
super.init()
cellClass = CommentCell.self
cellHeight = 25
}
}
class CommentCell: DCCell<CommentCellModel> {
// The detailed code can be found in the project example
override func cellModelDidUpdate() {
super.cellModelDidUpdate()
commentLabel.text = cellModel.comment
}
}
组合 CellModel
完成界面中基本元素 CellModel 和 Cell 的定义后,下一步就是通过 ContainerModel 去组装界面,组装 CellModel 有两个小技巧:
- 找到界面中连续重复出现的 Cell,使用 ContainerModel 来组装,比如上面例子中的 CommentCell;
- 如果有多个 Cell 循环出现,就可以用一个 ContainerModel 来组装,比如上面例子中4个不同的Cell组成了每个帖子。
组装 CommentModel
这里定义 PostCommentsContainerModel
类来组装每个帖子中的 CommentModel。
class PostCommentsContainerModel: DCContainerModel {
init(with comments: [String]) {
super.init()
for comment in comments {
let model = CommentCellModel()
model.comment = comment
addSubmodel(model)
}
}
}
组装一个帖子的 ContainerModel
另外一个循环出现的 UI 元素就是每个帖子,每个帖子中包含4种类型的 Cell,我们通过定义一个 PostItemContainerModel
类来组装这些 Cell。
class PostItemContainerModel: DCContainerModel {
// The detailed code can be found in the project example
init(with post: PostData) {
super.init()
let userModel = UserInfoCellModel()
userModel.name = post.username
userModel.isHoverTop = true
let photoModel = PhotoCellModel()
let interactiveModel = InteractiveCellModel()
let commentsCM = PostCommentsContainerModel(with: post.comments)
addSubmodels([userModel, photoModel, interactiveModel, commentsCM])
}
}
提示:ContainerModel 有个非常强大的功能,它不仅可以组装 CellModel,也可以组装 ContainerModel,比如上面的 commentsCM。
组装根的 ContainerModel
现在每个帖子可以用 PostItemContainerModel
来表示,最后我们定一个 PostListContainerModel
类型来组装所有的帖子,这个 ContainerModel 被称为根 ContainerModel。
class PostListContainerModel: DCContainerModel {
// The detailed code can be found in the project example
override func cmDidLoad() {
super.cmDidLoad()
for data in mockData {
let infoCM = PostInfoContainerModel(with: data)
infoCM.bottomSeparator = DCSeparatorModel(color: .clear, height: 10)
addSubmodel(infoCM)
}
}
}
加载 ContainerModel
加载 containerModel 和上面的简单列表一样,只需要创建一个 DCContainerTableView
然后调用 loadCM()
方法就可以将 UI 列表显示出来。
let dcContainerTableView = DCContainerTableView()
// Omit layout code, the detailed code can be found in the project example
let postListContainerModel = PostListContainerModel()
dcContainerTableView.loadCM(postListContainerModel)
复杂列表的结构如下图所示:
post_list_3.png现在我们学习了怎么使用 DCFrame 创建一个复杂列表,可以发现和创建一个简单列表一样简单,都需要如下三步:
- 首先,创建 UI 中的基础元素 Cell 和 CellModel;
- 其次,使用 ContainerModel 来组装 CellModel;
- 最后,使用 ContainerTableView 去加载一个 ContainerModel。
模块间通信
现在我们来扩展一些上面的复杂列表例子,添加两个模块间通信的功能,点击 InteractiveCell,改变 PhotoCell 的背景色如下所示:
module_communication_1.gifDCFrame 提供了强大的事件传递和数据共享能力,可以很容易实现页面中两个不同模块间的通信问题。要实现上面功能,我们需要如下三步:
- 扩展复杂列表中的
InteractiveCell
和PhotoCell
; - 定义事件和共享数据;
- 在 ContainerModel 中响应事件和进行数据共享。
扩展 Cell 功能
为了支持上面的点击事件,我们需要为 InteractiveCell
中的 Button 添加点击事件,如下所示:
class InteractiveCell: DCCell<InteractiveCellModel> {
// The detailed code can be found in the project example
private lazy var likeButton: UIButton = {
return createButton(with: "Like")
}()
private lazy var commentButton: UIButton = {
return createButton(with: "Comment")
}()
private lazy var shareButton: UIButton = {
return createButton(with: "Share")
}()
private func createButton(with title: String) -> UIButton {
let button = UIButton()
// The detailed code can be found in the project example
button.addTarget(self, action: #selector(touch(sender:)), for: .touchUpInside)
return button
}
@objc func touch(sender: UIButton) {
switch sender {
case likeButton:
// do something
case commentButton:
// do something
case shareButton:
// do something
default: break
}
}
PhotoCell
需要添加一个新的 UILabel
,去显示按钮点击后的文案,也需要为 PhotoCellModel
添加一个 text 字符串属性,如下所示:
class PhotoCellModel: DCCellModel {
var text = ""
// The detailed code can be found in the project example
}
class PhotoCell: DCCell<PhotoCellModel> {
// The detailed code can be found in the project example
lazy var infoLabel: UILabel = {
let label = UILabel()
label.font = UIFont.boldSystemFont(ofSize: 15)
label.textColor = UIColor.darkText
label.textAlignment = .center
contentView.addSubview(label)
return label
}()
override func cellModelDidUpdate() {
super.cellModelDidUpdate()
infoLabel.text = cellModel.text
contentView.backgroundColor = cellModel.color
}
override func layoutSubviews() {
super.layoutSubviews()
infoLabel.frame = CGRect(x: 0, y: 0, width: bounds.width, height: infoLabel.font.lineHeight)
infoLabel.center = CGPoint(x: contentView.bounds.width / 2, y: contentView.bounds.height / 2)
}
}
定义事件和共享数据
现在 InteractiveCell
具备了响应点击事件的能力,我们可以通过定义一个 DCEventID
来进行事件的传递,如下所示:
class InteractiveCell: DCCell<InteractiveCellModel> {
static let likeTouch = DCEventID()
static let commentTouch = DCEventID()
static let shareTouch = DCEventID()
// The detailed code can be found in the project example
@objc func touch(sender: UIButton) {
switch sender {
case likeButton:
sendEvent(Self.likeTouch, data: sender.titleLabel?.text)
case commentButton:
sendEvent(Self.commentTouch, data: sender.titleLabel?.text)
case shareButton:
sendEvent(Self.shareTouch, data: sender.titleLabel?.text)
default: break
}
}
}
对于 PhotoCell
来说,只需要改变背景色和文案,所以它可以定义一个 DCSharedDataID
来订阅数据的变化,如下所示:
class PhotoCell: DCCell<PhotoCellModel> {
static let data = DCSharedDataID()
// The detailed code can be found in the project example
override func cellModelDidLoad() {
super.cellModelDidLoad()
subscribeData(Self.data) { [weak self] (text: String, color: UIColor) in
guard let `self` = self else { return }
self.cellModel.text = text
self.cellModel.color = color
self.infoLabel.text = text
self.contentView.backgroundColor = color
}
}
}
注意:在Cell中订阅数据变化,必须放在 cellModelDidLoad()
方法中。
处理模块间的通信
在 DCFrame 中 sendEvent()
方法可以将事件传递到 ContainerModel 中,然后事件会继续延着 ContainerModel 向根的 ContainerModel 传递,在传递链上的每个 ContainerModel 都可以响应这个事件。
对于共享数据,一般是在 ContainerModel 中进行共享数据,在 ContainerModel 所组装的 CellModel 和 Cell 中订阅数据的变化。所以两个不同模块间的通信,需要依赖 ContainerModel 去进行处理,如下图所示:
module_communication_2.png上图中 Model_A 和 Model_B 进行通信,需要在根的 CM(ContainerModel) 中进行处理,因为他们最近的公共 CM 就是根CM。而在这个例子中, InteractiveCell
和 PhotoCell
间的通信可以放在 PostItemContainerModel
中进行处理,因为 PostItemContainerModel
是这两个 Cell 最近的公共 CM,如下所示:
class PostItemContainerModel: DCContainerModel {
// The detailed code can be found in the project example
override func cmDidLoad() {
super.cmDidLoad()
subscribeEvent(InteractiveCell.likeTouch) { [weak self] (text: String) in
self?.shareData((text, UIColor.red), to: PhotoCell.data)
}.and(InteractiveCell.commentTouch) { [weak self] (text: String) in
self?.shareData((text, UIColor.yellow), to: PhotoCell.data)
}.and(InteractiveCell.shareTouch) { [weak self] (text: String) in
self?.shareData((text, UIColor.blue), to: PhotoCell.data)
}
}
}
注意:在 ContainerModel 中订阅事件和共享数据,必须放在 cmDidLoad() 中进行处理。
总结
上面我们首先通过一个简单例子讲解了怎么使用DCFrame来组装一个列表;然后通过一个复杂列表了解到 ContaienrModel 有着强大的组合功能,正因为这个功能使得管理复杂界面变得非常容易;最后介绍了怎样轻松实现无耦合的模块间通信。界面组合和模块间通信是DCFrame的核心功能,当然还有很多有趣的例子(参考了IGListKit的例子)和功能可以在这里找到:DCFrame例子。
网友评论