BLACK FRIDAY SALE: Save big on all my Swift books and bundles! >>

SOLVED: SwiftUI MVVM with Multi-Page Form - ViewModel Not Updating View

Forums > SwiftUI

I have a multi-page form where all of the views of the form share the same ObservableObject(s). What I would like to do is split up each individual view into an MVVM workflow so that the UI stuff is in the view and the data stuff is in the viewModel.

Unfortunately, it seems that the viewModel is not updating the view when I set my loadingState (which is in another Observable class). Instead it gets stuck at the default value of .loading which shows a ProgressView().

I have checked to ensure that the loadingState variable is being set by using a print statement (shown in the onAppear) and it is being set correctly.

What is the proper procedure for instituting an MVVM workflow when you have multiple other ObservedObjects being utilized in each view? This is for the latest SwiftUI/XCode for iOS 15+.

Here's a bit of code to further explain (obviously very stripped down for this post, but you get the idea if you read my explanation above):

Main View:

import Combine
import SwiftUI

struct UsernameView: View {

    @StateObject var viewModel: ViewModel
    @StateObject var network: NetworkMonitor = NetworkMonitor()
    @StateObject var dataTransfer: NetworkTransfer = NetworkTransfer()

    var body: some View {
        VStack {
            List {
                switch viewModel.loadingState {
                    case .loading:
                        Section {
                            ProgressView()
                        }

                    case .loaded:
                        Section {
                            Text("I am loaded")
                        }

                    case .failed:
                        Section {
                            Text("Something went wrong")
                        }
                }
            }
        }
        .onAppear() {
            viewModel.phraseLoader()
            print("LoadingState: \(viewModel.loadingState.loadingState)")
        }
    }
}

View Model:

import Combine
import SwiftUI

extension UsernameView {
    class ViewModel: CreateUser {

        @Published var loadingState: LoadingState = LoadingState()
        let dataTransfer: NetworkTransfer
        let network: NetworkMonitor
        var user_phrase: [UserPhrase] = []

        init(dataTransfer: NetworkTransfer, network: NetworkMonitor) {
                    self.dataTransfer = dataTransfer
                    self.network = network
        }

        func phraseLoader() {

            let jsonFetchPhraseURL = URL(string: "https://api.foo.com/phrases")
            let jsonFetchPhraseTask = dataTransfer.jsonFetch(jsonFetchPhraseURL, defaultValue: [UserPhrase]())

            guard network.isNetworkActive else { loadingState.loadingAlert = true; return }

            jsonFetchPhraseTask.sink (receiveCompletion: { completion in
                switch completion {
                    case .failure:
                        self.loadingState.loadingState = .failed
                    case .finished:
                        self.loadingState.loadingState = .loaded
                }
            },
            receiveValue: { loadedPhrase in
               self.user_phrase = loadedPhrase
            }).store(in: &dataTransfer.requests)
        }
    }
}

   

For anyone else looking at this the answer was a little confusing, but after thinking about Swift's concurrency for a day or two, I realized what needed to be done.

First, what was the problem?

The way the run loop ticks occur caused a timing issue when the LoadingState was being created in the view model and then @Published back to the view, because everything occurs onAppear.

The solution:

If you create the LoadingState on the previous page of the form via @StateObject var loadingState = LoadingState(), and then pass that through via the navigationLink as loadingState: loadingState, you can then do an @ObservedObject var loadingState: LoadingState in the view and a var loadingState: LoadingState in the view model (with an entry to the initializer).

This way, the page that actually needs the loading state has one already created for it and it's just monitoring it for changes, instead of trying to create and monitor at the same time.

Going a bit further:

What if you have a multi-page form where all of the pages need their own LoadingState? It's actually easier than you might think. All you need to do is create another instance of LoadingState on the previous page and name it whatever you want to pass through.

For example, you can have two independent LoadingStates like this: @ObservedObject loadingState = LoadingState() @StateObject loadingState2 = LoadingState()

Those are two completely independent instantiations of LoadingState. One (loadingState) is already active from the previous page and is being observed. The other is a new LoadingState that is being created for the next page.

In your navigationLink, you'll want to pass through the "new" LoadingState (i.e. loadingState2) and so you type loadingState: loadingState2. In your next page you can rename loadingState2 to whatever you want (probably just loadingState to keep things easy).

That's all there is to it!

   

Hi, happy you found a solution. I'm curious, at which point are you passing in the observable objects into the view model. It's something I've been struggling with. Thanks.

   

Hacking with Swift is sponsored by RevenueCat

SPONSORED In-app subscriptions are a pain to implement, hard to test, and full of edge cases. RevenueCat makes it straightforward and reliable so you can get back to building your app. Oh, and it's free if your app makes less than $10k/mo.

Learn more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.