How to vertically overlap and hide labels in a custom UITableView cell with dynamic height and width to reduce cell size and save space

馋奶兔 提交于 2021-02-11 12:21:19

问题


I have a messaging cell that has 3 rows of UILabel, 1. Sender Name, 2. Message 3. Sent Time. Initially, I was keeping an offset of 10 between each of the labels. But I realized that I was wasting space in cases where either the message was too small or if the message wrapped over to a new line and had empty space on the right. So I decided to move the label showing sent time up to save some of that space for the case where the message was too small. I constraint the container like this:

messageBubble.widthAnchor.constraint(lessThanOrEqualTo: self.contentView.widthAnchor, constant: -100)

So I thought I could rely on contentView.frame.width - 100 to get the max container width. But when the reusable cell gets dequeued, this value changes and is not fixed. So for the initial load, my table view cells with small messages look decent but after scrolling for a bit, the width calculations go all wrong. Also, the message label text seems to take up a lot of padding which wastes even more space.

Can someone help me fix the constraints so that the sent time label overlaps the message label if there is space? (Small messages and wrapping to new line messages both). Also, I wanted to hide the sender's name if the previous message in that section was also sent by him. (Eg In the last cell, you will be hidden) I had added the code for it at the end of my cell configuration method. But unfortunately, since this is not a stack view, setting the sender's name UILabel to hidden leaves a blank space. I think this could be solved by setting the height and width of that label to 0 and the priority of its trailing anchor low. And finally setting the top of the message label to the message bubble's top. But since both the bubble width and height are set dynamically I am not sure how to show the sender name label again properly. Since the sender name label's width is also set dynamically and its contents wrap in case of a longer name.

Before scrolling:

Message: Test 1
Sender Name Width: 53.666666666666664
Message Label Width: 47.333333333333336
Sent Time Label Width: 51.666666666666664
Total Message and Sent Time label width: 109.0
Message bubble width: 220.0
Message bubble width from screen: 275.0
Message Bubble Width > Total Sent and Message Width? true

Message: Test message 2 that is a bit longer and goes to line 2
Sender Name Width: 53.666666666666664
Message Label Width: 339.3333333333333
Sent Time Label Width: 51.666666666666664
Total Message and Sent Time label width: 401.0
Message bubble width: 220.0
Message bubble width from screen: 275.0
Message Bubble Width > Total Sent and Message Width? false

After scrolling:

Message: Test 1
Sender Name Width: 53.666666666666664
Message Label Width: 47.333333333333336
Sent Time Label Width: 65536.0
Total Message and Sent Time label width: 65593.33333333333
Message bubble width: 255.0
Message bubble width from screen: 275.0
Message Bubble Width > Total Sent and Message Width? false

Message: Test message 2 that is a bit longer and goes to line 2
Sender Name Width: 65536.0
Message Label Width: 65536.0
Sent Time Label Width: 65536.0
Total Message and Sent Time label width: 131082.0
Message bubble width: 255.0
Message bubble width from screen: 275.0
Message Bubble Width > Total Sent and Message Width? false

Hiererachy with padding of labels:

The custom cell code:

class DiscussionChatMessageCell: UITableViewCell {
    
    private let messageLabel: UITextView
    private let senderNameLabel: UILabel
    private let messageSentTimeLabel: UILabel
    private let messageBubble: UIView
    
    private var bubbleLeadingConstraint: NSLayoutConstraint!
    private var bubbleTrailingConstraint: NSLayoutConstraint!
    private var messageLabelMessageBubbleTrailingConstraint: NSLayoutConstraint!
    private var sentTimeLabelMessageBubbleLeadingConstraint: NSLayoutConstraint!
    private var sentTimeLabelMessageLabelLeadingConstraint: NSLayoutConstraint!
    private var sentTimeTrailingAnchor: NSLayoutConstraint!
    private var positiveOffsetSentTimeTopAnchor: NSLayoutConstraint!
    private var negativeOffsetSentTimeTopAnchor: NSLayoutConstraint!

    private let verticalSpacing: CGFloat = 0
        
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        
        messageLabel = UITextView()
        senderNameLabel = UILabel()
        messageSentTimeLabel = UILabel()
        messageBubble = UIView()
        
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        
        selectionStyle = .none
        
        self.contentView.addSubview(messageBubble)
        messageBubble.translatesAutoresizingMaskIntoConstraints = false
        
        messageBubble.addSubview(senderNameLabel)
        senderNameLabel.translatesAutoresizingMaskIntoConstraints = false
        senderNameLabel.numberOfLines = 0
        senderNameLabel.lineBreakMode = .byCharWrapping
        senderNameLabel.font =  getFont(name: .HelveticaNeueBold, size: .large)
        senderNameLabel.textColor = .white
        
        messageBubble.addSubview(messageLabel)
        messageLabel.translatesAutoresizingMaskIntoConstraints = false
        
