如何在 SwiftUI List/ForEach 中动态创建部分并避免“无法推断复杂的闭包返回类型" [英] How to dynamically create sections in a SwiftUI List/ForEach and avoid "Unable to infer complex closure return type"

查看:32
本文介绍了如何在 SwiftUI List/ForEach 中动态创建部分并避免“无法推断复杂的闭包返回类型"的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试重新创建与股票 iOS 日历应用程序中的事件列表相匹配的视图.我有以下代码生成一个事件列表,每个事件按日期分成自己的部分:

I'm trying to recreate a view that matches the event list in the stock iOS Calendar app. I have the following code which generates a list of events with each event separated into its own section by Date:

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

这段代码的问题在于,如果同一日期有两个事件,它们会被分成具有相同标题的不同部分,而不是被组合到同一部分.

The problem with this code is that if there are two events on the same date, they are separated into different sections with the same title instead of being grouped together into the same section.

作为一个 Swift 新手,我的直觉是做这样的事情:

As a Swift novice, my instinct is to do something like this:

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 中,这给了我错误无法推断复杂闭包返回类型;添加显式类型以消除歧义",因为我在 ForEach{} 中调用任意代码(self.date =occurrence.start),这是不允许.

But in Swift, this gives me the error "Unable to infer complex closure return type; add explicit type to disambiguate" because I'm calling arbitrary code (self.date = occurrence.start) inside ForEach{}, which isn't allowed.

实现这一点的正确方法是什么?是否有更动态的方式来执行此操作,或者我是否需要以某种方式抽象 ForEach{} 之外的代码?

What's the correct way to implement this? Is there a more dynamic way to execute this, or do I need to abstract the code outside of ForEach{} somehow?

Occurrence 对象如下所示:

The Occurrence object looks like this:

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

更新:以下代码为我提供了一个字典,其中包含以相同日期为键的出现数组:

Update: The following code got me a dictionary which contains arrays of occurrences keyed by the same date:

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

但是,如果我尝试在我的视图中使用它,如下所示:

However, if I try and use this in my View as follows:

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

(当我开始工作时,部分内容已被注释掉)

(Section stuff commented out while I get the main bit working)

我收到此错误:无法将类型 '_.Element' 的值转换为预期的参数类型 'Occurrence'

I get this error: Cannot convert value of type '_.Element' to expected argument type 'Occurrence'

推荐答案

参考我对你的问题的评论,数据应该在显示之前分成几部分.

In reference to my comment on your question, the data should be put into sections before being displayed.

这个想法是有一个对象数组,其中每个对象包含一个出现的数组.所以我们简化了你的出现对象(对于这个例子)并创建了以下内容:

The idea would be to have an array of objects, where each object contains an array of occurrences. So we simplify your occurrence object (for this example) and create the following:

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

接下来我们需要一个对象来表示在给定日期发生的所有事件.我们将其称为 Day 对象,但名称对于本示例来说并不重要.

Next we need an object to represent all the occurrences that occur on a given day. We'll call it a Day object, however the name is not too important for this example.

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

所以我们要做的是将一组 Occurrence 对象转换成一组 Day 对象.

So what we have to do is take an array of Occurrence objects and convert them into an array of Day objects.

我创建了一个简单的结构来执行实现这一目标所需的所有任务.显然,您希望修改它以使其与您拥有的数据相匹配,但关键是您将拥有一组 Day 对象,然后您可以轻松地显示这些对象.我在代码中添加了注释,以便您可以清楚地看到每件事情在做什么.

I have created a simple struct that performs all the tasks that are needed to make this happen. Obviously you would want to modify this so that it matches the data that you have, but the crux of it is that you will have an array of Day objects that you can then easily display. I have added comments through the code so that you can clearly see what each thing is doing.

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.

This then allows the ContentView to simply be two nested 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)
    }
}

这是最终的结果.

这篇关于如何在 SwiftUI List/ForEach 中动态创建部分并避免“无法推断复杂的闭包返回类型"的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