如何在内容更改时调整 contentView 的大小?

How can I get the contentView to resize when the content changes?

下面您可以看到两个 post,一个有图像,一个没有。我在视图周围设置了边界,以便更好地理解正在发生的事情。我希望没有图像的 posts 比有 posts 的图像小。我试图通过执行以下操作来做到这一点:

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

当 post 加载时,如果 post 没有图像(可选),我设置 messageImageViewHeightConstraint.constant = 0。这可以折叠 imageView。不幸的是,正如您所看到的,textView 扩展以覆盖剩余的 space。我不想要这个,我希望 contentView 的固有尺寸缩小,我只希望文本扩展以满足内容的固有尺寸。我怎样才能做到这一点?提前谢谢你。

编辑:更多代码供参考

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

编辑#2 根据建议,这里是新代码:

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

约束错误: 2021-05-11 13:18:28.184077-0700 GroupUp[8223:1981252] [LayoutConstraints] 无法同时满足约束条件。 可能至少以下列表中的约束之一是您不想要的。 (1)查看每个约束并尝试找出您不期望的; (2) 找到添加了不需要的约束或约束的代码并修复它。 “”, “”, “” 将尝试通过打破约束来恢复 在 UIViewAlertForUnsatisfiableConstraints 创建一个符号断点以在调试器中捕获它。 中列出的 UIView 的 UIConstraintBasedLayoutDebugging 类别中的方法也可能有帮助。

这是因为图像视图仍然有一个锚点到文本视图的顶部,一个锚点到内容视图的底部,所以文本视图从来没有锚点到底部内容视图来调整它自己和内容视图本身,它只有图像视图顶部的锚点。

如果您将 imageView 的背景颜色设置为红色,并将高度设置为 2 而不是 0,您就会看到发生了什么。

您可以采用多种方法来解决此问题,我个人认为对性能最友好的方法是仅在您知道要处理的数据时设置文本视图和图像视图及其约束在这里。图像或无图像。因为现在如果你没有图像,你的视图层次结构中就会有一个空的 imageView,它只是坐在那里占用内存(和约束计算)。如果你默认有一些 constraints/anchors 并根据新数据更改它们,这将意味着重新计算已经计算过的约束,这会降低性能。

所以我的方法看起来像:

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

}