在列表行中制作等宽的 SwiftUI 视图

Posted

技术标签:

【中文标题】在列表行中制作等宽的 SwiftUI 视图【英文标题】:Make Equal-Width SwiftUI Views in List Rows 【发布时间】:2019-07-11 21:50:18 【问题描述】:

我有一个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() 修饰符来设置最小宽度、理想宽度和最大宽度,但我需要确保文本可以通过动态类型设置缩小和增长;我选择不使用占父元素百分比的宽度,因为我不确定如何确保本地化文本始终适合允许的宽度。

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

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

【问题讨论】:

【参考方案1】:

我使用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 = $0
    
    .frame(width: self.maxLabelWidth)

    Divider()

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

【讨论】:

这正是我正在寻找的行为!你还记得你在哪里了解到PreferenceKey 协议吗?我查看了文档,但它非常缺乏。 是的,不幸的是,SwiftUI 的文档几乎不存在。我从这个article 了解到PreferenceKey。到目前为止,他们只发布了几篇文章,但他们发布的那些已经对我帮助很大,所以我会留意他们何时发布更多文章。 @graycampbell scaledToFill 导致我的内容挤压该行的其余内容。如果我删除它,那么修改后的内容就会变得非常薄。如何测量要修改的内容的原始宽度? 这已经获得了 28 票,所以它一定在某个时候奏效了。我遇到了一个已提交雷达的错误,但没有提供解决方案。 “绑定首选项 WidthPreferenceKey 尝试每帧更新多次。”有没有办法让这个工作? 我将列表嵌入到滚动视图中。我删除了它,错误消失了。再次感谢!

以上是关于在列表行中制作等宽的 SwiftUI 视图的主要内容,如果未能解决你的问题,请参考以下文章

如何在 SwiftUI 中制作水平列表?

在 SwiftUI 的 List 行中添加形状

SwiftUI 制作一个只显示一定数量行的列表

在 SwiftUi 中的视图之间传递列表

如何在 SwiftUI 列表行中具有相同的比例?

SwiftUI - 列表行中的多个按钮