Sorting of Date objects in ForEach loop

Hi all, I am a complete newbie here. Probably an easy fix but I cannot get this to work:

I am trying to sort an array of Time objects so that in the UI the subject that happens first appears first. Here is the code:

import SwiftUI
import SwiftData


@available(iOS 18.0, *)
struct SubjectView: View {
    @Environment(\.modelContext) private var context
    @Query private var subjects: [Subject]
    @State private var showSheet = false
    private var daysOfWeek = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
    @State private var selectedDay: String = ""
    @AppStorage("showPreviousEvents") var showPreviousSubjects: Bool = true
    @State private var skillsPresented = false
    @State private var selectedTest: Test?
    var body: some View {
        ScrollViewReader { pos in
            NavigationStack {
                VStack(spacing: 0) {
                    ScrollView(.horizontal, showsIndicators: false) {
                        HStack {
                            ForEach(daysOfWeek, id: \.self) { day in
                                Button {
                                    selectedDay = day
                                    withAnimation {
                                        pos.scrollTo(day, anchor: .center)
                                    }
                                } label: {
                                    Text(day)
                                        .font(.largeTitle)
                                        .fontWeight(.semibold)
                                        .foregroundColor(selectedDay == day ? .black : .gray)
                                        .padding(.horizontal, 3)
                                        .id(day)
                                }
                            }
                        }
                        .padding()
                    }
                    
                    Divider()
                    
                    List(subjects) { subject in
                        
                        ForEach(
                            subject.day
                                
                                .filter { $0.dayName == selectedDay }
                                
                                .filter { showPreviousSubjects ? combineDateAndTime(date: Date(), time: $0.endTime) > Date() : true }
                                
                        ) { time in
                            ZStack {
                                NavigationLink(destination: {
                                    UpdatedEditView(subject: subject)
                                }, label: {
                                    EmptyView()
                                })
                                ZStack {
                                    RoundedRectangle(cornerRadius: 10)
                                        .foregroundColor(Color(hex: subject.colour))
                                        .frame(height: 80)
                                    HStack {
                                        VStack(alignment: .leading, spacing: 3) {
                                            Text(subject.title)
                                            
                                                .bold().font(.title)
                                            
                                            HStack {
                                                Image(systemName: "clock")
                                                Text("\(shortTime(time.startTime)) - \(shortTime(time.endTime))")
                                            }
                                            Spacer()
                                        }
                                        
                                        Spacer()
                                        VStack {
                                            HStack (spacing: 3){
                                                Image(systemName: "mappin.and.ellipse")
                                                Text(subject.classroom)
                                                
                                            }.padding(.trailing).padding(.top, 8)
                                            Spacer()
                                        }
                                    }.foregroundColor(.white)
                                        .padding(.top)
                                        .padding(.leading, 10.0)
                                }
                                .swipeActions {
                                    Button(role: .destructive) {
                                        for time in subject.day {
                                            EventHelper().deleteEvent(time.identifier ?? "")
                                        }
                                        context.delete(subject)
                                    } label: {
                                        Image(systemName: "trash")
                                    }
                                }
                            }
                        }
                        .listRowSeparator(.hidden).listRowBackground(Color.clear)
                    }
                    .listStyle(.plain)
                    .sheet(isPresented: $showSheet) {
                        AddSubjectView()
                    }
                    .background { Color(.systemGray6) }
                }.toolbar {
                    ToolbarItem(placement: .topBarTrailing) {
                        Button("Add Subject", systemImage: "plus") {
                            showSheet.toggle()
                            
                            
                            
                        }
                    }
                    ToolbarItem(placement: .topBarLeading) {
                        NavigationLink {
                            List(subjects) { subject in
                                ForEach(subject.tests.sorted { $0.date > $1.date }) { test in
                                    Button {
                                        selectedTest = test
                                    } label: {
                                        ZStack {
                                            RoundedRectangle(cornerRadius: 10).frame(height: 80).foregroundColor(Color(hex: subject.colour))
                                            
                                                HStack {
                                                    VStack (alignment: .leading, spacing: 0){
                                                        Text(test.name).font(.title2).bold().padding([.leading, .top]).padding(.bottom, 10)
                                                        HStack (spacing: 3){
                                                            Image(systemName: "calendar.circle")
                                                            Text(formattedDate(test.date))
                                                            
                                                        }.padding(.leading)
                                                    
                                                    Spacer()
                                                }
                                                Spacer()
  
                                            }
                                        }.foregroundColor(.white)
                                    }.padding(.horizontal, 5).listRowSeparator(.hidden).listRowBackground(Color.clear).swipeActions {
                                        Button(role: .destructive) {
                                            subject.tests.removeAll { $0 == test}
                                            try? context.save()
                                            
                                        } label: {
                                            Image(systemName: "trash")
                                        }
                                    }
                                }
                            }
                                
                            } label: {
                                Image(systemName: "graduationcap")
                            }
                            
                        }
                        
                        
                        
                    }
                }
                .onAppear {
                    
                    let currentDayName = DateFormatter()
                    currentDayName.dateFormat = "EEEE"
                    selectedDay = currentDayName.string(from: Date())
                    withAnimation {
                        pos.scrollTo(selectedDay, anchor: .center)
                    }
                    
                    
                }.ignoresSafeArea().sheet(item: $selectedTest) { test in
                    TestEditView(selectedTest: test)
                }
            }
        }
    }


