TableViewCell中贝塞尔蒙版绘制及动态布局异常问题求助
问题描述
- 开发类似Tinder的聊天界面,使用TableViewCell承载消息,每个Cell包含自定义
testView作为消息气泡 - 需求:动态计算Label的合适宽度,并对带圆角的消息气泡进行裁剪
- 问题表现:静态裁剪常量时一切正常;动态计算Label宽度后,首次加载正常,但TableView重载后布局完全崩溃
- 实现方式:在
testView的draw(_ rect:)方法中进行视图裁剪,根据文本和字体计算宽度,单行时裁剪气泡到对应宽度
补充说明
- 使用静态裁剪常量的Cell,TableView重载多少次都正常
- TableView重载后Cell尺寸变化,但
draw方法仅被最后一个Cell调用
相关代码
自定义消息气泡类(testView)
class testView: UIView { var currentUser = Bool() var perfectWidthLabel = CGFloat() var label = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 45)) override func draw(_ rect: CGRect) { if currentUser { label.frame.size.width = self.frame.width - 37 }else { label.frame.size.width = self.frame.size.width - 20 } var indentFromLeft: CGFloat = 0 var indentFromRight: CGFloat = 0 if self.frame.height < 55 { indentFromLeft = self.frame.width - label.intrinsicContentSize.width - 37 indentFromRight = self.frame.width - label.intrinsicContentSize.width - 20 } var maskPath = UIBezierPath() if currentUser { var leftRadius = self.frame.height / 2 if self.frame.height > 55 { leftRadius = self.frame.height / 3 } let topLeftRadius = CGSize(width: leftRadius, height: leftRadius) let topRightRadius = CGSize(width: 23, height: 23) let bottomLeftRadius = CGSize(width: leftRadius, height: leftRadius) let bottomRightRadius = CGSize(width: 10, height: 10) maskPath = UIBezierPath(shouldRoundRect: bounds, topLeftRadius: topLeftRadius, topRightRadius: topRightRadius, bottomLeftRadius: bottomLeftRadius, bottomRightRadius: bottomRightRadius, indentFromLeft: indentFromLeft, indentFromRight: 0) } else { var rightRadius = self.frame.height / 2 if self.frame.height > 55 { rightRadius = self.frame.height / 3 } let topLeftRadius = CGSize(width: 23, height: 23) let topRightRadius = CGSize(width: rightRadius, height: rightRadius) let bottomLeftRadius = CGSize(width: 10, height: 10) let bottomRightRadius = CGSize(width: rightRadius, height: rightRadius) maskPath = UIBezierPath(shouldRoundRect: bounds, topLeftRadius: topLeftRadius, topRightRadius: topRightRadius, bottomLeftRadius: bottomLeftRadius, bottomRightRadius: bottomRightRadius, indentFromLeft: 0, indentFromRight: indentFromRight) } let shape = CAShapeLayer() shape.path = maskPath.cgPath layer.mask = shape } }
TableView代理方法
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if section == 0 {return 0} return structMessagesArr[section - 1].messages.count } func numberOfSections(in tableView: UITableView) -> Int { return structMessagesArr.count + 1 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "currentChatCell", for: indexPath) as! CurrentChatCell let message = structMessagesArr[indexPath.section - 1].messages[indexPath.row] cell.messageLabel.text = message.body let sender = message.sender cell.selectionStyle = .none if sender == currentAuthUser.ID { let view = cell.messageBubble as! testView view.currentUser = true view.label.text = cell.messageLabel.text view.label.font = cell.messageLabel.font if message.messagedWritingOnServer {cell.statusMessage.image = UIImage(systemName: "checkmark")} if message.messageRead {cell.statusMessage.image = UIImage(named: "MessageSend")} if message.messageLike {cell.statusMessage.image = UIImage(named: "LikeMessageRed")} cell.statusMessage.isHidden = false cell.heartView.isHidden = true cell.avatar.image = UIImage() cell.messageBubble.backgroundColor = UIColor(named: "CurrentUserMessageColor") cell.messageLabel.textAlignment = .right cell.messageLabel.textColor = .white } // 此处省略对方消息的配置逻辑 return cell }
解决方案
问题根源
draw(_ rect:)是绘制内容的方法,不是布局更新的正确时机,TableView复用Cell时无法保证每次都触发该方法,导致重载后只有部分Cell执行裁剪逻辑- 直接操作frame+在draw方法中修改layer.mask,会和AutoLayout的布局逻辑冲突,引发复用后的布局混乱
- 自定义Label冗余,无需在
testView内维护额外Label来计算宽度,直接使用Cell自带的messageLabel更准确
具体修复步骤
1. 重构testView,将裁剪逻辑移至layoutSubviews
layoutSubviews是UIView布局更新的专属时机,每次布局变化都会触发,适合处理动态裁剪:
class MessageBubbleView: UIView { var currentUser = false weak var messageLabel: UILabel? // 抽离常量,方便统一修改 private enum Constants { static let currentUserLeftSpacing: CGFloat = 37 static let otherUserRightSpacing: CGFloat = 20 static let largeCornerRadius: CGFloat = 23 static let smallCornerRadius: CGFloat = 10 } override func layoutSubviews() { super.layoutSubviews() guard let messageLabel = messageLabel else { return } var indentFromLeft: CGFloat = 0 var indentFromRight: CGFloat = 0 if bounds.height < 55 { let textWidth = messageLabel.intrinsicContentSize.width if currentUser { indentFromLeft = bounds.width - textWidth - Constants.currentUserLeftSpacing } else { indentFromRight = bounds.width - textWidth - Constants.otherUserRightSpacing } } let maskPath: UIBezierPath if currentUser { let leftRadius = bounds.height > 55 ? bounds.height / 3 : bounds.height / 2 maskPath = UIBezierPath(shouldRoundRect: bounds, topLeftRadius: CGSize(width: leftRadius, height: leftRadius), topRightRadius: CGSize(width: Constants.largeCornerRadius, height: Constants.largeCornerRadius), bottomLeftRadius: CGSize(width: leftRadius, height: leftRadius), bottomRightRadius: CGSize(width: Constants.smallCornerRadius, height: Constants.smallCornerRadius), indentFromLeft: indentFromLeft, indentFromRight: 0) } else { let rightRadius = bounds.height > 55 ? bounds.height / 3 : bounds.height / 2 maskPath = UIBezierPath(shouldRoundRect: bounds, topLeftRadius: CGSize(width: Constants.largeCornerRadius, height: Constants.largeCornerRadius), topRightRadius: CGSize(width: rightRadius, height: rightRadius), bottomLeftRadius: CGSize(width: Constants.smallCornerRadius, height: Constants.smallCornerRadius), bottomRightRadius: CGSize(width: rightRadius, height: rightRadius), indentFromLeft: 0, indentFromRight: indentFromRight) } let shapeLayer = CAShapeLayer() shapeLayer.path = maskPath.cgPath layer.mask = shapeLayer } // 修改属性时主动触发布局更新 var isCurrentUser: Bool { set { currentUser = newValue setNeedsLayout() } get { currentUser } } }
2. 修改TableViewCell配置逻辑
在cellForRowAt中传递Cell的messageLabel引用,并主动触发布局:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "currentChatCell", for: indexPath) as! CurrentChatCell let message = structMessagesArr[indexPath.section - 1].messages[indexPath.row] cell.messageLabel.text = message.body let sender = message.sender cell.selectionStyle = .none let bubbleView = cell.messageBubble as! MessageBubbleView if sender == currentAuthUser.ID { bubbleView.isCurrentUser = true bubbleView.messageLabel = cell.messageLabel bubbleView.setNeedsLayout() cell.statusMessage.isHidden = false cell.heartView.isHidden = true cell.avatar.image = UIImage() cell.messageBubble.backgroundColor = UIColor(named: "CurrentUserMessageColor") cell.messageLabel.textAlignment = .right cell.messageLabel.textColor = .white // 状态图标逻辑 if message.messagedWritingOnServer { cell.statusMessage.image = UIImage(systemName: "checkmark") } else if message.messageRead { cell.statusMessage.image = UIImage(named: "MessageSend") } else if message.messageLike { cell.statusMessage.image = UIImage(named: "LikeMessageRed") } } else { bubbleView.isCurrentUser = false bubbleView.messageLabel = cell.messageLabel bubbleView.setNeedsLayout() // 对方消息UI配置 cell.statusMessage.isHidden = true cell.heartView.isHidden = false cell.messageBubble.backgroundColor = UIColor(named: "OtherUserMessageColor") cell.messageLabel.textAlignment = .left cell.messageLabel.textColor = .black // 补充头像等其他配置 } return cell }
3. 确保Cell使用AutoLayout正确约束
- 给
messageLabel添加与气泡的间距约束(对应常量中的37、20),高度随内容自适应 - 给
messageBubble设置最大宽度约束(比如屏幕宽度的70%),避免气泡过宽
4. 重载TableView时的正确姿势
确保structMessagesArr数据更新完成后,调用tableView.reloadData(),避免局部刷新导致的布局不一致
内容的提问来源于stack exchange,提问作者Denim
相关产品推荐
相关产品推荐

