Проблема с вычислением границы строки, когда строка содержит '\ n'

Sep 23 2020

Я разрабатываю функцию стиля Whatsapp внутри нашего приложения. сообщения анализируются из JSON, а затем создаются как текстовые (+ необязательное изображение) сообщения внутри UITableView (каждое сообщение представляет собой настраиваемую ячейку). Пузырь сообщения рисуется с использованием пути Безье на основе расчета текстового фрейма с использованием метода boundingRect. Позже UILabel и UIImage добавляются как подпредставления UIStackview, и как StackView, так и представление пузыря сообщений ограничиваются представлением контейнера.

Иногда, когда текст содержит '\ n', UILabel либо обрезается (с '...'), либо течет вниз под представлением пузыря сообщений, в зависимости от нижнего приоритета привязки представления стека (выше / ниже, чем приоритет захвата содержимого UILabel ), но другие сообщения, содержащие символы новой строки, отображаются правильно. Я предполагаю, что расчет фрейма строки обрабатывает '\ n' как 2 символа вместо новой строки.

Когда я попытался протестировать тот же код на игровой площадке (с более простым макетом, только UILabel и пузырьковое представление, без контейнерных представлений, без табличного представления и без ограничений), все, казалось, работало нормально, и пузырь расширялся, чтобы адаптироваться к добавленным новым строкам.

Основываясь на этом потоке, я попытался заменить код методом sizeThatFits, но результат тот же. В конце концов, я подсчитал количество вхождений '\ n' внутри строки и вручную добавил высоту к фрейму, но это влияет как на плохие, так и на хорошие сообщения, вокруг которых теперь есть дополнительное пространство.

Снимки экрана, соответствующий код и журналы консоли прилагаются ниже. Надеюсь, это поможет кому-то в этом разобраться.

Изменить: изменение ширины messageView с UIScreen.main.bounds.width * 0.73 на UIScreen.main.bounds.width * 0.8 устранило проблему. Однако я до сих пор не могу понять, почему это затронуло только определенные сообщения. Буду признателен за любую дополнительную информацию по этому поводу.

ChatMessageModel.swift

fileprivate func setText(_ label: ClickableUILabel, _ text: String, _ shouldLimitSize: Bool, _  shouldOpenLinks: Bool) {
    
    ...

    // set text frame
    let textFrameHeight: CGFloat = shouldLimitSize ? 40.0 : .greatestFiniteMagnitude
    
    let constraintRect = CGSize(width: innerContentWidth, height: textFrameHeight)
    
    let boundingBox = text.boundingRect(with: constraintRect,
                                        options: .usesLineFragmentOrigin,
                                        attributes: [.font: label.font!],
                                        context: nil)
    
    // width must have minimum value for short text to appear centered
    let widthCeil = ceil(boundingBox.width)
    let constraintWidthWithInset = constraintRect.width - 30
    
    var height: CGFloat
    
    if text.isEmpty {
        height = 0
    } else {
        // min value of 40
        height = max(ceil(boundingBox.height), 40) + 5
    }
  
    // ***** This part fixes bad messages but messes up good messages ****

    // add extra height for newLine inside text
    if let newLineCount = label.text?.countInstances(of: "\n"), newLineCount > 0 {
        LOG("found \n")
        height += CGFloat((newLineCount * 8))
    }

    label.frame.size = CGSize(width:max(widthCeil, constraintWidthWithInset),
                              height: height)
    label.setContentHuggingPriority(UILayoutPriority(200), for: .horizontal)
}
fileprivate func setTextBubble(_ label: UILabel, _ image: String?, _ video: String?, _ shouldLimitSize: Bool) -> CustomRoundedCornerRectangle  {
        
        // configure bubble size
        
        var contentHeight = CGFloat()
        
        if imageDistribution! == .alongsideText {
            contentHeight = max(label.frame.height, contentImageView.frame.height)
        } else {
            contentHeight = label.frame.height + contentImageView.frame.height + 20
        }
        
        // messages with no text on main feed should have smaller width
        let width: CGFloat = shouldLimitSize && (label.text ?? "").isEmpty ? 150.0 : UIScreen.main.bounds.width * 0.73
        let bubbleFrame = CGRect(x: 0, y: 0, width: width, height: contentHeight + 20)
        
        let messageView = CustomRoundedCornerRectangle(frame: bubbleFrame)
        messageView.heightAnchor.constraint(equalToConstant: bubbleFrame.size.height).isActive = true
        messageView.widthAnchor.constraint(equalToConstant: bubbleFrame.size.width).isActive = true
        messageView.translatesAutoresizingMaskIntoConstraints = false
        
        self.messageViewFrame = bubbleFrame
        
        return messageView
    }
