Search code examples
arrayslistsortingswiftuigrouping

SwiftUI - Sorting a structured array and show date item into a Section Header


I'm trying to show some section Header which is based on data in my structured array.

When I add a value in my array from my app I ask to enter a date. This one is save as a String cause I don't know how to save it differently from a Textfield..

So in my array I've got an enter "date" formatting like : DD/MM/YYYY

Now I need to have a view where I list all the array Items sorting by date, where the most recent date is show on the top of the screen and more the user scroll down more the date is far in a past.

So my structured array is defined like that :

struct Flight: Identifiable{
    let id = UUID().uuidString
    
    let date: String
    
    let depPlace: String
    let arrPlace: String

    
    init (date: String, depPlace: String, arrPlace: String){
        self.date = date
        
        self.depPlace = depPlace
        self.arrPlace = arrPlace

    }
    
    init(config: NewFlightConfig){
        self.date = config.date
        
        self.depPlace = config.depPlace
        self.arrPlace = config.arrPlace
        
    }
    
}

and NewFlightConfig :

struct NewFlightConfig{

    var date: String = ""

    var depPlace: String = ""
    var arrPlace: String = ""

}

The TextField where I ask for the date :

TextField("DD/MM/YYYY", text: $flightConfig.date)
     .padding()
     .background(.white)
     .cornerRadius(20.0)
     .keyboardType(.decimalPad)
     .onReceive(Just(flightConfig.date)) { inputValue in
               if inputValue.count > 10 {
                       self.flightConfig.date.removeLast()
               }else if inputValue.count == 2{
                       self.flightConfig.date.append("/")
               }else if inputValue.count == 5{
                       self.flightConfig.date.append("/")
               }
      }

Finally my Homepage with my list which is defined as follow :

ScrollView{
    VStack {
          ForEach(flightLibrary.testFlight) {date in
             Section(header: Text(date.date).font(.title2).fontWeight(.semibold)) {
                     ForEach(flightLibrary.testFlight) {flight in
                         ZStack {
                             RoundedRectangle(cornerRadius: 16, style: .continuous)
                                  .fill(Color.white)
                                  .shadow(color: Color(Color.RGBColorSpace.sRGB, white: 0, opacity: 0.2), radius: 4)
                                  LogbookCellView(flight: flight)                         
                                    }
                                }
                            }
                        }
                    }.padding(.horizontal, 16)
                }

Where I've trying to deal with Dictionary to fill the SectionHeader Text but seems to didn't work...

var entryCollatedByDate: [String : [Flight]] {
        Dictionary(grouping: flightLibrary, by: { $0.date })
     }

I'm not very familiar with how to sorted data and grouped data into arrays.

My final objectif is to have something like that :

Section Header 1 -> 15/09/2022 
        Array Items 1 -> last items with same Header date 
        Array Items 2 -> last items with same Header date 
Section Header 2 -> 14/09/2022 
        Array Items 3 -> last items with same Header date 
        Array Items 4 -> last items with same Header date 
        Array Items 5 -> last items with same Header date
[...]
Section Header N -> DD/MM/YYYY
        Array Items N -> last items with same Header date

Hope to be clear about my problem

Thanks for your help


