如何正确确保自定义UITableViewCell可重复使用

时间:2018-12-12 20:02:20

标签: ios swift uitableview

我有一个UITableViewController正在渲染自定义UITableViewCell'。

此单元格与聊天消息有关,因此除了元素的约束方式之外,配置几乎相同。

  

bot单元为:头像>消息

     

用户单元格是消息<头像

我希望将这些组合到一个自定义单元中,并简单地在模型上启用origin属性,从而允许我选择要应用的约束。

这可以处理5或6条消息,直到我运行了包含30条消息的测试,并且某些单元格开始继承两组锚点,或者只是继承应分配给其他单元格的随机属性。

我可以看到错误提示约束无效,并且我相信这是由于单元没有准备好正确重用。

(
    "<NSLayoutConstraint:0x600002533930 UIImageView:0x7fb401514d40.leading == UILayoutGuide:0x600003f18e00'UIViewLayoutMarginsGuide'.leading   (active)>",
    "<NSLayoutConstraint:0x600002526990 UITextView:0x7fb40200a200'I am a Person.'.leading == UILayoutGuide:0x600003f18e00'UIViewLayoutMarginsGuide'.leading + 15   (active)>",
    "<NSLayoutConstraint:0x6000025271b0 UITextView:0x7fb40200a200'I am a Person.'.trailing == UIImageView:0x7fb401514d40.leading - 15   (active)>"
)

ChatMessageCell

class ChatMessageCell: UITableViewCell {
    fileprivate var content: ChatMessage? {
        didSet {
            guard let text = content?.text else { return }
            messageView.text = text

            guard let origin = content?.origin else { return }
            setupSubViews(origin)
        }
    }

    fileprivate var messageAvatar: UIImageView = {
        let imageView = UIImageView(frame: .zero)
        imageView.layer.cornerRadius = 35 / 2
        imageView.layer.masksToBounds = true
        return imageView
    }()

    fileprivate var messageView: UITextView = {
        let textView = UITextView()
        textView.isScrollEnabled = false
        textView.isSelectable = false
        textView.sizeToFit()
        textView.layoutIfNeeded()
        textView.contentInset = UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10)
        textView.layer.cornerRadius = 10
        textView.layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMaxXMinYCorner, .layerMinXMinYCorner]
        textView.translatesAutoresizingMaskIntoConstraints = false
        return textView
    }()

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        backgroundColor = UIColor.clear
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setContent(as content: ChatMessage) {
        self.content = content
    }

    override func prepareForReuse() {
        content = nil
    }
}

extension ChatMessageCell {
    fileprivate func setupSubViews(_ origin: ChatMessageOrigin) {
        let margins = contentView.layoutMarginsGuide

        [messageAvatar, messageView].forEach { v in contentView.addSubview(v) }

        switch origin {
        case .system:
            messageAvatar.image = #imageLiteral(resourceName: "large-bot-head")
            messageAvatar.anchor(
                top: margins.topAnchor, leading: margins.leadingAnchor, size: CGSize(width: 35, height: 35)
            )
            messageView.anchor(
                top: margins.topAnchor, leading: messageAvatar.trailingAnchor, bottom: margins.bottomAnchor, trailing: margins.trailingAnchor,
                padding: UIEdgeInsets(top: 5, left: 15, bottom: 0, right: 15)
            )
        case .user:
            let userContentBG = UIColor.hexStringToUIColor(hex: "00f5ff")
            messageAvatar.image = UIImage.from(color: userContentBG)
            messageAvatar.anchor(
                top: margins.topAnchor, trailing: margins.trailingAnchor, size: CGSize(width: 35, height: 35)
            )
            messageView.layer.backgroundColor = userContentBG.cgColor
            messageView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMinYCorner, .layerMinXMinYCorner]
            messageView.anchor(
                top: margins.topAnchor, leading: margins.leadingAnchor, bottom: margins.bottomAnchor, trailing: messageAvatar.leadingAnchor,
                padding: UIEdgeInsets(top: 5, left: 15, bottom: 0, right: 15)
            )
        }
    }
}

ChatController

class ChatController: UITableViewController {
    lazy var viewModel: ChatViewModel = {
        let viewModel = ChatViewModel()
        return viewModel
    }()

    fileprivate let headerView: UIView = {
        let view = UIView(frame: .zero)
        view.backgroundColor = .white
        return view
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.reloadData = { [weak self] in
            DispatchQueue.main.async {
                self?.tableView.reloadData()
            }
        }

        configureViewHeader()
        configureTableView()
        registerTableCells()
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        tableView.contentInset = UIEdgeInsets(top: 85, left: 0, bottom: 0, right: 0)
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.history.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let item = viewModel.history[indexPath.row]
        let cell = tableView.dequeueReusableCell(withClass: ChatMessageCell.self)
        cell.setContent(as: item)
        cell.layoutSubviews()
        return cell
    }
}

