Во-первых, стоит понимать, что в случае заполнения 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)
)
Результат:
Хорошо, но иногда вам действительно нужно измерить вид и передать его размер вверх по иерархии. В 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)
)
Это выглядит так:
Нам бы очень хотелось, чтобы левые края пузырьков были выровнены. Это означает, что мы должны сделать отправителя 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())
}
}
И наконец мы можем видеть результат:
Вы можете найти окончательный код игровой площадки в этой сущности .