SwiftUI - UI 元素的大小

标签 swift swiftui swiftui-list

如何获取 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)
)

结果:

text with fitted rounded rectangle background

好的,但有时您确实需要测量 View 并将其大小传递到层次结构中。在 SwiftUI 中, View 可以通过称为“首选项”的方式向上层次结构发送信息。苹果还没有彻底记录偏好系统,但有些人已经弄清楚了。特别是kontikithis 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)
)

看起来像这样:

ConversationView showing two messages, but the senders are different lengths so the message bubbles are not aligned

我们真的希望这些消息气泡的左边缘对齐。这意味着我们需要使发送方的文本具有相同的宽度。我们将通过使用新修饰符 .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())
    }
}

最后我们可以看到结果:

conversation view with message bubbles aligned

您可以找到最终的 Playground 代码 in this gist .

关于SwiftUI - UI 元素的大小,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60229093/

相关文章:

ios - 是否可以访问 xcode 中的所有通知

swift - 如何使用Swinject注册符合ObservableObject的协议(protocol)?

ios - 如何在 swift 中集成 PayU Money

swift - 核心数据地震演示在预览中崩溃

objective-c - ObjectiveC 项目中的 SwiftUI - 列表在预览中工作,而不是在运行时工作

ios - swift 如何处理 session 中的 nsjson

ios - 使用ForEach删除动画的SwiftUI自定义列表不起作用

SwiftUI 将@Published View 模型对象值传递给@Binding

swift - 如何正确地将 RealmDB 结果对象映射到 SwiftUI 列表?

swiftui - 在SwiftUI中无法两次选择同一行