SwiftUI: ObservableObject не работает с инициализацией fetchRequest

У меня есть ObservableObject, объявленный в моем DateView следующим образом:

import SwiftUI

    class SelectedDate: ObservableObject {
        @Published var selectedMonth: Date = Date()
        
        var startDateOfMonth: Date {
            let components = Calendar.current.dateComponents([.year, .month], from: self.selectedMonth)
            let startOfMonth = Calendar.current.date(from: components)!
            return startOfMonth
        }
    
        var endDateOfMonth: Date {
            var components = Calendar.current.dateComponents([.year, .month], from: self.selectedMonth)
            components.month = (components.month ?? 0) + 1
            let endOfMonth = Calendar.current.date(from: components)!
            return endOfMonth
        }
    }

struct DateView: View {
// other code
}

Я могу получить доступ к startDateOfMonth и endDateOfMonth в других представлениях, например:

Text("\(self.selectedDate.startDateOfMonth)")

Но у меня возникает проблема, когда я пытаюсь использовать эти переменные из ObservableObject внутри инициализатора fetchRequest в моем TransactionsListView:

    import SwiftUI
    
    struct TransactionsListView: View {
    
    @Environment(\.managedObjectContext) var managedObjectContext
        var fetchRequest: FetchRequest<NPTransaction>
        var transactions: FetchedResults<NPTransaction> { fetchRequest.wrappedValue }
    @EnvironmentObject var selectedDate: SelectedDate

    // other code
            init() {
                    fetchRequest = FetchRequest<NPTransaction>(entity: NPTransaction.entity(), sortDescriptors: [
                        NSSortDescriptor(keyPath: \NPTransaction.date, ascending: false)
                    ], predicate: NSPredicate(format: "date >= %@ AND date < %@", self.selectedDate.startDateOfMonth as NSDate, self.selectedDate.endDateOfMonth as NSDate))
            }
    
    var body: some View {
    // other code
    }
    }

Я получаю сообщение об ошибке:

Переменная self.fetchRequest использовалась перед инициализацией

Что я делаю не так? Я пытался использовать эти переменные внутри init без self. Та же ошибка. Единственный способ, которым это работало, заключался в том, что эти startDateOfMonth и endDateOfMonth были не в ObservableObject, а как переменные в представлении предков, которые я передавал в качестве параметров своему представлению с помощью fetchRequest init. Но у меня мало таких представлений, и я хотел использовать ObservableObject, чтобы не передавать одни и те же параметры в подвиды все время.

3 ответа

Решение

Проблема в том, что нельзя использовать @EnvironmentObject в init(). Возможный вариант - передатьEnvironmentObjectв качестве параметра View. Затем используйте этот параметр внутри файла init. Вот конструктор для представления, тогда вы можете получить доступ к start и endTime с локальной переменнойselectedDate.

init(selectedDate : SelectedDate) 
{
   //now you can access selectedDate here and use it in FetchRequest
   fetchRequest = FetchRequest<NPTransaction>(entity: NPTransaction.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \NPTransaction.date, ascending: false)], predicate: NSPredicate(format: "date >= %@ AND date < %@", selectedDate.startDateOfMonth as NSDate, selectedDate.endDateOfMonth as NSDate))
}

Где бы вы ни называли это представление, пройдите мимо EnvironmentObject как параметр.

                                    //Declared as Environment Object
TransactionsListView(selectedDate : self.selectedDate)

У тебя не должно быть init() в View. Создаются представления и вызывается метод body (который должен быть максимально быстрым), затем визуализируется фактическое представление, а затем все структуры представления отбрасываются, отображая все, что вы делаете внутриinitбессмысленно и просто замедлит работу SwiftUI. Вам необходимо использовать оболочки свойств, которые позволяют данным сохраняться между многократным созданием представления. Однако в вашем случае вы не можете использовать@FetchRequest потому что он статичен, и вам нужно динамически изменять его в зависимости от выбранных дат.

Вы можете исправить это, сделав выбранную дату структурой, которая использует @Binding если он передан или @State если твой Viewнесет ответственность за это. Затем выполните загрузку с помощьюNSFetchedResultsController в TransactionsListViewModel : ObservableObject с помощью @Published для Array<NPTransaction> транзакций и создайте его в своем View с помощью @StateObjectчто позволяет ему сохраняться между несколькими запусками View. Вызвать метод выборки модели из.onAppear() который будет выполнять выборку с использованием выбранных дат, обновит @Published транзакции, о которой будет уведомлен SwiftUI, и вызывает View тело, чтобы называться снова, и ваш List можно обновлять транзакциями в пределах вашего диапазона дат.

Непроверенный образец кода:

NPTransaction.swift

import Foundation
import CoreData

@objc(NPTransaction)
public class NPTransaction: NSManagedObject {

    @nonobjc public class func sortedFetchRequest() -> NSFetchRequest<NPTransaction> {
        let fr : NSFetchRequest<NPTransaction> = NPTransaction.fetchRequest()
        fr.sortDescriptors = [NSSortDescriptor(keyPath: \NPTransaction.date, ascending: false)]
        return fr
    }
    
}

Транзакции ListViewModel.swift

class TransactionsListViewModel : NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
    
    lazy var fetchedResultsController : NSFetchedResultsController<NPTransaction> = {
        let frc = NSFetchedResultsController<NPTransaction>(fetchRequest: NPTransaction.sortedFetchRequest(), managedObjectContext: PersistenceController.shared.container.viewContext, sectionNameKeyPath: nil, cacheName: nil)
        frc.delegate = self
        return frc
    }()
        
    @Published
    var transactions : Array<NPTransaction> = []
    
    func fetch(selectedDate:SelectedDate){
        fetchedResultsController.fetchRequest.predicate = NSPredicate(format: "date >= %@ AND date < %@", selectedDate.startDateOfMonth as NSDate, selectedDate.endDateOfMonth as NSDate)
        try! fetchedResultsController.performFetch()
        transactions = fetchedResultsController.fetchedObjects ?? []
    }
    
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        transactions = fetchedResultsController.fetchedObjects ?? []
    }
}

TransactionsListView.swift

import SwiftUI
    
    struct TransactionsListView: View {
    
    @StateObject var model = TransactionsListViewModel()
    @Binding var selectedDate: SelectedDate

    var body: some View {
        List {
            ForEach(model.transactions) { item in
                ...
            }
        }
        .onAppear() {
            model.fetch(selectedDate)
        }
    }

Swift гарантирует, что все свойства инициализируются после инициализации.

Когда вы обращаетесь к свойству объекта, предполагается, что объект уже инициализирован, что не так, когда вы обращаетесь к нему в середине инициализации.

Что происходит, когда вы инициализируете fetchRequest, вы получаете доступ self.selectedDate (то же самое для компилятора, независимо от того, добавляете вы себя или нет), что предполагает, что self инициализирован, что означает, что, fetchRequest, как одно из свойств self, готов к использованию. Отсюда ошибка компилятора:

Переменная self.fetchRequest использовалась перед инициализацией

Сделать fetchRequestвычисляемое или ленивое свойство должно работать. (не слишком знаком с@FetchRequest)

Надеюсь это поможет.

Другие вопросы по тегам