SwiftUI中的自定义UIViewRepresentable UITextView的框架高度问题

时间:2020-02-27 15:50:57

标签: ios swiftui

我正在通过UIViewRepresentable为SwiftUI构建自定义UITextView。它旨在显示NSAttributedString,并处理链接按下操作。一切正常,但是当我在带有内联标题的NavigationView内显示此视图时,框架高度完全混乱了。

import SwiftUI

struct AttributedText: UIViewRepresentable {
  class Coordinator: NSObject, UITextViewDelegate {
    var parent: AttributedText

    init(_ view: AttributedText) {
      parent = view
    }

    func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
      parent.linkPressed(URL)
      return false
    }
  }

  let content: NSAttributedString
  @Binding var height: CGFloat
  var linkPressed: (URL) -> Void

  public func makeUIView(context: Context) -> UITextView {
    let textView = UITextView()
    textView.backgroundColor = .clear
    textView.isEditable = false
    textView.isUserInteractionEnabled = true
    textView.delegate = context.coordinator
    textView.isScrollEnabled = false
    textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
    textView.dataDetectorTypes = .link
    textView.textContainerInset = .zero
    textView.textContainer.lineFragmentPadding = 0
    return textView
  }

  public func updateUIView(_ view: UITextView, context: Context) {
    view.attributedText = content

    // Compute the desired height for the content
    let fixedWidth = view.frame.size.width
    let newSize = view.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))

    DispatchQueue.main.async {
      self.height = newSize.height
    }
  }

  func makeCoordinator() -> Coordinator {
    Coordinator(self)
  }
}


struct ContentView: View {

  private var text: NSAttributedString {
    NSAttributedString(string: "Eartheart is the principal settlement for the Gold Dwarves in East Rift and it is still the cultural and spiritual center for its people. Dwarves take on pilgrimages to behold the great holy city and take their trips from other countries and the deeps to reach their goal, it use to house great temples and shrines to all the Dwarven pantheon and dwarf heroes but after the great collapse much was lost.\n\nThe lords of their old homes relocated here as well the Deep Lords. The old ways of the Deep Lords are still the same as they use intermediaries and masking themselves to undermine the attempts of assassins or drow infiltrators. The Gold Dwarves outnumber every other race in the city and therefor have full control of the city and it's communities.")
  }

  @State private var height: CGFloat = .zero

