I'm trying to recreate a view that matches the event list in the stock iOS Calendar app. I have the following code which generates a list of events with each event separated into its own section by Date:
var body: some View {
NavigationView {
List {
ForEach(userData.occurrences) { occurrence in
Section(header: Text("\(occurrence.start, formatter: Self.dateFormatter)")) {
NavigationLink(
destination: OccurrenceDetail(occurrence: occurrence)
.environmentObject(self.userData)
) {
OccurrenceRow(occurrence: occurrence)
}
}
}
}
.navigationBarTitle(Text("Events"))
}.onAppear(perform: populate)
}
The problem with this code is that if there are two events on the same date, they are separated into different sections with the same title instead of being grouped together into the same section.
As a Swift novice, my instinct is to do something like this:
ForEach(userData.occurrences) { occurrence in
if occurrence.start != self.date {
Section(header: Text("\(occurrence.start, formatter: Self.dateFormatter)")) {
NavigationLink(
destination: OccurrenceDetail(occurrence: occurrence)
.environmentObject(self.userData)
) {
OccurrenceRow(occurrence: occurrence)
}
}
} else {
NavigationLink(
destination: OccurrenceDetail(occurrence: occurrence)
.environmentObject(self.userData)
) {
OccurrenceRow(occurrence: occurrence)
}
}
self.date = occurrence.start
But in Swift, this gives me the error "Unable to infer complex closure return type; add explicit type to disambiguate" because I'm calling arbitrary code (self.date = occurrence.start) inside ForEach{}, which isn't allowed.
What's the correct way to implement this? Is there a more dynamic way to execute this, or do I need to abstract the code outside of ForEach{} somehow?
Edit: The Occurrence object looks like this:
struct Occurrence: Hashable, Codable, Identifiable {
var id: Int
var title: String
var description: String
var location: String
var start: Date
var end: String
var cancelled: Bool
var public_occurrence: Bool
var created: String
var last_updated: String
private enum CodingKeys : String, CodingKey {
case id, title, description, location, start, end, cancelled, public_occurrence = "public", created, last_updated
}
}
Update: The following code got me a dictionary which contains arrays of occurrences keyed by the same date:
let myDict = Dictionary( grouping: value ?? [], by: { occurrence -> String in
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
return dateFormatter.string(from: occurrence.start)
})
self.userData.latestOccurrences = myDict
However, if I try and use this in my View as follows:
ForEach(self.occurrencesByDate) { occurrenceSameDate in
// Section(header: Text("\(occurrenceSameDate[0].start, formatter: Self.dateFormatter)")) {
ForEach(occurrenceSameDate, id: occurrenceSameDate.id){ occurrence in
NavigationLink(
destination: OccurrenceDetail(occurrence: occurrence)
.environmentObject(self.userData)
) {
OccurrenceRow(occurrence: occurrence)
}
}
// }
}
(Section stuff commented out while I get the main bit working)
I get this error: Cannot convert value of type '_.Element' to expected argument type 'Occurrence'
In reference to my comment on your question, the data should be put into sections before being displayed.
The idea would be to have an array of objects, where each object contains an array of occurrences. So we simplify your occurrence object (for this example) and create the following:
struct Occurrence: Identifiable {
let id = UUID()
let start: Date
let title: String
}
Next we need an object to represent all the occurrences that occur on a given day. We'll call it a Day
object, however the name is not too important for this example.
struct Day: Identifiable {
let id = UUID()
let title: String
let occurrences: [Occurrence]
let date: Date
}
So what we have to do is take an array of Occurrence
objects and convert them into an array of Day
objects.
I have created a simple struct that performs all the tasks that are needed to make this happen. Obviously you would want to modify this so that it matches the data that you have, but the crux of it is that you will have an array of Day
objects that you can then easily display. I have added comments through the code so that you can clearly see what each thing is doing.
struct EventData {
let sections: [Day]
init() {
// create some events
let first = Occurrence(start: EventData.constructDate(day: 5, month: 5, year: 2019), title: "First Event")
let second = Occurrence(start: EventData.constructDate(day: 5, month: 5, year: 2019, hour: 10), title: "Second Event")
let third = Occurrence(start: EventData.constructDate(day: 5, month: 6, year: 2019), title: "Third Event")
// Create an array of the occurrence objects and then sort them
// this makes sure that they are in ascending date order
let events = [third, first, second].sorted { $0.start < $1.start }
// create a DateFormatter
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
// We use the Dictionary(grouping:) function so that all the events are
// group together, one downside of this is that the Dictionary keys may
// not be in order that we require, but we can fix that
let grouped = Dictionary(grouping: events) { (occurrence: Occurrence) -> String in
dateFormatter.string(from: occurrence.start)
}
// We now map over the dictionary and create our Day objects
// making sure to sort them on the date of the first object in the occurrences array
// You may want a protection for the date value but it would be
// unlikely that the occurrences array would be empty (but you never know)
// Then we want to sort them so that they are in the correct order
self.sections = grouped.map { day -> Day in
Day(title: day.key, occurrences: day.value, date: day.value[0].start)
}.sorted { $0.date < $1.date }
}
/// This is a helper function to quickly create dates so that this code will work. You probably don't need this in your code.
static func constructDate(day: Int, month: Int, year: Int, hour: Int = 0, minute: Int = 0) -> Date {
var dateComponents = DateComponents()
dateComponents.year = year
dateComponents.month = month
dateComponents.day = day
dateComponents.timeZone = TimeZone(abbreviation: "GMT")
dateComponents.hour = hour
dateComponents.minute = minute
// Create date from components
let userCalendar = Calendar.current // user calendar
let someDateTime = userCalendar.date(from: dateComponents)
return someDateTime!
}
}
This then allows the ContentView
to simply be two nested ForEach
.
struct ContentView: View {
// this mocks your data
let events = EventData()
var body: some View {
NavigationView {
List {
ForEach(events.sections) { section in
Section(header: Text(section.title)) {
ForEach(section.occurrences) { occurrence in
NavigationLink(destination: OccurrenceDetail(occurrence: occurrence)) {
OccurrenceRow(occurrence: occurrence)
}
}
}
}
}.navigationBarTitle(Text("Events"))
}
}
}
// These are sample views so that the code will work
struct OccurrenceDetail: View {
let occurrence: Occurrence
var body: some View {
Text(occurrence.title)
}
}
struct OccurrenceRow: View {
let occurrence: Occurrence
var body: some View {
Text(occurrence.title)
}
}
This is the end result.
This is actually two questions.
In the data part, please upgrade userData.occurrences
from [Occurrence] to [[
Occurrence
]] (I called it latestOccurrences
, here)
var self.userData.latestOccurrences = Dictionary(grouping: userData.occurrences) { (occurrence: Occurrence) -> String in
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
return dateFormatter.string(from: occurrence.start)
}.values
In the swiftUI, you just reorganize the latest data:
NavigationView {
List {
ForEach(userData.latestOccurrences, id:\.self) { occurrenceSameDate in
Section(header: Text("\(occurrenceSameDate[0].start, formatter: DateFormatter.init())")) {
ForEach(occurrenceSameDate){ occurrence in
NavigationLink(
destination: OccurrenceDetail(occurrence: occurrence)
.environmentObject(self.userData)
) {
OccurrenceRow(occurrence: occurrence)
}
}
}
}
}
.navigationBarTitle(Text("Events"))
}.onAppear(perform: populate)
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