简体   繁体   中英

Group list data in SwiftUI for display in sections

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?

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM