`ForEach` 遍历自定义类数组时,如果没有更多上下文信息,表达式类型不明确。

3

我知道有很多关于编译时错误 Type of expression is ambiguous without more context 的问题,但是我读了很多,似乎不明白为什么我的会出现这种情况。

首先,我有:

protocol ExpensePeriod: AnyObject, Identifiable, Hashable {
    associatedtype Period: ExpensePeriod
    
    var type: Calendar.Component { get set }
    var interval: DateInterval { get set }
    var start: Date { get }
    var end: Date { get }
    var expenses: FetchRequest<Expense> { get }
    
    static func array(from startDate: Date, to endDate: Date) -> [Period]
    
    init(from date: Date)
}

然后:

extension ExpensePeriod {
    
    var start: Date { interval.start }
    var end: Date { interval.end }
    var expenses: FetchRequest<Expense> {
        FetchRequest<Expense>(
            entity: Expense.entity(),
            sortDescriptors: [NSSortDescriptor(key: "datetime", ascending: false)],
            predicate: NSPredicate(
                format: "datetime > %@ AND datetime < %@",
                argumentArray: [start, end]
            )
        )
    }
    
    static func array(of timeComponent: Calendar.Component, from startDate: Date, to endDate: Date) -> [Self] {
        var currentDate = startDate
        var array = [Self(from: currentDate)]
        while !Calendar.current.dateInterval(of: timeComponent, for: currentDate)!.contains(endDate) {
            currentDate = Calendar.current.date(byAdding: timeComponent, value: 1, to: currentDate)!
            array.append(Self(from: currentDate))
        }
        return array
    }
    
    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.interval == rhs.interval
    }
    
    func hash(into hasher: inout Hasher) {
        hasher.combine(interval)
    }
}

接下来:

final class ExpenseYear: ExpensePeriod {
    
    typealias Period = ExpenseYear
    
    var type: Calendar.Component
    var interval: DateInterval
    
    var year: Int { Calendar.current.component(.year, from: interval.start) }
    var expenseMonths: [ExpenseMonth] {
        return ExpenseMonth.array(from: start, to: end)
    }
    
    static func array(from startDate: Date, to endDate: Date) -> [ExpenseYear] {
        array(of: .year, from: startDate, to: endDate)
    }
    
    init(from date: Date) {
        self.type = .year
        self.interval = Calendar.current.dateInterval(of: type, for: date)!
    }
}

现在是主要的SwiftUI视图:

struct ListView: View {
    
    @Environment(\.managedObjectContext) private var managedObjectContext
    @FetchRequest(
        entity: Expense.entity(),
        sortDescriptors: [NSSortDescriptor(key: "datetime", ascending: false)]
    ) var expenses: FetchedResults<Expense>
    
    @State private var showingNewExpenseSheet = false
    @State private var showingPreferencesSheet = false
    
    private var dateFormatter: DateFormatter {
        let formatter = DateFormatter()
        formatter.dateFormat = "YYYY MMM"
        return formatter
    }
    
    var body: some View {
        NavigationView {
            List {
                ForEach(ExpenseYear.array(from: expenses.last!, to: expenses.first!)) { expenseYear in
                    ForEach(expenseYear.expenseMonths) { expenseMonth in
                        MonthlyListView(expenseMonth)
                    }
                    Text("\(0)")
                }.onDelete(perform: deleteExpenseItem)
            }
            .navigationBarTitle("Expenses")
            .navigationBarHidden(true)
        }
    }
    
    func deleteExpenseItem(at offsets: IndexSet) {
        for index in offsets {
            let expense = expenses[index]
            managedObjectContext.delete(expense)
        }
        do {
            try managedObjectContext.save()
        } catch {
            print("Wasn't able to save after delete due to \(error)")
        }
    }
}

struct MonthlyListView: View {
    @Environment(\.managedObjectContext) private var managedObjectContext
    var expenseFetchRequest: FetchRequest<Expense>
    var expenses: FetchedResults<Expense> {
        expenseFetchRequest.wrappedValue
    }
    let expenseMonth: ExpenseMonth
    
    init(_ month: ExpenseMonth) {
        self.expenseMonth = month
        self.expenseFetchRequest = month.expenses
    }
    
    var body: some View {
        Section(header: Text("\(expenseMonth.month)")) {
            ForEach(expenses) { expense in
                ExpenseRowItemView(expense)
            }
        }
    }
}

ExpenseRowItemView仅显示各种日期/备注项目。

而Expense实体如下: enter image description here

Type of expression is ambiguous without more context 似乎发生在ForEach(ExpenseYear.array(from: expenses.last!, to: expenses.first!)) 。我使用的是Xcode 12 beta。谢谢你的帮助。

