SwiftUI — Exploiting Xcode 11 Canvas

How to make best use of the previews feature within Xcode 11 while writing production ready code?

Kerem Karatal
8 min readJan 12, 2020
Xcode 11 with its Preview Canvas

Xcode 11 introduced a great new Canvas feature in 2019 alongside the SwiftUI introduction. Mattt has written a very informative blog post on it. Also if you have not watched it yet, there is a great WWDC19 talk on using Canvas and previews in Xcode 11.

Apple published a high quality set of tutorials on learning SwiftUI. However one thing that bothered me in those tutorials was that, they were still quite far from what people do in real life scenarios. Usually in a production system, you load the images and data from a backend API, and furthermore you need to do this in a way that does not block the main UI thread. Those tutorials had the benefit of working with beautiful preview screens rendered in Xcode but all that data were still loaded synchronously after the app launches.

In a real-life scenario, we may like to be able to iterate on the UI without worrying about the backend APIs, which may not even exist yet. Xcode 11 Canvas do give you the possibility to do it all within Xcode and with fast turnaround without having to build your code or use the simulators. But if you simply follow the architecture that is shown in those tutorials, you will have to change your code significantly once you start adding your backend APIs. Is there a better way of separating concerns cleanly so that you don’t have to significantly rewrite your app to make it production ready? After all, this is the kind of promise SwiftUI with Xcode 11 Canvas hinted at, but yet it is hard to find any good real-world like examples of how to go about it.

As an added benefit, the separation of concerns also introduce a very clean way for designers and engineers to work together on an app. While the designers and UI focused engineers iterate on the user experience, the “backend of the frontend” engineers can focus on how to most efficiently bring the data in and apply the necessary business logic. In fact it should be possible to create a fully mocked up and production ready user experience without any connection to backend and iterate on it continuously.

Of course much of the above have been discussed and written about in various architectural choices. I will not be re-discussing or introducing a specific architectural pattern in this blog post, but rather focus on the specific problem of making the Xcode Canvas work without using backend APIs in a production ready-like (of course making it fully production ready is beyond the scope here) setup. The sample app we will be working on, is yet another movie app, mainly because the TMDB APIs are easy to use and there are good movie image assets available (posters, backdrops etc.).

Let’s go ahead and get started, shall we?

Introducing Preview Configuration

