String Bounding Korrektes Berechnungsproblem, wenn der String '\ n' enthält

Sep 23 2020

Ich entwickle eine WhatsApp-Funktion in unserer App. Nachrichten werden von JSON analysiert und dann als Textnachrichten (+ optionales Bild) in einer UITableView erstellt (jede Nachricht ist eine benutzerdefinierte Zelle). Die Nachrichtenblase wird mithilfe des Bezier-Pfads gezeichnet, basierend auf der Berechnung des Textrahmens mithilfe der boundingRect-Methode. Später werden UILabel und UIImage als Unteransichten einer UIStackview hinzugefügt, und sowohl die StackView- als auch die Nachrichtenblasenansicht sind auf eine Containeransicht beschränkt.

Manchmal, wenn der Text '\ n' enthält, wird das UILabel entweder abgeschnitten (mit '...') oder fließt unter die Nachrichtenblasenansicht, abhängig von der unteren Ankerpriorität der Stapelansicht (höher / niedriger als die inhaltliche Priorität des UILabels) ), aber andere Nachrichten, die Zeilenumbrüche enthalten, werden korrekt angezeigt. Ich vermute, dass die Rahmenberechnung der Zeichenfolge das '\ n' als 2 Zeichen anstelle einer neuen Zeile behandelt.

Als ich versuchte, denselben Code auf einem Spielplatz zu testen (mit einem einfacheren Layout, nur UILabel und Blasenansicht, keine Containeransichten, keine Tabellenansicht und keine Einschränkungen), schien alles gut zu funktionieren und die Blase würde sich erweitern, um sich an die hinzugefügten Zeilenumbrüche anzupassen.

Basierend auf diesem Thread habe ich versucht, den Code durch die sizeThatFits-Methode zu ersetzen, immer noch das gleiche Ergebnis. Schließlich zählte ich die Vorkommen von '\ n' innerhalb der Zeichenfolge und fügte dem Frame manuell Höhe hinzu, aber dies wirkt sich sowohl auf die schlechten als auch auf die guten Nachrichten aus, die inzwischen zusätzlichen Platz um sie herum haben.

Screenshots, relevanter Code und Konsolenprotokolle sind unten angefügt. Hoffentlich hilft es jemandem, dies herauszufinden.

Bearbeiten: Durch Ändern der Breite von messageView von UIScreen.main.bounds.width * 0.73 in UIScreen.main.bounds.width * 0.8 wurde das Problem behoben. Ich kann jedoch immer noch nicht herausfinden, warum es nur bestimmte Nachrichten betraf. Für weitere Informationen hierzu bin ich dankbar.

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!))
    }
}

Protokoll der abgeschnittenen Nachricht:

(
"<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)>

Protokoll einer Nachricht mit Zeilenumbruch, die in Ordnung angezeigt wurde (es gibt ein Breitenproblem, aber ich glaube nicht, dass es etwas mit diesem Problem zu tun hat).

(
"<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)>

Nachricht abschneiden

gute Nachricht, jetzt mit zusätzlichem Platz

Einschränkungen bei der Beschriftung von Nachrichten

schlechte Nachrichtenstapeleinschränkungen

Gute Einschränkungen für die Nachrichtenbezeichnung

Gute Einschränkungen für den Nachrichtenstapel

Antworten

1 DonMag Sep 23 2020 at 23:52

Ich denke, Sie werden feststellen, dass es viel besser funktioniert, wenn Sie das automatische Layout die gesamte Größe übernehmen lassen. Sie müssen sich nicht auf die Berechnung der Größe von Textbegrenzungsrahmen verlassen.

Hier ist ein Beispiel mit einigen Beispieldaten:

und nach dem Scrollen, um einige Nachrichten ohne Inhaltsbilder anzuzeigen:

Der Code, den ich verwendet habe:


Beispielstruktur und Daten

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]),
    ]
}

Table View Controller

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
    }
    
}

Zellklasse

Möglicherweise möchten Sie den Abstand anpassen, aber die Kommentare, die das Layout erläutern, sollten klar machen, wo Änderungen vorgenommen werden müssen.

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
    }
}

Zusätzliche Klassen

Für die "Chat-Blasenansicht", "Bildansicht mit abgerundeten Ecken" und "Hintergrundansicht mit Farbverlauf"

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)
        
    }
}

Und Beispielbilder (zum Vergrößern anklicken):

content1.png content2.png content3.png

pro1.png pro2.png pro3.png