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 postAt 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 backgroundOne 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 SwiftUISo let's dive in and examine the difference SwiftUI can make for the following screen which is part of 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 = confirmPasswordFieldAnd 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. 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 ElevateTextF