SwiftUI - UI 元素的大小
SwiftUI - Size of an UI element
如何在渲染后获取 UI 元素的大小 (width/height) 并将其传递回父级以重新渲染?
示例:
父视图 (ChatMessage) 包含一个 RoundedRectangle,其上放置了来自子视图 (ChatMessageContent) 的文本 - 聊天气泡样式。
问题是在渲染父级时我不知道文本的大小,因为文本可能有 5、6 等行,具体取决于消息文本的长度。
struct ChatMessage: View {
@State var message: Message
@State var messageHeight: CGFloat = 28
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(Color.red).opacity(0.9)
.frame(width: 480, height: self.messageHeight)
ChatMessageContent(message: self.$message, messageHeight: self.$messageHeight)
.frame(width: 480, height: self.messageHeight)
}
}
}
struct ChatMessageContent: View {
@Binding var message: Message
@Binding var messageHeight: CGFloat
var body: some View {
GeometryReader { geometry in
Text(self.message.message)
.lineLimit(nil)
.multilineTextAlignment(.center)
.onAppear {self.messageHeight = geometry.size.height; print(geometry.size.height}
}
}
}
在提供的示例中,messageHeight 保持在 28,并且不会在父项上进行调整。我希望 messageHeight 更改为 Text 元素的实际高度,具体取决于它显示的文本行数。
例如。两行 -> messageHeight = 42,三行 -> messageHeight = 56。
如何获得 UI 元素(在本例中为文本)的实际大小,因为 GeometryReader 似乎无法解决问题?它还读取 geometry.size.height = 28(从父视图传递)。
首先,值得理解的是,在 Text
后面填充 RoundedRectangle
的情况下,您不需要测量文本或将大小发送到视图层次结构中。您可以将其配置为选择完全适合其内容的高度。然后使用 .background
修饰符添加 RoundedRectangle
。示例:
import SwiftUI
import PlaygroundSupport
let message = String(NotificationCenter.default.debugDescription.prefix(300))
PlaygroundPage.current.setLiveView(
Text(message)
.fixedSize(horizontal: false, vertical: true)
.padding(12)
.frame(width: 480)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.red.opacity(0.9))
)
.padding(12)
)
结果:
好的,但有时您确实需要测量视图并将其大小传递到层次结构中。在 SwiftUI 中,视图可以在称为“首选项”的东西中向上发送信息。 Apple 尚未彻底记录偏好系统,但有些人已经弄明白了。特别是 kontiki has described it starting with this article at swiftui-lab。 (swiftui-lab 的每篇文章都很棒。)
那么让我们举一个例子,我们确实需要使用首选项。 ConversationView
显示邮件列表,每封邮件都标有发件人:
struct Message {
var sender: String
var body: String
}
struct MessageView: View {
var message: Message
var body: some View {
HStack(alignment: .bottom, spacing: 3) {
Text(message.sender + ":").padding(2)
Text(message.body)
.fixedSize(horizontal: false, vertical: true).padding(6)
.background(Color.blue.opacity(0.2))
}
}
}
struct ConversationView: View {
var messages: [Message]
var body: some View {
VStack(alignment: .leading, spacing: 4) {
ForEach(messages.indices) { i in
MessageView(message: self.messages[i])
}
}
}
}
let convo: [Message] = [
.init(sender: "Peanutsmasher", body: "How do I get the size (width/height) of an UI element after its been rendered and pass it back to the parent for re-rendering?"),
.init(sender: "Rob", body: "First, it's worth understanding blah blah blah…"),
]
PlaygroundPage.current.setLiveView(
ConversationView(messages: convo)
.frame(width: 480)
.padding(12)
.border(Color.black)
.padding(12)
)
看起来像这样:
我们真的很想让这些消息气泡的左边缘对齐。这意味着我们需要使发送方 Text
具有相同的宽度。我们将通过使用新修饰符 .equalWidth()
扩展 View
来实现。我们将像这样将修饰符应用于发件人 Text
:
struct MessageView: View {
var message: Message
var body: some View {
HStack(alignment: .bottom, spacing: 3) {
Text(message.sender + ":").padding(2)
.equalWidth(alignment: .trailing) // <-- THIS IS THE NEW MODIFIER
Text(message.body)
.fixedSize(horizontal: false, vertical: true).padding(6)
.background(Color.blue.opacity(0.2))
}
}
}
在 ConversationView
之后,我们将使用另一个新修饰符 .equalWidthHost()
.
定义等宽视图的“域”
struct ConversationView: View {
var messages: [Message]
var body: some View {
VStack(alignment: .leading, spacing: 4) {
ForEach(messages.indices) { i in
MessageView(message: self.messages[i])
}
} //
.equalWidthHost() // <-- THIS IS THE NEW MODIFIER
}
}
在我们可以实现这些修饰符之前,我们需要定义一个 PreferenceKey
(which we will use to pass the widths up the view hierarchy from the Text
s to the host) and an EnvironmentKey
(我们将使用它来将选定的宽度从主机向下传递到 Text
s)。
通过为首选项定义 defaultValue
以及组合两个值的方法,类型符合 PreferenceKey
。这是我们的:
struct EqualWidthKey: PreferenceKey {
static var defaultValue: CGFloat? { nil }
static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
switch (value, nextValue()) {
case (_, nil): break
case (nil, let next): value = next
case (let a?, let b?): value = max(a, b)
}
}
}
一个类型通过定义一个defaultValue
来符合EnvironmentKey
。由于 EqualWidthKey
已经这样做了,我们可以将 PreferenceKey
重用为 EnvironmentKey
:
extension EqualWidthKey: EnvironmentKey { }
我们还需要添加访问器 EnvironmentValues
:
extension EnvironmentValues {
var equalWidth: CGFloat? {
get { self[EqualWidthKey.self] }
set { self[EqualWidthKey.self] = newValue }
}
}
现在我们可以实现 ViewModifier
将首选项设置为其内容的宽度,并将环境宽度应用于其内容:
struct EqualWidthModifier: ViewModifier {
var alignment: Alignment
@Environment(\.equalWidth) var equalWidth
func body(content: Content) -> some View {
return content
.background(
GeometryReader { proxy in
Color.clear
.preference(key: EqualWidthKey.self, value: proxy.size.width)
}
)
.frame(width: equalWidth, alignment: alignment)
}
}
默认情况下,GeometryReader
会填充其父项给它的 space 数量。这不是我们想要衡量的,所以我们将 GeometryReader
放在 background
修饰符中,因为背景视图始终是其前景内容的大小。
我们可以使用 EqualWidthModifier
类型在 View
上实现 equalWidth
修饰符:
extension View {
func equalWidth(alignment: Alignment) -> some View {
return self.modifier(EqualWidthModifier(alignment: alignment))
}
}
接下来,我们为主机实现另一个ViewModifier
。此修饰符将已知宽度(如果有)放入环境中,并在 SwiftUI 计算最终偏好值时更新已知宽度:
struct EqualWidthHost: ViewModifier {
@State var width: CGFloat? = nil
func body(content: Content) -> some View {
return content
.environment(\.equalWidth, width)
.onPreferenceChange(EqualWidthKey.self) { self.width = [=19=] }
}
}
现在我们可以实现 equalWidthHost
修饰符:
extension View {
func equalWidthHost() -> some View {
return self.modifier(EqualWidthHost())
}
}
最后我们可以看到结果:
您可以找到最终的 playground 代码 in this gist。
如何在渲染后获取 UI 元素的大小 (width/height) 并将其传递回父级以重新渲染?
示例:
父视图 (ChatMessage) 包含一个 RoundedRectangle,其上放置了来自子视图 (ChatMessageContent) 的文本 - 聊天气泡样式。
问题是在渲染父级时我不知道文本的大小,因为文本可能有 5、6 等行,具体取决于消息文本的长度。
struct ChatMessage: View {
@State var message: Message
@State var messageHeight: CGFloat = 28
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 8)
.fill(Color.red).opacity(0.9)
.frame(width: 480, height: self.messageHeight)
ChatMessageContent(message: self.$message, messageHeight: self.$messageHeight)
.frame(width: 480, height: self.messageHeight)
}
}
}
struct ChatMessageContent: View {
@Binding var message: Message
@Binding var messageHeight: CGFloat
var body: some View {
GeometryReader { geometry in
Text(self.message.message)
.lineLimit(nil)
.multilineTextAlignment(.center)
.onAppear {self.messageHeight = geometry.size.height; print(geometry.size.height}
}
}
}
在提供的示例中,messageHeight 保持在 28,并且不会在父项上进行调整。我希望 messageHeight 更改为 Text 元素的实际高度,具体取决于它显示的文本行数。
例如。两行 -> messageHeight = 42,三行 -> messageHeight = 56。
如何获得 UI 元素(在本例中为文本)的实际大小,因为 GeometryReader 似乎无法解决问题?它还读取 geometry.size.height = 28(从父视图传递)。
首先,值得理解的是,在 Text
后面填充 RoundedRectangle
的情况下,您不需要测量文本或将大小发送到视图层次结构中。您可以将其配置为选择完全适合其内容的高度。然后使用 .background
修饰符添加 RoundedRectangle
。示例:
import SwiftUI
import PlaygroundSupport
let message = String(NotificationCenter.default.debugDescription.prefix(300))
PlaygroundPage.current.setLiveView(
Text(message)
.fixedSize(horizontal: false, vertical: true)
.padding(12)
.frame(width: 480)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.red.opacity(0.9))
)
.padding(12)
)
结果:
好的,但有时您确实需要测量视图并将其大小传递到层次结构中。在 SwiftUI 中,视图可以在称为“首选项”的东西中向上发送信息。 Apple 尚未彻底记录偏好系统,但有些人已经弄明白了。特别是 kontiki has described it starting with this article at swiftui-lab。 (swiftui-lab 的每篇文章都很棒。)
那么让我们举一个例子,我们确实需要使用首选项。 ConversationView
显示邮件列表,每封邮件都标有发件人:
struct Message {
var sender: String
var body: String
}
struct MessageView: View {
var message: Message
var body: some View {
HStack(alignment: .bottom, spacing: 3) {
Text(message.sender + ":").padding(2)
Text(message.body)
.fixedSize(horizontal: false, vertical: true).padding(6)
.background(Color.blue.opacity(0.2))
}
}
}
struct ConversationView: View {
var messages: [Message]
var body: some View {
VStack(alignment: .leading, spacing: 4) {
ForEach(messages.indices) { i in
MessageView(message: self.messages[i])
}
}
}
}
let convo: [Message] = [
.init(sender: "Peanutsmasher", body: "How do I get the size (width/height) of an UI element after its been rendered and pass it back to the parent for re-rendering?"),
.init(sender: "Rob", body: "First, it's worth understanding blah blah blah…"),
]
PlaygroundPage.current.setLiveView(
ConversationView(messages: convo)
.frame(width: 480)
.padding(12)
.border(Color.black)
.padding(12)
)
看起来像这样:
我们真的很想让这些消息气泡的左边缘对齐。这意味着我们需要使发送方 Text
具有相同的宽度。我们将通过使用新修饰符 .equalWidth()
扩展 View
来实现。我们将像这样将修饰符应用于发件人 Text
:
struct MessageView: View {
var message: Message
var body: some View {
HStack(alignment: .bottom, spacing: 3) {
Text(message.sender + ":").padding(2)
.equalWidth(alignment: .trailing) // <-- THIS IS THE NEW MODIFIER
Text(message.body)
.fixedSize(horizontal: false, vertical: true).padding(6)
.background(Color.blue.opacity(0.2))
}
}
}
在 ConversationView
之后,我们将使用另一个新修饰符 .equalWidthHost()
.
struct ConversationView: View {
var messages: [Message]
var body: some View {
VStack(alignment: .leading, spacing: 4) {
ForEach(messages.indices) { i in
MessageView(message: self.messages[i])
}
} //
.equalWidthHost() // <-- THIS IS THE NEW MODIFIER
}
}
在我们可以实现这些修饰符之前,我们需要定义一个 PreferenceKey
(which we will use to pass the widths up the view hierarchy from the Text
s to the host) and an EnvironmentKey
(我们将使用它来将选定的宽度从主机向下传递到 Text
s)。
通过为首选项定义 defaultValue
以及组合两个值的方法,类型符合 PreferenceKey
。这是我们的:
struct EqualWidthKey: PreferenceKey {
static var defaultValue: CGFloat? { nil }
static func reduce(value: inout CGFloat?, nextValue: () -> CGFloat?) {
switch (value, nextValue()) {
case (_, nil): break
case (nil, let next): value = next
case (let a?, let b?): value = max(a, b)
}
}
}
一个类型通过定义一个defaultValue
来符合EnvironmentKey
。由于 EqualWidthKey
已经这样做了,我们可以将 PreferenceKey
重用为 EnvironmentKey
:
extension EqualWidthKey: EnvironmentKey { }
我们还需要添加访问器 EnvironmentValues
:
extension EnvironmentValues {
var equalWidth: CGFloat? {
get { self[EqualWidthKey.self] }
set { self[EqualWidthKey.self] = newValue }
}
}
现在我们可以实现 ViewModifier
将首选项设置为其内容的宽度,并将环境宽度应用于其内容:
struct EqualWidthModifier: ViewModifier {
var alignment: Alignment
@Environment(\.equalWidth) var equalWidth
func body(content: Content) -> some View {
return content
.background(
GeometryReader { proxy in
Color.clear
.preference(key: EqualWidthKey.self, value: proxy.size.width)
}
)
.frame(width: equalWidth, alignment: alignment)
}
}
默认情况下,GeometryReader
会填充其父项给它的 space 数量。这不是我们想要衡量的,所以我们将 GeometryReader
放在 background
修饰符中,因为背景视图始终是其前景内容的大小。
我们可以使用 EqualWidthModifier
类型在 View
上实现 equalWidth
修饰符:
extension View {
func equalWidth(alignment: Alignment) -> some View {
return self.modifier(EqualWidthModifier(alignment: alignment))
}
}
接下来,我们为主机实现另一个ViewModifier
。此修饰符将已知宽度(如果有)放入环境中,并在 SwiftUI 计算最终偏好值时更新已知宽度:
struct EqualWidthHost: ViewModifier {
@State var width: CGFloat? = nil
func body(content: Content) -> some View {
return content
.environment(\.equalWidth, width)
.onPreferenceChange(EqualWidthKey.self) { self.width = [=19=] }
}
}
现在我们可以实现 equalWidthHost
修饰符:
extension View {
func equalWidthHost() -> some View {
return self.modifier(EqualWidthHost())
}
}
最后我们可以看到结果:
您可以找到最终的 playground 代码 in this gist。