Как динамически создавать разделы в SwiftUI List / ForEach и избежать «Невозможно определить тип возвращаемого значения сложного замыкания» - PullRequest
1 голос
/ 27 октября 2019

Я пытаюсь воссоздать представление, которое соответствует списку событий в стандартном приложении iOS Calendar. У меня есть следующий код, который генерирует список событий с каждым событием, разделенным на свой раздел по дате:

var body: some View {
    NavigationView {
        List {
            ForEach(userData.occurrences) { occurrence in
                Section(header: Text("\(occurrence.start, formatter: Self.dateFormatter)")) {
                    NavigationLink(
                        destination: OccurrenceDetail(occurrence: occurrence)
                            .environmentObject(self.userData)
                    ) {
                        OccurrenceRow(occurrence: occurrence)
                    }
                }
            }
        }
        .navigationBarTitle(Text("Events"))
    }.onAppear(perform: populate)
}

Проблема с этим кодом заключается в том, что если в одну и ту же дату есть два события, ониразделенные на разные разделы с одинаковым названием вместо того, чтобы группироваться в один и тот же раздел.

Как новичок в Swift, мой инстинкт должен сделать что-то вроде этого:

ForEach(userData.occurrences) { occurrence in
                if occurrence.start != self.date {
                    Section(header: Text("\(occurrence.start, formatter: Self.dateFormatter)")) {
                        NavigationLink(
                            destination: OccurrenceDetail(occurrence: occurrence)
                                .environmentObject(self.userData)
                        ) {
                            OccurrenceRow(occurrence: occurrence)
                        }
                    }
                } else {
                    NavigationLink(
                        destination: OccurrenceDetail(occurrence: occurrence)
                            .environmentObject(self.userData)
                    ) {
                        OccurrenceRow(occurrence: occurrence)
                    }
                }
                self.date = occurrence.start

Но в Swift, это дает мне ошибку «Невозможно определить сложный тип возврата замыкания; добавить явный тип для устранения неоднозначности», потому что я вызываю произвольный код (self.date = adventrence.start) внутри ForEach {}, что недопустимо.

Как правильно это реализовать? Есть ли более динамичный способ выполнить это, или мне нужно каким-то образом абстрагировать код вне ForEach {}?

Редактировать: Объект Occurrence выглядит следующим образом:

struct Occurrence: Hashable, Codable, Identifiable {
    var id: Int
    var title: String
    var description: String
    var location: String
    var start: Date
    var end: String
    var cancelled: Bool
    var public_occurrence: Bool
    var created: String
    var last_updated: String

    private enum CodingKeys : String, CodingKey {
        case id, title, description, location, start, end, cancelled, public_occurrence = "public", created, last_updated
    }
}

Обновление: Следующий код получил мне словарь, который содержит массивы вхождений, обозначенных одной и той же датой:

let myDict = Dictionary( grouping: value ?? [], by: { occurrence -> String in
                            let dateFormatter = DateFormatter()
                            dateFormatter.dateStyle = .medium
                            dateFormatter.timeStyle = .none
                            return dateFormatter.string(from: occurrence.start)
                        })
            self.userData.latestOccurrences = myDict

Однако, если я попытаюсь использовать это в моем представлении следующим образом:

ForEach(self.occurrencesByDate) { occurrenceSameDate in
//                    Section(header: Text("\(occurrenceSameDate[0].start, formatter: Self.dateFormatter)")) {
                    ForEach(occurrenceSameDate, id: occurrenceSameDate.id){ occurrence in
                        NavigationLink(
                            destination: OccurrenceDetail(occurrence: occurrence)
                                .environmentObject(self.userData)
                        ) {
                            OccurrenceRow(occurrence: occurrence)
                            }
                        }
//                    }
                }

(Раздел закомментирован, пока я работаю с основным битом)

Я получаю эту ошибку: Невозможно преобразовать значение типа '_.Element' в ожидаемый тип аргумента 'Вхождение'

Ответы [ 2 ]

2 голосов
/ 28 октября 2019

Что касается моего комментария к вашему вопросу, данные должны быть разбиты на разделы перед отображением.

Идея состоит в том, чтобы иметь массив объектов, где каждый объект содержит массив вхождений. Поэтому мы упростим ваш объект вхождения (для этого примера) и создадим следующее:

struct Occurrence: Identifiable {
    let id = UUID()
    let start: Date
    let title: String
}

Далее нам нужен объект для представления всех вхождений, которые происходят в данный день. Мы назовем его Day объектом, однако имя не слишком важно для этого примера.

struct Day: Identifiable {
    let id = UUID()
    let title: String
    let occurrences: [Occurrence]
    let date: Date
}

Итак, нам нужно взять массив из Occurrence объектов и преобразовать их в массив из Day объектов.

Я создал простую структуру, которая выполняет все задачи, необходимые для этого. Очевидно, что вы захотите изменить это так, чтобы оно соответствовало имеющимся у вас данным, но суть в том, что у вас будет массив Day объектов, которые вы затем сможете легко отобразить. Я добавил комментарии через код, чтобы вы могли четко видеть, что делает каждая вещь.

struct EventData {
    let sections: [Day]

    init() {
        // create some events
        let first = Occurrence(start: EventData.constructDate(day: 5, month: 5, year: 2019), title: "First Event")
        let second = Occurrence(start: EventData.constructDate(day: 5, month: 5, year: 2019, hour: 10), title: "Second Event")
        let third = Occurrence(start: EventData.constructDate(day: 5, month: 6, year: 2019), title: "Third Event")

        // Create an array of the occurrence objects and then sort them
        // this makes sure that they are in ascending date order
        let events = [third, first, second].sorted { $0.start < $1.start }

        // create a DateFormatter 
        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .medium
        dateFormatter.timeStyle = .none

        // We use the Dictionary(grouping:) function so that all the events are 
        // group together, one downside of this is that the Dictionary keys may 
        // not be in order that we require, but we can fix that
        let grouped = Dictionary(grouping: events) { (occurrence: Occurrence) -> String in
            dateFormatter.string(from: occurrence.start)
        }

        // We now map over the dictionary and create our Day objects
        // making sure to sort them on the date of the first object in the occurrences array
        // You may want a protection for the date value but it would be 
        // unlikely that the occurrences array would be empty (but you never know)
        // Then we want to sort them so that they are in the correct order
        self.sections = grouped.map { day -> Day in
            Day(title: day.key, occurrences: day.value, date: day.value[0].start)
        }.sorted { $0.date < $1.date }
    }

    /// This is a helper function to quickly create dates so that this code will work. You probably don't need this in your code.
    static func constructDate(day: Int, month: Int, year: Int, hour: Int = 0, minute: Int = 0) -> Date {
        var dateComponents = DateComponents()
        dateComponents.year = year
        dateComponents.month = month
        dateComponents.day = day
        dateComponents.timeZone = TimeZone(abbreviation: "GMT")
        dateComponents.hour = hour
        dateComponents.minute = minute

        // Create date from components
        let userCalendar = Calendar.current // user calendar
        let someDateTime = userCalendar.date(from: dateComponents)
        return someDateTime!
    }

}

Это позволяет ContentView быть просто двумя вложенными ForEach.

struct ContentView: View {

    // this mocks your data
    let events = EventData()

    var body: some View {
        NavigationView {
            List {
                ForEach(events.sections) { section in
                    Section(header: Text(section.title)) {
                        ForEach(section.occurrences) { occurrence in
                            NavigationLink(destination: OccurrenceDetail(occurrence: occurrence)) {
                                OccurrenceRow(occurrence: occurrence)
                            }
                        }
                    }
                }
            }.navigationBarTitle(Text("Events"))
        }
    }
}

// These are sample views so that the code will work
struct OccurrenceDetail: View {
    let occurrence: Occurrence

    var body: some View {
        Text(occurrence.title)
    }
}

struct OccurrenceRow: View {
    let occurrence: Occurrence

    var body: some View {
        Text(occurrence.title)
    }
}

Это конечный результат.

Nested Views

1 голос
/ 27 октября 2019

Это на самом деле два вопроса.

В части данных, пожалуйста, обновите userData.occurrences с [Вхождения] до [[Вхождения]] (я назвал его latestOccurrences, здесь)

   var self.userData.latestOccurrences = Dictionary(grouping: userData.occurrences) { (occurrence: Occurrence) -> String in
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
return dateFormatter.string(from:  occurrence.start)
 }.values

В swiftUI выпросто реорганизовать последние данные:

    NavigationView {
    List {
        ForEach(userData.latestOccurrences, id:\.self) { occurrenceSameDate in
            Section(header: Text("\(occurrenceSameDate[0].start, formatter: DateFormatter.init())")) {
                ForEach(occurrenceSameDate){ occurrence in
                NavigationLink(
                    destination: OccurrenceDetail(occurrence: occurrence)
                        .environmentObject(self.userData)
                ) {
                    OccurrenceRow(occurrence: occurrence)
                    }
                }
            }
        }
    }
    .navigationBarTitle(Text("Events"))
}.onAppear(perform: populate)
...