        messageLabel.delegate = self
        messageLabel.isEditable = false
        messageLabel.isSelectable = true
        messageLabel.dataDetectorTypes = .all
        messageLabel.textContainer.lineBreakMode = .byWordWrapping
        messageLabel.isScrollEnabled = false
        messageLabel.backgroundColor = .clear
        //        messageLabel.isUserInteractionEnabled = true
        messageLabel.font = getFont(name: .HelveticaNeue, size: .medium)
        
        messageBubble.addSubview(messageSentTimeLabel)
        messageSentTimeLabel.translatesAutoresizingMaskIntoConstraints = false
        messageSentTimeLabel.lineBreakMode = .byCharWrapping
        messageSentTimeLabel.numberOfLines = 0
        messageSentTimeLabel.font = getFont(name: .HelveticaNeueItalic, size: .small)
        
        // set hugging and compression resistance for Name label
        senderNameLabel.setContentCompressionResistancePriority(.required, for: .vertical)
        senderNameLabel.setContentHuggingPriority(.required, for: .vertical)
        
        // create bubble Leading and Trailing constraints
        bubbleLeadingConstraint = messageBubble.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor, constant: 5)
        bubbleTrailingConstraint = messageBubble.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor, constant: -5)
        
        messageLabelMessageBubbleTrailingConstraint = messageLabel.trailingAnchor.constraint(equalTo: messageBubble.trailingAnchor, constant: -5)
        sentTimeLabelMessageBubbleLeadingConstraint = messageSentTimeLabel.leadingAnchor.constraint(equalTo: messageBubble.leadingAnchor, constant: 5)
        sentTimeLabelMessageLabelLeadingConstraint = messageSentTimeLabel.leadingAnchor.constraint(equalTo: messageLabel.trailingAnchor, constant: 5)
        
        sentTimeTrailingAnchor = messageSentTimeLabel.trailingAnchor.constraint(equalTo: messageBubble.trailingAnchor, constant: -5)
        
        positiveOffsetSentTimeTopAnchor = messageSentTimeLabel.topAnchor.constraint(equalTo: messageLabel.bottomAnchor, constant: verticalSpacing)
        negativeOffsetSentTimeTopAnchor = messageSentTimeLabel.topAnchor.constraint(equalTo: messageLabel.bottomAnchor, constant: -1 * 5)
        
        // priority will be changed in configureCell()
        bubbleLeadingConstraint.priority = .defaultHigh
        bubbleTrailingConstraint.priority = .defaultLow
        messageLabelMessageBubbleTrailingConstraint.priority = .defaultHigh
        sentTimeLabelMessageBubbleLeadingConstraint.priority = .defaultHigh
        sentTimeLabelMessageLabelLeadingConstraint.priority = .defaultLow
        positiveOffsetSentTimeTopAnchor.priority = .defaultHigh
        negativeOffsetSentTimeTopAnchor.priority = .defaultLow
        
        NSLayoutConstraint.activate([
            
            bubbleLeadingConstraint,
            bubbleTrailingConstraint,
            
            messageBubble.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 5),
            messageBubble.bottomAnchor.constraint(equalTo:  self.contentView.bottomAnchor, constant: -5),
            
            messageBubble.widthAnchor.constraint(lessThanOrEqualTo: self.contentView.widthAnchor, constant: -100),
            
            senderNameLabel.topAnchor.constraint(equalTo: messageBubble.topAnchor, constant: 5),
            senderNameLabel.leadingAnchor.constraint(equalTo: messageBubble.leadingAnchor, constant: 5),
            //          Causes constraint errors that need to be broken
            senderNameLabel.trailingAnchor.constraint(equalTo: messageBubble.trailingAnchor, constant: -5),
            //          Causes lesser constraint errors that need to be broken
            //            senderNameLabel.trailingAnchor.constraint(equalTo: messageLabel.trailingAnchor),
            
            messageLabel.topAnchor.constraint(equalTo: senderNameLabel.bottomAnchor, constant: verticalSpacing),
            messageLabel.leadingAnchor.constraint(equalTo: messageBubble.leadingAnchor, constant: 5),
            
            messageLabelMessageBubbleTrailingConstraint,
            
//            messageLabel.bottomAnchor.constraint(equalTo: messageSentTimeLabel.topAnchor, constant: -10),
            
            
//            sentTimeLabelMessageBubbleLeadingConstraint,
            sentTimeLabelMessageLabelLeadingConstraint,
            
            //            messageSentTimeLabel.trailingAnchor.constraint(equalTo: messageBubble.trailingAnchor, constant: -10),
            messageSentTimeLabel.bottomAnchor.constraint(equalTo: messageBubble.bottomAnchor, constant: -5),
            messageSentTimeLabel.trailingAnchor.constraint(equalTo: messageBubble.trailingAnchor, constant: -5),
            
            positiveOffsetSentTimeTopAnchor,
            negativeOffsetSentTimeTopAnchor
            
        ])
        
        // corners will have radius: 10
        messageBubble.layer.cornerRadius = 5
    }
    
    func getMessageLabel() -> UITextView {
        return messageLabel
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configureCell(message: DiscussionMessage, isSender: Bool, previousMessage: DiscussionMessage?) {
        
        let senderName = isSender ? "You" : message.userName
        senderNameLabel.text = senderName + " " + message.userCountryEmoji
        
        let date = Date(timeIntervalSince1970: message.messageTimestamp)
        
        let dayTimePeriodFormatter = DateFormatter()
        dayTimePeriodFormatter.timeZone = .current
        
        dayTimePeriodFormatter.dateFormat = "hh:mm a"
        let dateString = dayTimePeriodFormatter.string(from: date)
        
        messageLabel.text = message.message
        
        messageSentTimeLabel.text = dateString
        
        messageLabel.textColor = isSender ? .black : .white
        senderNameLabel.textColor = isSender ? .black : .white
        messageSentTimeLabel.textColor = isSender ? transparentBlack : transparentWhite
//        messageSentTimeLabel.textAlignment = isSender ? .right : .left
        
        bubbleLeadingConstraint.priority = isSender ? .defaultLow : .defaultHigh
        bubbleTrailingConstraint.priority = isSender ? .defaultHigh : .defaultLow
        
        messageBubble.backgroundColor = isSender ? accentColor : .gray
        
        let senderCorners: CACornerMask = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner]
        let nonSenderCorners: CACornerMask =  [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMaxXMaxYCorner]
        
        let cellWidthFromScreenWidth = UIScreen.main.bounds.width - 100
        let senderNameLabelWidth = senderNameLabel.intrinsicContentSize.width
        let messageLabelWidth = messageLabel.intrinsicContentSize.width
        let sentTimeLabelWidth = messageSentTimeLabel.intrinsicContentSize.width
        let messagaeAndSentTimeLabelWidth = messageLabelWidth + sentTimeLabelWidth + 10
        let maxMessageBubbleWidth = contentView.frame.width - 100
        let shouldShowMessageAndSentTimeInLine: Bool = maxMessageBubbleWidth > messagaeAndSentTimeLabelWidth
        
        print("Message: \(message.message)")
        print("Sender Name Width: \(senderNameLabelWidth)")
        print("Message Label Width: \(messageLabelWidth)")
        print("Sent Time Label Width: \(sentTimeLabelWidth)")
        print("Total Message and Sent Time label width: \(messagaeAndSentTimeLabelWidth)")
        print("Message bubble width: \(maxMessageBubbleWidth)")
        print("Message bubble width from screen: \(cellWidthFromScreenWidth)")
        print("Message Bubble Width > Total Sent and Message Width? \(messagaeAndSentTimeLabelWidth < maxMessageBubbleWidth)")
        print()
        
        if shouldShowMessageAndSentTimeInLine {
            if senderNameLabelWidth < messagaeAndSentTimeLabelWidth {
                sentTimeLabelMessageLabelLeadingConstraint.priority = .defaultHigh
                messageLabelMessageBubbleTrailingConstraint.priority = .defaultLow
                positiveOffsetSentTimeTopAnchor.priority = .defaultLow
                negativeOffsetSentTimeTopAnchor.priority = .defaultHigh
            } else {
                positiveOffsetSentTimeTopAnchor.priority = .defaultLow
                negativeOffsetSentTimeTopAnchor.priority = .defaultHigh
            }
        } else {
            sentTimeLabelMessageLabelLeadingConstraint.priority = .defaultLow
            messageLabelMessageBubbleTrailingConstraint.priority = .defaultHigh
            positiveOffsetSentTimeTopAnchor.priority = .defaultHigh
            negativeOffsetSentTimeTopAnchor.priority = .defaultLow
        }
        
        
        messageBubble.layer.maskedCorners = isSender ? senderCorners : nonSenderCorners
        //        updateLastReadMessageTimestamp(message: message)
        //
        //        if let previousMessage = previousMessage {
        //            if message.userEmailAddress == previousMessage.userEmailAddress && message.userCountryCode == previousMessage.userCountryCode && isSender {
        //                senderNameLabel.isHidden = true
        //            } else {
        //                senderNameLabel.isHidden = false
        //            }
        //        } else {
        //            senderNameLabel.isHidden = false
        //        }
    }
    
    func updateLastReadMessageTimestamp(message: DiscussionMessage) {
        let defaults = UserDefaults.standard
        let previousLastReadMessageTimestamp = defaults.double(forKey: lastReadMessageTimestampKey)
        if previousLastReadMessageTimestamp < message.messageTimestamp {
            defaults.setValue(message.messageTimestamp, forKey: lastReadMessageTimestampKey)
            defaults.setValue(message.messageID, forKey: lastReadMessageIDKey)
            //            print("Saving last read message timestamp: ", message.messageTimestamp, " message id: ", message.messageID)
            saveLastReadMessageTimestamp()
        }
    }
}

extension DiscussionChatMessageCell: UITextViewDelegate {
    func textViewDidChangeSelection(_ textView: UITextView) {
        textView.selectedTextRange = nil
    }
}

来源:https://stackoverflow.com/questions/66100071/how-to-vertically-overlap-and-hide-labels-in-a-custom-uitableview-cell-with-dyna

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!