September 9, 2019

    Behind SwiftUI Previews

    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 normal build - a typical build as defined by the active Xcode scheme and build settings
    • the Previews build - a separate build, that extends the normal one with additional compilation options

    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
    • Incremental changes
    • Breaking changes

    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.

    change_incrementalIncremental 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
    change_breaking

    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.

    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

    @_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(sourceFile:) attribute
    • #sourceLocation(file:line:) directive
    • @_dynamicReplacement attribute
    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.

    @_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:

    #import 
    
    @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.

    Damian Malarczyk - Software Engineer

    Discover how Guardsquare provides industry-leading protection for mobile apps.

    Request Pricing

    Other posts you might be interested in