I'm attempting to build a todo app using SwiftUI + Swift Data. I've created a model like this:
@Model
final class toDoItem: Identifiable {
var title: String = ""
var emoji: String = ""
var notes: String = ""
var toDoColor: String = ""
var toDoLocation: String = ""
var timeStamp: Date = Date.now
var isCompleted: Bool = false
var isCritical: Bool = false
var isOverdue: Bool = false
//Saving the image somewhere else and loading only the reference in the Model
@Attribute(.externalStorage)
var image: Data?
//Creating a relationship between a category and a todo.
@Relationship(deleteRule:.nullify, inverse: \Category.items)
var category: Category?
//initializing all vars
init(title: String = "", emoji: String = "", notes: String = "", toDoColor: String = "", toDoLocation: String = "", timeStamp: Date = .now, isCompleted: Bool = false, isCritical: Bool = false, isOverdue: Bool = false) {
self.title = title
self.emoji = emoji
self.notes = notes
self.toDoColor = toDoColor
self.toDoLocation = toDoLocation
self.timeStamp = timeStamp
self.isCompleted = isCompleted
self.isCritical = isCritical
self.isOverdue = isOverdue
}
}
I want to create a list to display all the todos with a section for each item's timeStamp.
For example, if I have four todos with two of them having Sep 19th 2024 as the timeStamp and the other two having Sep 21st 2024, I want a list with two sections: one with Sep 19th 2024 as the header with the relative two todo items, and another one with Sep 21st 2024 containing the other two todo items.
I also have a search bar and need to filter the todos according to the user's search. This is the code for my list view:
struct MainView: View {
@Environment(\.modelContext) var context
@Query private var items: [toDoItem]
@State private var searchQuery = ""
@State private var selectedSortOptions = SortOptions.allCases.first!
var filteredItems: [toDoItem] {
if searchQuery.isEmpty{
return items.sort(on: selectedSortOptions)
}
let filteredItems = items.compactMap{ item in
//Search for content in todo Title
let titleContainsQuery = item.title.range(of: searchQuery, options: .caseInsensitive) != nil
//Search for content in todo Emoji
let emojiContainsQuery = item.emoji.range(of: searchQuery, options: .caseInsensitive) != nil
//Search for content in todo Category Title
let categoryContainsQuery = item.category?.catTitle.range(of: searchQuery, options: .caseInsensitive) != nil
return (titleContainsQuery || categoryContainsQuery || emojiContainsQuery) ? item : nil
}
return filteredItems.sort(on: selectedSortOptions)
}
//MAIN VIEW Content
var body: some View {
NavigationStack{
List{
ForEach(filteredItems) { item in
ForEach(item.timeStamp) { day in
Section{
...
}
}
}
}
}
}
}
I'm having an issue with the second ForEach loop, which contains a filter for each timeStamp. I'm getting the error "Generic struct 'ForEach' requires that 'Date' conform to 'RandomAccessCollection'". What am I doing wrong? I'm fairly new to coding and SwiftUI and I feel like I'm overlooking something. Thank you.
I managed to find the solution after multiple attempts. To display my toDoItem list with sections grouped by timeStamp, I realized that I needed to first group my items by the day component of their timeStamp. This can be achieved by creating a dictionary, where each key is a Date representing a specific day, and the corresponding value is an array of toDoItem objects associated with that day:
// Filtered and grouped items
var filteredItems: [Date: [toDoItem]] {
let filteredItems: [toDoItem]
if searchQuery.isEmpty {
filteredItems = items
} else {
filteredItems = items.filter { item in
let titleContainsQuery = item.title.range(of: searchQuery, options: .caseInsensitive) != nil
let emojiContainsQuery = item.emoji.range(of: searchQuery, options: .caseInsensitive) != nil
let categoryContainsQuery = item.category?.catTitle.range(of: searchQuery, options: .caseInsensitive) != nil
return titleContainsQuery || emojiContainsQuery || categoryContainsQuery
}
}
// Here's where the magic happens: creating the dictionary!
let groupedItems = Dictionary(grouping: filteredItems) { item in
Calendar.current.startOfDay(for: item.timeStamp)
}
return groupedItems
}
Finally, this is the updated "ForEach":
// Iterate over the grouped items to create sections
ForEach(filteredItems.keys.sorted(), id: \.self) { date in
Section(header: Text(date, style: .date)) {
ForEach(filteredItems[date] ?? []) { item in
...
}
}
}
Key Changes and Explanations:
1. Grouping the Items:
• Calendar.current.startOfDay(for: item.timeStamp) extracts the date component (ignoring the time) to group items by their day.
2. Using ForEach with Grouped Data:
• filteredItems.keys.sorted() sorts the dates for the section headers.
• Each section is created using a Section view, with the header being the date and the content being the filtered toDoItem items for that date.
3. Display Items within Each Section:
• Inside each section, another ForEach loop iterates over the toDoItem instances corresponding to that specific day.
Search Functionality:
The search functionality filters the items as required, and the results are grouped by date before being displayed in the list.
This structure fixed my issue and provided a tidy way to display to-do item entries grouped by their timeStamp into sections.