【问题标题】:How to dynamically create sections in a SwiftUI List/ForEach and avoid "Unable to infer complex closure return type"如何在 SwiftUI List/ForEach 中动态创建部分并避免“无法推断复杂的闭包返回类型”
【发布时间】:2019-10-26 21:23:25
【问题描述】:

我正在尝试重新创建与股票 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 中,这给了我错误“无法推断复杂的闭包返回类型;添加显式类型以消除歧义”,因为我在 ForEach{} 中调用任意代码 (self.date =occurrence.start),这是'不允许。

实现这一点的正确方法是什么?是否有更动态的方式来执行此操作,或者我是否需要以某种方式在 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”的值转换为预期的参数类型“发生”

【问题讨论】:

  • 您能否包括您对发生的定义?
  • 您可能希望重新格式化您传递给 ForEach 的数据,以便在尝试显示之前已按日期划分。
  • @LuLuGaGa,更新为 Occurrence。这是一个简单的对象,通过可编码的方式填充 JSON 数据。
  • @Andrew,你有这方面的例子吗?我之前尝试过创建返回 NavigationLink 对象的函数,但我无法让它工作。

标签: ios swift swiftui swiftui-list


【解决方案1】:

参考我对您问题的评论,数据应在显示之前放入部分中。

这个想法是有一个对象数组,其中每个对象都包含一个出现的数组。因此,我们简化了您的发生对象(对于本示例)并创建以下内容:

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 对象数组,然后您可以轻松地显示这些对象。我已经通过代码添加了 cmets,以便您可以清楚地看到每件事在做什么。

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

这是最终结果。

【讨论】:

  • 感谢您让这一切变得如此简单!在这个和 E.Com 的回答之间,我对在 Swift 中工作时的良好实践有了更好的理解,并且得到了一些我想要的东西。再次感谢!
  • 如何用这个实现 onDelete?我尝试将其添加到 ForEach 中,但它会弄乱 IndexSet 并删除错误的项目。
  • @fasoh 您需要跟踪节索引和行索引以确保删除正确的项目。理想情况下,您希望在最里面的 .onDelete ForEach
  • 谢谢,我知道如何正确跟踪索引,但它提出了另一个问题。在此示例中,我们将事件作为数组获取,据我了解,它应该是用于允许添加和编辑项目的 @ObservedObject。知道部分和出现索引,如何实现实际删除?据我了解,我应该从原始事件数组中删除,以便正确更新这些部分并将其传播到视图。
  • @fasoh 在这个例子中,我创建的数据对象是为了给出一个工作示例,除了如何获取项目列表并将它们转换为分组列表。是否使用观察对象在当时并不是特别重要,因为最初的问题发布者已经有了数据源。
【解决方案2】:

这实际上是两个问题。

在数据部分,请将userData.occurrences从[Occurrence]升级为[[ 发生 ]](这里我叫它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)

【讨论】:

  • 这将为发生数组中的每个项目创建一个 DateFormatter。创建格式化程序的成本很高,最好在分组之外创建格式化程序。
  • @E.Coms 非常感谢,它让我走上了正确的道路。我无法让您的示例按原样工作,它给了我诸如“无法将'Dictionary.Values'类型的值分配给'[String:[Occurrence]]'”之类的错误,而我无法弄清楚如何使变量正确符合。我会用我目前的状态更新问题。
  • 您不需要字典,但需要字典的值,即 [[occurrence]]
  • 这是一个二维数组
  • 啊!我想我明白了,我得出了这个结论,但不太明白如何继续下去。我明天试试!再次感谢。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多