Guardsquare Devblog

Technical notes from the Guardsquare engineering teams.

Behind SwiftUI Previews

Posted at — Sep 10, 2019

Recently, Apple introduced ‘SwiftUI’, a new framework for building native UIs across all Apple’s platforms. The core selling point of the framework is that it allows defining application interfaces in a declarative way.

SwiftUI is expected to significantly simplify the process of making applications, largely due to its deep integration with Xcode in the form of SwiftUI Previews. This new feature in Xcode 11 enables the visualization and editing of UI elements under different conditions without recompiling or even re-running the application.

In this blog, we take a look under the hood of SwiftUI Previews and discuss the language features that were added to make SwiftUI Previews possible.

Previews in Xcode

The new Xcode preview functionality requires two main components, a preview build of the user’s application and a new Xcode extension providing a live view of an application. In this article, we’ll refer to the first as the preview application, or preview binary when referring to the executable of the application, and the latter as Xcode’s Preview extension.

In order to generate a live interface in the Preview extension, Xcode manages two build variants of an application:

The Previews build resides alongside the normal build in the Xcode build artefacts. These builds don’t share any object files or other artefacts; therefore, the normal build remains unaffected by the Xcode’s Preview extension integration. The additional options used in the Previews build enable its dynamic behaviour and allow for the interactive UI modifications.

The normal build of an application produces application’s main binary and optionally frameworks and/or plugins. In the case of the Previews build, Xcode additionally generates a derived source file for each of the original source files involved in a previewable view. The derived source file modifies the original source code in a way that allows for granular UI modifications and replaces the original file’s implementation at runtime through method swizzling.

The Xcode build system compiles each of the derived source files into a separate standalone dynamic library. The libraries will later on be loaded by the preview application. Modifications shown by the Xcode Preview extension rely on the modifications made by these derived source files to reflect the application interface changes.

Code changes propagation

The new Preview extension is able to display UI changes without the need for recompilation in most cases. In order to clearly define the impact of any potential code change on the Preview extension, we divide them into the following three distinct groups:

Minor tweaks do not require any code compilation. They leverage the new dynamic capabilities of Swift runtime to update the UI. An example of this type of change is modification of a string literal.

Incremental changes

Incremental changes result in regeneration and recompilation of the derived source files. These could be text colour changes, font size modifications, or any other code modification that doesn’t only change literals. This type of change results in an activity indicator below the active Xcode’s Preview extension interface.

The last type of changes, breaking changes, require full recompilation of the preview binary and always cause the automatic preview generation to be paused. This would happen when a significant amount of source code is altered or when the binary compatibility between the application and the derived source code libraries is broken. A typical example is the addition of a new instance property to a type.

Breaking changes

Build artefacts

To examine any newly introduced build artefacts, we created an example SwiftUI application, ‘SwiftyApp’. Inside the build artefacts of the normal build there is now a new intermediate folder ‘Previews’:

.
├── Intermediates.noindex
│   ├── Previews  [NEW]
│   ├── SwiftyApp.build
│   └── XCBuildData
└── Products
    └── Debug-iphoneos

Inside the Previews build artefacts we notice the same typical structure minus the ‘Previews’ folder. The preview sources derived from the original source code can be found in the ‘Objects-normal’ directory.

.
└── SwiftyApp
    ├── Intermediates.noindex
    │   ├── SwiftyApp.build
    │   │   └── Debug-iphonesimulator
    │   │       └── SwiftyApp.build
    │   │           ├── Base.lproj
    │   │           ├── DerivedSources
    │   │           └── Objects-normal [SwiftUI Derviced Sources]
    │   └── XCBuildData
    └── Products
        └── Debug-iphonesimulator
            ├── SwiftyApp.app
            │   ├── Base.lproj
            │   │   └── LaunchScreen.storyboardc
            │   ├── SwiftyApp.momd
            │   └── _CodeSignature
            └── SwiftyApp.swiftmodule

Derived Source Files for Previews

Focussing on a single view in our ‘SwiftyApp’ example we can compare an original view source and a derived one. You’ll find similar file name for the derived sources with the addition of a ‘preview-thunk’ suffix.

TextView.swift

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import SwiftUI

struct TextView : View {
    var body: some View {
        Text("Hello Guardsquare!")
    }
}

#if DEBUG
struct TextView_Previews : PreviewProvider {
    static var previews: some View {
        TextView()
    }
}
#endif

TextView.3.preview-thunk.swift

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@_private(sourceFile: "TextView.swift") import SwiftyApp
import SwiftUI

#if DEBUG
extension TextView_Previews {
    @_dynamicReplacement(for: previews) private static var
    __preview__previews: some View {
        #sourceLocation(
          file: ".../iOS/SwiftyApp/SwiftyApp/TextView.swift",
          line: 20
        )
        AnyView(
          __designTimeSelection(
            TextView(),
            "#7464.[2].[0].[0].[0].property.[0].[0]"
          )
        )
#sourceLocation()
    }
}
#endif

extension TextView {
    @_dynamicReplacement(for: body) private var
    __preview__body: some View {
        #sourceLocation(
          file: ".../SwiftyApp/SwiftyApp/TextView.swift",
          line: 13
        )
        AnyView(
          __designTimeSelection(
            Text(
              __designTimeString(
                "#7464.[1].[0].property.[0].[0].arg[0].value",
                fallback: "Hello Guardsquare!"
              )
            ),
            "#7464.[1].[0].property.[0].[0]"
          )
        )
#sourceLocation()
    }
}

