SwiftUI is a new framework that creates user interfaces for iOS and other Apple operating system applications. SwiftUI is completely programmatic (user interfaces in SwiftUI are built exclusively by writing Swift code). This is in stark contrast to previous frameworks such as UIKit which use a combination of Swift or Objective-C code and storyboards. Storyboards are graphical interfaces which allow engineers or designers to build user interfaces without writing code.
As someone who prefers solutions that involve writing code exclusively, SwiftUI is a welcome addition to the iOS landscape and a positive change in direction from Apple. Using GUIs such as storyboard to build interfaces have many downsides, such as difficulties when working in large groups on a single UI file and confusing merge conflicts when using version control systems. Since I'm very passionate about programming (as most software engineers are), altering values in a GUI to change a UI is a lot less exciting than coding!
This article introduces SwiftUI and walks through a page in an existing iOS application that I converted to SwiftUI from UIKit and Storyboard. I also describe the steps needed to integrate SwiftUI components into a UIKit application. All the code discussed in this article comes from my SaintsXCTF iOS application and lives in a saints-xctf-ios repository.
SwiftUI is a new approach for creating user interfaces in Apple operating system applications, such as iPhone and Apple Watch apps. Released in September 2019, SwiftUI allows UIs to be built entirely in Swift code. Apple has excellent tutorials for getting started with SwiftUI and for learning how to code SwiftUI views in XCode.
SwiftUI is a declarative UI framework, similar to React.js. Declarative programming frameworks involve coding the desired end state instead of coding iterative commands to achieve an end state. Behind the scenes, SwiftUI is able to translate the desired end state into iterative commands which build the UI. Engineers do not need to worry about these details as SwiftUI abstracts them away. Declarative frameworks are often easier to work with and faster to code in. A comparison in the front-end programming world is how the declarative library React.js is easier to work with then the iterative library JQuery for building web pages.
In my SaintsXCTF iOS application, the page I converted from UIKit and Storyboard to SwiftUI is the create exercise log page. When loaded, this page appears like so:

Users enter details about their exercise, such as the time taken and mileage.

After entering information about an exercise, users can create the exercise log, which displays it on the application home page and their profile page.

User's can also edit existing exercise logs and resubmit them.


