A first look into SwiftUI

A first look into SwiftUI

July 24th, 2019

Here at Touchwonders one of our credos is “Be creative and try new things”, which is why we constantly look into new and emerging technologies. At the WWDC19 SwiftUI was introduced which has a lot of potential for mobile application development. In this post I would like to focus on how I used a tiny subset of SwiftUI in one of our production apps to set up a programmatically constructed view with drastically less code than its UIKit counterpart. The goal of this exercise it to get a first hands-on idea of what SwiftUI is and what it can do.

What I won’t cover in this post

At this point a lot has been written about SwiftUI already so the intention is not to repeat what is already out there or creating a massive linkdump. If you’re looking for tutorials or some general info on how to get started with SwiftUI I recommend this article from hackingwithswift.com.

A bit of background

One of the apps we are working on at Touchwonders is Elevate, an application that promotes health in the workplace. We chose to not use interface builder in Elevate because it generates hard to read xml code which makes merge requests a mess and it’s hard to find conflicting constraints (especially if some constraints are created programmatically and some are created in interface builder). So we prefer building our views programmatically. The downside of this approach is that it does lead to a lot of UI related code.

Using SwiftUI

So let’s dive in and examine the difference SwiftUI can make for the following screen which is part of the “create account” flow. screenshot showing the create account flow The button in the lower right corner, title and background are all reusable components so they are not included in the code but other than that this is the code using UIKit:

This is where I constructed our custom text fields.


let placeholderColor = Elevate.Colors.lightTextOnDarkBackground.ui.withAlphaComponent(ColorConstants.placeholderAlpha)
 
let fullNameField = ElevateTextField(style: .light)
let fullNameFieldPlaceholder = NSLocalizedString("create-account.full-name-field.placeholder-text", comment: "Full name")
fullNameField.attributedPlaceholder = NSAttributedString(string: fullNameFieldPlaceholder,
                                                      attributes: [NSAttributedStringKey.foregroundColor: placeholderColor,
                                                                   NSAttributedStringKey.font: Elevate.Font.h3light.font])
fullNameField.tag = CreateAccountContext.Constants.fullNameField
fullNameField.textContentType = UITextContentType.name
fullNameField.autocorrectionType = .no
fullNameField.returnKeyType = .next
fullNameField.delegate = interactor
addSubview(fullNameField)
self.fullNameField = fullNameField
 
let choosePasswordField = ElevateTextField(style: .light)
let choosePasswordFieldPlaceholder = NSLocalizedString("create-account.choose-password-field.placeholder-text",
                                                    comment: "Choose a password")
choosePasswordField.attributedPlaceholder = NSAttributedString(string: choosePasswordFieldPlaceholder,
                                                         attributes: [NSAttributedStringKey.foregroundColor: placeholderColor,
                                                                      NSAttributedStringKey.font: Elevate.Font.h3light.font])
choosePasswordField.tag = CreateAccountContext.Constants.choosePasswordField
choosePasswordField.autocorrectionType = .no
choosePasswordField.returnKeyType = .next
choosePasswordField.delegate = interactor
choosePasswordField.autocapitalizationType = .none
choosePasswordField.isSecureTextEntry = true
addSubview(choosePasswordField)
self.choosePasswordField = choosePasswordField
 
let confirmPasswordField = ElevateTextField(style: .light)
let confirmPasswordFieldPlaceholder = NSLocalizedString("create-account.confirm-password-field.placeholder-text",
                                                     comment: "Confirm password")
confirmPasswordField.attributedPlaceholder = NSAttributedString(string: confirmPasswordFieldPlaceholder,
                                                               attributes: [NSAttributedStringKey.foregroundColor: placeholderColor,
                                                                            NSAttributedStringKey.font: Elevate.Font.h3light.font])
confirmPasswordField.tag = CreateAccountContext.Constants.confirmPasswordField
confirmPasswordField.autocorrectionType = .no
confirmPasswordField.returnKeyType = .next
confirmPasswordField.delegate = interactor
confirmPasswordField.autocapitalizationType = .none
confirmPasswordField.isSecureTextEntry = true
addSubview(confirmPasswordField)
self.confirmPasswordField = confirmPasswordField

And this is where I configured the layout using constraints.


fullNameField.leftToSuperview(offset: LayoutConstants.sideOffset)
fullNameField.rightToSuperview(offset: LayoutConstants.sideOffset)
fullNameField.height(LayoutConstants.fieldHeight)
 
choosePasswordField.leftToSuperview(offset: LayoutConstants.sideOffset)
choosePasswordField.rightToSuperview(offset: LayoutConstants.sideOffset)
choosePasswordField.height(LayoutConstants.fieldHeight)
 
confirmPasswordField.leftToSuperview(offset: LayoutConstants.sideOffset)
confirmPasswordField.rightToSuperview(offset: LayoutConstants.sideOffset)
confirmPasswordField.height(LayoutConstants.fieldHeight)

fullNameField.topToBottom(of: pageTitle,
                          offset: LayoutConstants.fullNameFieldTopOffset,
                          relation: .equal,
                          priority: .defaultLow)
choosePasswordField.topToBottom(of: fullNameField,
                                offset: LayoutConstants.fieldTopOffset,
                                relation: .equal,
                                priority: .defaultLow)
confirmPasswordField.topToBottom(of: choosePasswordField,
                                 offset: LayoutConstants.fieldTopOffset,
                                 relation: .equal,
                                 priority: .defaultLow)
 
