You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
}

解决方案

问题根源

  1. draw(_ rect:)是绘制内容的方法,不是布局更新的正确时机,TableView复用Cell时无法保证每次都触发该方法,导致重载后只有部分Cell执行裁剪逻辑
  2. 直接操作frame+在draw方法中修改layer.mask,会和AutoLayout的布局逻辑冲突,引发复用后的布局混乱
  3. 自定义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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 07:07:08