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

18

我有一个List,用于显示当前月份的日期。每行包括简写的星期几,一个Divider和当天的数字,这些内容都在VStack中。然后将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()
        }
    }

}

ContentView 中使用 DayListItem 的实例:

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)
        }
    }

}
代码的结果如下:

app在Xcode模拟器中运行的屏幕截图

可能不太明显,但是分隔符没有对齐,因为文本的宽度从一行到另一行会有所变化。我想实现的是让包含日期信息的视图具有相同的宽度,以便它们在视觉上对齐。
我尝试使用GeometryReaderframe()修饰符来设置最小宽度、理想宽度和最大宽度,但我需要确保文本可以根据动态类型设置而收缩和增长;我选择不使用父级的百分比宽度,因为我不确定本地化文本是否始终适合允许的宽度。
我如何修改我的视图,使每一行中的每个视图具有相同的宽度,而不考虑文本的宽度?
关于动态类型,我将创建不同的布局用于更改该设置时使用。
2个回答

39
我使用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”协议,允许您在视图之间通信偏好设置时使用此结构体作为键。
接下来,创建一个名为“DayListItemGeometry”的“View” - 这将用于确定“DayListItem”中“VStack”的宽度:
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时都会得到更新。请注意,顺序很重要。在.background.onPreferenceChange之前放置.frame修饰符将不起作用。

Canvas Screenshot


1
是的,不幸的是 SwiftUI 的文档几乎不存在。我从这篇文章中了解到了 PreferenceKey。他们目前只发布了几篇文章,但这些文章已经对我有很大帮助了,所以我会关注他们发布更多文章的情况。 - graycampbell
1
@graycampbell scaledToFill 会导致我的内容挤压行中其余的内容。如果我将其移除,那么修改后的内容就会变得非常瘦。我该如何测量要修改的原始内容的宽度? - Ian Warburton
1
这个问题已经得到了28个赞,所以它在某个时候肯定是有效的。我遇到了一个错误,已经提交了一个radar,但没有提供解决方案。"Bound preference WidthPreferenceKey tried to update multiple times per frame." 有没有办法让它工作? - Mozahler
@IanWarburton 我不确定问题出在哪里。我已经很久没有看过这个了。我有时间的时候会尝试去看一下。 - graycampbell
1
我在滚动视图中嵌入了列表。我将其移除后,错误消失了。再次感谢! - Mozahler
显示剩余6条评论

2
我曾试图实现类似的功能。我的一行标签中的文本长度从2个字符到20个字符不等,这会破坏水平对齐。我想让该行中的此列具有固定宽度。以下是我应用的非常简单的解决方案,它对我有效。希望它也能为其他人带来好处。
var body: some View { // view for each row in list
        VStack(){
            HStack {
                  Text(wire.labelValueDate)
                        .
                        .
                        .foregroundColor(wire.labelColor)
                        .fixedSize(horizontal: true, vertical: false)
                        .frame(width: 110.0, alignment: .trailing)
                    }
                  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接