fileprivate func layoutSubviews(_ containerView: UIView, _ messageView: CustomRoundedCornerRectangle, _ timeLabel: UILabel, _ profileImageView: UIImageView, _ profileName: UILabel, _ label: UILabel, _ contentImageView: CustomImageView, _ imagePlacement: imagePlacement) {
        
        // container view
        containerView.addSubview(messageView)
        containerView.translatesAutoresizingMaskIntoConstraints = false
        
        containerView.autoSetDimension(.width, toSize: UIScreen.main.bounds.width * 0.8)
        containerView.autoPinEdge(.bottom, to: .bottom, of: messageView)
        messageView.autoPinEdge(.top, to: .top, of: containerView, withOffset: 23)
        
        // time label
        containerView.addSubview(timeLabel)
        timeLabel.autoPinEdge(.bottom, to: .top, of: messageView)
        timeLabel.autoPinEdge(.leading, to: .leading, of: containerView, withOffset: -2)
        
        // profile image
        containerView.addSubview(profileImageView)
        profileImageView.autoPinEdge(.trailing, to: .trailing, of: containerView, withOffset: 15)
        profileImageView.autoPinEdge(.top, to: .top, of: containerView, withOffset: 30)
        messageView.autoPinEdge(.trailing, to: .leading, of: profileImageView, withOffset: 15)
        
        // profile name
        containerView.addSubview(profileName)
        profileName.autoAlignAxis(.horizontal, toSameAxisOf: timeLabel)
        profileName.autoPinEdge(.trailing, to: .trailing, of: messageView, withOffset: -2)
        
        if isSameAuthor {
            profileName.isHidden = true
            profileImageView.isHidden = true
        }
        
        // content stack view
        let contenStackView = UIStackView(forAutoLayout: ())
        messageView.addSubview(contenStackView)
        
        if imageDistribution! == .alongsideText {
            contenStackView.axis = NSLayoutConstraint.Axis.horizontal
            contenStackView.alignment = UIStackView.Alignment.center
        } else {
            contenStackView.axis = NSLayoutConstraint.Axis.vertical
            contenStackView.alignment = UIStackView.Alignment.trailing
        }
        
        contenStackView.spacing = 5.0
        contenStackView.autoPinEdge(.leading, to: .leading, of: messageView, withOffset: 15)
        contenStackView.autoPinEdge(.trailing, to: .trailing, of: messageView, withOffset: -40)
        contenStackView.autoPinEdge(.top, to: .top, of: messageView, withOffset: 10)

        let bottomConstraint = contenStackView.bottomAnchor.constraint(equalTo: messageView.bottomAnchor, constant: -10)
        bottomConstraint.priority = UILayoutPriority(800)
        bottomConstraint.isActive = true
        
        
        //Add Chat image and Message
        contenStackView.addArrangedSubview(contentImageView)
        if imagePlacement == .alongsideText || !label.text!.isEmpty { // do not insert empty labels if above text
            contenStackView.addArrangedSubview(label)
        }
    }

CustromRoundedCorenerRectangle.swift

