[英]Frame height problem with custom UIViewRepresentable UITextView in SwiftUI
I'm building a custom UITextView for SwiftUI, via UIViewRepresentable.我正在通过 UIViewRepresentable 为 SwiftUI 构建自定义 UITextView。 It's meant to display
NSAttributedString
, and handle link presses.它旨在显示
NSAttributedString
,并处理链接按下。 Everything works, but the frame height is completely messed up when I show this view inside of a NavigationView
with an inline title.一切正常,但是当我在带有内联标题的
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()
}
}
When you run this code, you will see that the AttributedText
cell will be too small to hold its content.运行此代码时,您将看到
AttributedText
单元格太小而无法容纳其内容。
When you remove the displayMode: .inline
parameter from the navigationBarTitle
, it shows up fine.当您从
navigationBarTitle
删除displayMode: .inline
参数时,它显示正常。
But if I add another row to display the height value ( Text("\\(height)")
), it again breaks.但是,如果我添加另一行来显示高度值(
Text("\\(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. height
值本身是正确的,只是框架实际上没有那么高。 Is there a workaround?有解决方法吗?
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.将
ScrollView
与VStack
一起使用确实解决了问题,但由于内容在实际应用程序中的显示方式,我真的更喜欢使用List
。
If you will not change text, you can calculate width and height and use them as frame even without binding.如果您不更改文本,即使没有绑定,您也可以计算宽度和高度并将它们用作框架。
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
}
With Extension:带扩展:
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
}
}
So I had this exact issue.所以我有这个确切的问题。
The solution isnt pretty but I found one that works:解决方案并不漂亮,但我找到了一个有效的解决方案:
Firstly, you need to subclass UITextView so that you can pass its content size back to SwiftIU:首先,您需要继承 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))
}
}
Once you have done this, you need to create a UIViewRepresentable for your custom UITextView:完成此操作后,您需要为自定义 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()
}
}
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.
出于某种原因,仅仅在视图上使用 .frame 是行不通的。 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)
I managed to find a version of my AttributedText
View that mostly works.我设法找到了一个最有效的
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)
}
}
In certain cases you can see the view suddenly grow in size, but in almost all my screens where I am using this, it's a massive improvement.在某些情况下,您可以看到视图的大小突然变大,但在我使用它的几乎所有屏幕中,这是一个巨大的改进。 Auto-sizing UITextView in SwiftUI is still a huge headache though, and any answers that improve this would be greatly appreciated :)
尽管如此,在 SwiftUI 中自动调整 UITextView 的大小仍然是一个令人头疼的问题,任何可以改善这一点的答案都将不胜感激:)
To get the height of UIViewRepresentable View, it is placed in the background of text with the same height.要获取 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
)
I recently refactored some code in our Application to SwiftUI and also found some similar approaches obviously found on Stackoverflow.我最近将我们的应用程序中的一些代码重构为 SwiftUI,并且还发现了一些明显在 Stackoverflow 上发现的类似方法。 After some research, try and errors I ended up with quite a simple solution which completely suites our purposes:
经过一些研究、尝试和错误,我最终得到了一个非常简单的解决方案,它完全符合我们的目的:
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
return view
}
func updateUIView(_ uiView: UITextView, context: Context) {
guard let attributedText = attributedText else {
generateAttributedText()
return
}
uiView.attributedText = attributedText
}
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
}
}
}
Formatter格式化程序
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
}
}
Sample样本
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))
}
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.