带有 UIViewRepresentable 的多行标签

Multiline label with UIViewRepresentable

我正在尝试通过 UIViewRepresentable 在 SwiftUI 中使用通过 UIKit 创建的组件。我想要实现的结果是在我的视图顶部有一个文本字段,我的 UIKit 视图以自动高度堆叠在底部。

问题是这个组件包含一个多行标签,据我所知,自动高度很难正常工作,所以我的 UIKit 组件占用了所有可用的 space在我的 VStack 中。

Expected result Actual result

这是我在 playground 中用于测试的代码。我试着玩拥抱优先级但对我没有任何作用,如果我用 swift UI 视图测试它工作正常..有什么想法吗?

import UIKit
import Foundation
import SwiftUI
import PlaygroundSupport

class InformationView: UIView {
    lazy var label = UILabel()
    lazy var button = UIButton(type: .custom)

    override init(frame: CGRect) {
        super.init(frame: frame)

        backgroundColor = .red

        addSubview(label)
        addSubview(button)

        label.text = "zaeiof azoeinf aozienf oaizenf oazeinf oaziefj oazeijf oaziejf aozijf oaizje foazeafjoj"
        label.numberOfLines = 0
        label.setContentHuggingPriority(.required, for: .vertical)

        label.translatesAutoresizingMaskIntoConstraints = false
        button.translatesAutoresizingMaskIntoConstraints = false

        button.setTitle("My button title", for: .normal)
        button.setContentCompressionResistancePriority(.required, for: .horizontal)

        addConstraints([
            label.leadingAnchor.constraint(equalTo: leadingAnchor),
            label.topAnchor.constraint(equalTo: topAnchor),
            label.bottomAnchor.constraint(equalTo: bottomAnchor),
            label.trailingAnchor.constraint(equalTo: button.leadingAnchor, constant: -10),
            button.trailingAnchor.constraint(equalTo: trailingAnchor),
            button.centerYAnchor.constraint(equalTo: centerYAnchor)
        ])
    }

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

struct InformationRepresenter: UIViewRepresentable {
    func makeUIView(context: Context) -> InformationView {
        let view = InformationView(frame: .zero)
        view.setContentHuggingPriority(.required, for: .vertical)

        return view
    }

    func updateUIView(_ uiView: InformationView, context: Context) {

    }
}

struct Info: View {
    var body: some View {
        return HStack {
            Text("zaeiof azoeinf aozienf oaizenf oazeinf oaziefj oazeijf oaziejf aozijf oaizje foazeafjoj")

            Button("My button title", action: {
                print("test")
            })
        }
    }
}

struct ContentView: View {
    @State var text = ""

    var body: some View {
        VStack {
            TextField("Field", text: $text)
                .padding()

            Spacer()

            // SwiftUI works
            // Info().background(Color.red).padding()

            // UIViewRepresentable doesn't
            InformationRepresenter().padding()
        }
    }
}

PlaygroundPage.current.setLiveView(ContentView().frame(width: 400, height: 800, alignment: .top))

这里发生了一些事情。

  • InformationView 没有 intrinsicContentSize。 SwiftUI 依靠这个 属性 来确定 UIView 的理想尺寸。
  • fixedSize修饰符。此修饰符强制视图保持其理想大小而不是增长以填充其父级。

您不能添加 intrinsicContentSize,因为内容大小是动态的;它基于您标签中的行数。

您不能添加 fixedSize 修饰符,因为没有 intrinsicContentSize,这会将大小设置为 (0,0)。

一个解决方案是将 InformationView 包装在 UIView 中,该 UIView 测量 InformationView 大小,并根据该测量更新其自身的 intrinsicContentSize

您的 InformationView 应该填满屏幕的宽度;它没有固有宽度。另一方面,固有高度应等于 compressed system layout size 的高度。这是“基于其约束的视图的最佳大小”。

这是包装纸:

class IntrinsicHeightView<ContentView: UIView>: UIView {
  var contentView: ContentView

  init(contentView: ContentView) {
    self.contentView = contentView
    super.init(frame: .zero)
    backgroundColor = .clear
    addSubview(contentView)
  }

  @available(*, unavailable) required init?(coder _: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  private var contentHeight: CGFloat = .zero {
    didSet { invalidateIntrinsicContentSize() }
  }

  override var intrinsicContentSize: CGSize {
    .init(width: UIView.noIntrinsicMetric, height: contentHeight)
  }

  override var frame: CGRect {
    didSet {
      guard frame != oldValue else { return }

      contentView.frame = self.bounds
      contentView.layoutIfNeeded()

      let targetSize = CGSize(width: frame.width, height: UIView.layoutFittingCompressedSize.height)

      contentHeight = contentView.systemLayoutSizeFitting(
        targetSize,
        withHorizontalFittingPriority: .required,
        verticalFittingPriority: .fittingSizeLevel).height
    }
  }
}

这个IntrinsicHeightView是一个通用的UIView,当它的框架发生变化时,它会根据其内容视图的压缩系统布局高度重新计算其intrinsicContentSize的高度。

这是对 InformationRepresenter 的更新:

struct InformationRepresenter: UIViewRepresentable {
    func makeUIView(context: Context) -> IntrinsicHeightView<InformationView> {
        return IntrinsicHeightView(contentView: InformationView())
    }

    func updateUIView(_ uiView: IntrinsicHeightView<InformationView>, context: Context) {
    }
}

最后,在 SwiftUI 中使用它时:

InformationRepresenter()
  .padding()
  .fixedSize(horizontal: false, vertical: true)

以这种方式使用 fixedSize 修饰符允许 UIView 填充父级,但使用 intrinsicContentSize.height 作为其高度。这里是final result。祝你好运!