class CustomRoundedCornerRectangle: UIView {
    lazy var shapeLayer = CAShapeLayer()
    var frameToUse: CGRect?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup(frame: frame)
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setup(frame: CGRect(x: 0, y: 0, width: 300, height: 100))
    }
    
    func setup(frame: CGRect) {
        
        // keep frame for later use
        frameToUse = frame
        
        // create CAShapeLayer
                
        // apply other properties related to the path
        shapeLayer.fillColor = UIColor.white.cgColor
        shapeLayer.lineWidth = 1.0
        shapeLayer.strokeColor = UIColor(red: 212/255, green: 212/255, blue: 212/255, alpha: 1.0).cgColor
        shapeLayer.position = CGPoint(x: 0, y: 0)
        
        // add the new layer to our custom view
        self.layer.addSublayer(shapeLayer)
    }
    
    func updateBezierPath(frame: CGRect) {
        let path = UIBezierPath()
        let largeCornerRadius: CGFloat = 18
        let smallCornerRadius: CGFloat = 10
        let upperCornerSpacerRadius: CGFloat = 2
        let imageToArcSpace: CGFloat = 5
        var rect = frame
        
        // bezier frame is smaller than messageView frame
        rect.size.width -= 20
        
        // move to starting point
        path.move(to: CGPoint(x: rect.minX + smallCornerRadius, y: rect.maxY))
       
        // draw bottom left corner
        path.addArc(withCenter: CGPoint(x: rect.minX + smallCornerRadius, y: rect.maxY - smallCornerRadius), radius: smallCornerRadius,
                    startAngle: .pi / 2, // straight down
                    endAngle: .pi, // straight left
                    clockwise: true)
        
        // draw left line
        path.addLine(to: CGPoint(x: rect.minX, y: rect.minY + smallCornerRadius))
        
        // draw top left corner
        path.addArc(withCenter: CGPoint(x: rect.minX + smallCornerRadius, y: rect.minY + smallCornerRadius), radius: smallCornerRadius,
            startAngle: .pi, // straight left
            endAngle: .pi / 2 * 3, // straight up
            clockwise: true)
        
        // draw top line
        path.addLine(to: CGPoint(x: rect.maxX - largeCornerRadius, y: rect.minY))
        
        // draw concave top right corner
        // first arc
        path.addArc(withCenter: CGPoint(x: rect.maxX + largeCornerRadius, y: rect.minY + upperCornerSpacerRadius), radius: upperCornerSpacerRadius, startAngle: .pi / 2 * 3, // straight up
            endAngle: .pi / 2, // straight left
            clockwise: true)
        
        // second arc
        path.addArc(withCenter: CGPoint(x: rect.maxX + largeCornerRadius + imageToArcSpace, y: rect.minY + largeCornerRadius + upperCornerSpacerRadius * 2 + imageToArcSpace), radius: largeCornerRadius + imageToArcSpace, startAngle: CGFloat(240.0).toRadians(), // up with offset
            endAngle: .pi, // straight left
            clockwise: false)
        
        // draw right line
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - smallCornerRadius))
        
        // draw bottom right corner
        path.addArc(withCenter: CGPoint(x: rect.maxX - smallCornerRadius, y: rect.maxY - smallCornerRadius), radius: smallCornerRadius,
                    startAngle: 0, // straight right
            endAngle: .pi / 2, // straight down
                    clockwise: true)
        
        // draw bottom line to close the shape
        path.close()
        
        shapeLayer.path = path.cgPath
    }
}

extension CGFloat {
    func toRadians() -> CGFloat {
        return self * CGFloat(Double.pi) / 180.0
    }
}

CustomChatTableViewCell.swift

class ChatMessageCell: UITableViewCell {
    
    let horizontalInset: CGFloat = 30.0
    let bottomInset: CGFloat = 10.0
    var topInset: CGFloat = 5.0
    
    var didSetupConstraints = false
 
    var messageObject: ChatMessageModel?
    weak var delegate: Notify?
    
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    // what we will call from our tableview method
    func configure(with item: ChatItem?, previousItem: ChatItem?, delegate: Notify?) {
 
        if let safeItem = item {

            messageObject = ChatMessageModel().createMessage(chatItem: safeItem, previousItem: previousItem, shouldLimitSize: false,shouldAddMediaTap: true, imagePlacement: .aboveText, shouldOpenLinks: true)
            messageObject?.delegate = delegate

            let messageContainerView = messageObject?.containerView
            contentView.addSubview(messageContainerView!)
            contentView.backgroundColor = .clear
            backgroundColor = .clear
            selectionStyle = .none
            
            // pin together messages from same author
            if safeItem.user?.name == previousItem?.user?.name {
                topInset = -10.0
            } else {
                topInset = 5.0
            }
            
            messageContainerView?.autoPinEdge(toSuperviewEdge: .top, withInset: topInset)
            messageContainerView?.autoAlignAxis(.vertical, toSameAxisOf: contentView, withOffset: 0)
            messageContainerView?.autoPinEdge(toSuperviewEdge: .bottom, withInset: bottomInset)
        }
    }
    
