Fixing the dreaded SwiftUI paused previews
Many people report that they
have to repeatedly click ‘resume’ for SwiftUI previews to actually appear
or
preview fails with a cryptical ‘Command CompileSwiftSources failed with a nonzero exit code’ error, even when previous compile commands can be run in a shell with zero exit code
For me, this happened in a project with a shell script set as a build step before compilation.
What happened
For some reason, previews are building in a non-standard environment, and my shell script was failing. Xcode 13 however didn’t check the error code after running the script, but it continued with a compile phase, and then showed the script’s error code as an error code coming from build step.
I guess this can be very confusing in lot of other cases when you have a failing pre-build step.
Another issue is that I was copying the files into the project with cp, and this always changed the file modification time, leading Xcode to rebuild the project and invalidate the caches every time the build script ran.
Here is what Apple said about that (FB9634311):
If a project has a shell script build phase that modifies the source, what happens is:
1. Previews kicks off a build
2. The build modifies the project’s source
3. Previews detects the changes to the project’s source and kicks off another update
4. But the build from # 1 didn’t finish, so we cancel and do it again
5. Which then modifies the project’s source again…
6. (and the earth melts)While the breakage is surfaced the worst with previews, it’s also silently and constantly invalidating your build cache and making incremental builds impossible for the build system, so it’s a good thing to fix in the long term.
Solution, part 1: skip ‘run script‘ phase when building previews
This prevented generating errors during the ‘Build for previews’.
You can detect if the project is being built in preview mode, and handle it in script. In my case, I would assume that the changes were made in preceding build already, so I just terminated the script prematurely.
And to be extra sure, I cleaned up the error code at the end of the script:
This seems to have fixed one part of problem.
Solution, part 2: use rsync -t instead of cp
Instead of cp, which modifies the destination file’s modification timestamp, and causes Xcode to think that the file changed, I used rsync -t (it’s just a drop-in replacement: cp sourceFile destinationFile → rsync -t sourceFile destinationFile).
This preserves the original file’s timestamp, so when a build step is run with a same parameters as before, the file doesn’t appear changed, and Xcode is happy reusing it’s caches.