Как динамически создавать разделы в SwiftUI List / ForEach и избегать невозможности определить тип возвращаемого значения сложного закрытия

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

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 = instance.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 в ожидаемый тип аргумента "Происшествие".


person Spielo    schedule 26.10.2019    source источник
comment
Не могли бы вы включить свое определение возникновения?   -  person LuLuGaGa    schedule 27.10.2019
comment
Вероятно, вы захотите переформатировать данные, которые вы передаете в ForEach, чтобы они уже были разделены по дате, прежде чем пытаться их отобразить.   -  person Andrew    schedule 27.10.2019
comment
@LuLuGaGa, обновлено происшествием. Это простой объект, который заполняется данными JSON с помощью codable.   -  person Spielo    schedule 27.10.2019
comment
@ Андрей, у тебя есть пример? Я пробовал это раньше, создавая функции, возвращающие объекты NavigationLink, но у меня не получалось заставить их работать.   -  person Spielo    schedule 27.10.2019


Ответы (2)


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

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

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

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

«Вложенные

person Andrew    schedule 27.10.2019
comment
Спасибо, что сделали это так легко! Между этим и ответом E.Com я стал намного лучше понимать передовую практику при работе в Swift и получил что-то, работающее именно так, как я этого хотел. Спасибо еще раз! - person Spielo; 28.10.2019
comment
Как с этим реализовать onDelete? Я пытался добавить его в ForEach, но он портит IndexSet и удаляет не те элементы. - person fasoh; 31.10.2019
comment
@fasoh Вам нужно будет отслеживать индекс раздела и индекс строки, чтобы убедиться, что вы удаляете правильный элемент. В идеале вы хотели бы, чтобы .onDelete находился на самой внутренней стороне ForEach - person Andrew; 01.11.2019
comment
Спасибо, разобрался, как правильно отслеживать индексы, но возник другой вопрос. В этом примере мы получаем вхождения в виде массива, который, насколько я понимаю, должен быть объектом @ObservedObject, используемым для добавления и редактирования элементов. Зная как раздел, так и индекс вхождения, как осуществить фактическое удаление? Насколько я понимаю, я должен удалить из исходного массива вхождений, чтобы правильно обновить и распространить разделы в представление. - person fasoh; 01.11.2019
comment
@fasoh В этом примере созданный мной объект данных должен был дать рабочий пример, он не имеет особого отношения к вопросу, кроме того, как взять список элементов и преобразовать их в сгруппированный список. Использовать ли наблюдаемый объект или нет, не было особенно важно в то время, поскольку исходный плакат с вопросом уже имел источник данных. - person Andrew; 02.11.2019
comment
@fasoh Понятно, что у вас есть еще вопросы. Я бы предложил записать соответствующие части в новый вопрос, чтобы вы могли четко выразить все, что вам нужно сделать, поскольку комментарии к SO на самом деле не позволяют провести подробное обсуждение. Во-первых, код здесь плохо форматируется; а во-вторых, вы ограничены 600 символами, поэтому вам сложно полностью выразить себя. - person Andrew; 02.11.2019
comment
Похоже, это именно то, что мне нужно для моего текущего проекта. Но как его использовать с CoreData? Эти структуры должны быть объектами или одна должна быть продолжением другой? - person mallow; 27.03.2020
comment
@mallow Я бы посоветовал написать то, что у вас есть, в новом вопросе, поскольку комментарии не место для такого рода обсуждений. Комментарии имеют ограниченное пространство, и форматирование кода работает некорректно. Я хотел бы включить в ваш вопрос, что вы сделали до сих пор, что вы пробовали, и четко изложить, чего вы пытаетесь достичь. Не забудьте включить MVE, чтобы те, кто пытается вам помочь, лучше могли это сделать. - person Andrew; 27.03.2020
comment
Спасибо, @ Андрей. Я уже задавал вопрос: stackoverflow.com/questions/60831611/ Но теперь, следуя вашему совету, я добавил код, который у меня уже есть, и добавил больше примеров того, что я уже пробовал. Спасибо за совет! Надеюсь, после этого у меня будет больше шансов на ответ - person mallow; 27.03.2020

На самом деле это два вопроса.

В части данных обновите 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)
person E.Coms    schedule 27.10.2019
comment
Это создаст DateFormatter для каждого элемента в массиве вхождений. Форматировщики дороги в создании, было бы лучше создать форматировщик вне группировки. - person Andrew; 27.10.2019
comment
@ E.Coms Большое спасибо за это, это поставило меня на правильный путь. Мне не удалось заставить ваш пример работать как есть, он выдал мне такие ошибки, как «Невозможно присвоить значение типа« Dictionary ‹String, [Occurrence]›. Values ​​»для типа« [String: [Occurrence]] », и я не смог» t решить, как заставить переменную правильно согласовываться. Я обновлю вопрос, указав свой текущий статус. - person Spielo; 27.10.2019
comment
Вам не нужен dict, но значения dict, которые есть [[событие]] - person E.Coms; 27.10.2019
comment
Это 2-мерный массив - person E.Coms; 27.10.2019
comment
Ах! Я думаю, что понимаю, я пришел к такому выводу, но не совсем понимал, что делать дальше. Завтра попробую! Спасибо еще раз. - person Spielo; 27.10.2019
comment
Все заработало! Большое спасибо и вам, и Эндрю. Ваш ответ / пример дал мне гораздо лучшее понимание того, что происходит за кулисами. - person Spielo; 28.10.2019