    override func prepareForReuse() {
        messageObject?.containerView.removeFromSuperview()
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        // redraw message background        
        messageObject?.messageView?.updateBezierPath(frame: (messageObject!.messageView!.frameToUse!))
    }
}

журнал вырубленных сообщений:

(
"<NSLayoutConstraint:0x600000294960 Sport5.CustomRoundedCornerRectangle:0x7f9af3c9e990.height == 89   (active)>",
"<NSLayoutConstraint:0x6000002dc8c0 V:[Sport5.CustomRoundedCornerRectangle:0x7f9af3c9e990]-(0)-|   (active, names: '|':UIView:0x7f9af3ce99a0 )>",
"<NSLayoutConstraint:0x6000002ddef0 V:|-(23)-[Sport5.CustomRoundedCornerRectangle:0x7f9af3c9e990]   (active, names: '|':UIView:0x7f9af3ce99a0 )>",
"<NSLayoutConstraint:0x600000237890 V:|-(-10)-[UIView:0x7f9af3ce99a0]   (active, names: '|':UITableViewCellContentView:0x7f9af3cdd730 )>",
"<NSLayoutConstraint:0x600000237610 UIView:0x7f9af3ce99a0.bottom == UITableViewCellContentView:0x7f9af3cdd730.bottom - 10   (active)>",
"<NSLayoutConstraint:0x600000203ca0 'UIView-Encapsulated-Layout-Height' UITableViewCellContentView:0x7f9af3cdd730.height == 108   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x600000294960 Sport5.CustomRoundedCornerRectangle:0x7f9af3c9e990.height == 89   (active)>

журнал сообщения с новой строкой, которое отображалось нормально (есть проблема с шириной, но я не думаю, что это как-то связано с этой проблемой)

(
"<NSLayoutConstraint:0x600003de94a0 Sport5.CustomImageView:0x7fc7fd4c0540.width == 273.24   (active)>",
"<NSLayoutConstraint:0x600003deaf80 Sport5.CustomRoundedCornerRectangle:0x7fc7fd4e2730.width == 302.22   (active)>",
"<NSLayoutConstraint:0x600003d3fde0 H:|-(15)-[UIStackView:0x7fc7ff2d8430]   (active, names: '|':Sport5.CustomRoundedCornerRectangle:0x7fc7fd4e2730 )>",
"<NSLayoutConstraint:0x600003d3fe30 UIStackView:0x7fc7ff2d8430.trailing == Sport5.CustomRoundedCornerRectangle:0x7fc7fd4e2730.trailing - 40   (active)>",
"<NSLayoutConstraint:0x600003de9d10 'UISV-canvas-connection' UIStackView:0x7fc7ff2d8430.leading == _UILayoutSpacer:0x60000219f660'UISV-alignment-spanner'.leading   (active)>",
"<NSLayoutConstraint:0x600003deba20 'UISV-canvas-connection' H:[Sport5.CustomImageView:0x7fc7fd4c0540]-(0)-|   (active, names: '|':UIStackView:0x7fc7ff2d8430 )>",
"<NSLayoutConstraint:0x600003dea8f0 'UISV-spanning-boundary' _UILayoutSpacer:0x60000219f660'UISV-alignment-spanner'.leading <= Sport5.CustomImageView:0x7fc7fd4c0540.leading   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x600003de94a0 Sport5.CustomImageView:0x7fc7fd4c0540.width == 273.24   (active)>

сократить сообщение

хорошее сообщение, теперь с дополнительным пространством

ограничения метки плохого сообщения

плохие ограничения стека сообщений

хорошие ограничения метки сообщения

хорошие ограничения стека сообщений

Ответы

1 DonMag Sep 23 2020 at 23:52

Я думаю, вы обнаружите, что это работает намного лучше, если вы позволите автоматическому макету обрабатывать все размеры. Нет необходимости рассчитывать размеры ограничивающей рамки текста.

Вот пример с некоторыми образцами данных:

и после прокрутки, чтобы увидеть некоторые сообщения без изображений содержимого:

Код, который я использовал:


Пример структуры и данных

struct MyMessageStruct {
    var time: String = " "
    var name: String = " "
    var profileImageName: String = ""
    var contentImageName: String = ""
    var message: String = " "
}

class SampleData: NSObject {
    let sampleStrings: [String] = [
        "First message with short text.",
        "Second message with longer text that should cause word wrapping in this cell.",
        "Third message with some embedded newlines.\nThis line comes after a newline (\"\\n\"), so we can see if that works the way we want.",
        "Message without content image.",
        "Longer Message without content image.\n\nWith a pair of embedded newline (\"\\n\") characters giving us a \"blank line\" in the message text.",
        "The sixth message, also without a content image."
    ]
    
    lazy var sampleData: [MyMessageStruct] = [
        MyMessageStruct(time: "08:36", name: "Bob",   profileImageName: "pro1", contentImageName: "content1", message: sampleStrings[0]),
        MyMessageStruct(time: "08:47", name: "Bob",   profileImageName: "pro1", contentImageName: "content2", message: sampleStrings[1]),
        MyMessageStruct(time: "08:59", name: "Joe",   profileImageName: "pro2", contentImageName: "content3", message: sampleStrings[2]),
        MyMessageStruct(time: "09:06", name: "Steve", profileImageName: "pro3", contentImageName:         "", message: sampleStrings[3]),
        MyMessageStruct(time: "09:21", name: "Bob",   profileImageName: "pro1", contentImageName:         "", message: sampleStrings[4]),
        MyMessageStruct(time: "09:45", name: "Joe",   profileImageName: "pro2", contentImageName:         "", message: sampleStrings[5]),
    ]
}

Контроллер представления таблицы

class ChatTableViewController: UITableViewController {
    
    var myData: [MyMessageStruct] = SampleData().sampleData
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // register the cell
        tableView.register(ChatMessageCell.self, forCellReuseIdentifier: "chatCell")
        
        tableView.separatorStyle = .none
        tableView.backgroundView = GrayGradientView()
    }
    
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return myData.count
    }
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "chatCell", for: indexPath) as! ChatMessageCell
        
        // don't show the profile image if this message is from the same person
        //  as the previous message
        var isSameAuthor = false
        if indexPath.row > 0 {
            if myData[indexPath.row].name == myData[indexPath.row - 1].name {
                isSameAuthor = true
            }
        }
        
        cell.fillData(myData[indexPath.row], isSameAuthor: isSameAuthor)
        
        return cell
    }
    
}

