0
votes

Below you can see two posts, one with an image, and one without. I placed borders around the views to better understand what is happening. I want the posts without images to be sized smaller than images with posts. I attempted to do this by doing the following:

func setupViews() {

      backgroundColor = UIColor.white

      addSubview(titleLabel)
      addSubview(iconImageView)
      addSubview(messageTextView)
      addSubview(messageImageView)

      iconImageView.anchor(top: contentView.topAnchor,
                           leading: contentView.leadingAnchor,
                           bottom: nil, trailing: nil, 
                           padding: .init(top: 0, left: 8, bottom: 0, right: 0), size: CGSize(width: 44, height: 44))

      titleLabel.anchor(top: contentView.topAnchor, 
                        leading: iconImageView.trailingAnchor,
                        bottom: nil, trailing: nil,
                        padding: .init(top: 12, left: 8, bottom: 0, right: 0))

      messageTextView.anchor(top: titleLabel.bottomAnchor,
                             leading: contentView.leadingAnchor,
                             bottom: nil,
                             trailing: contentView.trailingAnchor,
                             padding: .init(top: 4, left: 10, bottom: 0, right: 10))

      messageImageViewHeightConstraint = messageImageView.heightAnchor.constraint(equalToConstant: 200)
      messageImageViewHeightConstraint.isActive = true
      messageImageView.anchor(top: messageTextView.bottomAnchor, 
                              leading: contentView.leadingAnchor,
                              bottom: contentView.bottomAnchor,
                              trailing: contentView.trailingAnchor,
                              padding: .init(top: 4, left: 10, bottom: 0, right: 10))
}

When the posts are loading, I set the messageImageViewHeightConstraint.constant = 0 if the post does not have an image (optionals). This works to collapse the imageView. Unfortunately as you can see the textView expands to cover the remaining space. I don't want this, I want the contentView's intrinsic size to shrink, and I just want the text to expand to meet the content's intrinsic size. How can I do this? Thank you in advance.

Edit: more code for reference

     private let iconImageView: UIImageView = {
      let iv = UIImageView()
      iv.contentMode = .scaleAspectFit
      iv.layer.cornerRadius = 10
      iv.translatesAutoresizingMaskIntoConstraints = false
      iv.clipsToBounds = true
      iv.layer.borderWidth = 1
      return iv
 }()

 private let titleLabel: UILabel = {
      let label = UILabel()
      label.numberOfLines = 0
      label.translatesAutoresizingMaskIntoConstraints = false
      label.textColor = .black
      label.layer.borderWidth = 1
      return label
 }()

 private let messageTextView: UILabel = {
      let labelView = UILabel()
      labelView.numberOfLines = 0
      labelView.translatesAutoresizingMaskIntoConstraints = false
      labelView.font = UIFont.systemFont(ofSize: 14)
      labelView.layer.borderWidth = 1
      return labelView
 }()

 private let messageImageView: UIImageView = {
      let imageView = UIImageView()
      imageView.contentMode = .scaleAspectFit
      imageView.layer.masksToBounds = true
      imageView.layer.borderWidth = 1
      imageView.translatesAutoresizingMaskIntoConstraints = false
      return imageView
 }()

Example image

Edit #2 After following suggestions, here is the new code:

var post: Post?{
      didSet{
           guard let post = post else {return}

           // Adding user's name
           let attributedText = NSMutableAttributedString(string: post.author.name + " → " + post.group.name, attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 14)])

           // Adding date and user's first name
           let dateFormatter = DateFormatter()
           dateFormatter.dateStyle = .long
           dateFormatter.timeStyle = .short
           attributedText.append(NSAttributedString(string: "\n" + dateFormatter.string(from: post.timeCreated), attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12), NSAttributedString.Key.foregroundColor: UIColor(r: 155/255, g: 161/255, b: 171/255)]))

           // Increasing Spacing
           let paragraphStyle = NSMutableParagraphStyle()
           paragraphStyle.lineSpacing = 4
           attributedText.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, attributedText.length))

           titleLabel.attributedText = attributedText

           // Setting profile image
           iconImageView.setImage(for: post.author, setContentMode: .scaleAspectFit)

           DispatchQueue.main.async {
                self.setupTextAndImageSubviews()
           }
      }
 }
     override init(frame: CGRect) {
      super.init(frame: frame)

      setupDefaultViews()
 }

 required init?(coder: NSCoder) {
      fatalError("init(coder:) has not been implemented")
 }

 func setupDefaultViews(){
      backgroundColor = UIColor.white

      addSubview(titleLabel)
      addSubview(iconImageView)

      iconImageView.anchor(top: contentView.topAnchor, leading: contentView.leadingAnchor, bottom: nil, trailing: nil, padding: .init(top: 0, left: 8, bottom: 0, right: 0), size: CGSize(width: 44, height: 44))
      titleLabel.anchor(top: contentView.topAnchor, leading: iconImageView.trailingAnchor, bottom: nil, trailing: nil, padding: .init(top: 12, left: 8, bottom: 0, right: 0))
 }

 private func setupTextAndImageSubviews() {
      addSubview(messageTextView)

      var textViewBottomAnchor: NSLayoutYAxisAnchor? = contentView.bottomAnchor

      if self.post?.messageImageURL != nil {
           textViewBottomAnchor = nil // dont need to anchor text view to bottom if image exists
      }

      // Setting body text
      messageTextView.text = self.post?.body

      messageTextView.anchor(top: titleLabel.bottomAnchor,
                             leading: contentView.leadingAnchor,
                             bottom: textViewBottomAnchor,
                             trailing: contentView.trailingAnchor,
                             padding: .init(top: 4, left: 10, bottom: 0, right: 10))

      guard let imageURL = self.post?.messageImageURL else {return} // if no image exists, return, preventing image view from taking extra memory and performance to initialize and calculate constraints

      // initialize here instead of globally, so it doesnt take extra memory holding this when no image exists.
      let messageImageView: UIImageView = {
           let imageView = UIImageView()
           imageView.kf.setImage(with: imageURL, placeholder: UIImage(systemName: "person.crop.circle.fill")!.withTintColor(.gray).withRenderingMode(.alwaysOriginal))
           imageView.contentMode = .scaleAspectFit
           imageView.layer.masksToBounds = true
           imageView.layer.borderWidth = 1
           imageView.translatesAutoresizingMaskIntoConstraints = false
           return imageView
      }()

      addSubview(messageImageView)
      messageImageView.anchor(top: messageTextView.bottomAnchor,
                              leading: contentView.leadingAnchor,
                              bottom: contentView.bottomAnchor,
                              trailing: contentView.trailingAnchor,
                              padding: .init(top: 4, left: 10, bottom: 0, right: 10))
 }

