[英]SwiftUI - How to vertically align a Text view with another Text view to make something similar to a Table with proper alignment?
所以我基本上需要在 SwiftUI 中制作一个类似于此的表格:
正如您在屏幕截图中看到的那样,我很难将字段与其对应的列对齐,如“John”应该完全以“Name”为中心,“EUR”应该以“Currency”为中心等等。正如你可以在代码上看到,我目前正在用堆栈间距参数观察它,但这简直太糟糕了,甚至不适用于所有设备,而且数据是动态的,并且来自 API 调用,因此它无法工作。 如何在 SwiftUI 上对齐? 我需要支持 iOS 13。
当前代码:
struct ContentView: View {
var body: some View {
VStack(spacing: 0) {
ZStack {
TitleBackground()
Text("Clients")
.foregroundColor(.white)
}
HStack(spacing: 30) {
Text("Name")
Text("Balance")
Text("Currency")
Spacer()
}
.foregroundColor(Color(#colorLiteral(red: 0.4783782363, green: 0.4784628153, blue: 0.4783664346, alpha: 1)))
.frame(maxWidth:.infinity)
.padding(12)
.background(Color(#colorLiteral(red: 0.9332349896, green: 0.9333916306, blue: 0.9332130551, alpha: 1)))
RowView()
RowView()
RowView()
RowView()
}
}
}
struct RowView : View {
var body: some View {
HStack(spacing: 30) {
Text("John")
Text("$5300")
Text("EUR")
Spacer()
}
.padding(12)
}
}
struct TitleBackground: View {
var body: some View {
ZStack(alignment: .bottom){
Rectangle()
.frame(maxWidth: .infinity, maxHeight: 60)
.foregroundColor(.red)
.cornerRadius(15)
//We only need the top corners rounded, so we embed another rectangle to the bottom.
Rectangle()
.frame(maxWidth: .infinity, maxHeight: 15)
.foregroundColor(.red)
}
}
}
有两种方法可以解决这个问题。 首先,您可以使用两个具有相同列设置的 LazyVGrids
,但您必须修复一些大小。第二种方法是您可以使用PreferenceKeys
解决此问题。
LazyVGrid
:
`首选项键`:struct LazyVGridView: View { let columns = [ GridItem(.fixed(100)), GridItem(.fixed(100)), GridItem(.adaptive(minimum: 180)) ] var body: some View { VStack { LazyVGrid(columns: columns) { Text("Name") .padding() Text("Balance") .padding() Text("Currency") .padding() } .foregroundColor(Color(#colorLiteral(red: 0.4783782363, green: 0.4784628153, blue: 0.4783664346, alpha: 1))) .background(Color(#colorLiteral(red: 0.9332349896, green: 0.9333916306, blue: 0.9332130551, alpha: 1))) LazyVGrid(columns: columns) { ForEach(clients) { client in Text(client.name) .padding() Text(client.balance.description) .padding() Text(client.currency) .padding() } } } } }
struct PrefKeyAlignmentView: View { let clients: [ClientInfo] = [ ClientInfo(name: "John", balance: 300, currency: "EUR"), ClientInfo(name: "Jennifer", balance: 53000, currency: "EUR"), ClientInfo(name: "Jo", balance: 30, currency: "EUR"), ClientInfo(name: "Jill", balance: 530000, currency: "EUR") ] @State private var col1Width: CGFloat = 100 @State private var col2Width: CGFloat = 100 @State private var col3Width: CGFloat = 110 var body: some View { VStack { HStack() { Text("Name") .padding() // The background view takes on the size of the Text + the padding, and the GeometryReader reads that size .background(GeometryReader { geometry in Color.clear.preference( //This sets the preference key value with the width of the background view key: Col1WidthPrefKey.self, value: geometry.size.width) }) // This set the frame to the column width variable defined above .frame(width: col1Width) Text("Balance") .padding() .background(GeometryReader { geometry in Color.clear.preference( key: Col2WidthPrefKey.self, value: geometry.size.width) }) .frame(width: col2Width) Text("Currency") .padding() .background(GeometryReader { geometry in Color.clear.preference( key: Col3WidthPrefKey.self, value: geometry.size.width) }) .frame(width: col3Width) Spacer() } ForEach(clients) { client in HStack { Text(client.name) .padding() .background(GeometryReader { geometry in Color.clear.preference( key: Col1WidthPrefKey.self, value: geometry.size.width) }) .frame(width: col1Width) Text(client.balance.description) .padding() .background(GeometryReader { geometry in Color.clear.preference( key: Col2WidthPrefKey.self, value: geometry.size.width) }) .frame(width: col2Width) Text(client.currency) .padding() .background(GeometryReader { geometry in Color.clear.preference( key: Col3WidthPrefKey.self, value: geometry.size.width) }) .frame(width: col3Width) Spacer() } } } //This sets the various column widths whenever a new entry as found. //It keeps the larger of the new item's width, or the last largest width. .onPreferenceChange(Col1WidthPrefKey.self) { col1Width = max($0,col1Width) } .onPreferenceChange(Col2WidthPrefKey.self) { col2Width = max($0,col2Width) } .onPreferenceChange(Col3WidthPrefKey.self) { col3Width = max($0,col3Width) } } } struct ClientInfo: Identifiable { let id = UUID() let name: String let balance: Int let currency: String } private extension PrefKeyAlignmentView { struct Col1WidthPrefKey: PreferenceKey { static let defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = max(value, nextValue()) } } struct Col2WidthPrefKey: PreferenceKey { static let defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = max(value, nextValue()) } } struct Col3WidthPrefKey: PreferenceKey { static let defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = max(value, nextValue()) } } }
我不认为使用alignmentGuides 会像您必须处理多个视图一样工作,并且对齐指南仅对齐一组视图,而不是内部视图。
这两种解决方案都会起作用。 PreferenceKeys
将自动适应所有内容,但理论上可能会超出屏幕的范围,其中包含太多信息。 LazyVGrid
更严格,但如果数据太长,可能会导致截断/换行。如果您有两个不同的网格,我认为您不能为所有列使用 LazyVGrid
的自适应布局。可以将两者混合并使用PreferenceKeys
设置LazyVGrid
宽度,但我没有亲自尝试过。
用于测试的数据源:
struct ClientInfo: Identifiable { let id = UUID() let name: String let balance: Int let currency: String } let clients: [ClientInfo] = [ ClientInfo(name: "John", balance: 300, currency: "EUR"), ClientInfo(name: "Jennifer", balance: 53000, currency: "EUR"), ClientInfo(name: "Jo", balance: 30, currency: "EUR"), ClientInfo(name: "Jill", balance: 530000, currency: "EUR") ]
编辑:在研究答案时,我忘记您仅限于 iOS LazyVGrids
不可用。 我把LazyVGrid
答案LazyVGrid
,因为你可以使用if #available(os:)
如果你想将它用于以后的操作系统,或者其他人正在寻找更当前的解决方案。
我看到 Yrb 在我之前发布了一个类似的答案,但无论如何我都会发布这个,因为我的解决方案比他们的解决方案更具模块化/可组合性。
你想要这样的布局:
我们将实现一个 API,以便我们可以使用以下代码为表格视图获取布局:
enum ColumnId: Hashable {
case name
case balance
case currency
}
struct TableView: View {
var body: some View {
WidthPreferenceDomain {
VStack(spacing: 0) {
HStack(spacing: 30) {
Text("Name")
.widthPreference(ColumnId.name)
Text("Balance")
.widthPreference(ColumnId.balance)
Text("Currency")
.widthPreference(ColumnId.currency)
Spacer()
}
.frame(maxWidth:.infinity)
.padding(12)
.background(Color(#colorLiteral(red: 0.9332349896, green: 0.9333916306, blue: 0.9332130551, alpha: 1)))
ForEach(0 ..< 4) { _ in
RowView()
}
}
}
}
}
在上面的代码中:
ColumnId
类型来标识每个表列。WidthPreferenceDomain
容器视图,我们将在下面实现它。widthPreference
修饰符,我们将在下面实现。 我们还需要像这样在RowView
使用widthPreference
修饰符:
struct RowView : View {
var body: some View {
HStack(spacing: 30) {
Text("John")
.widthPreference(ColumnId.name)
Text("$5300")
.widthPreference(ColumnId.balance)
Text("EUR")
.widthPreference(ColumnId.currency)
Spacer()
}
.padding(12)
}
}
我已将此答案的完整代码放在一个要点中,以便于复制。
那么,我们如何实现WidthPreferenceDomain
和widthPreference
呢?
我们需要测量 Name 列中所有项目的宽度,包括 Name 标题本身,以便我们可以将 Name 列的所有项目设置为具有相同的宽度。 我们需要对 Balance 和 Currency 列重复该过程。
要测量项目,我们可以使用GeometryReader
。 因为GeometryReader
是一个贪婪的视图,它会扩展以填充与其父提供的空间一样多的空间,所以我们不想在GeometryReader
包装任何表项。 相反,我们希望在每个表项的background
放置一个GeometryReader
。 background
视图被赋予与其父视图相同的框架。
为了收集测量的宽度,我们可以通过定义符合PreferenceKey
协议的类型来使用“首选项”。 由于我们想要收集所有列的宽度,我们将宽度存储在Dictionary
,以列 id 为键。 要合并同一列的两个宽度,我们取宽度的最大值,以便每列扩展以适合其所有项目。
fileprivate struct WidthPreferenceKey: PreferenceKey {
static var defaultValue: [AnyHashable: CGFloat] { [:] }
static func reduce(value: inout [AnyHashable : CGFloat], nextValue: () -> [AnyHashable : CGFloat]) {
value.merge(nextValue(), uniquingKeysWith: { max($0, $1) })
}
}
要将收集到的宽度分配回表项,我们可以通过定义符合EnvironmentKey
协议的类型来使用“环境”。 EnvironmentKey
只有一个要求,一个static var defaultValue
属性,它匹配PreferenceKey
的defaultValue
属性,因此我们可以重新使用我们已经定义的WidthPreferenceKey
类型:
extension WidthPreferenceKey: EnvironmentKey { }
要使用环境,我们还需要为EnvironmentValues
添加一个属性:
extension EnvironmentValues {
fileprivate var widthPreference: WidthPreferenceKey.Value {
get { self[WidthPreferenceKey.self] }
set { self[WidthPreferenceKey.self] = newValue }
}
}
现在我们可以定义widthPreference
修饰符:
extension View {
public func widthPreference<Domain: Hashable>(_ key: Domain) -> some View {
return self.modifier(WidthPreferenceModifier(key: key))
}
}
fileprivate struct WidthPreferenceModifier: ViewModifier {
@Environment(\.widthPreference) var widthPreference
var key: AnyHashable
func body(content: Content) -> some View {
content
.fixedSize(horizontal: true, vertical: false)
.background(GeometryReader { proxy in
Color.clear
.preference(key: WidthPreferenceKey.self, value: [key: proxy.size.width])
})
.frame(width: widthPreference[key])
}
}
此修改器使用背景GeometryReader
测量修改后的视图的宽度并将其存储在首选项中。 如果来自环境的widthPreference
具有宽度,它还会设置视图的宽度。 请注意, widthPreference[key]
在第一次布局时将为nil
,但这没关系,因为frame(width: nil)
是一个“惰性”修饰符; 它没有效果。
现在我们可以实现WidthPreferenceDomain
。 它需要接收首选项并将其存储在环境中:
public struct WidthPreferenceDomain<Content: View>: View {
@State private var widthPreference: WidthPreferenceKey.Value = [:]
private var content: Content
public init(@ViewBuilder content: () -> Content) {
self.content = content()
}
public var body: some View {
content
.environment(\.widthPreference, widthPreference)
.onPreferenceChange(WidthPreferenceKey.self) { widthPreference = $0 }
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.