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!