TransWikia.com

Type of expression is ambiguous without more context in `ForEach` over array of custom class

Stack Overflow Asked by Dovizu on February 7, 2021

I know there’s a lot of questions asking about Type of expression is ambiguous without more context compile-time errors, but I read a lot of them and don’t seem to understand why mine’s happening.

First, I have

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

ad then:

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

and then:

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

now the main SwiftUI view:

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 just displays various datetime/notes items.

And Expense entity looks like:
enter image description here

Type of expression is ambiguous without more context seems to be happening at ForEach(ExpenseYear.array(from: expenses.last!, to: expenses.first!)) . I’m on Xcode 12 beta. Thank you for your help.

(If you are wondering why I’m going through all of this trouble just to render a list of expenses: I used to have a function that runs through every expense’s datetime and build a nested structure of years and months for SwiftUI to render (to use Section, etc.). But I don’t think this is a scalable/performant approach since this function would be called every time the view is rendered, hitting every single entry in Core Data, so I thought I’d have each monthly list handle its own FetchRequest with its own date boundaries, which would also make dynamic views such as "select a month to view a list of transactions by this month" easier. Please let me know if there’s a better way as well.)

One Answer

It seems like you didn't conform to ExpensePeriod in ExpenseYear you are missing start and end variables (most likely it's the source of error, but it's hard to tell)

After conforming, if the error persists I would replace in the loop the MonthlyListView view with Text and I would keep replacing stuff until I find the source of the error.

This error usually occurs when you are missing a keyword or formatting a loop. Most of the time it just means the compiler can't interpret what you wrote.

I would solve the issue but the code above is missing stuff to be able to run it just by copying and pasting it.

EDIT:

So your issue lies in the forEach because if you pay close attention, your code look like this ForEach(ExpenseYear.array(from: expenses.last!, to: expenses.first!)) however, expenses is defined as follows var expenses: FetchedResults<Expense> where each item from this array will be of type Expense in your ExpenseYear array your header looks like this tatic func array(from startDate: Date, to endDate: Date) -> [ExpenseYear] which the first and 2nd parameter are of type Date yet you are passing them an item of type Expense. expenses.last! returns a Expense object and that's not a Date! so to solve the issue you would first have to do something like this expenses.last!.datetime!

So changing your code to this

ForEach(ExpenseYear.array(from: expenses.last!.datetime!, to: expenses.first!.datetime!), id: .id) { expense in

should solve your problem. Keep in mind the following

  1. Change this code to reflect everywhere in your app, I only changed it in 1 single instance as I have commented out the rest of your code.

  2. Force unwrapping is always a bad idea, so I would suggest you handle dates correctly but guard unwrapping them first.

Also, I know you commented that I didn't need to implement start and end in ExpenseYear but unfortunately, I wasn't able to compile without implementing them so I had to.

Alternatively, you can change the .array protocol to take in an Expense instead of Date and then you handle how to return an array from the object Expense so your protocol would look something like this

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

where you have already taken care of guarding against nil dates and you don't have to change anything except implementation (I honestly would prefer this method)

I know to implement the second method you would have to change how you set your protocol and bunch of other stuff, so what you can do is instead pass an optional Date to your array, something like this static func array(from startExpense: Date?, to endExpense: Date?) -> [Period]

and then guard unwrap them else return empty array. But you still have the issue of unwrapping .last and .first expense in your ForEach loop.

Good luck with the rest!

Answered by Muhand Jumah on February 7, 2021

Add your own answers!

Ask a Question

Get help from others!

© 2024 TransWikia.com. All rights reserved. Sites we Love: PCI Database, UKBizDB, Menu Kuliner, Sharing RPP