SwiftUI 中自定义 UIViewRepresentable UITextView 的框架高度问题 [英] Frame height problem with custom UIViewRepresentable UITextView in SwiftUI

查看:45
本文介绍了SwiftUI 中自定义 UIViewRepresentable UITextView 的框架高度问题的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm building a custom UITextView for SwiftUI, via UIViewRepresentable. It's meant to display NSAttributedString, and handle link presses. Everything works, but the frame height is completely messed up when I show this view inside of a NavigationView with an inline title.

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 单元格太小而无法容纳其内容.

When you run this code, you will see that the AttributedText cell will be too small to hold its content.

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

When you remove the displayMode: .inline parameter from the navigationBarTitle, it shows up fine.

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

But if I add another row to display the height value (Text("\(height)")), it again breaks.

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

Maybe it's some kind of race condition triggered by view updates via state changes? The height value itself is correct, it's just that the frame isn't actually that tall. Is there a workaround?

使用 ScrollViewVStack 确实解决了问题,但我真的更喜欢使用 List 由于内容显示在真实应用中.

Using ScrollView with a VStack does solve the problem, but I'd really really prefer to use a List due to the way the content is shown in the real app.

推荐答案

所以我遇到了这个问题.

So I had this exact issue.

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

The solution isnt pretty but I found one that works:

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

Firstly, you need to subclass UITextView so that you can pass its content size back to 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:

Once you have done this, you need to create a UIViewRepresentable for your custom UITextView:

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 是行不通的.该视图只是忽略其给定的框架.但是放到几何阅读器里,好像就按预期增长了.

Now that we have easy access to the view's content size, we can use that to force the view to fit into a container of that size. For some reason, simply using a .frame on the view doesnt work. The view just ignores the frame its given. But when putting it into a geometry reader, it seems to grow as expected.

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

这篇关于SwiftUI 中自定义 UIViewRepresentable UITextView 的框架高度问题的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