Constraint error: 2021-05-11 13:18:28.184077-0700 GroupUp[8223:1981252] [LayoutConstraints] Unable to simultaneously satisfy constraints. Probably at least one of the constraints in the following list is one you don't want. (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. "<NSLayoutConstraint:0x283552170 V:[UILabel:0x10598a4e0]-(4)-[UIImageView:0x10881ca00] (active)>", "<NSLayoutConstraint:0x283550af0 UIImageView:0x10881ca00.bottom == UIView:0x10598a750.bottom (active)>", "<NSLayoutConstraint:0x28356db80 UILabel:0x10598a4e0.bottom == UIView:0x10598a750.bottom (active)>" Will attempt to recover by breaking constraint <NSLayoutConstraint:0x283552170 V:[UILabel:0x10598a4e0]-(4)-[UIImageView:0x10881ca00] (active)> Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger. The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.

1
Good points. I thought that I did have a complete top to bottom chain of constrains? Both the title label and the icon image label are both constrained to the top of the content view, the message text view is attached to the bottom of the title label, and the message text view (which is optional) is attached to the bottom of the text view, which is then attached to the bottom of the contentView. As far as what the contentView is, it is a collectionViewCell that is supposed to house a icon (imageView), titleview (UI Label), messageTextView (UILablel), and messageImageView (another UIImageView) - Kafka
The point is to make a Facebook style news feed - Kafka

1 Answers

0
votes

It's because the image view still has an anchor to the top of the text view, and one to the bottom of the content view, so the text view never has an anchor to bottom content view to resize it self and the content view itself, it only has an anchor to the top of the image view.

if you set your imageView's background color to something like red and set the height 2 instead of 0 you would see what's happening.

there are multiple routes you can take to fixing this, the one that I personally think would be the most performance friendly would be to only set the text view and image view and their constraints when you know what data you are dealing with here. image or no image. since right now if you have no image there is an empty imageView inside your view hierarchy just sitting there taking memory (and constraint calculation). and if you were to have some constraints/anchors by default and change them based on new data it would mean re calculating constraints that have already been calculated which would cost performance.

so my approach would look something like:

var data: yourDataModel? {
    didSet {
     self.updateUI()
    }
}

private func updateUI() {

//do all the normal stuff you do with your data here

   //run in main thread in case your data is being loaded from the background thread
   DispatchQueue.main.async {
      self.setupContentSubviews() 
   }

}

private func setupContentSubviews() {

   addSubview(messageTextView)

   var textViewBottomAnchor: NSLayoutYAxisAnchor? = contentView.bottomAnchor

   if self.data.image != nil {
      textViewBottomAnchor = nil // dont need to anchor text view to bottom if image exists
   }

   messageTextView.anchor(top: titleLabel.bottomAnchor,
                          leading: contentView.leadingAnchor,
                          bottom: textViewBottomAnchor,
                          trailing: contentView.trailingAnchor,
                          padding: .init(top: 4, left: 10, bottom: 0, right: 10))

   guard let image = self.data.image else {return} // if no image exists, return, preventing image view from taking extra memory and performance to initialize and calculate constraints

   // initialize here instead of globally, so it doesnt take extra memory holding this when no image exists.
   private let messageImageView: UIImageView = {
         let imageView = UIImageView(image: image)
         imageView.contentMode = .scaleAspectFit
         imageView.layer.masksToBounds = true
         imageView.layer.borderWidth = 1
         imageView.translatesAutoresizingMaskIntoConstraints = false
         return imageView
    }()

    addSubview(messageImageView)
    messageImageViewHeightConstraint = messageImageView.heightAnchor.constraint(equalToConstant: 200)
    messageImageViewHeightConstraint.isActive = true
    messageImageView.anchor(top: messageTextView.bottomAnchor, 
                            leading: contentView.leadingAnchor,
                            bottom: contentView.bottomAnchor,
                            trailing: contentView.trailingAnchor,
                            padding: .init(top: 4, left: 10, bottom: 0, right: 10))
     }

}