Understand why SwiftUI previews keep getting paused and how to improve the situation


If you work with SwiftUI or have even just tried SwiftUI previews, then you’ve seen this annoying message: Automatic preview updating paused
. To some devs, it happens all the time and is extremely frustrating.
In this article, I’ll explain why this happens and how it can be solved. Let’s dig in!
Let’s follow the crumbs to understand what happens.
When we get the message, the (i) symbol offers some more context:
Automatic preview updating pauses when the preview file is edited in a way that causes the containing module to be rebuilt.
It kind of makes sense. Changing any code in a module (for example your app module) normally warrants a rebuild to get the new changes into the product. But why is it suddenly a problem? Isn’t that what happens anyway? Turns out, no. Let’s have a look at how to live editing works.
Turns out, when we’re editing a preview live (when it works as it should), Xcode doesn’t rebuild the module on every single change.
When previews are activated, Xcode builds the current scheme for testing. When you make subsequent changes, there is no re-building happening to reflect changes on the canvas.
Swift has a special feature to support live changes of SwiftUI previews — called dynamic function replacement
. The attribute for it is @_dynamicReplacement(for:)
. This feature did not go through the formal evolution process – it was pitched and implemented back in 2018, ahead of the initial reveal of SwiftUI.
struct MyStruct {
dynamic func x() {
print("x")
}
}extension SettingsRoute {
@_dynamicReplacement(for: x())
func y() {
print("y - replaced dynamically")
}
}
Whenever x()
is called, y()
‘s implementation will be used instead. This is Swift’s reply to swizzling.
Because ‘hot reloading’ is implemented using dynamic replacement, it has limitations:
- It’s applicable only when we change the implementation of a function, computed variable, initializer or subscript.
- For SwiftUI previews, dynamism is applied to all declarations in the current file.
So it’s actually easier to list when previews can be updated live.
Any other changes are not supported by dynamic replacement. For example:
- changing function signature in any way (even just changing from
internal
toprivate
) - adding or removing functions or variables
- changing initial value of a non-computed property
- doing any edits in other files
And since these changes don’t fall under dynamic replacement, the previews have to get paused until the next proper rebuild.
If you want to learn more about the under-the-hood workings of SwiftUI previews, I like Behind SwiftUI Previews by Damian Malarczyk.
For me, understanding why things are already happening helps with the frustration. But let’s see how to fix it
So what can we do to improve the situation?
First, we can write our code in a way that allows for more dynamic replacement.
But there would still be a lot of situations when previews have to pause. Come to think of it, what are we most annoyed about? Having to resume them! So let’s automate that.
The most common cause of previews pausing I’ve seen is variable declarations:
If you have a variable declaration with initial value, editing it will pause the previews:
var color = Color.red // change to Color.green, previews are paused
This applies to any variables in the current file — global or instance variables.
To fix it, change the variable to be computed. That way, it can be dynamically replaced when edited:
var color: Color { Color.red }
Another possible reason is build scripts in Build Phases that cause changes to the project. It could be build number incrementing or code generation. I haven’t run into it myself, but if you get unexplained pauses — it’s a direction worth looking into.
Imagine this — you’re making changes to the code, and previews get paused. You try to resume previews — they fail. Possibly due to build errors, but errors are not always properly surfaced. Maybe you forgot to change test code — errors there also cause previews to fail (because previews are built for testing).
To figure the best way to solve this, I took a step back. What do we do to verify our code changes normally? We build or run. Then we either get more compilation errors and continue fixing them, or all succeeds and we’re happy.
I figured, previews should auto-resume after a successful build. And it’s been working pretty well for me so far.
I couldn’t find any programmatic way to resume previews in Xcode, but we can trigger the keyboard shortcut.
Here’s just two simple steps to set it up:
First, place the script somewhere in your system, for example in ~/scripts
:
touch ~/scripts/cmd_opt_p.sh
chmod +x ~/scripts/cmd_opt_p.sh
open ~/scripts/cmd_opt_p.sh #edit with your favorite editor
Then paste this:
#!/bin/sh
osascript -e 'tell application "System Events" to keystroke "p" using {command down, option down}'
Second, configure Xcode to trigger this script whenever a build succeeds:
In Preferences -> Behaviours -> Build Succeeds -> Run
select the newly created script:
Voila! First time the script triggers, it’ll ask to allow Xcode to use accessibility features to control the computer.
Here’s a couple of things to keep in mind:
- If Xcode is not the active application by the time a build operation succeeds, the shortcut will be triggered in the currently focused application. Cmd+Option+P is not a common shortcut, so most likely it won’t do anything. If you’d like it to force-switch to Xcode, add this to the script:
osascript -e 'activate application "Xcode"'
. - After a normal build succeeds and the shortcut is triggered, a second build is made — a special build for previews. I think that it’s fine — since the first build succeeded, the second one will be very fast. Still faster than pressing Cmd+Option+P manually.
- There’s no easy way to distinguish between builds for running and build for testing from Xcode Behaviours. If tests fail to build, previews will fail to resume, even if build for running succeeded and triggered the automation. When this happens, instead of
Cmd+B
useCmd+Shift+B
to trigger a build for testing. Once it succeeds, the automation will be triggered again.
Even though this method is not perfect, it works quite well for me, both in SwiftUI and UIKit projects.