SwiftUI applications are split into many views (components) that make up the user interface. For my exercise log page, there are two top level views. The first, CreateExerciseLogView
, is for creating a new exercise log. The second, EditExerciseLogView
, is for editing an existing exercise log.
CreateExerciseLogView
, which is located in a CreateExerciseLogView.swift file, has the following content:
import SwiftUI
struct CreateExerciseLogView: View {
@StateObject var log = ExerciseLog()
@StateObject var meta = ExerciseLogMeta(isExisting: false)
@StateObject var createLog = CreateExerciseLog()
@StateObject var form = ExerciseLogForm()
var body: some View {
ExerciseLogView(log: log, meta: meta, createLog: createLog, form: form)
}
}
EditExerciseLogView
, which is located in a EditExerciseLogView.swift file, is coded very similarly.
import SwiftUI
struct EditExerciseLogView: View {
@ObservedObject var log = ExerciseLog()
@StateObject var meta = ExerciseLogMeta(isExisting: true)
@StateObject var createLog = CreateExerciseLog()
@StateObject var form = ExerciseLogForm()
var body: some View {
ExerciseLogView(log: log, meta: meta, createLog: createLog, form: form)
.onAppear(perform: {
if !meta.existingLogInitialized {
form.displayedTime = log.time
}
})
}
}
Both CreateExerciseLogView
and EditExerciseLogView
are structs of type View
. The View
protocol defines a struct that is part of a user interface. When a custom view is created that follows the View
protocol, the body
property of type some View
must be implemented. Both CreateExerciseLogView
and EditExerciseLogView
have bodies containing an ExerciseLogView
view. ExerciseLogView
is the main view holding the create/edit exercise log form. It takes four objects as arguments which help customize the views appearance: log
, meta
, createLog
, and form
.
These four variables are initialized with the @StateObject
annotation in both CreateExerciseLogView
and EditExerciseLogView
; except for log
in EditExerciseLogView
, which is initialized with @ObservedObject
. These annotations are property wrappers which label variables that hold state in Swift views. Swift views are controlled by application state; every time the state within a view changes, the view re-renders. For a more in-depth analysis on these annotations, check out this article on SwiftUI property wrappers1. In short, @ObservedObject
holds an object in application state with a value passed from another view. @StateObject
also holds an object in application state, but it is initialized within the current view. In other words, @ObservedObject
watches a state object that is owned by another view, while @StateObject
creates a state object in the current view.
In the EditExerciseLogView
view, log
is annotated with @ObservedObject
because its parent view sends information about a log that needs to be edited. All the other state objects are created in and owned by CreateExerciseLogView
and EditExerciseLogView
.
Objects annotated with @StateObject
and @ObservedObject
must conform to a ObservableObject
protocol. For example, the log
object is a class of type ExerciseLog
, which is defined in ExerciseLog.swift.
import SwiftUI
class ExerciseLog: ObservableObject {
@Published var id: Int? = nil
@Published var name = ""
@Published var location = ""
var date = Date()
@Published var exerciseType = ExerciseType.run
@Published var distance = ""
@Published var metric = Metric.miles
@Published var time = ""
@Published var feel = 6.0
@Published var description = ""
func reset() {
name = ""
location = ""
date = Date()
exerciseType = ExerciseType.run
distance = ""
metric = Metric.miles
time = ""
feel = 6.0
description = ""
}
}
ExerciseLog
contains properties representing the attributes of an exercise log. Some of these properties, such as id
, are annotated with @Published
. When values of properties with the @Published
annotation change, the UI updates accordingly2. Properties without @Published
do not cause UI re-renders when their values change.
reset()
is simply a helper method to revert properties to their default values.
The other three state objects in CreateExerciseLogView
and EditExerciseLogView
are coded similarly to ExerciseLog
. meta
of type ExerciseLogMeta
(located in ExerciseLogMeta.swift), createLog
of type CreateExerciseLog
(located in CreateExerciseLog.swift), and form
of type ExerciseLogForm
(located in ExerciseLogForm.swift) are shown below.
// ExerciseLogMeta.swift
import SwiftUI
class ExerciseLogMeta: ObservableObject {
init(isExisting: Bool) {
self.isExistingLog = isExisting
}
var isExistingLog = false
var existingLogInitialized = false
}
// CreateExerciseLog.swift
import SwiftUI
class CreateExerciseLog: ObservableObject {
@Published var creating = false
@Published var created = false
@Published var error = false
@Published var errorMessage: String? = nil
func createExerciseLog(exerciseLog: ExerciseLog, completion: @escaping () -> Void) -> Void {
creating = true
created = false
error = false
errorMessage = nil
let log = convertToLog(exerciseLog)
APIClient.logPostRequest(withLog: log, fromController: nil) {
(newlog) -> Void in
self.creating = false
if newlog != nil {
self.created = true
completion()
} else {
self.error = true
self.errorMessage = "Failed to Create New Exercise Log"
}
}
}
func updateExerciseLog(newLog: ExerciseLog, existingLog: Log, completion: @escaping () -> Void) -> Void {
creating = true
created = false
error = false
errorMessage = nil
let log = convertToLog(newLog, existingLog)
log.log_id = existingLog.log_id
APIClient.logPutRequest(withLogID: log.log_id!, andLog: log, fromController: nil) {
(newlog) -> Void in
self.creating = false
if newlog != nil {
self.created = true
completion()
} else {
self.error = true
self.errorMessage = "Failed to Create New Exercise Log"
}
}
}
func convertToLog(_ exerciseLog: ExerciseLog, _ existingLog: Log = Log()) -> Log {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let formattedDate = dateFormatter.string(from: exerciseLog.date)
let timeFormat = "00:00:00"
let end = timeFormat.index(timeFormat.endIndex, offsetBy: 0 - exerciseLog.time.count - 1)
let formattedTime = "\(timeFormat[timeFormat.startIndex...end])\(exerciseLog.time)"
let user = SignedInUser.user
let log = existingLog
log.name = exerciseLog.name
log.username = user.username
log.first = user.first
log.last = user.last
log.location = exerciseLog.location
log.date = formattedDate
log.type = exerciseLog.exerciseType.rawValue
log.distance = Double(exerciseLog.distance)
log.metric = exerciseLog.metric.rawValue
log.time = formattedTime
log.feel = Int(exerciseLog.feel)
log.log_description = exerciseLog.description
return log
}
}
// ExerciseLogForm.swift
class ExerciseLogForm: ObservableObject {
@Published var isEditingName = false
@Published var nameStatus = InputStatus.initial
@Published var isEditingLocation = false
@Published var isEditingDate = false
@Published var distanceStatus = InputStatus.initial
@Published var isEditingDistance = false
@Published var displayedTime = ""
@Published var timeStatus = InputStatus.initial
@Published var isEditingTime = false
@Published var isEditingDescription = false
@Published var showCanceling = false
func reset() {
nameStatus = InputStatus.initial
distanceStatus = InputStatus.initial
displayedTime = ""
timeStatus = InputStatus.initial
}
}
Slightly different than the others, CreateExerciseLog
not only contains state data in properties with @Published
annotations, but also has functions for making API calls. createExerciseLog()
and updateExerciseLog()
make API calls to create and update exercise logs, respectively.
As previously mentioned, both CreateExerciseLogView
and EditExerciseLogView
are wrappers around an ExerciseLogView
view, which displays a form to create or edit an exercise log. ExerciseLogView
, which is located in an ExerciseLogView.swift file, is shown below.
import SwiftUI
struct ExerciseLogView: View {
@EnvironmentObject var existingLog: ExistingLog
@ObservedObject var log: ExerciseLog
@ObservedObject var meta: ExerciseLogMeta
@ObservedObject var createLog: CreateExerciseLog
@ObservedObject var form: ExerciseLogForm
var body: some View {
ScrollView {
VStack(alignment: .leading) {
Text(meta.isExistingLog ? "Edit Exercise Log" : "Create Exercise Log")
.font(.title)
.foregroundColor(.black)
.bold()
.alert(isPresented: $createLog.error) {
Alert(
title: Text(
meta.isExistingLog ?
"An unexpected error occurred while updating the exercise log." :
"An unexpected error occurred while creating an exercise log."
),
primaryButton: .default(
Text("Cancel"),
action: {
createLog.error = false
}
),
secondaryButton: .cancel(
Text("Try Again"),
action: {
createLog.error = false
if meta.isExistingLog {
createLog.updateExerciseLog(newLog: log, existingLog: existingLog.log ?? Log()) {}
} else {
createLog.createExerciseLog(exerciseLog: log) {
log.reset()
form.reset()
}
}
}
)
)
}
ExerciseLogFormView(log: log, meta: meta, createLog: createLog, form: form)
}
.padding()
.padding(.top, 20)
}
.progressViewStyle(
CircularProgressViewStyle(tint: Color(UIColor(Constants.saintsXctfRed)))
)
}
}
In ExerciseLogView
, properties log
, meta
, createLog
, and form
are all annotated with @ObservedObject
because they are owned by parent views and are passed as parameters to the current view. This view introduces a new property existingLog
with an @EnvironmentObject
annotation. Properties annotated with @EnvironmentObject
are accessible to all other views in the application3. You can think of it as SwiftUI's mechanism for global state. If the user is editing an existing exercise log, the existingLog
property is populated with its contents.
The body
property of ExerciseLogView
initializes some elements in the user interface. The outermost element, ScrollView
, creates a view with scrollable content3. The view inside ScrollView
is defined as VStack(alignment: .leading)
. VStack
is a view whose children are arranged vertically4. The alignment: .leading
argument sets the alignment of children in VStack
on the x and y axes5. Inside VStack
is a Text
view, which is the header of the page. It either contains the text "Create Exercise Log" or "Edit Exercise Log".
Method calls can be chained on views to customize them. For example, the Text
view has font()
, foregroundColor()
, bold()
, and alert()
method calls attached to it. The first three methods customize the appearance of the text. The fourth method, alert()
, displays an alert in case an error occurs while creating or editing an exercise log.
ExerciseLogView
contains another custom view, ExerciseLogFormView
. It exists in a ExerciseLogFormView.swift file. This view along with ExerciseLogActionsView
in the ExerciseLogActionsView.swift file make up the remainder of the create/edit exercise log page. Feel free to view them and use their code in your own projects.
Integrating SwiftUI views into a UIKit application is relatively straightforward. The main approach is to wrap SwiftUI views within a UIViewController
class. For the create/edit exercise log page, I have two UIViewController
classes that wrap SwiftUI views - CreateExerciseLogViewController
and EditExerciseLogViewController
. Both are shown below and are available in CreateExerciseLogViewController.swift and EditExerciseLogViewController.swift files, respectively.
// CreateExerciseLogViewController.swift
import UIKit
import SwiftUI
class CreateExerciseLogViewController: UIViewController {
@IBOutlet weak var container: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let childView = UIHostingController(rootView: CreateExerciseLogView())
addChild(childView)
childView.view.frame = container.bounds
container.addSubview(childView.view)
childView.didMove(toParent: self)
}
}
// EditExerciseLogViewController.swift
import UIKit
import SwiftUI
class EditExerciseLogViewController: UIViewController {
@IBOutlet weak var container: UIView!
var log: Log? = nil
override func viewDidLoad() {
super.viewDidLoad()
let exerciseLog = ExerciseLog()
if let logObject = log {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let dateObject = dateFormatter.date(from: logObject.date) ?? Date()
exerciseLog.id = logObject.log_id
exerciseLog.name = logObject.name
exerciseLog.location = logObject.location ?? ""
exerciseLog.date = dateObject
exerciseLog.exerciseType = ExerciseType(rawValue: logObject.type!) ?? ExerciseType.run
exerciseLog.distance = logObject.distance != nil ? String(logObject.distance!) : ""
exerciseLog.metric = Metric(rawValue: logObject.metric!) ?? Metric.miles
exerciseLog.time = logObject.time ?? ""
exerciseLog.feel = Double(logObject.feel)
exerciseLog.description = logObject.log_description ?? ""
}
let existingLog = ExistingLog(log ?? Log())
let childView = UIHostingController(rootView: EditExerciseLogView(log: exerciseLog).environmentObject(existingLog))
addChild(childView)
childView.view.frame = container.bounds
container.addSubview(childView.view)
childView.didMove(toParent: self)
}
}
Within both the CreateExerciseLogViewController
and EditExerciseLogViewController
views, a variable container
of type UIView
exists. This container view holds SwiftUI views. Configuring container
to hold SwiftUI views is done within the viewDidLoad()
method.
The UIHostingController()
class is used to create a UIKit view controller for SwiftUI views6. Once a SwiftUI view is wrapped within a UIHostingController()
instance, it can be added as a subview to a UIKit view.
The main difference between CreateExerciseLogViewController
and EditExerciseLogViewController
is that CreateExerciseLogViewController
wraps SwiftUI views within UIKit using UIHostingController(rootView: CreateExerciseLogView())
, and EditExerciseLogViewController
wraps SwiftUI views within UIKit using UIHostingController(rootView: EditExerciseLogView(log: exerciseLog).environmentObject(existingLog))
. The SwiftUI view CreateExerciseLogView
takes no arguments while EditExerciseLogView
takes a single log
argument. The exerciseLog
object passed to EditExerciseLogView()
is assigned to a property annotated with @ObservedObject
. Similarly, the exerciseLog
object passed to EditExerciseLogView().environmentObject()
is assigned to a property annotated with @EnvironmentObject
.
With the addition of SwiftUI, I'm excited to create user interfaces in Apple applications again! Configuring user interfaces in code using Swift is fun and I can't wait to learn more. All the code discussed in this article is available in my saints-xctf-ios repository on GitHub.