We will be introducing a new configuration option (on top of the default Debug and Release options you would normally get in default projects. Let’s call this Preview to indicate that we will use this to see previews in the Xcode 11 Canvas.

The idea is to introduce conditional compilation so that when we want to see the preview content in the Canvas, instead of loading it from the TMDB API we will invoke the part of the code that loads it from the local preview folder. Another alternative is to make this decision in runtime rather than build time, but that requires the code to detect that it is running in the Canvas instead of the App during runtime. Since preview is not a concept that is ever needed in a running app, it is better to make this decision in build time than in runtime.

To start building this, we can start with duplicating the Debug configuration and name it the Preview configuration:

On top of the existing Debug & Release, we add a new Preview configuration

And then do not forget to introduce the Active Compilation Condition condition flag as shown in the screenshot below:

You can type “active” to filter to the relevant setting, and make sure to set PREVIEW flag

Now we are ready to use the PREVIEW flag as the conditional compilation flag in our Swift code. As you can see in the below code, we can make a compile time decision for whether we like to call the preview loading function or the one that downloads the image from the TMDB API.

Build time decision to load preview image

Now when we can swap between the Preview, Debug, Release configurations in the the Edit Schemes dialog:

In the Preview version of the build, we will see everything loaded locally and in the Debug/Release versions, we will use the API. We can keep using the Preview version during the development phase as it is a duplicate of the Debug version.

Handling Images

While in preview mode, we don’t want to download images from an Internet connection. Instead Xcode 11 introduced a new category of assets called Preview Assets. These assets are meant to be used when designing your user experience using the Xcode Canvas. They are stored under a special folder called Preview Content and they get stripped out when you ship your app to the AppStore.

Basically we need a way to get the images from the Preview Assets catalog when in Canvas (Preview configuration) and from the TMDB API when running within the released app. The slightly tricky part is that, one can be done synchronously, whereas the other needs to be done asynchronously.

Of course, the easiest solution would be to use the same placeholder image like below and forget about showing rich images:

Prototyping with placeholder images

But that simply does not lend itself to an inspiring preview. Instead, perhaps we would like to be able to use some real looking placeholder images like below:

Prototyping with real samples of images

These preview images will be stored in the Preview Assets catalog, so we don’t need to worry about bloating the size of our app. They can be downloaded manually from the TMDB API and stored in the catalog.

Let’s take the example of a MoviePoster, which displays the poster image of a movie:

MoviePoster.swift

(Line 7) Add a reference to an ObservableObject that is responsible from resolving the image either from the TMDB API or from the Preview Assets catalog. This needs to be asynchronous in Debug/Release configurations so we need to observe the changes to update our UI.

(Line 15) Call to fetch the image. Notice that when we are in Preview configuration, this should block and return the image, as we need the Canvas to update right away.

And in order to resolve the image, we introduced an ImageResolver object: (abbreviated in the snippet below, see Github repo for the full implementation)

ImageResolver.swift

(Line 2) Publish the changes to the image, which will be loaded asynchronously in Debug/Release configurations.

(Line 7) This calls into our helper function (see below) to either load the image from the API endpoint or load from it from the Preview Assets catalog.

(Line 10) This is the asynchronous image loading API to load images from the TMDB API.

And our image loading helpers are included in our PreviewSupport folder:

PreviewImage.swift

(Line 4) The simple wrapper function, that makes the compile time decision to either call our asynchronous implementation, passed through the fetcher parameter or load it from the PreviewAssets catalog.

(Line 13) The function to load from the PreviewAssets catalog. Notice that it is a blocking function and loads the data synchronously to make sure it is shown in Xcode 11 Canvas. These are all wrapped under PREVIEW flags to prevent them shipping in the production code and being called accidentally due to a bug.

Handling Data

Next thing we would like to tackle is, decoupling the data retrieval from UI development.

Let’s say we have an ObservableObject called a MovieCollection, which provides a collection of movies. In the released application, the data comes from a backend API but for the Canvas, we would like to provide the data from a JSON file stored in the Preview Content folder alongside the Preview Assets (which will end up getting stripped out for the AppStore version of the app)

Essentially the same JSON response from TMDB endpoint

Our MovieCollection object simply chooses between calling the backend API vs. loading from the stored JSON object as such below: (abbreviated in the snippet below, see Github repo for the full implementation)

MovieCollection pseudo code snippet

(Line 5) Publish the final response, so the UI can be updated asynchronously after loading the results.

(Line 10) Call the helper function fetchPreviewData to fetch the content either from the file in Preview configuration or from the TMDB API. Similar to the image loading case, again we pass in the function to load from TMDB API in the fetcher parameter.

(Line 14) The function that implements asynchronous fetching of movies.

And again similar to the image loading case, we have preview helper functions in the PreviewSupport folder:

Conclusion

In this blog post, we explored a SwiftUI project setup, where the UI can be built independently from the backend API code. The full project can be found in my Github account. I used the APIs from TMDB, but of course did not provide my TMDB API secret, so you will need to get your own from the TMDB site.

When we introduced the new build configuration Preview, it applied to both the Canvas and the simulator. So when we run things in the simulator, we also see the locally stored preview images and data. This may be desirable, so you can walk through the whole app without ever hitting the backend services. But if it is not, then this approach creates a bit of a chore where you need to frequently switch between Debug and Preview modes in the schemes editor. Perhaps in the future, Apple can introduce more direct support for this in Xcode, where it is possible to have compilation flags that only apply to the Canvas builds. Currently there is no apparent way to be able to do this.

--

--

Kerem Karatal

Technical Director at @xing_esp, prior worked at @soundcloud, @microsoft, co-founded NCompass Labs (ActiveX, Sharepoint Web Content Management)