Класс ячейки

Вы, вероятно, захотите настроить интервал, но комментарии, объясняющие макет, должны прояснить, где вносить изменения.

class ChatMessageCell: UITableViewCell {
    
    let timeLabel = UILabel()
    let nameLabel = UILabel()
    let profileImageView = RoundImageView()
    let bubbleView = CustomRoundedCornerRectangle()
    let stackView = UIStackView()
    let contentImageView = UIImageView()
    let messageLabel = UILabel()
    
    var contentImageHeightConstraint: NSLayoutConstraint!
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        
        [timeLabel, nameLabel, profileImageView, bubbleView, stackView, contentImageView, messageLabel].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
        }
        
        // MARK: add cell elements
        
        contentView.addSubview(timeLabel)
        contentView.addSubview(nameLabel)
        contentView.addSubview(profileImageView)
        contentView.addSubview(bubbleView)

        bubbleView.addSubview(stackView)
        
        stackView.addArrangedSubview(contentImageView)
        stackView.addArrangedSubview(messageLabel)

        // MARK: cell element constraints
        
        // make constraints relative to the default cell margins
        let g = contentView.layoutMarginsGuide
        
        NSLayoutConstraint.activate([
            
            // timeLabel Top: 0 / Leading: 20
            timeLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            timeLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            
            // nameLabel Top: 0 / Trailing: 30
            nameLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
            nameLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -30.0),
            
            // profile image
            //  Top: bubbleView.top + 6
            profileImageView.topAnchor.constraint(equalTo: bubbleView.topAnchor, constant: 6.0),
            //  Trailing: 0 (to contentView margin)
            profileImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
            //  Width: 50 / Height: 1:1 (to keep it square / round)
            profileImageView.widthAnchor.constraint(equalToConstant: 50.0),
            profileImageView.heightAnchor.constraint(equalTo: profileImageView.widthAnchor),
            
            // bubbleView
            //  Top: timeLabel.bottom + 4
            bubbleView.topAnchor.constraint(equalTo: timeLabel.bottomAnchor, constant: 4.0),
            //  Leading: timeLabel.leading + 16
            bubbleView.leadingAnchor.constraint(equalTo: timeLabel.leadingAnchor, constant: 16.0),
            //  Trailing: profile image.leading - 4
            bubbleView.trailingAnchor.constraint(equalTo: profileImageView.leadingAnchor, constant: -4.0),
            //  Bottom: contentView.bottom
            bubbleView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
            
            // stackView (to bubbleView)
            //  Top / Bottom: 12
            stackView.topAnchor.constraint(equalTo: bubbleView.topAnchor, constant: 12.0),
            stackView.bottomAnchor.constraint(equalTo: bubbleView.bottomAnchor, constant: -12.0),
            //  Leading / Trailing: 16
            stackView.leadingAnchor.constraint(equalTo: bubbleView.leadingAnchor, constant: 16.0),
            stackView.trailingAnchor.constraint(equalTo: bubbleView.trailingAnchor, constant: -16.0),

        ])
        
        // contentImageView height ratio - will be changed based on the loaded image
        // we need to set its Priority to less-than Required or we get auto-layout warnings when the cell is reused
        contentImageHeightConstraint = contentImageView.heightAnchor.constraint(equalTo: contentImageView.widthAnchor, multiplier: 2.0 / 3.0)
        contentImageHeightConstraint.priority = .defaultHigh
        contentImageHeightConstraint.isActive = true

        // messageLabel minimum Height: 40
        // we need to set its Priority to less-than Required or we get auto-layout warnings when the cell is reused
        let c = messageLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 40.0)
        c.priority = .defaultHigh
        c.isActive = true
        
        // MARK: element properties
        
        stackView.axis = .vertical
        stackView.spacing = 6
        
        // set label fonts and alignment here
        timeLabel.font = UIFont.systemFont(ofSize: 14, weight: .regular)
        nameLabel.font = UIFont.systemFont(ofSize: 14, weight: .bold)
        timeLabel.textColor = .gray
        nameLabel.textColor = UIColor(red: 0.175, green: 0.36, blue: 0.72, alpha: 1.0)
        
        // for now, I'm just setting the message label to right-aligned
        //  likely using RTL
        messageLabel.textAlignment = .right
        
        messageLabel.numberOfLines = 0
        
        contentImageView.backgroundColor = .blue
        contentImageView.contentMode = .scaleAspectFit
        contentImageView.layer.cornerRadius = 8
        contentImageView.layer.masksToBounds = true
        
        profileImageView.contentMode = .scaleToFill
        
        // MARK: cell background
        backgroundColor = .clear
        contentView.backgroundColor = .clear
    }
    
    func fillData(_ msg: MyMessageStruct, isSameAuthor: Bool) -> Void {
        timeLabel.text = msg.time
        nameLabel.text = msg.name
        
        nameLabel.isHidden = isSameAuthor
        profileImageView.isHidden = isSameAuthor
        
        if !isSameAuthor {
            if !msg.profileImageName.isEmpty {
                if let img = UIImage(named: msg.profileImageName) {
                    profileImageView.image = img
                }
            }
        }
        if !msg.contentImageName.isEmpty {
            contentImageView.isHidden = false
            if let img = UIImage(named: msg.contentImageName) {
                contentImageView.image = img
                let ratio = img.size.height / img.size.width
                contentImageHeightConstraint.isActive = false
                contentImageHeightConstraint = contentImageView.heightAnchor.constraint(equalTo: contentImageView.widthAnchor, multiplier: ratio)
                contentImageHeightConstraint.priority = .defaultHigh
                contentImageHeightConstraint.isActive = true
            }
        } else {
            contentImageView.isHidden = true
        }
        messageLabel.text = msg.message
    }
}

