(What the Frack)When I first saw this, my initial reaction was that my InitListViewModel wasn’t being released for some reason and that I had a bug.
I spent some time trying to figure out how to release the view model.
I spent some more time trying to figure out how to recreate and/or reassign my ViewModel to my View’s object binding parameter.
Then I noticed something else.
Rerun the app… and then wait a couple of seconds before tapping the button for the first time.
Do so, and you won’t see an empty screen, but a screen full of data.
And that’s when the light began to dawn.
The Method Behind the MadnessIf you stop to think about it, it’s pretty clear what’s going on.
Almost every view in SwiftUI is a struct.
Structs must be initialized when created.
When we initialize InitListView our InitListViewModel is also created and passed as a parameter to the View.
Which means its init method is called at that point in time.
And which in turn calls our load method.
Our “API” call is made at that time, and returned two seconds later.
But when does this occur?In our InitMenuView NavigationButton, when we specified our destination.
NavigationButton(destination: InitListView(model: InitListViewModel()) )InitListViewModel is initialized at that point in time.
RamificationsThis has some pretty massive ramifications.
Make a home screen with a dozen navigation buttons, with a dozen destinations, and every one will be initialized on launch.
Make a disclosable List View with a hundred list items, and every destination will be initialized on display.
Fortunately, there is a solution, although at this time it’s just a partial one.
Lifecycle ManagementSwiftUI provides a set of View modifiers that are called during the View’s lifecycle, and one of those is onAppear.
Let’s refactor our code.
We’ll keep everything in the ViewModel the same, but remove the init function.
And we’ll add an onAppear modifier to our main list, with a closure that calls our ViewModel’s load function directly.
This is similar to how we manage events in UIKit, where we often call or configure our view model from viewDidLoad.
Run the new code, wait a few seconds, then tap List Sample.
You’ll see our expected empty screen, then our data screen.
Success!Right?Another ProblemWell, we fixed our first problem, but now we have another one.
Tap the back button, then tap List Sample again and you’ll see our loaded data… and now we have an even more insidious problem.
And one which, I’ll admit, has yet to lend itself to a solution as I’ve yet to find a good way to “reset” the ViewModel.
The NavigationButton destination just sits there with an initialized view that it apparently plans on keeping forever.
Yes, you could write some code to empty the array, but that does nothing as it simply assumes the view state hasn’t changed.
Attempt to empty the array and call self.
send(self) before the view is presented and you’ll get an application crash.
(Another bug?)Much sadness.
Completion HandlerI could have chalked up the first “bug” as not being a bug at all.
It’s simply in the nature of how Swift works.
The second problem however, leads me to believe that it’s a bug after all.
How to fix it?An easy solution (for Apple), would be to change the nature of navigation buttons and the like.
Each problem stems from the fact that the view struct is initialized prematurely.
As such, if I were writing this API, I’d introduce a variant of NavigationButton that takes a factory closure as its destination parameter and that closure would return a new View and a new ViewModel each and every time it’s invoked.
Every time you press the button you get a new, factory fresh instance of your view.
More to the point, you’re not prematurely initializing dozens, hundreds, or thousands of the silly things.
(Can you imagine this in a list?)Now, there might be times you’d actually want to maintain the state of the given screen, and for that you could keep the existing method signature.
It’s how, after all, Tab Views can and could maintain the state of each individual Tab View.
The initialization problem is, by the way, why we usually want to use a PassthroughSubject in our code.
We don’t want any view updates until we’re ready for them.
Regardless, the bottom line here is that you need to be very aware of what code you put into the initialization functions of your SwiftUI Views and ViewModels, as that code can and will be called, and often when you least expect it.
EditsIt was noted in the comments that, as a rule, View constructors should not have side effects.
This doesn’t fix our NavigationButton bug, but does point to the fact that our onAppear solution appears to be the correct one.
An earlier version used a @State variable instead of an @ObjectBinding variable inside the View, but all this does is push the initialization from within the View to outside of the View, with fundamentally the same result.
Until next time.