Search code examples
swiftuitableviewautolayout

UITableViewCell auto-layout issue with centerXAnchor set


I am trying to build an messaging interface and faced this issue. As this is messaging app, a message bubble either align leading or trailing. I thought doing this kind if thing, creating a subview called bubbleView which will align itself using a computed variable called bubbleViewCenterXConstraintValue.

override var frame: CGRect {
    didSet {
        print(frame, bubbleView.frame)
        self.setNeedsLayout()
    }
}

private var bubbleViewCenterXConstraintValue: CGFloat {
    print(UIScreen.main.bounds.width, contentView.frame.width, bubbleView.frame.width, ((UIScreen.main.bounds.width - bubbleView.bounds.width) / 2))
    return ((UIScreen.main.bounds.width - bubbleView.bounds.width) / 2)
}

override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    
    backgroundColor = .clear
    contentView.backgroundColor = .systemTeal
    
    bubbleView.addSubviews(messageLabel, hourLabel)
    
    messageLabel.leadingAnchor.constraint(equalTo: bubbleView.leadingAnchor, constant: 12).isActive = true
    messageLabel.trailingAnchor.constraint(equalTo: bubbleView.trailingAnchor, constant: -12).isActive = true
    messageLabel.topAnchor.constraint(equalTo: bubbleView.topAnchor, constant: 12).isActive = true
    
    hourLabel.trailingAnchor.constraint(equalTo: messageLabel.trailingAnchor).isActive = true
    hourLabel.topAnchor.constraint(equalTo: messageLabel.bottomAnchor, constant: 4).isActive = true
    hourLabel.bottomAnchor.constraint(equalTo: bubbleView.bottomAnchor, constant: -12).isActive = true
    
    contentView.addSubview(bubbleView)
    
    bubbleView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4).isActive = true
    bubbleView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4).isActive = true
    bubbleView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor, constant: bubbleViewCenterXConstraintValue).isActive = true
    bubbleView.widthAnchor.constraint(lessThanOrEqualToConstant: availableMaxWidth).isActive = true
}

However, whenever bubbleViewCenterXConstraintValue is called the frame of bubbleView is zero so it is miscalculating my logic. Logic does not include left or right alignment right now, it just tries to align to right.

When I looked into those print results I realized another weird thing. Width of contentView is 320 which is not equal to the UIScreen.main.bounds.width(390).

(0.0, 0.0, 320.0, 44.0) (0.0, 0.0, 0.0, 0.0)
390.0 320.0 0.0 195.0
(0.0, 0.0, 390.0, 100.66666666666667) (0.0, 0.0, 0.0, 0.0)
(0.0, 0.0, 390.0, 100.66666412353516) (243.66666666666666, 4.0, 292.6666666666667, 92.66666666666667)
(0.0, 0.0, 390.0, 100.66666412353516) (243.66666666666666, 4.0, 292.6666666666667, 92.66666666666667)

I need a way to make those auto-layout code to update whenever the frame of bubbleView is set. Because I know that if its width would be 292 initially this code would work. I tried the current non working solution setNeedsLayout() and overriding layoutSubviews but that did not work too.

I know I am missing something and the solution is just right there, however I cant see right now and need some help.

Appreciated in advance.

Note: Also looked for a solution in the site could not find anything similar. If you think this is duplicate feel free to tell me or get in action.


Solution

  • I'd have thought the easier way to do this would be to set leading and trailing constraints for the bubble. This would make it easy to decide which side of the screen to align to. You could also use autolayout for the width parameter, using a multiplier based on parent view width to make it cope with any size screen.

    I'm not going to work it into your code as it would be too long, but hopefully the below example function will show the principles I'd recommend:

       func addBubble(alignLeft: Bool, offset: CGFloat) {
          let bubble = UIView()
          bubble.backgroundColor = .systemBlue
          let parentView = view.safeAreaLayoutGuide  //your parent view may be different
          parentView.addSubview(bubble)
    
          bubble.translatesAutoresizingMaskIntoConstraints = false
          
          //quick and dirty top and bottom constraints for the example
          bubble.topAnchor.constraint(equalTo: parentView.topAnchor, constant: 10 + offset).isActive = true
          bubble.bottomAnchor.constraint(equalTo: parentView.topAnchor, constant: 40 + offset).isActive = true
    
          //set the width of the bubble proportional to the size of the parent view
          bubble.widthAnchor.constraint(equalTo: parentView.widthAnchor, multiplier: 2/3).isActive = true
    
          // implement the left or right alignment
          if alignLeft {
             bubble.leadingAnchor.constraint(equalTo: parentView.leadingAnchor, constant: 4).isActive = true
          } else {
             bubble.trailingAnchor.constraint(equalTo: parentView.trailingAnchor, constant: -4).isActive = true
          }
       }
    

    This way all you have to do is anchor the bubble's sub views to the bubbles top/bottom/leading/trailing constraints and they will also resize with the bubble to cope with all screen sizes.