It’s clear that each of the expressions from the original source file is wrapped in a _designTime-prefixed function call, e.g. __designTimeSelection, __designTimeString. These wrappers are the bridge between custom views in the preview binary and Xcode’s Preview extension. Their signature shows us the two wrapper arguments; some kind of id string and the original value, as implemented in the user code. This mechanism is what enables the fine-grained replacement of views and their contents by the Xcode Preview extension.

Using ‘nm’ to list all designTime-symbols below, we also identify a __designTimeValues dictionary. The first string argument to the wrapper functions is used as the key to this dictionary. The second argument, as the label indicates, is the fallback value.

SwiftUI.__designTimeFloat<A where A: Swift.ExpressibleByFloatLiteral>(_: Swift.String, fallback: A) -> A
SwiftUI.__designTimeString<A where A: Swift.ExpressibleByStringLiteral>(_: Swift.String, fallback: A) -> A
SwiftUI.(__designTimeValues in _4ACD7C27549C2B22539AB532181AB7F8) : Dictionary<Swift.String : Any>
SwiftUI.__designTimeBoolean<A where A: Swift.ExpressibleByBooleanLiteral>(_: Swift.String, fallback: A) -> A
SwiftUI.__designTimeInteger<A where A: Swift.ExpressibleByIntegerLiteral>(_: Swift.String, fallback: A) -> A

Language Features

In addition, when inspecting the derived source code files, several new language features immediately pop out:

Private imports

By default, when a Swift module B imports another module A, only definitions with a public or higher access level modifier are visible to B. Private imports allow to additionally expose entities with private or internal access level. They do not, however, allow to import whole modules or types, instead specific source files of the imported module have to be specified explicitly. The attribute requires the imported module to be compiled using the -enable-private-imports flag.

1
2
3
@_private(sourceFile: "TextView.swift") import SwiftyApp
// extending private type from TextView file
extension TextView { ... }

Private imports are required in the context of SwiftUI Previews because the argument used in the @_dynamicReplacement(for:) attribute in the derived source files requires a specific entity reference.

#sourceLocation

This slightly more exotic compiler directive is responsible for forcing accurate debug information in the derived source files. It causes the compiler to basically copy the debug information from the user code that triggers code generation to the actual generated code. This way developers can e.g. trace a crash in the generated code back to their own original source.

@_dynamicReplacement

This attribute is used to leverage Swift’s native method swizzling and is the secret behind the dynamism of the Xcode’s Previews extension. We have covered the feature in Swift Native method swizzling article. Relevant detail here is that the mechanism requires explicit opt-in through the dynamic modifier.

How is then Xcode able to leverage method swizzling in user code?
The answer is: through a new compilation flag -enable-implicit-dynamic. The flag makes the compiler assume that every function could be swizzled, without the need to explicitly specify it in the source.

SwiftUI Previews bootstrap process

The last piece of the puzzle is how Xcode is able to display the UI of a running preview application. We won’t go into too much detail here, but instead list a few interesting points.

First of all, note that no additional code is compiled into the preview application. It also isn’t linked with any other dynamic library than those used in the normal build of the application. Rendering of the previewed UI is managed entirely by the system frameworks. This can be seen by setting an environment variable XCODE_RUNNING_FOR_PREVIEWS=1 when running the application from the normal build, which will result in an empty UI window.

Besides the use of the environment variable, a new XCPreviewKit framework is dynamically loaded at runtime into the preview application. The framework is used to manage the view hierarchy of additional interface windows. Those additional windows are used to render the interface displayed in the Xcode Preview extension.

Xcode uses interprocess communication (XPC Services API) to display specific UI elements in the running preview application. Usage of XPC can be seen in the interface headers of the new Xcode 11 plugins UVKit & UVIntegration frameworks. e.g. the UVDTXSpringBoardControlMessage.h header from UVIntegration, that explicitly mentions XPC:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#import <DTXConnectionServices/DTXConnectionServices.h>

@interface UVDTXSpringBoardControlMessage : DTXMessage

+ (instancetype)messageForKillingPID:(NSNumber *)pid;
+ (instancetype)messageForStartObservingPID:(NSNumber *)pid;
+ (instancetype)messageForProcessIdentifierForBundleIdentifier:(NSString *)bundleIdentifier;
@end

@interface UVDTXXPCDebuggingMessage : DTXMessage
+ (instancetype)messageForXPCServiceDebuggingIdentifier:(NSString *)identifier 
                                            environment:(NSDictionary *)environment
                                                options:(NSDictionary *)options;
@end

To verify XPC usage, it is possible to attach a debugger to a running preview application and set breakpoints on XPC communication functions. Or an XPC snooping tool, such as XPoCe by Jonathan Levin, could be used.

Summary

Looking at how Xcode internally manages the SwiftUI Previews, we were able to discover new compiler features that made its implementation possible. Reaching the goal of ABI stability in Swift 5 was a big milestone and has opened multiple new possibilities. It did not, however, slow down the language evolution. It is exciting to witness how Swift is becoming a full-fledged citizen of the Apple ecosystem.