繁体   English   中英

SwiftUI - 如何将一个文本视图与另一个文本视图垂直对齐,以制作类似于表格的正确对齐方式?

[英]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)
    }
}

我已将此答案的完整代码放在一个要点中,以便于复制

那么,我们如何实现WidthPreferenceDomainwidthPreference呢?

我们需要测量 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属性,它匹配PreferenceKeydefaultValue属性,因此我们可以重新使用我们已经定义的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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM