SwiftUI - Размер элемента пользовательского интерфейса - PullRequest
0 голосов
/ 14 февраля 2020

Как получить размер (ширину / высоту) элемента пользовательского интерфейса после его рендеринга и передать его родителю для повторного рендеринга?

Пример. Родительское представление (ChatMessage) содержит RoundedRectangle, в который помещается текст из дочернего представления (ChatMessageContent) - стиль пузыря чата.
Проблема в том, что я не знаю размер текста при рендеринге родителя, так как текст может иметь 5, 6 и т. Д. c. строк в зависимости от длины текста сообщения.

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.

Как получить фактический размер элемента пользовательского интерфейса (в данном случае текст), поскольку GeometryReader, похоже, не справляется с задачей? Он также читает geometry.size.height = 28 (который передается из родительского представления).

1 Ответ

1 голос
/ 14 февраля 2020

Во-первых, стоит понимать, что в случае заполнения RoundedRectangle за Text вам не нужно измерять текст или отправлять размер вверх по иерархии представления. Вы можете настроить его, чтобы выбрать высоту, которая точно соответствует его содержанию. Затем добавьте RoundedRectangle, используя модификатор .background. Пример:

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

Хорошо, но иногда вам действительно нужно измерить вид и передать его размер вверх по иерархии. В SwiftUI представление может отправлять информацию вверх по иерархии в нечто, называемое «предпочтение». Apple еще не полностью документировала систему предпочтений, но некоторые люди поняли это. В частности, kontiki описал это, начиная с этой статьи в 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

Нам бы очень хотелось, чтобы левые края пузырьков были выровнены. Это означает, что мы должны сделать отправителя Text одинаковой ширины. Мы сделаем это, расширив View новым модификатором .equalWidth(). Мы применим модификатор к отправителю 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 (который мы будем использовать для передачи ширины вверх по иерархии представления из Text с хосту) и EnvironmentKey (которые мы будем использовать для передачи выбранной ширины от хоста к Text с).

Тип соответствует PreferenceKey путем определения defaultValue для предпочтения и метода для объединения двух значений. Вот наши:

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)
        }
    }
}

Тип соответствует EnvironmentKey, определяя defaultValue. Поскольку 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, потому что фоновое представление всегда соответствует размеру его содержимого переднего плана.

Мы можем реализовать модификатор equalWidth View, используя этот EqualWidthModifier тип:

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

Вы можете найти окончательный код игровой площадки в этой сущности .

...