extension ChatController {
    fileprivate func configureViewHeader() {
        let margins = view.safeAreaLayoutGuide
        view.addSubview(headerView)
        headerView.anchor(
            top: margins.topAnchor, leading: margins.leadingAnchor, trailing: margins.trailingAnchor,
            size: CGSize(width: 0, height: 70)
        )
    }

    fileprivate func configureTableView() {
        tableView.tableFooterView = UIView()
        tableView.allowsSelection = false
        tableView.rowHeight = UITableView.automaticDimension
        tableView.estimatedRowHeight = 200
        tableView.separatorStyle = .none
        tableView.backgroundColor = UIColor.clear
    }

    fileprivate func registerTableCells() {
        tableView.register(cellWithClass: ChatMessageCell.self)
    }
}

在此处可以查看视图如何滚动变化的示例。... Pre scroll Post scroll

我的扩展程序应用于

  @discardableResult
    func anchor(top: NSLayoutYAxisAnchor? = nil, leading: NSLayoutXAxisAnchor? = nil, bottom: NSLayoutYAxisAnchor? = nil, trailing: NSLayoutXAxisAnchor? = nil, padding: UIEdgeInsets = .zero, size: CGSize = .zero) -> AnchoredConstraints {
        translatesAutoresizingMaskIntoConstraints = false
        var anchoredConstraints = AnchoredConstraints()

        if let top = top {
            anchoredConstraints.top = topAnchor.constraint(equalTo: top, constant: padding.top)
        }

        if let leading = leading {
            anchoredConstraints.leading = leadingAnchor.constraint(equalTo: leading, constant: padding.left)
        }

        if let bottom = bottom {
            anchoredConstraints.bottom = bottomAnchor.constraint(equalTo: bottom, constant: -padding.bottom)
        }

        if let trailing = trailing {
            anchoredConstraints.trailing = trailingAnchor.constraint(equalTo: trailing, constant: -padding.right)
        }

        if size.width != 0 {
            anchoredConstraints.width = widthAnchor.constraint(equalToConstant: size.width)
        }

        if size.height != 0 {
            anchoredConstraints.height = heightAnchor.constraint(equalToConstant: size.height)
        }

        [anchoredConstraints.top, anchoredConstraints.leading, anchoredConstraints.bottom, anchoredConstraints.trailing, anchoredConstraints.width, anchoredConstraints.height].forEach { $0?.isActive = true }

        return anchoredConstraints
    }

2 个答案:

答案 0 :(得分:1)

在您的ChatMessageCell类中,移动:

[messageAvatar, messageView].forEach { v in contentView.addSubview(v) }

setupSubViews(...)init(...)。使用当前代码,每次设置内容时都会调用setupSubViews。您只想在初始化单元格时将子视图添加到单元格的contentView

从那里,您需要检查如何添加约束。如果您的.anchor(...)函数/扩展名首先是删除所有现有的约束,那么您应该没事。


编辑:

这是另一种选择-您可能会发现使用起来更容易。

由于您具有相同的子视图,因此请设置两个约束数组。然后激活或停用适当的设置(以及设置颜色,边角等):

class ChatMessageCell: UITableViewCell {
    fileprivate var content: ChatMessage? {
        didSet {
            guard let text = content?.text else { return }
            messageView.text = text

            guard let origin = content?.origin else { return }
            setupSubViews(origin)
        }
    }

    fileprivate var messageAvatar: UIImageView = {
        let imageView = UIImageView(frame: .zero)
        imageView.layer.cornerRadius = 35 / 2
        imageView.layer.masksToBounds = true
        imageView.translatesAutoresizingMaskIntoConstraints = false
        return imageView
    }()

    fileprivate var messageView: UITextView = {
        let textView = UITextView()
        textView.isScrollEnabled = false
        textView.isSelectable = false
        textView.sizeToFit()
        textView.layoutIfNeeded()
        textView.contentInset = UIEdgeInsets(top: 5, left: 10, bottom: 5, right: 10)
        textView.layer.cornerRadius = 10
        textView.translatesAutoresizingMaskIntoConstraints = false
        return textView
    }()

