EXPEDIA GROUP TECHNOLOGY — SOFTWARE

Lessons in SwiftUI

Getting started with SwiftUI in Vrbo iOS apps

Kari Grooms
Expedia Group Technology

--

Screenshot of SwiftUI code and UIKit code side-by-side implementing the same UI
Image by author

With the second major release of SwiftUI®, we can now use it to build an entire app for multiple Apple® platforms. After the announcements at WWDC 2020, it was clear that we needed to bring SwiftUI capabilities to the Vrbo™️ (part of Expedia Group™) iOS apps as soon as possible. Here are some of the lessons we’ve learned.

SwiftUI == Less Code

The code speaks for itself; here’s an apples to apples comparison of how to implement the simple view below in SwiftUI versus UIKit.

A mostly blank view with a label and button in the center

SwiftUI Implementation

UIKit Implementation

The code above leverages SwiftUI view modifiers added to Vrbo’s internal UI library. The UIKit equivalents to these SwiftUI view modifiers are many times larger in terms of lines of code.

SwiftUI in iOS 13 != SwiftUI in iOS 14

Just because some functionality or component exists in UIKit, does not mean that there will be an equivalent in SwiftUI. UIPageViewController, UIProgressView and others did not have a SwiftUI equivalent until iOS 14 and there are a few that still don’t (UISearchController, UIImagePickerController, etc.)¹. A complete list of UIKit components and their SwiftUI counterparts can be found here.

Until our app drops support for iOS 13, we likely won’t be able to leverage all of the new SwiftUI capabilities highlighted at WWDC 2020.

Lazy Stacks

Specifically, we need to keep our UICollectionViews around while we support iOS 13. Their replacements, LazyVStack and LazyHStack, give us the same benefit of a UICollectionView by only creating children when needed for performance. Until then, we can likely use SwiftUI Views inside of the collection view cells, but not for the collection view itself.

SwiftUI does not replace UIKit

At first I thought SwiftUI was a replacement for UIKit, but it is not. SwiftUI leverages UIKit. It is not an API written completely on top of UIKit though. For example, VStacks and HStacks do not render UIStackViews under the hood², but in some cases portions of UIKit are used.
For example, SegmentedPickerStyle leverages UISegmentedControl from UIKit and there are likely other examples where SwiftUI is dependent on some part of UIKit.

Composition order of view modifiers matters

In UIKit, we use class properties to apply styles. We use ViewModifiers to apply styles in SwiftUI and applying them in a different order may have different results.

Preview of button where the background color is only applied to the text and not the full size of the button. There is no visible corner radius being applied to the button.
SwiftUI Preview

In the example above, we applied the background color modifier before our size modifiers. We need to flip the order of these to ensure our background color fills the entire size of the button.

Preview of button where the background color is applied correctly. The background extends to the full size of the button and there is a visible corner radius.
SwiftUI Preview

Order matters when it comes to duplicate view modifiers as well. With view modifiers, the first application of a modifier takes priority over any subsequent applications.

Text is blue instead of black.
SwiftUI Preview

View modifiers can be applied to several Views at once

I could definitely see a use case where we apply some sort of.textBase() modifier at a very top level View…maybe even an entire module or entire app.

All of the text is styled with the same .body font style, except for the third line, where the font has been overridden with the .headline font style.
SwiftUI Preview

Keep logic separate from SwiftUI Views for unit testing

At the time of writing, there doesn’t seem to be an easy way to inspect views in unit tests without digging into internals (directly or via an external library). We can work around some of this though. By keeping the View logic in its own object, we can unit test that object instead of the View. Let’s look at this in practice with buttons.

Variations of buttons in the Vrbo app: icon only, default, primary, overlay, link, inverse and buttons in dark mode
Variations of buttons in the Vrbo app: small, extra small, medium, large, multi-line and disabled

We primarily use four styles of buttons in the Vrbo iOS app. Each of these button styles supports different sizes, each style has an inverse version of itself, buttons can have icons or not and we support dark mode too, so we have quite a few variations.

In SwiftUI, the method for styling buttons is the .buttonStyle() view modifier, which accepts a ButtonStyle. Internally, we have a single ButtonStyle that handles all of our variations and we’ve moved all of the view logic for these to a ButtonViewModel.