Definitions of Time and Subject:

import Foundation
import SwiftData
import SwiftUI
import UIKit

@available(iOS 17, *)

@available(iOS 17, *)
@Model
class Item: Identifiable {
    var name: String = ""
    init(name: String) {
        self.name = name
        
    }
    
}


@available(iOS 17, *)
@Model
class Subject: Identifiable {
    var id = UUID()
    var title: String
    var classroom: String
    var teacherName: String
    var items: [Item]?
    var day: [Time]
    var colour: String
    var eOrS: Int = 1
    var tests: [Test] = []
    
    init(subjectName: String, classroom: String, teacherName: String, items: [Item]?, day: [Time], colour: String, eOrS: Int, tests: [Test]) {
        self.title = subjectName
        self.classroom = classroom
        self.teacherName = teacherName
        self.items = items
        self.day = day
        self.colour = colour
        self.eOrS = eOrS
        self.tests = tests
    }
}
@available(iOS 17, *)
@Model
class Time: Identifiable {
    var dayName: String
    var startTime: Date
    var endTime: Date
    var identifier: String?
    init(dayName: String, startTime: Date, endTime: Date) {
        self.dayName = dayName
        self.startTime = startTime
        self.endTime = endTime
        
    }
}



@available(iOS 17, *)
@Model
class Term: Identifiable {
    var id = UUID()
    var name: String
    var startDate: Date
    var endDate: Date
    
    init(name: String, startDate: Date, endDate: Date) {
        self.name = name
        self.startDate = startDate
        self.endDate = endDate
    }
}

@available(iOS 17, *)
@Model
class Test: Identifiable {
    var name: String
    var date: Date
    var skills: [String]
    
    init(name: String, date: Date, skills: [String]) {
        self.name = name
        self.date = date
        self.skills = skills
    }
}

You will see a list with an embedded foreach loop, I need to sort that foreach loop by time.startTime. I cannot figure it out. Help is very much appreciated.

Thank you.

It is an easy fix, but you posted too much code. Please ideally whittle it down to a minimal reproducible example, and if you don't know enough to do that, post enough code for it to compile, instead.

Here is some simplified code.

MyApp:

import SwiftUI
import SwiftData
@available(iOS 18, *)
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }.modelContainer(for: [Subject.self])

    }
}

Here is the problematic code. You will just have to populate a SwiftData database. I think it is related to the fact that the date object is stored in a database.

import SwiftUI
import SwiftData


@available(iOS 18.0, *)

struct SwiftUIView: View {
    @Query private var subjects: [Subject]
    
    @State private var selectedDay: String = ""
    var body: some View {
        
        List(subjects) { subject in
            
            ForEach(
                subject.day.sorted { $0.startTime < $1.startTime}
            ) { time in
                
                Text("\(subject.title) - \(time.startTime)")
            }
            
        }
    }
    
}

@available(iOS 17, *)
@Model
class Subject: Identifiable {
    var id = UUID()
    var title: String
    
    var day: [Time]
    
    
    init(id: UUID = UUID(), title: String, day: [Time]) {
        self.id = id
        self.title = title
        self.day = day
    }
}

@available(iOS 17, *)
@Model
class Time: Identifiable {
    var dayName: String
    var startTime: Date
    var endTime: Date
    var identifier: String?
    init(dayName: String, startTime: Date, endTime: Date) {
        self.dayName = dayName
        self.startTime = startTime
        self.endTime = endTime
        
    }
}

#Preview {
    if #available(iOS 18.0, *) {
        SwiftUIView()
    } else {
        // Fallback on earlier versions
    }
}

Thanks so much for your help. Really appreciated!

