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

Time:09-17

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

CodePudding user response:

In response to @workingdog support Ukraine in the comments just before.

I have no errors in my code but with this array :

class FlightLibrary: ObservableObject{
       
    @Published var testFlight = [
        Flight(date: Date(), depPlace: "LFLI", arrPlace: "LFLP"),
        Flight(date: Date(), depPlace: "LFLP", arrPlace: "LFLB"),
        Flight(date: Date(), depPlace: "LFLB", arrPlace: "LFLG")      
    ]
    
}

This return something like :

Section Header 1 : Today Date 
   Item 1 :testFlight[0]
   Item 2 :testFlight[0]
   Item 3 :testFlight[0]
Section Header 2 : Today Date 
   Item 1 :testFlight[0]
   Item 2 :testFlight[0]
   Item 3 :testFlight[0]

When I append a value into testFlight, via a page in the app where user could fill some textFields to set date, dePlace and arrPlace, then dismiss my page the scrollview is not updated correctly. I've my new item but I have no Section Header ...

My add button code in the other view :

@Environment(\.dismiss) private var dismiss
@ObservedObject var flightLibrary: FlightLibrary
@State var flightConfig = NewFlightConfig()
.navigationBarItems(trailing: Button(action: {
    let newFlight = Flight(config: flightConfig)
    flightLibrary.testFlight.append(newFlight)
    dismiss()
     }, label: {
          Text("Save")
    }))

To update the scrollview I go in an other page and back (to reload the .onAppear) to the page where I have my scrollview and now it works with all section header but !

If there is only one date section header is correct but if there is two or more item with the same date it create a new section header for each item but it add all the item with the same date in each section ...

Exemple :

Item1 = Date 1 
Item2 = Date 1
Item3 = Date 2
Item4 = Date 1 

result of : 

    Section Header 1 : Date1 
       Item 1 
       Item 2 
       Item 4 
    Section Header 2 : Date1 
       Item 1 
       Item 2 
       Item 4 
    Section Header 3 : Date1 
       Item 1 
       Item 2 
       Item 4 
    Section Header 4 : Date2 
       Item 3

CodePudding user response:

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)
        }
    }
}
  • Related