swiftuiswiftdata

SwiftUI - ForEach model class variable


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.


Solution

  • 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.