美文网首页iOS点点滴滴程序员
使用DCFrame轻松组合iOS界面

使用DCFrame轻松组合iOS界面

作者: CC果冻爽o | 来源:发表于2020-09-15 10:56 被阅读0次

    概述

    DCFrame是一个swift组合界面框架,在线上迭代了2年,目前已经比较稳定,使用该框架可以实现:

    1. 轻松组合管理复杂UI模块;
    2. 零成本迁移和重用UI模块;
    3. 模块间无耦合通信。

    这篇文档会使用3个例子由浅如深介绍怎么使用DCFrame构建iOS界面,并且轻松实现模块间的通信。

    简单列表

    第一个例子我们来学习怎样使用DCFrame来创建一个单一cell的简单列表,如下图所示:

    simple_list_1.png

    要创建这样一个简单列表,需要如下三步:

    1. 定义一个 CellModel 和 Cell 类型;
    2. 创建一个 ContainerModel 来包装 CellModel;
    3. 使用 ContainerTableView 去加载 ContainerModel。

    创建 CellModel 和 Cell

    定义一个 CellModel 类型需要满足如下几个条件:

    1. 需要继承自 DCCellModel
    2. 定义Cell所需要的数据类型;
    3. 设置对应Cell的类型和高度。

    在这个例子里面,Cell只需要一个字符串数据,所以我可以用如下方式定义 CellModel:

    class SimpleLabelModel: DCCellModel {
        var text: String = ""
        
        required init() {
            super.init()
            cellClass = SimpleLabelCell.self
            cellHeight = 50
        }
    }
    

    定义一个 Cell 类型也需要3步:

    1. 需要继承自 DCCell;
    2. 定义UI元素,在setupUI()方法中进行布局;
    3. 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需要满足如下条件:

    1. 需要继承自 DCContainerModel
    2. 一些初始化逻辑可以放在 cmDidLoad() 中,比如:这个例子中创建列表操作;
    3. 使用 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 也可以很容易的创建一个复杂列表,仍然需要三个步骤:

    1. 定义界面列表出现的所有 CellModel 和 Cell 类型;
    2. 创建 ContainerModel 去组合这些 CellModel;
    3. 使用 DCContainerTableView 去加载一个根的 ContainerModel。

    创建界面中的 Cell 和 CellModel

    这个列表中我们有 4 个不同的 Cell 类型,如下图所示:

    post_list_2.png

    UserCell and UserModel:

    在这个例子中,UserCell 包含一个 UILabel 来显示名字,对应的 UserModel 也只需要包含一个 name 字符串类型数据。所以我们可以定义 UserInfoCellUserInfoCellModel 类来表示:

    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 有两个小技巧:

    1. 找到界面中连续重复出现的 Cell,使用 ContainerModel 来组装,比如上面例子中的 CommentCell;
    2. 如果有多个 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 创建一个复杂列表,可以发现和创建一个简单列表一样简单,都需要如下三步:

    1. 首先,创建 UI 中的基础元素 Cell 和 CellModel;
    2. 其次,使用 ContainerModel 来组装 CellModel;
    3. 最后,使用 ContainerTableView 去加载一个 ContainerModel。

    模块间通信

    现在我们来扩展一些上面的复杂列表例子,添加两个模块间通信的功能,点击 InteractiveCell,改变 PhotoCell 的背景色如下所示:

    module_communication_1.gif

    DCFrame 提供了强大的事件传递和数据共享能力,可以很容易实现页面中两个不同模块间的通信问题。要实现上面功能,我们需要如下三步:

    1. 扩展复杂列表中的 InteractiveCellPhotoCell
    2. 定义事件和共享数据;
    3. 在 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。而在这个例子中, InteractiveCellPhotoCell 间的通信可以放在 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例子

    相关文章

      网友评论

        本文标题:使用DCFrame轻松组合iOS界面

        本文链接:https://www.haomeiwen.com/subject/cnwxyktx.html