    fileprivate var systemConstraints = [NSLayoutConstraint]()
    fileprivate var userConstraints = [NSLayoutConstraint]()

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }

    func setContent(as content: ChatMessage) {
        self.content = content
    }

    func commonInit() -> Void {

        backgroundColor = .clear

        let margins = contentView.layoutMarginsGuide

        [messageAvatar, messageView].forEach { v in contentView.addSubview(v) }

        systemConstraints = [
            messageAvatar.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: 0.0),

            messageView.leadingAnchor.constraint(equalTo: messageAvatar.trailingAnchor, constant: 15.0),
            messageView.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant: -15),
        ]

        userConstraints = [
            messageView.leadingAnchor.constraint(equalTo: margins.leadingAnchor, constant: 15.0),

            messageAvatar.trailingAnchor.constraint(equalTo: margins.trailingAnchor, constant: 0.0),
            messageAvatar.leadingAnchor.constraint(equalTo: messageView.trailingAnchor, constant: 15),
        ]

        NSLayoutConstraint.activate([
            // messageAvatar width/height/top is the same for each origin "type"
            messageAvatar.topAnchor.constraint(equalTo: margins.topAnchor, constant: 0.0),
            messageAvatar.heightAnchor.constraint(equalToConstant: 35),
            messageAvatar.widthAnchor.constraint(equalToConstant: 35),

            // messageView width/height/top is the same for each origin "type"
            messageView.topAnchor.constraint(equalTo: margins.topAnchor, constant: 5.0),
            messageView.bottomAnchor.constraint(equalTo: margins.bottomAnchor, constant: 0.0),
            ])

    }

}

extension ChatMessageCell {
    fileprivate func setupSubViews(_ origin: ChatMessageOrigin) {

        switch origin {
        case .system:
            NSLayoutConstraint.deactivate(userConstraints)
            NSLayoutConstraint.activate(systemConstraints)
            messageView.backgroundColor = .white
            messageAvatar.backgroundColor = .red
            messageView.layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMaxXMinYCorner, .layerMinXMinYCorner]

        default:
            NSLayoutConstraint.deactivate(systemConstraints)
            NSLayoutConstraint.activate(userConstraints)
            messageView.backgroundColor = .cyan
            messageAvatar.backgroundColor = .blue
            messageView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMinYCorner, .layerMinXMinYCorner]

        }

    }
}

注意:我使用的是Swift 4.1,因此有一些语法更改(但显而易见)。

答案 1 :(得分:0)

当您使用两种不同的单元布局时,使用两种不同的单元类别将是处理问题的另一种方法。

ChatMessageCell

class ChatMessageCell: UITableViewCell {

    fileprivate var content: ChatMessage? {
        didSet {
            guard let text = content?.text else { return }
            messageView.text = text
        }
    }

    //...    

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        backgroundColor = UIColor.clear
        setupSubViews()
    }
    fileprivate func setupSubViews() {
        [messageAvatar, messageView].forEach { v in contentView.addSubview(v) }
    }

    //...
}

class UserMessageCell: ChatMessageCell {
    fileprivate override func setupSubViews() {
        super.setupSubViews()
        let margins = contentView.layoutMarginsGuide

        let userContentBG = UIColor.hexStringToUIColor(hex: "00f5ff")
        messageAvatar.image = UIImage.from(color: userContentBG)
        messageAvatar.anchor(
            top: margins.topAnchor, trailing: margins.trailingAnchor, size: CGSize(width: 35, height: 35)
        )
        messageView.layer.backgroundColor = userContentBG.cgColor
        messageView.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMinYCorner, .layerMinXMinYCorner]
        messageView.anchor(
            top: margins.topAnchor, leading: margins.leadingAnchor, bottom: margins.bottomAnchor, trailing: messageAvatar.leadingAnchor,
            padding: UIEdgeInsets(top: 5, left: 15, bottom: 0, right: 15)
        )
    }
}

class SystemMessageCell: ChatMessageCell {
    fileprivate override func setupSubViews() {
        super.setupSubViews()
        let margins = contentView.layoutMarginsGuide

        messageAvatar.image = #imageLiteral(resourceName: "large-bot-head")
        messageAvatar.anchor(
            top: margins.topAnchor, leading: margins.leadingAnchor, size: CGSize(width: 35, height: 35)
        )
        messageView.anchor(
            top: margins.topAnchor, leading: messageAvatar.trailingAnchor, bottom: margins.bottomAnchor, trailing: margins.trailingAnchor,
            padding: UIEdgeInsets(top: 5, left: 15, bottom: 0, right: 15)
        )
    }
}

ChatController

class ChatController: UITableViewController {
    //...

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let item = viewModel.history[indexPath.row]
        let cell: ChatMessageCell
        switch item.origin {
        case .system:
            cell = tableView.dequeueReusableCell(withClass: SystemMessageCell.self)
        case .user:
            cell = tableView.dequeueReusableCell(withClass: UserMessageCell.self)
        }
        cell.setContent(as: item)
        cell.layoutSubviews()
        return cell
    }
}

extension ChatController {
    //...

    fileprivate func registerTableCells() {
        tableView.register(cellWithClass: SystemMessageCell.self)
        tableView.register(cellWithClass: UserMessageCell.self)
    }
}