通过 UIViewRepresentable 在 SwiftUI 中调整 UILabel 的大小,如 Text 以换行多行
Size a UILabel in SwiftUI via UIViewRepresentable like Text to wrap multiple lines
目标是通过 UIViewRepresentable
集成 UILabel
以与 Text
相同的方式调整大小 - 使用可用宽度并换行到多行以适合所有文本增加它所在的 HStack
的高度,而不是无限扩展宽度。这与 非常相似,尽管接受的答案不适用于我使用的涉及 ScrollView
、VStack
和 HStack
.[=34= 的布局]
struct ContentView: View {
var body: some View {
ScrollView {
VStack {
HStack {
Text("Hello, World")
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla tempor justo quam, quis suscipit leo sollicitudin vel.")
//LabelView(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla tempor justo quam, quis suscipit leo sollicitudin vel.")
}
HStack {
Text("Hello, World")
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla tempor justo quam, quis suscipit leo sollicitudin vel.")
}
Spacer()
}
}
}
}
struct LabelView: UIViewRepresentable {
var text: String
func makeUIView(context: UIViewRepresentableContext<LabelView>) -> UILabel {
let label = UILabel()
label.text = text
label.numberOfLines = 0
return label
}
func updateUIView(_ uiView: UILabel, context: UIViewRepresentableContext<LabelView>) {
uiView.text = text
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
在 HStack 中使用两个文本会产生所需的布局:
使用 Text 和 LabelView 会导致这种不需要的布局:
如果将 LabelView
包裹在 GeometryReader
中并将 width
传递给 LabelView
以设置 preferredMaxLayoutWidth
,则 0.0
用于一些理由。如果将 GeometryReader
移到 ScrollView
之外,您可以获得一个宽度,但它是滚动视图宽度,而不是 SwiftUI 为 HStack
中的 LabelView
建议的宽度.
如果我改为指定 label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
它会更好,但仍然不会显示所有文本,它会奇怪地截断 3 行的 LabelView
和 2 行的第二个 Text
行。
这里的问题是在ScrollView
中需要确定高度,但是representable没有提供。可能的解决方案是动态计算换行文本高度并明确指定。
注意:由于高度是动态计算的,它仅在 运行 时间内可用,因此无法通过预览进行测试。
测试 Xcode 12 / iOS 14
struct LabelView: View {
var text: String
@State private var height: CGFloat = .zero
var body: some View {
InternalLabelView(text: text, dynamicHeight: $height)
.frame(minHeight: height)
}
struct InternalLabelView: UIViewRepresentable {
var text: String
@Binding var dynamicHeight: CGFloat
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.text = text
DispatchQueue.main.async {
dynamicHeight = uiView.sizeThatFits(CGSize(width: uiView.bounds.width, height: CGFloat.greatestFiniteMagnitude)).height
}
}
}
}
这不是一个具体的答案。
我无意中发现固定大小修饰符可以帮助将 UILabel 自动调整为其内容大小。
struct ContentView: View {
let attributedString: NSAttributedString
var body: some View {
ScrollView {
LazyVStack(spacing: 10) {
RepresentedUILabelView(attributedText: attributedString)
.frame(maxHeight: 300)
.fixedSize(horizontal: false, vertical: true)
.background(Color.orange.opacity(0.5))
}
.padding()
}
}
}
struct RepresentedUILabelView: UIViewRepresentable {
typealias UIViewType = UILabel
var attributedText: NSAttributedString
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakMode = .byTruncatingTail
label.textAlignment = .justified
label.allowsDefaultTighteningForTruncation = true
// Compression resistance is important to enable auto resizing of this view,
// that base on the SwiftUI layout.
// Especially when the SwiftUI frame modifier applied to this view.
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
label.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
// Maybe this is not necessary.
label.clipsToBounds = true
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
print(#fileID, #function)
uiView.attributedText = attributedText
}
}
演示:
此外,如果您不想提供最大身高。您可以将 preferredMaxLayoutWidth
设置为您的屏幕宽度。如果你把它放在 updateUIView 方法中,每当屏幕方向改变时,这个方法也会被调用。
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.attributedText = attributedText
uiView.preferredMaxLayoutWidth = 0.9 * UIScreen.main.bounds.width
}
为了完整起见,这是我用来测试视图的属性文本设置。但是,它不应影响视图大小调整的工作方式。
func makeAttributedString(fromString s: String) -> NSAttributedString {
let content = NSMutableAttributedString(string: s)
let paraStyle = NSMutableParagraphStyle()
paraStyle.alignment = .justified
paraStyle.lineHeightMultiple = 1.25
paraStyle.lineBreakMode = .byTruncatingTail
paraStyle.hyphenationFactor = 1.0
content.addAttribute(.paragraphStyle,
value: paraStyle,
range: NSMakeRange(0, s.count))
// First letter/word
content.addAttributes([
.font : UIFont.systemFont(ofSize: 40, weight: .bold),
.expansion : 0,
.kern : -0.2
], range: NSMakeRange(0, 1))
return content
}
let coupleWords = "Hello, world!"
let multilineEN = """
Went to the woods because I wished to live deliberately, to front only the essential facts of life, and see if I could not learn what it had to teach, and not, when I came to die, discover that I had not lived. I did not wish to live what was not life, living is so dear! Nor did I wish to practise resignation, unless it was quite necessary?"
I went to the woods because I wished to live deliberately, to front only the essential facts of life, and see if I could.
I went to the woods because I wished to live deliberately, to front only the essential facts of life, and see if I could not learn what it had to teach, and not.
"""
当前答案中None的解决方案已经足够接近SwiftUI的本质。换个思路,既然 SwiftUI 对 Text 已经足够好了,为什么不做这样的事情呢?
Text(yourstr)
.foregroundColor(.clear) <<<<<<<--------
.multilineTextAlignment(.leading)
.frame(maxWidth:.infinity,maxHeight: .infinity, alignment: .leading)
.overlay(
Your ActiveLabelStack() <<<<<<<--------
,alignment: .center
)
简单的说,让Text告诉你高度,做Label的底。毕竟Text是SwiftUI的儿子
目标是通过 在 HStack 中使用两个文本会产生所需的布局:
使用 Text 和 LabelView 会导致这种不需要的布局:
如果将 如果我改为指定 UIViewRepresentable
集成 UILabel
以与 Text
相同的方式调整大小 - 使用可用宽度并换行到多行以适合所有文本增加它所在的 HStack
的高度,而不是无限扩展宽度。这与 ScrollView
、VStack
和 HStack
.[=34= 的布局]
struct ContentView: View {
var body: some View {
ScrollView {
VStack {
HStack {
Text("Hello, World")
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla tempor justo quam, quis suscipit leo sollicitudin vel.")
//LabelView(text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla tempor justo quam, quis suscipit leo sollicitudin vel.")
}
HStack {
Text("Hello, World")
Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla tempor justo quam, quis suscipit leo sollicitudin vel.")
}
Spacer()
}
}
}
}
struct LabelView: UIViewRepresentable {
var text: String
func makeUIView(context: UIViewRepresentableContext<LabelView>) -> UILabel {
let label = UILabel()
label.text = text
label.numberOfLines = 0
return label
}
func updateUIView(_ uiView: UILabel, context: UIViewRepresentableContext<LabelView>) {
uiView.text = text
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
LabelView
包裹在 GeometryReader
中并将 width
传递给 LabelView
以设置 preferredMaxLayoutWidth
,则 0.0
用于一些理由。如果将 GeometryReader
移到 ScrollView
之外,您可以获得一个宽度,但它是滚动视图宽度,而不是 SwiftUI 为 HStack
中的 LabelView
建议的宽度.label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
它会更好,但仍然不会显示所有文本,它会奇怪地截断 3 行的 LabelView
和 2 行的第二个 Text
行。
这里的问题是在ScrollView
中需要确定高度,但是representable没有提供。可能的解决方案是动态计算换行文本高度并明确指定。
注意:由于高度是动态计算的,它仅在 运行 时间内可用,因此无法通过预览进行测试。
测试 Xcode 12 / iOS 14
struct LabelView: View {
var text: String
@State private var height: CGFloat = .zero
var body: some View {
InternalLabelView(text: text, dynamicHeight: $height)
.frame(minHeight: height)
}
struct InternalLabelView: UIViewRepresentable {
var text: String
@Binding var dynamicHeight: CGFloat
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakMode = .byWordWrapping
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.text = text
DispatchQueue.main.async {
dynamicHeight = uiView.sizeThatFits(CGSize(width: uiView.bounds.width, height: CGFloat.greatestFiniteMagnitude)).height
}
}
}
}
这不是一个具体的答案。
我无意中发现固定大小修饰符可以帮助将 UILabel 自动调整为其内容大小。
struct ContentView: View {
let attributedString: NSAttributedString
var body: some View {
ScrollView {
LazyVStack(spacing: 10) {
RepresentedUILabelView(attributedText: attributedString)
.frame(maxHeight: 300)
.fixedSize(horizontal: false, vertical: true)
.background(Color.orange.opacity(0.5))
}
.padding()
}
}
}
struct RepresentedUILabelView: UIViewRepresentable {
typealias UIViewType = UILabel
var attributedText: NSAttributedString
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
label.numberOfLines = 0
label.lineBreakMode = .byTruncatingTail
label.textAlignment = .justified
label.allowsDefaultTighteningForTruncation = true
// Compression resistance is important to enable auto resizing of this view,
// that base on the SwiftUI layout.
// Especially when the SwiftUI frame modifier applied to this view.
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
label.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
// Maybe this is not necessary.
label.clipsToBounds = true
return label
}
func updateUIView(_ uiView: UILabel, context: Context) {
print(#fileID, #function)
uiView.attributedText = attributedText
}
}
演示:
此外,如果您不想提供最大身高。您可以将 preferredMaxLayoutWidth
设置为您的屏幕宽度。如果你把它放在 updateUIView 方法中,每当屏幕方向改变时,这个方法也会被调用。
func updateUIView(_ uiView: UILabel, context: Context) {
uiView.attributedText = attributedText
uiView.preferredMaxLayoutWidth = 0.9 * UIScreen.main.bounds.width
}
为了完整起见,这是我用来测试视图的属性文本设置。但是,它不应影响视图大小调整的工作方式。
func makeAttributedString(fromString s: String) -> NSAttributedString {
let content = NSMutableAttributedString(string: s)
let paraStyle = NSMutableParagraphStyle()
paraStyle.alignment = .justified
paraStyle.lineHeightMultiple = 1.25
paraStyle.lineBreakMode = .byTruncatingTail
paraStyle.hyphenationFactor = 1.0
content.addAttribute(.paragraphStyle,
value: paraStyle,
range: NSMakeRange(0, s.count))
// First letter/word
content.addAttributes([
.font : UIFont.systemFont(ofSize: 40, weight: .bold),
.expansion : 0,
.kern : -0.2
], range: NSMakeRange(0, 1))
return content
}
let coupleWords = "Hello, world!"
let multilineEN = """
Went to the woods because I wished to live deliberately, to front only the essential facts of life, and see if I could not learn what it had to teach, and not, when I came to die, discover that I had not lived. I did not wish to live what was not life, living is so dear! Nor did I wish to practise resignation, unless it was quite necessary?"
I went to the woods because I wished to live deliberately, to front only the essential facts of life, and see if I could.
I went to the woods because I wished to live deliberately, to front only the essential facts of life, and see if I could not learn what it had to teach, and not.
"""
None的解决方案已经足够接近SwiftUI的本质。换个思路,既然 SwiftUI 对 Text 已经足够好了,为什么不做这样的事情呢?
Text(yourstr)
.foregroundColor(.clear) <<<<<<<--------
.multilineTextAlignment(.leading)
.frame(maxWidth:.infinity,maxHeight: .infinity, alignment: .leading)
.overlay(
Your ActiveLabelStack() <<<<<<<--------
,alignment: .center
)
简单的说,让Text告诉你高度,做Label的底。毕竟Text是SwiftUI的儿子