(如果您想知道我为什么要经历所有这些麻烦来渲染一份支出清单:我曾经有一个函数,它遍历每个支出的日期,并构建嵌套的年份和月份结构,以便SwiftUI进行呈现(使用Section等)。但我认为这不是可扩展/高效的方法,因为每次渲染视图时都会调用此功能,击中Core Data中的每个条目,所以我想让每个月度列表处理其自己的FetchRequest及其自己的日期边界,这也将使"选择一个月份以查看该月份交易清单"之类的动态视图更容易。请告诉我是否有更好的方法。)


1
提供的快照中缺少许多组件,因此无法重现您的错误。因此,为了避免猜测,最好查看完整的项目代码。这可行吗? - Asperi
1个回答

1
似乎您没有遵循ExpenseYear中的ExpensePeriod,您缺少startend变量(最有可能是错误源,但很难确定)。
在符合要求后,如果错误仍然存在,我会将循环中的MonthlyListView视图替换为Text,并不断更换,直到找到错误源。
当您缺少关键字或格式化循环时,通常会出现此错误。大多数情况下,这意味着编译器无法解释您所写的内容。
我可以解决问题,但上面的代码缺少一些东西,无法通过复制和粘贴来运行它。 编辑: 你的问题在于 forEach,如果你仔细观察一下,你的代码看起来像这样 ForEach(ExpenseYear.array(from: expenses.last!, to: expenses.first!)) 但是,expenses 定义如下:var expenses: FetchedResults<Expense>,其中该数组中的每个项都将是 Expense 类型,在你的 ExpenseYear 数组中,你的头部看起来像这样 tatic func array(from startDate: Date, to endDate: Date) -> [ExpenseYear],其中第一个和第二个参数都是 Date 类型,而你却将它们传递为 Expense 类型。 expenses.last! 返回一个 Expense 对象,而不是 Date!因此,为了解决这个问题,你首先需要做的是这样的 expenses.last!.datetime! 所以将你的代码更改为以下内容应该可以解决问题。 ForEach(ExpenseYear.array(from: expenses.last!.datetime!, to: expenses.first!.datetime!), id: \.id) { expense in 请记住以下事项。
  1. 将此代码更改为在您的应用程序中反映到每个地方,我仅更改了一个单独的实例,因为我已注释掉了其余的代码。

  2. 强制解包始终是一个坏主意,所以我建议您正确处理日期,但首先保护解包它们。

另外,我知道您已经评论说我不需要在ExpenseYear中实现startend,但不幸的是,如果没有实现它们,我无法编译。

或者,您可以更改.array协议以接受Expense而不是Date,然后您可以处理如何从对象Expense返回数组,因此您的协议将如下所示

static func array(from startExpense: Expense, to endExpense: Expense) -> [Period]

and implementation can be something like this

static func array(from startExpense: Expense, to endExpense: Expense) -> [ExpenseYear] {
        guard let startDate = startExpense.datetime, let endDate = endExpense.datetime else {
            return []
        }
        
        return array(of: .year, from: startDate, to: endDate)
    }

您已经注意到了防止空日期的问题,除了实现方案(我个人更喜欢这种方法)之外,您无需更改任何内容。

我知道要实现第二种方法,您需要更改如何设置协议和其他一些内容,因此您可以将可选的 Date 传递给您的 array,类似于这样 static func array(from startExpense: Date?, to endExpense: Date?) -> [Period]

然后进行解包操作,否则返回空数组。但是在您的 ForEach 循环中仍然存在解包 .last.first 费用的问题。

祝您好运!


ExpenseYear符合ExpensePeriod,因为扩展已经处理了您所描述的缺失一致性问题。我已经完成了替换练习,并将其缩小到ExpenseYear.array(from: expenses.last!, to: expenses.first!) - Dovizu
这很奇怪,因为在我的端上它说需要符合规范。无论如何,我同意Asperi的评论。如果您可以发布您的问题,以便我们可以复制并粘贴以重现错误,那么对于我们来说解决它会更加容易。 - Muhand Jumah
我已经粘贴了更多的代码,希望现在可以复制。我正在使用Xcode 12 beta。 - Dovizu
明白了!请查看我编辑后的答案,解决你的问题。 - Muhand Jumah
谢谢,我有点生气自己没有注意到我甚至没有传递正确类型的参数...难道Xcode不应该告诉我,而只是抛出一个晦涩的“模棱两可”的错误吗?哎呀...再次感谢您的帮助,非常感激! - Dovizu
1
我很高兴能够帮忙,不用担心。相信我,在我的职业生涯中,我犯过很多愚蠢的错误。这是很正常的。至于xCode,它应该指向真正发生错误的地方,但自从swiftUI发布以来,xCode一直存在很多问题,特别是在这个领域。因此,当出现问题时,我开始注释掉一些代码。代码越少,xCode就越容易指向正确的方向。希望他们能尽快解决这个问题。 - Muhand Jumah

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接