Swift UI is not reloading my views

Liyicky
6 min readJul 9, 2022

Here’s a problem. I made a simple multiplication table practice game in swift ui.

I have some images of letters that I want to turn into words.

Works fine so far

Now I need to use those images of numbers to make equations for the player to solve.

Great game, only $9.99

Swift UI wont reload the view: Solution One

I downloaded this font from a fantasy bundle…

First problem I had was, the view wont reload the subviews even when I explicitly tell it to.

When I press the submit button, the submit() function is called and should changed the equation. All of the properties are properly updating but the view is not.

Solution:

The subview Word has a @State property called characters. For whatever reason, if this is a @State then swift will not redraw it when you try to change it in a super view. Remove the @State and the view will redraw.

TLDR

Swift UI wants you to control the state of one thing at a time and one thing only. Changing the state of a parent view will not effect the state of any children.

Making Swift UI reload the views:

So now my app can show some numbers, but then I got a crazy bug. Whenever there is a 2 digit number, the app crashes with an index is out of bounds. WTF???

I know the Word view can handle long strings because my app is full of them. And in the preview I have multiple digit numbers displayed.

Handling large numbers just fine in the preview.

But crashes when the random number is 2 digits.

It’s supposed to show 12 x 11

Then I try to force it:

currentEquation = Equation(intOne: 12, intTwo: 11, size: 50)

It’s still not working. Why?

It’s saying that I’m getting an out of bounds in this code.

ForEach(0..<characters.count) { index in
Letter(character: characters[index].uppercased())
}

How can that be possible if I feed in the string, it counts it, then I iterate over it?

What’s happening

Swift UI is not recreating the entire view object when I tell it to.

@Published var currentEquation: Equation = Equation(intOne: 10, intTwo: 10, size: 50)

This is crashing when I change it like this:

currentEquation = Equation(intOne: 1, intTwo: 1, size: 50)ForEach<Range<Int>, Int, Letter> count (1) != its initial count (2). `ForEach(_:content:)` should only be used for *constant* data. Instead conform data to `Identifiable` or use `ForEach(_:id:content:)` and provide an explicit `id`!Swift/StringRangeReplaceableCollection.swift:302: Fatal error: String index is out of bounds2022–07–07 14:45:48.360655+0900 MutiplicationPractice[41248:15123306] Swift/StringRangeReplaceableCollection.swift:302: Fatal error: String index is out of bounds

Once I make the Word view, it’s creating the ForEach object (which returns a view I think). Then when I change the currentEquation property. But it’s crashing. Swift UI somehow is redrawing the characters without recreating the ForEach. Or recreating anything for that matter.

This code .onAppear { print(“Word View created”) } attached to the Word view is only called once.

Let’s see how many times the Letter view is being drawn.

Super View, the 2 Word Views, then each word view has 1 letter view. Thats it. OnAppear will not get called again. I’m only changing the state of those objects. Super efficient but also super annoying. How do I get the behavior I want? I want Swift UI to redraw those views, an you can see that when it doesn’t it causes a huge amount of problems.

Set the @State to a view. So I expect that when I change the state to a new view, the old view will be entirely erased and a new view created. That’s not happening here.

I think the problem is that Swift UI hates complex states. It wants simple datatypes (string, int, etc). When you give it anything else, unexpected behavior emerges.

How can I fix this?

As you can see, Swift UI does not follow traditional object oriented thinking paradigms. We need to think of the app as a state of simple data, and the views reflecting that.

Swift UI won’t redraw entire subviews. It’s creating them and changing only the stuff that is different. Let’s respect that.

I change all the code. No @Published or @State properties are views. The result?

Literally exactly where I started hours ago. OnAppear is only called once, so Swift UI will NEVER update the Word view even when its parameters are a @Published property.

Right now I have a bunch of cascading views. Maybe passing the variable around too much is making Swift UI forget that it should be redrawing it.

Let try it by moving the Word view up the hierarchy.

Nope

Nothing. Let’s try sending the @Published string directly to the Letter view itself. That should work right?

Yes, here we can see it is happy to pull in the new images. So what’s going on here? Let’s make a spike and do some experiments.

Let’s start by making a simple spike with a sub view and a sub sub view.

Now lets see how it reacts when we try to recursively add a @State string.

Chaining parameters works. If a param is a @State object from a super-super view, it will still redraw when changed. So why isn’t it working on my app? Let’s see if ForEach is causing the problem.

Boobies wont change to titties no matter how many times I tap it.

So I recreated the problem in a spike. Even with simple Text objects it still is not changing. Makes sense. The array is created with the onAppear method. That method is only called once. How do I call it again? I’ve still not discovered how to force a view to redraw. Instead, let’s reverse our thinking. Maybe the way to think about it is, the state isn’t the word we want to process, instead we should process the word and that should be the state.

Boom goes the dynamite

Now it’s starting to make sense. If boobies changes to titties, that infers that…

It’s working! IT’S WORKING!

To surmise

If you’re having trouble with getting view to reload like I do, try to reverse your thinking. Swift UI is lazy and won’t remake views. On Appear is only called one time (because views are never redrawn until they are off the screen completely). So I moved the logic to a place I control. The parent view. (e.g. Can’t have the Word View make letters because the letters need to change. So I make the letters and give that to the Word View.)

Is there something in Swift UI that will run a closure when a property changes? Please let me know. Thanks for reading.

P.S.

Thanks to the artist for the great assets. They can be found here -> https://craftpix.net/freebies/kids-fantasy-game-gui/?utm_campaign=Website&utm_source=itch.io&utm_medium=public

Completed app can be found here -> https://github.com/liyicky/MushroomMultiples

--

--

Liyicky

Hey, I’m Jason Cheladyn. Going back to the coding world after 6 years of teaching English in Japan. https://www.twitter.com/liyicky https://www.liyicky.com