Swift SwiftUI

The Magic behind Xcode previews for SwiftUI

The WWDC19 marked the rise of a new era in iOS development. One of the most impactful announcements was SwiftUI a new declarative UI development framework for the whole Apple echo-system. This paradigm shift had its own impact on the Swift itself and pushed it to the spaces, where many new syntaxes have been introduced like property wrappers, opaque return types, result builders etc.

All these Swift new features enabled the magic behind SwiftUI. On the other hand, Xcode 11 has its own share of these new improvements, like Xcode Previews which magically and interactively renders any change happens inside SwiftUI view. In today’s article we are going to explore the working mechanism of Xcode Previews

Xcode Previews

The SwiftUI-powered Previews feature introduced in Xcode 11 provides a revolutionary new way of building UIs, which observes any changes in the SwiftUI view and re-render/recompile the view -sometimes- instantly.

Reducing iteration cycle times can really be a big productivity booster, and that’s something that Xcode Previews can definitely help us achieve — by turning time-consuming “Build and run” cycles into almost instant updates. They’re not perfect, and can (just like Xcode itself) sometimes be a bit unstable, but they’re a big leap forward for UIKit, AppKit and SwiftUI-based UI development.

As an example, let’s say that we’re using SwiftUI to build such a simple view

import SwiftUI

struct Example: View {
    var body: some View {
        Text("Hello World")
	        .padding()
    }
}

Now as developers we could of course constantly build and run the app, navigate to a screen that its being used on, and verify that everything renders correctly but that’s boring, repetitive and error prone. This type of automation is exactly what Xcode’s Previews feature is all about.

To create a preview, all that we have to do is to define a type conforming to the PreviewProvider protocol, and place it within the Swift file that we wish the preview to appear alongside of — like this:

struct Example_Previews: PreviewProvider {
    static var previews: some View {
        TextView()
    }
}

Back to our topic, how does that preview work? By taking a look the build artifacts of the app we will notice there are two different directories one represents our SwiftUI app main build target and a newly autogenerated Previews folder that resides alongside the normal build in the Xcode build directory, the latter one has the exact same structure of the original target build.

├── Intermediates.noindex
│   ├── Previews
│   ├── Sample.build
│   └── XCBuildData
└── Products

What is happening here is that in order for Xcode to generate the live interface of the view it needs to manage two different build variants, the first one is the original application build defined by the currently active schema that eventually will produce the main app executable file, while the second one is the Previews build which an extension of the original build by adding extra functionality which on its turn will produce the preview executable file.

Derived Source Files

The original build of the target produces the main executable app, when it comes to the Previews build, Xcode will generate a derived source file for each PreviewProvider protocol conformance. By utilizing method and property swizzling, the original source file is modified in a way that allows interactive UI previewing by exchanging the implementation at runtime.

Again, by inspecting the content of the Preview build of our example we can compare the original view source and a derived one. We’ll find similar file names for the derived sources with the addition of a preview-thunk suffix in the Objects-normal directory.

@_private(sourceFile: "ContentView.swift") import Example
import SwiftUI

extension ContentView_Previews {
    @_dynamicReplacement(for: previews) private static var
    __preview__previews: some View {
        #sourceLocation(
	       file: ".../Desktop/Samples/Example/ContentView.swift",
	       line: 19
         )
        AnyView(
          __designTimeSelection(
            ContentView(),
            "#4723.[2].[0].[0].[0].property.[0].[0]"
          )
        )
#sourceLocation()
    }
}

extension ContentView {
    @_dynamicReplacement(for: body) private var
    __preview__body: some View {
        #sourceLocation(
          file: ".../Desktop/Samples/Example/ContentView.swift",
          line: 12
        )
        AnyView(
	      Text(
			  __designTimeString(
				  "#4723.[1].[0].property.[0].[0].arg[0].value",
				   fallback: "Hello, world!"
				   )
			   )
		   .padding()
		   )
#sourceLocation()
    }
}

This reveals the secret behind the dynamism of SwiftUI previews where many of Swift private APIs has been used to make this possible, so let us find out the functionality of each of them.

Private Imports

Each derived source file starts with @_private(sourceFile:) attribute, by default the views in this example are internal so they are not visible outside their module. In order to be able to import and extend them we have either to make them public which is not always the case or import them privately, this is where @_private(sourceFile:) shines. This attribute however does not support importing the whole module, instead the source file should be explicitly specified. Also, it is worth mentioning that this attribute requires that the imported module to be complied with -enable-private-imports flag.

Now with all that said, the internal types is extended like follows:

@_private(sourceFile: "ContentView.swift") import Example
extension ContentView_Previews { ... }
extension ContentView { ... }

Swizzling

The second private attribute to appear is @_dynamicReplacement. It allows to exchange the implementation of dynamic methods and properties at runtime. It is the backbone of the SwiftUI previews. This was first pitched here and hopefully will be finalized in the near future.

Swift, as a static, strongly typed language, did not previously have any built-in mechanism that would allow to dynamically change the implementation of a method at runtime. Objective-C, on the other hand, has had this since its early days through method swizzling.

Starting from Swift 5.1, Swift provides its own method swizzling mechanism that does not rely on Objective-C’s message passing by introducing dynamic keyword and @_dynamicReplacement attribute.

To enable dynamism in Swift we have two different ways, first we have to mark every method or property with dynamic keyword explicitly, to put this into perspective let us take a look at how SwifUI view would look like with dynamic attributes.

import SwiftUI

struct ContentView: View {
    dynamic var body: some View {
        Text("Hello, world!")
            .foregroundColor(.blue)
            .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    dynamic static var previews: some View {
        ContentView()
    }
}

Now with this new modifer in place we can easly exchange the implementation at runtime like the following.

extension ContentView {
    @_dynamicReplacement(for: body)
    var _body: some View {
        Text("Another implementation")
    }
}

However, this approach seems odd not only being explicitly marking each method or property with dynamic just to make it previewable also it is error-prone and not needed in the release builds which has a significant impact on the performance. To overcome this problem, Apple introduced another way around this on the Swift compiler level. A new -enable-implicit-dynamic flag introduced just to make this possible. This flag implicitly makes each property or method in that module to be dynamic without explicitly specifying it. And that’s the secret behind SwiftUI previews.

sourceLocation

This attribute is used to make the generated code return the error messages back to the original code. Now if any error happens in the derived source file it will propagate it back to the original source file in the derived source file we had the following snippet:

#sourceLocation(file: ".../Desktop/Samples/Example/ContentView.swift", line: 12)

If there’s an error in the derived file, you probably want the error message to point at the original ContentView.swift file, not the generated ContentView.preview-thunk.swift file and this is exactly what #sourceLocation() does.

Dynamic Libraries

To find the last piece of the puzzle, let take another look at the Preview build folder we inspected earlier beside the preview thunk files we will find that each derived source file is compiled to a stand-alone dynamic library like this ContentView.preview-thunk.dylib.

The idea behind compiling each previewable view as a dylib is to help Xcode incrementally recompile only the changed files, by using hot-reload functionality and if any file changes then its module or (dylib) in other words is invalidated and recompiled without the need to rebuild the whole target. Then loaded, and its dynamic method methods will be called again, hence the updates is reflected.

Conclusion

Whether you’ve been using Xcode Previews since the first beta of Xcode 11, or whether they’re completely new to you, they look and feel magical. I hope that this article has given you an idea about their working mechanism also we have mentioned some of the new Swift features behind this.

Do you enjoy using Xcode’s Preview feature? Let me know — along with your questions, comments and feedback.