No, it's still taking too much detective work to figure out what you're asking. Perhaps put a project on GitHub that demonstrates the problem.

There are a few assumptions we're going to have to make in order to have some deterministic order. However, if Subjects always have at least one day then there shouldn't be much to worry about.

Array order isn't preserved with Core Data, so a computed property for the sorted model elements is common (by my observation). If you want sorted subjects by the time as well, you can do the same within the View. Without completely rewriting to match some other practices and doing a full review, I believe what you desire is below.

import SwiftUI
import SwiftData

struct ContentView: View {
    
    @Query
    private var subjects: [Subject]
    
    @State
    private var selectedDay: String = ""
    
    private var sortedSubjects: [Subject] {
        subjects.sorted { s1, s2 in
            // Could refine this, or don't worry if days.count > 0 always
            guard let s1First = s1.sortedDays.first else { return false }
            guard let s2First = s2.sortedDays.first else { return true }
            
            return s1First.startTime < s2First.startTime
        }
    }
    
    var body: some View {
        List(sortedSubjects) { subject in
            Section(subject.title) {
                ForEach(subject.days.sorted { $0.startTime < $1.startTime}) { time in
                    Text("\(subject.title) - \(time.startTime)")
                }
            }
        }
    }
}

@Model
class Subject: Identifiable {
    
    var id = UUID()
    var title: String
    var days: [Time]
    
    // Could also make the above `days` something like `daysStorage`
    // and have `days` have manual `get/set` that will always
    // return sorted `[Time]` and set `daysStorage` accordingly.
    var sortedDays: [Time] {
        days.sorted { $0.startTime < $1.startTime }
    }
    
    init(id: UUID = UUID(), title: String, day: [Time]) {
        self.id = id
        self.title = title
        self.days = day
    }
}

@Model
class Time: Identifiable {
    
    var dayName: String
    var startTime: Date
    var endTime: Date
    var identifier: String?
    
    init(dayName: String, startTime: Date, endTime: Date) {
        self.dayName = dayName
        self.startTime = startTime
        self.endTime = endTime
        
    }
}

#Preview {
    let config = ModelConfiguration(isStoredInMemoryOnly: true)
    let container = try! ModelContainer(for: Subject.self, configurations: config)
    
    // Math will be earlier
    let time = Time(dayName: "Monday", startTime: Date(timeIntervalSinceNow: 0), endTime: Date().addingTimeInterval(3600))
    let time2 = Time(dayName: "Monday", startTime: Date(timeIntervalSinceNow: 3600), endTime: Date().addingTimeInterval(7200))
    
    let subject = Subject(title: "Math", day: [time, time2])
    
    // Science will be later
    let laterTime = Time(dayName: "Monday", startTime: Date(timeIntervalSinceNow: 7200), endTime: Date().addingTimeInterval(10800))
    let laterTime2 = Time(dayName: "Monday", startTime: Date(timeIntervalSinceNow: 10800), endTime: Date().addingTimeInterval(14400))
    
    let laterSubject = Subject(title: "Science", day: [laterTime, laterTime2])
    
    // Show that insertion order does not matter
    container.mainContext.insert(laterSubject)
    container.mainContext.insert(subject)
    
    return ContentView()
        .modelContainer(container)
}

1 Like

Here is a full explanation. I have an array of Subjects. In each Subject there is a day array. The day array consists of Time objects. I want to sort those time objects by their startTime. startTime is a Date. For some reason, the sorting of those Dates is not working in the ForEach loop using this code:subject.day.sorted { $0.startTime < $1.startTime}

Hi all, so sorry to have used up your time, I found a fix. It was just me being stupid. I was sorting the subject.day instead of sorting subjects by subject.day

Here was the solution:

subjects.sorted {
    let firstTimeA = $0.day
       .filter { $0.dayName == selectedDay }
       .min(by: { $0.startTime < $1.startTime })?.startTime
    let firstTimeB = $1.day
       .filter { $0.dayName == selectedDay }
       .min(by: { $0.startTime < $1.startTime })?.startTime

                        return firstTimeA ?? Date.distantFuture < firstTimeB ?? Date.distantFuture
                    }

Thanks to @Pippin for helping me see my error.

1 Like

Note that this is really ineffective code in the UI: array has to be sorted each time view is being rendered. Your code lacks some type that will represent this logic, like keeping sorted list of subjects, selection and edition. This will also help by reducing the view complexity.

If that's an output of SwiftData, you can achieve sorted result particularly easy: just use sorting descriptor.

2 Likes