String Bounding String '\ n' içerdiğinde doğru hesaplama sorunu

Sep 23 2020

Uygulamamızın içinde Whatsapp tarzı bir özellik geliştiriyorum. mesajlar JSON'dan ayrıştırılır ve ardından bir UITableView içinde metin (+ isteğe bağlı resim) mesajları olarak oluşturulur (her mesaj özel bir hücredir). Mesaj balonu, boundingRect yöntemi kullanılarak metin çerçevesinin hesaplanmasına bağlı olarak Bezier Yolu kullanılarak çizilir. Daha sonra UILabel ve UIImage, bir UIStackview'un alt görünümleri olarak eklenir ve hem StackView hem de mesaj balonu görünümü bir konteyner görünümüyle sınırlandırılır.

Bazen metin '\ n' içerdiğinde UILabel ya kesiliyor ('...' ile) ya da yığın görünümünün alt bağlantı önceliğine bağlı olarak (UILabel'in içeriği kucaklayan önceliğinden daha yüksek / daha düşük) mesaj balonu görünümünün altına akıyor ), ancak satırsonu içeren diğer mesajlar doğru görünür. Tahminimce dizenin çerçeve hesaplaması '\ n' karakterini satırsonu yerine 2 karakter olarak ele alıyor.

Aynı kodu bir oyun alanında test etmeyi denediğimde (daha basit bir düzen ile, yalnızca UILabel ve kabarcık görünümü, kap görünümü yok, tablo görünümü yok ve kısıtlama yok) her şey yolunda gidiyor gibi görünüyordu ve balon, eklenen yeni satırlara uyum sağlamak için genişleyecekti.

Bu konuya dayanarak , kodu sizeThatFits Yöntemi ile değiştirmeyi denedim, yine de aynı sonuç. Sonunda, dizenin içindeki '\ n' oluşumlarını saymaya ve çerçeveye manuel olarak yükseklik ekledim, ancak bu hem kötü mesajları hem de iyi mesajları etkiliyor ve artık onları çevreleyen fazladan boşluk var.

Ekran görüntüleri, ilgili kod ve konsol günlükleri aşağıya eklenmiştir. Umarım birisinin bunu çözmesine yardımcı olur.

Düzenleme: messageView genişliğini UIScreen.main.bounds.width * 0.73'ten UIScreen.main.bounds.width * 0.8'e değiştirmek sorunu çözdü. Bununla birlikte, neden yalnızca belirli iletileri etkilediğini hala anlayamıyorum. Bununla ilgili daha fazla bilgi için minnettar olacağım.

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

kesinti mesajı günlüğü:

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

Tamam görüntülenen satırsonu ile bir mesajın günlüğü (bir genişlik sorunu var, ancak bu sorunla bir ilgisi olduğunu düşünmüyorum)

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

mesajı kısalt

iyi mesaj, şimdi fazladan boşlukla

kötü mesaj etiketi kısıtlamaları

kötü mesaj yığını kısıtlamaları

iyi mesaj etiketi kısıtlamaları

iyi mesaj yığını kısıtlamaları

Yanıtlar

1 DonMag Sep 23 2020 at 23:52

Otomatik mizanpajın tüm boyutlandırmayı işlemesine izin verirseniz çok daha iyi çalıştığını göreceksiniz. Metin sınırlayıcı kutu boyutlarının hesaplanmasına güvenmenize gerek yok.

İşte bazı örnek verilerle bir örnek:

ve içerik resimleri olmadan bazı mesajları görmek için kaydırdıktan sonra:

Kullandığım kod:


Örnek Yapı ve Veriler

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

Tablo Görünümü Denetleyicisi

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

Hücre Sınıfı

Muhtemelen aralığı değiştirmek isteyeceksiniz, ancak düzeni açıklayan yorumlar, nerede değişiklik yapılacağını netleştirmelidir.

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

Ek Sınıflar

"Sohbet balonu görünümü", "yuvarlatılmış köşeli resim görünümü" ve "degrade arka plan görünümü" için

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

Ve örnek resimler (tam boyutlar için tıklayın):

content1.png content2.png content3.png

pro1.png pro2.png pro3.png