struct ButtonViewModel {    // MARK: Config    let iconMode: ButtonIconMode? // .iconOnly
let inverse: Bool
let size: ButtonSize // .large, .medium, .small, .extraSmall
let type: ButtonType // .primary, .secondary, .link, .overlay

// ...
}

Our ButtonViewModel derives most of its values for the View from the above configuration models as demonstrated in the code snippet below.

Buttons can also have the following states:

The isEnabled property lets us know whether the button is disabled and the isPressed property lets us know if the button is being pressed. We reflect these states in our UI based off of these properties as well as the configuration models.

Here’s how we use this ViewModel in our ButtonStyle.

Now, we can unit test the ViewModel without needing to instantiate a SwiftUI View. Here are some examples of unit tests.

What about SwiftUI and UIKit together?

Like a lot of other apps, Vrbo is and will be in a hybrid state of SwiftUI and UIKit for a little while. How that looks is still taking shape, but Apple provides mechanisms for interoperability.

SwiftUI inside of UIKit

You can wrap SwiftUI Views inside UIViewControllers via UIHostingController, which is how we can start using SwiftUI immediately in our existing apps. It is a definite path forward.

UIKit inside of SwiftUI

You can also wrap UIViews and UIViewControllers inside of SwiftUI Views via UIViewRepresentable and UIViewControllerRepresentable. When I was experimenting with SwiftUI initially, I rewrote several portions of the Trip Boards UI while still leveraging existing UIViews in a few places.

A Trip Board Card where the majority of the View is implemented with SwiftUI, and a small section of the card; a cluster of user avatars at the bottom of the card, is implemented with UIKit
Trip Board Card

The example above was done for proof of concept purposes, but as we move forward with SwiftUI it will be interesting to see what use cases there are for wrapping like this. My initial thought was that it might make sense to work from the inside out, where you convert your innermost UIViews to SwiftUI views first, resulting in a more one-directional approach. When using a hybrid approach, it can quickly become like the movie Inception³ where it’s a View / UIView / UIViewController within a View / UIView / UIViewController instead of a dream within a dream.

Since SwiftUI Views are more performant than UIViews, it may also make sense to go ahead and convert existing code to SwiftUI versus wrapping existing UIKit code.

// One-directional approach- UIViewController
- UIHostingViewController
- SwiftUI View
- SwiftUI View
- SwiftUI View
// Hybrid approach- UIViewController
- UIHostingViewController
- SwiftUI View
- SwiftUI View
- SwiftUI View
- UIViewControllerRepresentable
- UIViewController
- UIView
- UIHostingViewController
- SwiftUI View

SwiftUI Previews did not work

I think one of the most exciting features of SwiftUI is Previews. It is like hot reloading for web development (it’s actually probably closer to live reloading vs hot reloading). Prior to this, Interface Builder was the mechanism for “previewing” views without building the app, but these were not truly a preview of how the UI would look. Previews also go a step further and allow you to have multiple instances for various states of your UI. So you could have a preview of your view for dark mode, light mode, iPhone or iPad, side-by-side while you’re developing!

SwiftUI Preview of a property card
SwiftUI Preview

I was super stoked for these, but then when it was SwiftUI play time…they didn’t work.

Local Environment

There are a few of requirements for SwiftUI Previews:

  • Xcode 11+
  • macOS Catalina
  • Have iOS 13 or later set as your Deployment Target for your Debug configuration

I met all of these requirements for my local environment, but Previews still did not work in the Vrbo iOS app or for our internal UI library, UIToolkit. So what was up?

Naming Collisions

In both projects, there were naming collisions that were causing SwiftUI Previews to not work. You cannot have a struct/class/enum/protocol that is the same name as your project. These were the fixes:

Another alternative would have been to rename the project instead of the object names.

Project Configuration

After implementing the above fixes, Previews were still not working in the Vrbo iOS app (No!!!). The reason for this may be less common, but the Vrbo iOS app is modularized, so the app is broken up into several frameworks. The app is also still using Carthage for some dependencies. There was an error that one of our dependencies could not be loaded. Previews were looking for these in the wrong place based on our Build Settings. We needed to update the @rpath to look for frameworks in the Carthage build directory for each of our frameworks.

Screenshot of where to update the @rpath in Xcode Build Settings

While there is a lot more to dive into with SwiftUI, I hope that these initial learnings have been helpful.

Thanks for reading! ☺

--

--