我有一个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)
}
}
我的扩展程序应用于
@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
}
答案 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)
}
}