关于ios:使用DCFrame轻松组合iOS界面

41次阅读

共计 10102 个字符,预计需要花费 26 分钟才能阅读完成。

概述

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

  1. 轻松组合治理简单 UI 模块;
  2. 零老本迁徙和重用 UI 模块;
  3. 模块间无耦合通信。

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

简略列表

第一个例子咱们来学习怎么应用 DCFrame 来创立一个繁多 cell 的简略列表,如下图所示:

要创立这样一个简略列表,须要如下三步:

  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 创立一个简略列表,当初咱们来创立一个帖子的列表,如下所示:

应用 DCFrame 也能够很容易的创立一个简单列表,依然须要三个步骤:

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

创立界面中的 Cell 和 CellModel

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

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)

简单列表的构造如下图所示:

当初咱们学习了怎么应用 DCFrame 创立一个简单列表,能够发现和创立一个简略列表一样简略,都须要如下三步:

  1. 首先,创立 UI 中的根底元素 Cell 和 CellModel;
  2. 其次,应用 ContainerModel 来组装 CellModel;
  3. 最初,应用 ContainerTableView 去加载一个 ContainerModel。

模块间通信

当初咱们来扩大一些下面的简单列表例子,增加两个模块间通信的性能,点击 InteractiveCell,扭转 PhotoCell 的背景色如下所示:

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 去进行解决,如下图所示:

上图中 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 例子。

正文完
 0