Дополнительные занятия

Для «вида пузыря чата», «вида изображения с закругленными углами» и «вида градиентного фона»

class CustomRoundedCornerRectangle: UIView {
    lazy var shapeLayer = CAShapeLayer()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setup()
    }
    
    func setup() {
        // apply properties related to the path
        shapeLayer.fillColor = UIColor.white.cgColor
        shapeLayer.lineWidth = 1.0
        shapeLayer.strokeColor = UIColor(red: 212/255, green: 212/255, blue: 212/255, alpha: 1.0).cgColor
        shapeLayer.position = CGPoint(x: 0, y: 0)
        
        // add the new layer to our custom view
        //self.layer.addSublayer(shapeLayer)
        self.layer.insertSublayer(shapeLayer, at: 0)
    }
    
    override func layoutSubviews() {
        
        let path = UIBezierPath()
        let largeCornerRadius: CGFloat = 18
        let smallCornerRadius: CGFloat = 10
        let upperCornerSpacerRadius: CGFloat = 2
        let imageToArcSpace: CGFloat = 5
        let rect = bounds
        
        // move to starting point
        path.move(to: CGPoint(x: rect.minX + smallCornerRadius, y: rect.maxY))
        
        // draw bottom left corner
        path.addArc(withCenter: CGPoint(x: rect.minX + smallCornerRadius, y: rect.maxY - smallCornerRadius), radius: smallCornerRadius,
                    startAngle: .pi / 2, // straight down
                    endAngle: .pi, // straight left
                    clockwise: true)
        
        // draw left line
        path.addLine(to: CGPoint(x: rect.minX, y: rect.minY + smallCornerRadius))
        
        // draw top left corner
        path.addArc(withCenter: CGPoint(x: rect.minX + smallCornerRadius, y: rect.minY + smallCornerRadius), radius: smallCornerRadius,
                    startAngle: .pi, // straight left
                    endAngle: .pi / 2 * 3, // straight up
                    clockwise: true)
        
        // draw top line
        path.addLine(to: CGPoint(x: rect.maxX - largeCornerRadius, y: rect.minY))
        
        // draw concave top right corner
        // first arc
        path.addArc(withCenter: CGPoint(x: rect.maxX + largeCornerRadius, y: rect.minY + upperCornerSpacerRadius), radius: upperCornerSpacerRadius, startAngle: .pi / 2 * 3, // straight up
                    endAngle: .pi / 2, // straight left
                    clockwise: true)
        
        // second arc
        path.addArc(withCenter: CGPoint(x: rect.maxX + largeCornerRadius + imageToArcSpace, y: rect.minY + largeCornerRadius + upperCornerSpacerRadius * 2 + imageToArcSpace), radius: largeCornerRadius + imageToArcSpace, startAngle: CGFloat(240.0).toRadians(), // up with offset
                    endAngle: .pi, // straight left
                    clockwise: false)
        
        // draw right line
        path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - smallCornerRadius))
        
        // draw bottom right corner
        path.addArc(withCenter: CGPoint(x: rect.maxX - smallCornerRadius, y: rect.maxY - smallCornerRadius), radius: smallCornerRadius,
                    startAngle: 0, // straight right
                    endAngle: .pi / 2, // straight down
                    clockwise: true)
        
        // draw bottom line to close the shape
        path.close()
        
        shapeLayer.path = path.cgPath
    }
}

extension CGFloat {
    func toRadians() -> CGFloat {
        return self * CGFloat(Double.pi) / 180.0
    }
}

class RoundImageView: UIImageView {
    override func layoutSubviews() {
        layer.masksToBounds = true
        layer.cornerRadius = bounds.size.height * 0.5
    }
}

class GrayGradientView: UIView {
    private var gradLayer: CAGradientLayer!
    
    override class var layerClass: AnyClass {
        return CAGradientLayer.self
    }
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() -> Void {
        
        let myColors: [UIColor] = [
            UIColor(white: 0.95, alpha: 1.0),
            UIColor(white: 0.90, alpha: 1.0),
        ]
        
        gradLayer = self.layer as? CAGradientLayer
        
        // assign the colors (we're using map to convert UIColors to CGColors
        gradLayer.colors = myColors.map({$0.cgColor})
        
        // start at the top
        gradLayer.startPoint = CGPoint(x: 0.25, y: 0.0)
        
        // end at the bottom
        gradLayer.endPoint = CGPoint(x: 0.75, y: 1.0)
        
    }
}

И образцы изображений (щелкните, чтобы увидеть полный размер):

content1.png content2.png content3.png

pro1.png pro2.png pro3.png