// - ensure there is always a minimum vertical space
fullNameField.topToBottom(of: pageTitle,
                          offset: LayoutConstants.minimumVerticalDistance,
                          relation: .equalOrGreater,
                          priority: .required)
choosePasswordField.topToBottom(of: fullNameField,
                                offset: LayoutConstants.minimumVerticalDistance,
                                relation: .equalOrGreater,
                                priority: .required)
confirmPasswordField.topToBottom(of: choosePasswordField,
                                 offset: LayoutConstants.minimumVerticalDistance,
                                 relation: .equalOrGreater,
                                 priority: .required)

There is a lot of constraint-related code because there are two different sets of topToBottom constraints; these ensure all fields remain visible when an error is shown on smaller devices like the iPhone SE. animated gif showing how the ui reacts to the keyboard popping up Now let’s have a look at the SwiftUI version of this code. The entire view is constructed and laid out in less lines than it took to even construct the views in the first place and is much nicer to read.


var body: some View {
    VStack(alignment: .leading) {
        ElevateTextFieldUI(placeholderText: NSLocalizedString("create-account.full-name-field.placeholder-text",
                                                              comment: "Full name"),
                           delegate: interactor,
                           secureTextEntry: false,
                           placeholderColor: Elevate.Colors.lightTextOnDarkBackground.ui.withAlphaComponent(0.5),
                           placeholderFont: Elevate.Font.h3light.font,
                           autocorrectionType: .no,
                           returnKeyType: .next,
                           autocapitalizationType: .none)
        .tag(CreateAccountContext.Constants.fullNameField)
        .frame(height: 32)
        .padding(.top, 23)
        Spacer().frame(minHeight: 7, idealHeight: 27, maxHeight: 27)
        ElevateTextFieldUI(placeholderText: NSLocalizedString("create-account.choose-password-field.placeholder-text",
                                                              comment: "Choose a password"),
                           delegate: interactor,
                           secureTextEntry: true,
                           placeholderColor: Elevate.Colors.lightTextOnDarkBackground.ui.withAlphaComponent(0.5),
                           placeholderFont: Elevate.Font.h3light.font,
                           autocorrectionType: .no,
                           returnKeyType: .next,
                           autocapitalizationType: .none)
        .tag(CreateAccountContext.Constants.choosePasswordField)
        .frame(height: 32)
        Spacer().frame(minHeight: 7, idealHeight: 27, maxHeight: 27)
        ElevateTextFieldUI(placeholderText: NSLocalizedString("create-account.confirm-password-field.placeholder-text",
                                                              comment: "Confirm password"),
                           delegate: interactor,
                           secureTextEntry: true,
                           placeholderColor: Elevate.Colors.lightTextOnDarkBackground.ui.withAlphaComponent(0.5),
                           placeholderFont: Elevate.Font.h3light.font,
                           autocorrectionType: .no,
                           returnKeyType: .next,
                           autocapitalizationType: .none)
        .tag(CreateAccountContext.Constants.confirmPasswordField)
        .frame(height: 32)
        Spacer()
    }
    .padding([.horizontal])
}

The spacer with it’s minHeight, idealHeight and maxHeight properties allows me to do the exact same thing as the autolayout priorities with again a lot less code.

Some additional code was needed to wrap our custom UIKit based text field in a view usable within SwiftUI:


struct ElevateTextFieldUI: UIViewRepresentable {
    let placeholderText: String
    weak var delegate: UITextFieldDelegate?
    let secureTextEntry: Bool
    let placeholderColor: UIColor
    let placeholderFont: UIFont
    let autocorrectionType: UITextAutocorrectionType
    let returnKeyType: UIReturnKeyType
    let autocapitalizationType: UITextAutocapitalizationType
 
    func updateUIView(_ uiView: ElevateTextField, context: Context) {
        uiView.attributedPlaceholder = NSAttributedString(string: self.placeholderText,
                                                                 attributes: [NSAttributedStringKey.foregroundColor: placeholderColor,
                                                                              NSAttributedStringKey.font: placeholderFont])
        uiView.autocorrectionType = autocorrectionType
        uiView.returnKeyType = returnKeyType
        uiView.autocapitalizationType = autocapitalizationType
        uiView.isSecureTextEntry = secureTextEntry
        uiView.delegate = delegate
    }
 
    func makeUIView(context: Context) -> ElevateTextField {
        let textfield = ElevateTextField(frame: CGRect.zero, style: .light)
        return textfield
    }
}

The struct obviously cannot have the same name as the class it is wrapping so that is why I called it ElevateTextFieldUI. However if you are all-in on SwiftUI the ElevateTextField would have also been built with SwiftUI and then this code would not be necessary.

Conclusion

One of the SwiftUI claims at the WWDC19 was that it drastically reduces the amount of codes needed. Playing with it hands-on really shows how little code you need to build a view in SwiftUI versus UIKit, which is fantastic. In this example case, around 33% less code was needed and with a more simple layout I think it would be around half of it’s UIKit counterpart. Also there is almost no constraint related code, which is amazing for applications with a longer lifespan that might require refactoring over time. If all of these pro’s can also help rid us of Interface Builder and it’s overly complicated xml files that make version control a real headache I would be very happy. It will probably take some time before our customers will profit from the SwiftUI advantages as it is only supported from iOS 13 onwards. So it is going to take time before SwiftUI will become fully ingrained in existing mobile applications.