I have a List
that displays days in the current month. Each row in the List
contains the abbreviated day, a Divider
, and the day number within a VStack
. The VStack
is then embedded in an HStack
so that I can have more text to the right of the day and number.
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()
}
}
}
Instances of DayListItem
are used in 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)
}
}
}
The result of the code is below:
It may not be obvious, but the dividers are not lined up because the width of the text can vary from row to row. What I would like to achieve is to have the views that contains the day information be the same width so that they are visually aligned.
I have tried using a GeometryReader
and the frame()
modifiers to set the minimum width, ideal width, and maximum width, but I need to ensure that the text can shrink and grow with Dynamic Type settings; I chose not to use a width that is a percentage of the parent because I was uncertain how to be sure that localized text would always fit within the allowed width.
How can I modify my views so that each view in the row is the same width, regardless of the width of text?
Regarding Dynamic Type, I will create a different layout to be used when that setting is changed.
I got this to work using GeometryReader
and Preferences
.
First, in ContentView
, add this property:
@State var maxLabelWidth: CGFloat = .zero
Then, in DayListItem
, add this property:
@Binding var maxLabelWidth: CGFloat
Next, in ContentView
, pass self.$maxLabelWidth
to each instance of DayListItem
:
List(currentMonth.days.identified(by: \.self)) { date in
DayListItem(date: date, maxLabelWidth: self.$maxLabelWidth)
}
Now, create a struct called 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
}
}
This conforms to the PreferenceKey
protocol, allowing you to use this struct as a key when communicating preferences between your views.
Next, create a View
called DayListItemGeometry
- this will be used to determine the width of the VStack
in DayListItem
:
struct DayListItemGeometry: View {
var body: some View {
GeometryReader { geometry in
Color.clear
.preference(key: MaxWidthPreferenceKey.self, value: geometry.size.width)
}
.scaledToFill()
}
}
Then, in DayListItem
, change your code to this:
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()
}
What I've done is I've created a GeometryReader
and applied it to the background of the VStack
. The geometry tells me the dimensions of the VStack
which sizes itself according to the size of the text. MaxWidthPreferenceKey
gets updated whenever the geometry changes, and after the reduce
function inside MaxWidthPreferenceKey
calculates the maximum width, I read the preference change and update self.maxLabelWidth
. I then set the frame of the VStack
to be .frame(width: self.maxLabelWidth)
, and since maxLabelWidth
is binding, every DayListItem
is updated when a new maxLabelWidth
is calculated. Keep in mind that the order matters here. Placing the .frame
modifier before .background
and .onPreferenceChange
will not work.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With