Мы собираемся реализовать два новых метода модификатора View
, чтобы мы могли написать это:
struct ContentView: View {
@State var labelWidth: CGFloat? = nil
@State var username = ""
@State var password = ""
var body: some View {
VStack {
HStack {
Text("User:")
.equalSizedLabel(width: labelWidth, alignment: .trailing)
TextField("User", text: $username)
}
HStack {
Text("Password:")
.equalSizedLabel(width: labelWidth, alignment: .trailing)
SecureField("Password", text: $password)
}
}
.padding()
.textFieldStyle(.roundedBorder)
.storeMaxLabelWidth(in: $labelWidth)
}
}
Два новых модификатора: equalSizedLabel(width:alignment:)
и storeMaxLabelWidth(in:)
.
Модификатор equalSizedLabel(width:alignment)
делает две вещи:
- Он применяет
width
и alignment
к своему содержимому (представления Text(“User:”)
и Text(“Password:”)
).
- Он измеряет ширину своего содержимого и передает его любому представлению предка, которое этого хочет.
Модификатор storeMaxLabelWidth(in:)
получает ширину, измеренную equalSizedLabel
, и сохраняет максимальную ширину в привязке $labelWidth
, которую мы передаем ей.
Итак, как мы реализуем эти модификаторы? Как мы передаем значение от представления потомка до предка? В SwiftUI мы делаем это, используя (в настоящее время недокументированную) систему «предпочтений».
Чтобы определить новое предпочтение, мы определяем тип, соответствующий PreferenceKey
. Чтобы соответствовать PreferenceKey
, мы должны определить значение по умолчанию для наших предпочтений, и мы должны определить, как объединить предпочтения нескольких подпредставлений. Мы хотим, чтобы нашим предпочтением была максимальная ширина всех меток, поэтому значение по умолчанию равно нулю, и мы объединяем предпочтения, беря максимум. Вот PreferenceKey
мы будем использовать:
struct MaxLabelWidth: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = max(value, nextValue())
}
}
Функция модификатора preference
устанавливает предпочтение, поэтому мы можем сказать .preference(key: MaxLabelWidth.self, value: width)
, чтобы установить наше предпочтение, но мы должны знать, что width
установить. Нам нужно использовать GeometryReader
, чтобы получить ширину, и это немного сложно сделать правильно, поэтому мы завернем это в ViewModifier
следующим образом:
extension MaxLabelWidth: ViewModifier {
func body(content: Content) -> some View {
return content
.background(GeometryReader { proxy in
Color.clear
.preference(key: Self.self, value: proxy.size.width)
})
}
}
То, что происходит выше, это то, что мы прикрепляем фон View
к контенту, потому что фон всегда имеет тот же размер, что и контент, к которому он прикреплен. Фон View
- это GeometryReader
, который (через proxy
) предоставляет доступ к своему собственному размеру. Мы должны предоставить GeometryReader
свой собственный контент. Поскольку мы на самом деле не хотим показывать фон за исходным контентом, мы используем Color.clear
в качестве контента GeometryReader
. Наконец, мы используем модификатор preference
, чтобы сохранить ширину как предпочтение MaxLabelWidth
.
Теперь можно определить методы модификатора equalSizedLabel(width:alignment:)
и storeMaxLabelWidth(in:)
:
extension View {
func equalSizedLabel(width: CGFloat?, alignment: Alignment) -> some View {
return self
.modifier(MaxLabelWidth())
.frame(width: width, alignment: alignment)
}
}
extension View {
func storeMaxLabelWidth(in binding: Binding<CGFloat?>) -> some View {
return self.onPreferenceChange(MaxLabelWidth.self) {
binding.value = $0
}
}
}
Вот результат: