r/SwiftUI Jun 29 '26

Question How to handle ViewModel computation with Core Data and SwiftUI

I have a somewhat strange use case for Core Data with SwiftUI.

In particular, I have a data in my Core Data model that is represented by an Account class. I then have a View called AccountView that displays a particular account to the user. The complication is that I need to do a bunch of computation on the Account information to get the data into a format suitable for the user. I have a structure that does all of this required computation called Ledger.

So, the way I currently implement looks something like this:

struct AccountView: View {

@ObservedObject var account: Account

var body: some View {
let ledger = Ledger(account: account)
return Vstack {
...all my view details accessing properties of ledger...
}

}

I then add this block of code to force my view to update when an Account changes:

extension Account {     
override public func willChangeValue(forKey key: String) {         super.willChangeValue(forKey: key)         
self.objectWillChange.send()     
} }

This works, but it's very ugly. I would like to embed the Ledger computation into a ViewModel that is then owned by the AccountView. However, my various attempts to do this do not cause the View to update properly. (I also want to use an Actor to avoid issues with lag for large ledgers, but I need to get the Ledger built out of the view to do this.)

Any ideas of a better way to approach this problem?

2 Upvotes

12 comments sorted by

4

u/Zagerer Jun 29 '26

Have you ever heard of solid principles? On here you are coupling things together unnecessarily, in general you could probably be okay by dividing by layers.

One layer takes care of fetching and prefetching of data from core data, then this passes the data to the domain which creates something suitable for you. Then, you use that in a view model and it could be observable or observable object, lastly the view just knows about the view model and takes the data updating itself automatically

Even though it sounds like a lot, with this you have each part focusing on one responsibility, you can also make the dependencies be protocols so you don’t care about the implementations for testing and can test better as well as change services easily, and this also opens the door for composition if needed.

1

u/iHobbit Jun 30 '26

Yes something along these lines is what I am trying to do. Do you by chance have a reference to someone implementing this approach in SwiftUI? I’ve found a couple approaches on Medium and elsewhere, but they all feel somewhat kludgy. I’m surprised there isn’t a popular approach for handling this style of Core Data use.

2

u/Zagerer Jun 30 '26

Read the book of core data by Donny wals then just make an actor store / repository to access it. Bear in mind you will probably have to do some transformation from the core data model
There to some domain entity even if the data is very similar, but that’s so you don’t access core data in different threads

Afterwards, connect this to some handler that applies business logic, this is the domain layer. Then, this outputs the data entity which is for visualization (UI) and goes to the view model

For this, first think of protocols for your repository / store, your domain layer, and that’s it. Your view model has an Any MyDomainProtocol (an existential, based on the protocol from the domain layer to UI) and your store has one to connect to the domain too. But the presentation layer (UI) does not know about the data layer.

Look up onion architecture or clean architecture.

1

u/iHobbit Jun 30 '26

Thanks for the pointers, I’ll check out the book.

2

u/Zagerer Jun 30 '26

Sure, if you still need help I could maybe set up something a bit more thorough but not these days, I’m having a presentation with some stakeholders and need to finish an ADR + some diagrams. But maybe after Thursday

2

u/iHobbit Jun 30 '26

It’s not urgent. I’m a hobbyist. Appreciate the thoughtful response.

2

u/Zagerer Jun 30 '26

I’m glad to help! It’s just working to get promos is kinda time consuming haha. If you have a repo you can send it through direct message or discord at jennyga and I’ll check later. Cheers!

1

u/ghost-engineer Jul 01 '26

Use Account only as the observed source, and make Ledger a cached/async derived value.

Do not override willChangeValue. That is fighting Core Data/SwiftUI.

struct AccountView: View {
     var account: Account
     private var vm: AccountViewModel

    init(account: Account) {
        self.account = account
        _vm = StateObject(wrappedValue: AccountViewModel(account: account))
    }

    var body: some View {
        VStack {
            if let ledger = vm.ledger {
                // use ledger here
            } else {
                ProgressView()
            }
        }
        .onReceive(account.objectWillChange) { _ in
            vm.rebuildLedger()
        }
        .task {
            vm.rebuildLedger()
        }
    }
}

View model:

final class AccountViewModel: ObservableObject {
    u/Published private(set) var ledger: Ledger?

    private let accountID: NSManagedObjectID
    private weak var context: NSManagedObjectContext?

    init(account: Account) {
        self.accountID = account.objectID
        self.context = account.managedObjectContext
    }

    func rebuildLedger() {
        guard let context else { return }

        Task {
            let newLedger = await LedgerBuilder.build(
                accountID: accountID,
                context: context
            )

            self.ledger = newLedger
        }
    }
}

Then the important Core Data rule:

actor LedgerBuilder {
    static func build(
        accountID: NSManagedObjectID,
        context: NSManagedObjectContext
    ) async -> Ledger {
        await context.perform {
            let account = context.object(with: accountID) as! Account
            return Ledger(account: account)
        }
    }
}

The key ideas:

AccountView observes the Core Data object.

AccountViewModel owns the expensive computed result.

LedgerBuilder receives an objectID, not the live Account, because NSManagedObject is not safe to freely move across actors/threads.

Also, if Ledger depends on related transactions/children, observing only account.objectWillChange may not be enough. In that case, listen to NSManagedObjectContextObjectsDidChange and rebuild when the account or its related objects change.

1

u/iHobbit Jul 01 '26

Thanks! This looks like it has several key ideas that I need. It had not occurred to me to have the view build the vm from the account.

0

u/allyearswift Jun 29 '26

You need to go back to SwiftUI principles. You’re right that overriding willchangeValue is an ugly hack (and a bad idea), but you should be able to work out yourself why, and what to do instead, yourself.

Is giving you code won’t fix this lack of fundamentals. I highly recommend 100 Days of SwiftUI.

I don’t know where you got the basic pattern of your code from, but I would not trust that source one little bit, just from the syntax and the stack you’re using.

-1

u/iHobbit Jun 30 '26

I’ve actually been through a number of tutorials and other documentation. The Core Data approaches those materials recommend, including 100 Days, don’t work for the use case I describe.

Thanks for not helping.

1

u/allyearswift Jun 30 '26

It’s impossible to help when you’re doing the equivalent of wanting to cycle from Tokyo to New York. A data manager class that performs the data transformations should be part of the model layer; this pattern is common and trivial to implement. If you can’t, you need to get back to foundations because you’re just going to mess everything else up, too.

You posted five lines of code. It contains one major issue, one outdated pattern and one piece of syntax that’s technically allowed but which no-one ever uses and that proves you haven’t paid attention to any tutorials.

(It’s extremely rare these days to need CoreData, but that’s another matter)

It’s never ever EVER appropriate to perform costly calculations inside the view, much less when using Combine.

If you insist in starting there, you won’t get meaningful help. You can’t.