0

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.

shadow of arman
  • 395
  • 1
  • 7
  • 16
Kafka
  • 117
  • 8
  • 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 May 04 '21 at 04:20
  • The point is to make a Facebook style news feed – Kafka May 04 '21 at 04:21

1 Answers1

0

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

}
shadow of arman
  • 395
  • 1
  • 7
  • 16
  • This is amazing! Thanks so much for the explanation, your approach makes a lot of sense to me. I'll get back to you to confirm that it works. – Kafka May 11 '21 at 18:04
  • I decided to go with your approach, and I'm running into a few issues. When I scroll up and down, I'm getting errors relating to the constraints. It appears as though the TextView wants to be at the bottom of the contentView, and so does the ImageView. I also get multiple different images pasted on top of each other. Finally, the issue does not appear to have been fixed. For some reason, the text boxes still take up all the space, even when no scrolling is done. How do you prevent the scrolling from messing up images? See above edit for new code. Thanks again for your help, really appreciated! – Kafka May 11 '21 at 19:23
  • Debugging shows that the logic is properly screening out posts with no ImageViews, but the messageTextView is still taking up all the space, instead of just taking up the minimum space required. Note that this is BEFORE any error relating to constraints pops up (as a result of scrolling up and down).This is... odd? – Kafka May 11 '21 at 19:38
  • use `willSet` for your `post` variable, and check if new value is equal to `post`, if so, return. else, then run your functions. I think your constraints are breaking cause your didSet is getting called multiple times and making those view and setting constraints multiple times per cell. also, I have no idea when and how you are checking if the post has an image or not. you should debug and see if your image view is still getting created when there is no image to check and let me know – shadow of arman May 11 '21 at 21:16
  • Hey, thank you for the help, but unfortunately I realized that collectionView is not the right tool for this (auto-sizing cells is difficult!) I switched to tableViews, and now have more questions. Feel free to follow along here: https://stackoverflow.com/questions/67697654/uilabel-disappears-in-stackview-when-there-is-too-much-content – Kafka May 26 '21 at 02:11