  var body: some View {
    NavigationView {
      List {
        AttributedText(content: text, height: $height, linkPressed: { url in print(url) })
          .frame(height: height)

        Text("Hello world")
      }
      .listStyle(GroupedListStyle())
      .navigationBarTitle(Text("Content"), displayMode: .inline)
    }
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

运行此代码时,您会看到AttributedText单元格太小而无法容纳其内容。

enter image description here

displayMode: .inline删除navigationBarTitle参数时,它显示得很好。

enter image description here

但是,如果我添加另一行以显示高度值(Text("\(height)")),它将再次中断。

enter image description here

也许这是由于状态更改导致视图更新触发的某种竞争条件? height值本身是正确的,只是框架实际上没有那么高。有解决方法吗?

使用ScrollViewVStack确实可以解决问题,但是由于内容在真实应用中的显示方式,我真的很喜欢使用List。 / p>

4 个答案:

答案 0 :(得分:1)

如果您不更改文本,则可以计算宽度和高度并将其用作框架,即使没有装订。

List {
        // you don't need binding height
        AttributedText(content: text, linkPressed: { url in print(url) })
          .frame(height: frameSize(for: text).height)

        Text("Hello world")
      }
func frameSize(for text: String, maxWidth: CGFloat? = nil, maxHeight: CGFloat? = nil) -> CGSize {
        let attributes: [NSAttributedString.Key: Any] = [
            .font: UIFont.preferredFont(forTextStyle: .body)
        ]
        let attributedText = NSAttributedString(string: text, attributes: attributes)
        let width = maxWidth != nil ? min(maxWidth!, CGFloat.greatestFiniteMagnitude) : CGFloat.greatestFiniteMagnitude
        let height = maxHeight != nil ? min(maxHeight!, CGFloat.greatestFiniteMagnitude) : CGFloat.greatestFiniteMagnitude
        let constraintBox = CGSize(width: width, height: height)
        let rect = attributedText.boundingRect(with: constraintBox, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil).integral
        return rect.size
    }

带有扩展名:

extension String {
    func frameSize(maxWidth: CGFloat? = nil, maxHeight: CGFloat? = nil) -> CGSize {
        let attributes: [NSAttributedString.Key: Any] = [
            .font: UIFont.preferredFont(forTextStyle: .body)
        ]
        let attributedText = NSAttributedString(string: self, attributes: attributes)
        let width = maxWidth != nil ? min(maxWidth!, CGFloat.greatestFiniteMagnitude) : CGFloat.greatestFiniteMagnitude
        let height = maxHeight != nil ? min(maxHeight!, CGFloat.greatestFiniteMagnitude) : CGFloat.greatestFiniteMagnitude
        let constraintBox = CGSize(width: width, height: height)
        let rect = attributedText.boundingRect(with: constraintBox, options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil).integral
        return rect.size
    }
}

答案 1 :(得分:1)

所以我遇到了这个问题。

解决方案并不漂亮,但我找到了一个有效的方法:

首先,您需要继承 UITextView,以便您可以将其内容大小传回给 SwiftIU:

public class UITextViewWithSize: UITextView {
    @Binding var size: CGSize
    
    public init(size: Binding<CGSize>) {
        self._size = size
        
        super.init(frame: .zero, textContainer: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    public override func layoutSubviews() {
        super.layoutSubviews()
        self.size = sizeThatFits(.init(width: frame.width, height: 0))
    }
}

完成此操作后,您需要为自定义 UITextView 创建一个 UIViewRepresentable:

public struct HyperlinkTextView: UIViewRepresentable {
    public typealias UIViewType = UITextViewWithSize
    
    private var text: String
    private var font: UIFont?
    private var foreground: UIColor?
    @Binding private var size: CGSize
    
    public init(_ text: String, font: UIFont? = nil, foreground: UIColor? = nil, size: Binding<CGSize>) {
        self.text = text
        self.font = font
        self.foreground = foreground
        self._size = size
    }
    
    public func makeUIView(context: Context) -> UIViewType {
        
        let view = UITextViewWithSize(size: $size)
        
        view.isEditable = false
        view.dataDetectorTypes = .all
        view.isScrollEnabled = false
        view.text = text
        view.textContainer.lineBreakMode = .byTruncatingTail
        view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
        view.setContentCompressionResistancePriority(.required, for: .vertical)
        view.textContainerInset = .zero
        
        if let font = font {
            view.font = font
        } else {
            view.font = UIFont.preferredFont(forTextStyle: .body)
        }
        
        if let foreground = foreground {
            view.textColor = foreground
        }
        
        view.sizeToFit()
        
        return view
    }
    
    public func updateUIView(_ uiView: UIViewType, context: Context) {
        uiView.text = text
        uiView.layoutSubviews()
    }
}

现在我们可以轻松访问视图的内容大小,我们可以使用它来强制视图适合该大小的容器。出于某种原因,仅仅在视图上使用 .frame 是行不通的。该视图只是忽略其给定的框架。但是放到几何阅读器里,好像就按预期增长了。

GeometryReader { proxy in
    HyperlinkTextView(bio, size: $bioSize)
        .frame(maxWidth: proxy.frame(in: .local).width, maxHeight: .infinity)
}
.frame(height: bioSize.height)

答案 2 :(得分:0)

我设法找到了最有效的AttributedText视图版本。

struct AttributedText: UIViewRepresentable {
  class HeightUITextView: UITextView {
    @Binding var height: CGFloat

    init(height: Binding<CGFloat>) {
      _height = height
      super.init(frame: .zero, textContainer: nil)
    }

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

    override func layoutSubviews() {
      super.layoutSubviews()
      let newSize = sizeThatFits(CGSize(width: frame.size.width, height: CGFloat.greatestFiniteMagnitude))
      if height != newSize.height {
        height = newSize.height
      }
    }
  }

  class Coordinator: NSObject, UITextViewDelegate {
    var parent: AttributedText

    init(_ view: AttributedText) {
      parent = view
    }

    func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
      parent.linkPressed(URL)
      return false
    }
  }

  let content: NSAttributedString
  @Binding var height: CGFloat
  var linkPressed: (URL) -> Void

  public func makeUIView(context: Context) -> UITextView {
    let textView = HeightUITextView(height: $height)
    textView.attributedText = content
    textView.backgroundColor = .clear
    textView.isEditable = false
    textView.isUserInteractionEnabled = true
    textView.delegate = context.coordinator
    textView.isScrollEnabled = false
    textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
    textView.dataDetectorTypes = .link
    textView.textContainerInset = .zero
    textView.textContainer.lineFragmentPadding = 0
    return textView
  }

  public func updateUIView(_ textView: UITextView, context: Context) {
    if textView.attributedText != content {
      textView.attributedText = content

      // Compute the desired height for the content
      let fixedWidth = textView.frame.size.width
      let newSize = textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))

      DispatchQueue.main.async {
        self.height = newSize.height
      }
    }
  }

  func makeCoordinator() -> Coordinator {
    Coordinator(self)
  }
}

在某些情况下,您可以看到视图突然变大,但是在我使用此功能的几乎所有屏幕上,这都是一个巨大的进步。不过,在SwiftUI中自动调整UITextView的大小仍然是一个巨大的难题,任何对此改善的答案将不胜感激:)

答案 3 :(得分:0)

获取UIViewRepresentable View的高度,将其放置在相同高度的文本背景中。

  private let text: String = "Eartheart is the principal settlement for the Gold Dwarves in East Rift and it is still the cultural and spiritual center for its people. Dwarves take on pilgrimages to behold the great holy city and take their trips from other countries and the deeps to reach their goal, it use to house great temples and shrines to all the Dwarven pantheon and dwarf heroes but after the great collapse much was lost.\n\nThe lords of their old homes relocated here as well the Deep Lords. The old ways of the Deep Lords are still the same as they use intermediaries and masking themselves to undermine the attempts of assassins or drow infiltrators. The Gold Dwarves outnumber every other race in the city and therefor have full control of the city and it's communities."


...

            Text(text)
                .font(.system(size: 12))
                .fixedSize(horizontal: false, vertical: true)
                .opacity(0)
                .background(
                    CustomUIViewRepresentableTextView(text: text)
                    // same font size 
                )