Home > OS >  Core Data with SwiftUI MVVM Feedback
Core Data with SwiftUI MVVM Feedback

Time:04-20

I am looking for a way to use CoreData Objects using MVVM (ditching @FetchRequest). After experimenting, I have arrived at this implementation:

Datable.swift:

protocol Datable {
    associatedtype Object: NSManagedObject
//MARK: - Mapping
    static func map(from object: Object) -> Self
    func map(from object: Object) -> Self
//MARK: - Entity
    var object: Object {get}
//MARK: - Fetching
    static var modelData: ModelData<Self> {get}
//MARK: - Writing
    func save()
}

extension Datable {
    static var modelData: ModelData<Self> {
        return ModelData()
    }
    func map(from object: Object) -> Self {
        return Self.map(from: object)
    }
    func save() {
        _ = object
        let viewContext = PersistenceController.shared.container.viewContext
        do {
            try viewContext.save()
        }catch {
            print(String(describing: error))
        }
    }
}

extension Array {
    func model<T: Datable>() -> [T] {
        return self.map({T.map(from: $0 as! T.Object)})
    }
}

ModelData.swift:

 class ModelData<T: Datable>: NSObject, ObservableObject, NSFetchedResultsControllerDelegate {
    var publishedData = CurrentValueSubject<[T], Never>([])
    private let fetchController: NSFetchedResultsController<NSFetchRequestResult>
    override init() {
        let fetchRequest = T.Object.fetchRequest()
        fetchRequest.sortDescriptors = []
        fetchController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: PersistenceController.shared.container.viewContext, sectionNameKeyPath: nil, cacheName: nil)
        super.init()
        fetchController.delegate = self
        do {
            try fetchController.performFetch()
            publishedData.value = (fetchController.fetchedObjects as? [T.Object] ?? []).model()
        }catch {
            print(String(describing: error))
        }
    }
    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        guard let data = controller.fetchedObjects as? [T.Object] else {return}
        self.publishedData.value = data.model()
    }
}

Attempt.swift:

struct Attempt: Identifiable, Hashable {
    var id: UUID?
    var password: String
    var timestamp: Date
    var image: Data?
}

//MARK: - Datable
extension Attempt: Datable {
    var object: AttemptData {
        let viewContext = PersistenceController.shared.container.viewContext
        let newAttemptData = AttemptData(context: viewContext)
        newAttemptData.password = password
        newAttemptData.timestamp = timestamp
        newAttemptData.image = image
        return newAttemptData
    }
    static func map(from object: AttemptData) -> Attempt {
        return Attempt(id: object.aid ?? UUID(), password: object.password ?? "", timestamp: object.timestamp ?? Date(), image: object.image)
    }
}

ViewModel.swift:

class HomeViewModel: BaseViewModel {
    @Published var attempts = [Attempt]()
    required init() {
        super.init()
        Attempt.modelData.publishedData.eraseToAnyPublisher()
            .sink { [weak self] attempts in
                self?.attempts = attempts
            }.store(in: &cancellables)
    }
}

So far this is working like a charm, however I wanted to check if this is the best way to do it and improve it if possible. Please note that I have been using @FetchRequest with SwiftUI for over a year now and decided to move to mvvm since I am using it in all my Storyboard projects.

CodePudding user response:

For a cutting edge way to wrap the NSFetchedResultsController in SwiftUI compatible code you might want to take a look at AsyncStream.

However, @FetchRequest currently is implemented as a DynamicProperty so if you did that too it would allow access the managed object context from the @Environment in the update func which is called on the DynamicProperty before body is called on the View. You can use an @StateObject internally as the FRC delegate.

Be careful with MVVM because it uses objects where as SwiftUI is designed to work with value types to eliminate the kinds of consistency bugs you can get with objects. See the doc Choosing Between Structures and Classes. If you build an MVVM object layer on top of SwiftUI you risk reintroducing those bugs. You're better off using the View data struct as it's designed and leave MVVM for when coding legacy view controllers. But to be perfectly honest, if you learn the child view controller pattern and understand the responder chain then there is really no need for MVVM view model objects at all.

And FYI, when using Combine's ObservableObject we don't sink the pipeline or use cancellables. Instead, assign the output of the pipeline to an @Published. However, if you aren't using Combine's flagship feature, CombineLatest, then perhaps reconsider if you should really be using Combine at all.

  • Related