Reputation: 2213
I'm trying to take a an array of items (structs) and display them in a grouped table view with SwiftUI.
My (simplified) models look like this:
struct CheckIn: Identifiable {
...
let id = UUID()
let date = Date().atMidnight // removes the time component
var completed: Bool
...
}
class Store: ObservableObject {
@Published var checkIns = [...] {
didSet { persist() }
}
}
Before showing the check-ins in a list, I want to group them by date. So I have another model:
struct DailyCheckIns {
let date: Date
let checkIns: [CheckIn]
}
// and a function to group the check-ins array:
func groupByDate(_ checkIns: [CheckIn]) -> [DailyCheckIns] {...}
The view is where I have the problem. The version below works but the data is not grouped obviously. By "works", I mean that the data is passed into CheckInView
and it can update its check-in, which is then correctly reflected in the store and in the UI.
struct ContentView: View {
@EnvironmentObject var store: Store
var body: some View {
NavigationView {
List {
ForEach(store.checkIns.indices) { idx in
CheckInView(checkIn: self.$store.checkIns[idx]) // checkIn is a @Binding
}
}
.navigationBarTitle("Check Ins")
}
}
}
This next version is my attempt at grouping the data. With this approach, I have to change CheckInView
's checkIn
property from @Binding
to @State
. The grouping works and the data is displayed but when the check-in's completion is toggled, the models update but the UI does not.
struct ContentView: View {
@EnvironmentObject var store: Store
var body: some View {
NavigationView {
List {
ForEach(groupByDate(store.checkIns), id: \.date) { daily in
Section(header: Text(dateFormatter.string(from: daily.date))) {
ForEach(daily.checkIns, id: \.id) { checkIn in
CheckInView(checkIn: checkIn) // I can't use a binding here, so in this version I need to make checkIn a @State.
}
}
}
}
.navigationBarTitle("Check Ins")
}
}
}
At the moment, I don't have CheckInView
modifying the check in directly. Instead it posts an update to the store and the store updates the model:
struct CheckInView: View {
@Binding var checkIn: CheckIn
@EnvironmentObject var store: Store
var body: some View {
HStack {
Button(action: {
self.store.update(checkIn: self.checkIn, with: true)
}) {
Image(systemName: "...")
.font(.largeTitle)
.foregroundColor(checkIn.completed ? .gray : .red)
}
.buttonStyle(BorderlessButtonStyle())
...
So the question is: how can I keep the list grouped and keep the bindings working all the way down the view hierarchy?
Upvotes: 6
Views: 5836
Reputation: 2213
I have figured out how to do this. Whether or not it's the most elegant solution, I don't know. It sure doesn't look elegant to me, but I can't think of another way I can do it.
In the code I posted, DailyCheckIns
is an intermediate model for the purpose of grouping check-ins. The function groupByDate
takes an array of CheckIn
s and turns them into DailyCheckIn
s for display. The problem was that DailyCheckIn
essentially holds copies of the data in the store, so I can't really use it to create bindings from that to the store.
The way I've found around this is to use DailyCheckIn
s for creating sections and the row count in each section, but when it comes to creating views that require a binding to the store, I use the store's data directly. To accomplish this, I had to change DailyCheckIn
s (and groupByDate
) to track the index of each CheckIn
in the store's property:
typealias CheckInWithIndex = (Int, CheckIn)
struct DailyCheckIns {
let date: Date
var checkIns: [CheckInWithIndex]
func appending(_ ci: CheckInWithIndex) -> DailyCheckIns {
DailyCheckIns(date: date, checkIns: checkIns + [ci])
}
}
private func groupByDate(_ checkIns: [CheckIn]) -> [DailyCheckIn] { ... }
struct ContentView: View {
@EnvironmentObject var store: Store
var body: some View {
NavigationView {
List {
ForEach(groupByDate(store.checkIns), id: \.date) { daily in
Section(header: Text(dateFormatter.string(from: daily.date))) {
ForEach(daily.checkIns, id: \.id) { checkInWithIndex in
CheckInView(checkIn: self.$store.checkIns[checkInWithIndex.0])
}
}
}
}
.navigationBarTitle("Check Ins")
}
}
}
Hope this helps someone. But also, I hope someone has a better solution to this kind of situation.
Upvotes: 3