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

Kev*_*ers 5 ios swiftui

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

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()
  }
}
Run Code Online (Sandbox Code Playgroud)

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

在此处输入图片说明

当您displayMode: .inline从 中删除参数时navigationBarTitle,它显示正常。

在此处输入图片说明

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

在此处输入图片说明

也许这是通过状态更改的视图更新触发的某种竞争条件?该height值本身是正确的,它只是该帧实际上并没有那么高。有解决方法吗?

使用ScrollViewaVStack确实可以解决问题,但List由于内容在真实应用程序中的显示方式,我真的更喜欢使用 a 。

Dan*_*arx 8

我最近将应用程序中的一些代码重构为 SwiftUI,并且还发现了一些在 Stackoverflow 上明显找到的类似方法。经过一些研究、尝试和错误,我最终得到了一个非常简单的解决方案,完全适合我们的目的:

  • 支持属性字符串的 SwiftUI 文本组件
  • 支持 HTML 和可点击链接
  • 自动调整高度并且 UITextView 内不滚动
  • 支持iOS 13.0+
  • 便于使用
  • (可选)不可选择
    import UIKit
    import SwiftUI
    
    protocol StringFormatter {
        func format(string: String) -> NSAttributedString?
    }
    
    struct AttributedText: UIViewRepresentable {
        typealias UIViewType = UITextView
        
        @State
        private var attributedText: NSAttributedString?
        private let text: String
        private let formatter: StringFormatter
        private var delegate: UITextViewDelegate?
        
        init(_ text: String, _ formatter: StringFormatter, delegate: UITextViewDelegate? = nil) {
            self.text = text
            self.formatter = formatter
            self.delegate = delegate
        }
        
        func makeUIView(context: Context) -> UIViewType {
            let view = ContentTextView()
            view.setContentHuggingPriority(.required, for: .vertical)
            view.setContentHuggingPriority(.required, for: .horizontal)
            view.contentInset = .zero
            view.textContainer.lineFragmentPadding = 0
            view.delegate = delegate
            view.backgroundColor = .clear
            return view
        }
        
        func updateUIView(_ uiView: UITextView, context: Context) {
            guard let attributedText = attributedText else {
                generateAttributedText()
                return
            }
            
            uiView.attributedText = attributedText
            uiView.invalidateIntrinsicContentSize()
        }
        
        private func generateAttributedText() {
            guard attributedText == nil else { return }
            // create attributedText on main thread since HTML formatter will crash SwiftUI
            DispatchQueue.main.async {
                self.attributedText = self.formatter.format(string: self.text)
            }
        }
        
        /// ContentTextView
        /// subclass of UITextView returning contentSize as intrinsicContentSize
        private class ContentTextView: UITextView {
            override var canBecomeFirstResponder: Bool { false }
            
            override var intrinsicContentSize: CGSize {
                frame.height > 0 ? contentSize : super.intrinsicContentSize
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

格式化程序


    import Foundation
    
    class HTMLFormatter: StringFormatter {
        func format(string: String) -> NSAttributedString? {
            guard let data = string.data(using: .utf8),
                  let attributedText = try? NSAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil)
            else { return nil }
            
            return attributedText
        }
    }

Run Code Online (Sandbox Code Playgroud)

样本


    import SwiftUI
    
    struct AttributedTextListView: View {
        let html = """
                    <html>
                        <body>
                            <h1>Hello, world!</h1>
                            <span>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</span>
                            <a href="https://example.org">Example</a>
                        </body>
                    </html>
                    """
        var body: some View {
            List {
                Group {
                    // delegate is optional
                    AttributedText(html, HTMLFormatter(), delegate: nil)
                    AttributedText(html, HTMLFormatter(), delegate: nil)
                    AttributedText(html, HTMLFormatter(), delegate: nil)
                }.background(Color.gray.opacity(0.1))
            }
            
        }
    }

Run Code Online (Sandbox Code Playgroud)

最后结果