在列表行中创建等宽 SwiftUI 视图

Make Equal-Width SwiftUI Views in List Rows

我有一个 List 显示当月的天数。 List 中的每一行都包含缩写的日期、DividerVStack 中的日期编号。 VStack 然后嵌入到 HStack 中,这样我就可以在日期和数字的右侧添加更多文本。

struct DayListItem : View {

    // MARK: - Properties

    let date: Date

    private let weekdayFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "EEE"
        return formatter
    }()

    private let dayNumberFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "d"
        return formatter
    }()

    var body: some View {
        HStack {
            VStack(alignment: .center) {
                Text(weekdayFormatter.string(from: date))
                    .font(.caption)
                    .foregroundColor(.secondary)
                Text(dayNumberFormatter.string(from: date))
                    .font(.body)
                    .foregroundColor(.red)
            }
            Divider()
        }
    }

}

DayListItem 的实例在 ContentView 中使用:

struct ContentView : View {

    // MARK: - Properties

    private let dataProvider = CalendricalDataProvider()

    private var navigationBarTitle: String {
        let formatter = DateFormatter()
        formatter.dateFormat = "MMMM YYY"
        return formatter.string(from: Date())
    } 

    private var currentMonth: Month {
        dataProvider.currentMonth
    }

    private var months: [Month] {
        return dataProvider.monthsInRelativeYear
    }

    var body: some View {
        NavigationView {
            List(currentMonth.days.identified(by: \.self)) { date in
                DayListItem(date: date)
            }
                .navigationBarTitle(Text(navigationBarTitle))
                .listStyle(.grouped)
        }
    }

}

代码的结果如下:

它可能不明显,但分隔线没有对齐,因为文本的宽度可能因行而异。我想要实现的是让包含日期信息的视图具有相同的宽度,以便它们在视觉上对齐。

我试过使用 GeometryReaderframe() 修饰符来设置最小宽度、理想宽度和最大宽度,但我需要确保文本可以随着 Dynamic 收缩和增长类型设置;我选择不使用占父级百分比的宽度,因为我不确定如何确保本地化文本始终适合允许的宽度。

如何修改我的视图,使行中的每个视图都具有相同的宽度,而不考虑文本的宽度?

关于动态类型,我会创建一个不同的布局,以便在更改该设置时使用。

我使用 GeometryReaderPreferences 让这个工作。

首先,在 ContentView 中添加 属性:

@State var maxLabelWidth: CGFloat = .zero

然后,在 DayListItem 中添加 属性:

@Binding var maxLabelWidth: CGFloat

接下来,在ContentView中,将self.$maxLabelWidth传递给DayListItem的每个实例:

List(currentMonth.days.identified(by: \.self)) { date in
    DayListItem(date: date, maxLabelWidth: self.$maxLabelWidth)
}

现在,创建一个名为 MaxWidthPreferenceKey 的结构:

struct MaxWidthPreferenceKey: PreferenceKey {
    static var defaultValue: CGFloat = .zero
    
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        let nextValue = nextValue()
        
        guard nextValue > value else { return }
        
        value = nextValue
    }
}

这符合 PreferenceKey 协议,允许您在视图之间交流首选项时将此结构用作键。

接下来,创建一个名为 DayListItemGeometryView - 这将用于确定 DayListItemVStack 的宽度:

struct DayListItemGeometry: View {
    var body: some View {
        GeometryReader { geometry in
            Color.clear
                .preference(key: MaxWidthPreferenceKey.self, value: geometry.size.width)
        }
        .scaledToFill()
    }
}

然后,在 DayListItem 中,将您的代码更改为:

HStack {
    VStack(alignment: .center) {
        Text(weekdayFormatter.string(from: date))
            .font(.caption)
            .foregroundColor(.secondary)
        Text(dayNumberFormatter.string(from: date))
            .font(.body)
            .foregroundColor(.red)
    }
    .background(DayListItemGeometry())
    .onPreferenceChange(MaxWidthPreferenceKey.self) {
        self.maxLabelWidth = [=15=]
    }
    .frame(width: self.maxLabelWidth)

    Divider()
}

我所做的是创建了一个 GeometryReader 并将其应用于 VStack 的背景。几何图形告诉我 VStack 的尺寸,它会根据文本的大小自行调整大小。 MaxWidthPreferenceKey 会在几何形状发生变化时更新,并且在 MaxWidthPreferenceKey 中的 reduce 函数计算出最大宽度后,我读取首选项更改并更新 self.maxLabelWidth。然后我将 VStack 的框架设置为 .frame(width: self.maxLabelWidth),并且由于 maxLabelWidth 具有约束力,因此在计算新的 maxLabelWidth 时每个 DayListItem 都会更新。请记住,这里的顺序很重要。将 .frame 修饰符放在 .background.onPreferenceChange 之前将不起作用。