Solution

  • You could try this approach, where a function func asDate(...) is used to transform your String date to a Date on the fly. Then using Set and map, to get unique dates for the sections. These unique dates are sorted using the func asDate(...).

    struct ContentView: View {
        @State var flightLibrary = [Flight(date: "14/09/2022", depPlace: "depPlace-1", arrPlace: "arrPlace-1"),
                                    Flight(date: "15/09/2022", depPlace: "depPlace-2", arrPlace: "arrPlace-2"),
                                    Flight(date: "12/09/2022", depPlace: "depPlace-3", arrPlace: "arrPlace-3"),
                                    Flight(date: "14/09/2022", depPlace: "depPlace-1.2", arrPlace: "arrPlace-1.2")]
      
        func asDate(_ str: String) -> Date {
            let dateFormatter = DateFormatter()
            dateFormatter.dateFormat = "dd/MM/yyyy"
            return dateFormatter.date(from: str) ?? Date()
        }
    
        @State var uniqueDates = [String]()
        
        var body: some View {
            ScrollView{
                VStack {
                    ForEach(uniqueDates, id: \.self) { date in
                        Section(header: Text(date).font(.title2).fontWeight(.semibold)) {
                            ForEach(flightLibrary.filter({$0.date == date})) { flight in
                                ZStack {
                                    RoundedRectangle(cornerRadius: 16, style: .continuous)
                                        .fill(Color.white)
                                        .shadow(color: Color(Color.RGBColorSpace.sRGB, white: 0, opacity: 0.2), radius: 4)
                                    Text(flight.arrPlace)
                                }
                            }
                        }
                    }
                }.padding(.horizontal, 16)
            }
            .onAppear {
                // unique and sorted dates
                uniqueDates = Array(Set(flightLibrary.map{$0.date})).sorted(by: {asDate($0) > asDate($1)})
            }
        }
    }
    

    An alternative approach is to change the String date in Flight to type Date. Then using Set, map and sorted, to get unique and sorted dates for the sections.

    struct ContentView: View {
        @State var flightLibrary = [Flight]()
        @State var uniqueDates = [Date]()
        let frmt = DateFormatter()
        
        var body: some View {
            ScrollView{
                VStack {
                    ForEach(uniqueDates, id: \.self) { date in
                        Section(header: Text(frmt.string(from: date)).font(.title2).fontWeight(.semibold)) {
                          ForEach(flightLibrary.filter({Calendar.current.isDate($0.date, inSameDayAs: date)})) { flight in
                                ZStack {
                                    RoundedRectangle(cornerRadius: 16, style: .continuous)
                                        .fill(Color.white)
                                        .shadow(color: Color(Color.RGBColorSpace.sRGB, white: 0, opacity: 0.2), radius: 4)
                                    Text(flight.arrPlace)
                                }
                            }
                        }
                    }
                }.padding(.horizontal, 16)
            }
            .onAppear {
                frmt.dateFormat = "dd/MM/yyyy"
                frmt.timeZone = TimeZone(identifier: "UTC")
    
                // for testing only
                flightLibrary = [Flight(date: frmt.date(from: "14/09/2022")!, depPlace: "depPlace-1", arrPlace: "arrPlace-1"),
                                 Flight(date: frmt.date(from: "15/09/2022")!, depPlace: "depPlace-2", arrPlace: "arrPlace-2"),
                                 Flight(date: frmt.date(from: "12/09/2022")!, depPlace: "depPlace-3", arrPlace: "arrPlace-3"),
                                 Flight(date: frmt.date(from: "14/09/2022")!, depPlace: "depPlace-1.2", arrPlace: "arrPlace-1.2")]
                
                // unique and sorted dates
                uniqueDates = Array(Set(flightLibrary.map{$0.date})).sorted(by: {$0 > $1})
            }
        }
    }
     
     struct Flight: Identifiable, Hashable {
         let id = UUID().uuidString
         let date: Date // <-- here
         let depPlace: String
         let arrPlace: String
    
         init (date: Date, depPlace: String, arrPlace: String){ // <-- here
             self.date = date
             self.depPlace = depPlace
             self.arrPlace = arrPlace
         }
    
         init(config: NewFlightConfig) { 
             self.date = config.date
             self.depPlace = config.depPlace
             self.arrPlace = config.arrPlace
         }
     }
    
     struct NewFlightConfig {
         var date: Date = Date()  // <-- here
         var depPlace: String = ""
         var arrPlace: String = ""
     }
     
    

    EDIT-1: here is another approach that uses a class FlightModel: ObservableObject to hold your data and update the UI whenever flights is changed. It also has a convenience computed property for theuniqueDates. So in your addView, pass the flightModel to it (e.g @EnvironmentObject) and add new Flight to the flightModel.

    class FlightModel: ObservableObject {
        @Published var flights = [Flight]()
    
    var uniqueDates: [Date] {
        let arr = flights.compactMap{frmt.date(from: frmt.string(from: $0.date))}
        return Array(Set(arr.map{$0})).sorted(by: {$0 > $1})
    }
        
        let frmt = DateFormatter()
        
        init() {
            frmt.dateFormat = "dd/MM/yyyy"
            frmt.timeZone = TimeZone(identifier: "UTC")
            getData()
        }
        
        func getData() {
            // for testing only
        flights = [
            Flight(date: Date(), depPlace: "LFLI", arrPlace: "LFLP"),
            Flight(date: Date(), depPlace: "LFLP", arrPlace: "LFLB"),
            Flight(date: Date(), depPlace: "LFLB", arrPlace: "LFLG"),
            
            Flight(date: frmt.date(from: "14/09/2022")!, depPlace: "depPlace-1", arrPlace: "arrPlace-1"),
            Flight(date: frmt.date(from: "15/09/2022")!, depPlace: "depPlace-2", arrPlace: "arrPlace-2"),
            Flight(date: frmt.date(from: "12/09/2022")!, depPlace: "depPlace-3", arrPlace: "arrPlace-3"),
            Flight(date: frmt.date(from: "14/09/2022")!, depPlace: "depPlace-1.2", arrPlace: "arrPlace-1.2")
        ]
        }
    }
    
    struct ContentView: View {
        @StateObject var flightModel = FlightModel()
    
        var body: some View {
            ScrollView {
                VStack {
                    ForEach(flightModel.uniqueDates, id: \.self) { date in
                        Section(header: Text(flightModel.frmt.string(from: date)).font(.title2).fontWeight(.semibold)) {
                            ForEach(flightModel.flights.filter({Calendar.current.isDate($0.date, inSameDayAs: date)})) { flight in
                                ZStack {
                                    RoundedRectangle(cornerRadius: 16, style: .continuous)
                                        .fill(Color.white)
                                        .shadow(color: Color(Color.RGBColorSpace.sRGB, white: 0, opacity: 0.2), radius: 4)
                                    Text(flight.arrPlace)
                                }
                            }
                        }
                    }
                }.padding(.horizontal, 16)
            }
        }
    }