Как правильно убедиться, что пользовательский UITableViewCell может быть повторно использован - PullRequest
0 голосов
/ 12 декабря 2018

У меня есть UITableViewController, который выводит пользовательский UITableViewCell '.

Эти ячейки связаны с сообщениями чата, так как конфигурация практически идентична, за исключением того, как элементы ограничены.

ячейка бота: аватар> сообщение

ячейка пользователя сообщение <аватар </em>

Iнадеялся объединить их в одной пользовательской ячейке и просто включить свойство 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)
    }
}

Пример того, как виw изменения прокрутки можно увидеть здесь .... 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 голосов
/ 13 декабря 2018

Если у вас есть два разных макета ячеек, еще один способ решения вашей проблемы - наличие двух разных классов ячеек.

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)
    }
}
0 голосов
/ 13 декабря 2018

В вашем классе ChatMessageCell перейдите:

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

с setupSubViews(...) на init(...).С вашим текущим кодом setupSubViews вызывается каждый раз, когда вы устанавливаете контент.Вы только хотите добавить подпредставления к ячейке contentView, когда ячейка инициализирована.

Оттуда вам нужно проверить, как вы добавляете ограничения.Если ваш .anchor(...) func / extension сначала удаляет все существующие ограничения, вы должны быть в порядке.


Редактировать:

Вот еще один вариант - вы можетес ним легче работать.

Поскольку у вас одни и те же подпредставления, настройте два массива ограничений.Затем активируйте или деактивируйте соответствующий набор (а также настройку цветов, углов и т. Д.):

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, поэтому есть несколько изменений синтаксиса (но они будутбыть очевидным).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...