如何获取 UI 元素渲染后的大小(宽度/高度)并将其传回父级以重新渲染?
示例:
父 View (ChatMessage) 包含一个 RoundedRectangle,在其上放置 subview (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(从父 View 传递)。
最佳答案
首先,值得理解的是,在 Text
后面填充 RoundedRectangle
的情况下,您不需要测量文本或将尺寸发送到 View 等级制度。您可以将其配置为选择完全适合其内容的高度。然后使用 .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)
)
结果:
好的,但有时您确实需要测量 View 并将其大小传递到层次结构中。在 SwiftUI 中, View 可以通过称为“首选项”的方式向上层次结构发送信息。苹果还没有彻底记录偏好系统,但有些人已经弄清楚了。特别是kontiki以 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)
)
看起来像这样:
我们真的希望这些消息气泡的左边缘对齐。这意味着我们需要使发送方的文本具有相同的宽度。我们将通过使用新修饰符 .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()
定义等宽 View 的“域”。
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
(我们将使用它来将 View 层次结构中的宽度从 Text
传递到主机)和 EnvironmentKey
(我们将使用它来将选定的宽度从主机传递到 Text
)。
类型通过为首选项定义一个 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
会填充其父级提供的尽可能多的空间。这不是我们想要测量的,因此我们将 GeometryReader
放在 background
修饰符中,因为背景 View 始终是其前景内容的大小。
我们可以使用此 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 = $0 }
}
}
现在我们可以实现equalWidthHost
修饰符:
extension View {
func equalWidthHost() -> some View {
return self.modifier(EqualWidthHost())
}
}
最后我们可以看到结果:
您可以找到最终的 Playground 代码 in this gist .
关于SwiftUI - UI 元素的大小,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60229093/