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:
EditExerciseLogView, which is located in a EditExerciseLogView.swift file, is coded very similarly.
EditExerciseLogView are structs of type
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
EditExerciseLogView have bodies containing an
ExerciseLogView is the main view holding the create/edit exercise log form. It takes four objects as arguments which help customize the views appearance:
These four variables are initialized with the
@StateObject annotation in both
EditExerciseLogView; except for
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.
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
Objects annotated with
@ObservedObject must conform to a
ObservableObject protocol. For example, the
log object is a class of type
ExerciseLog, which is defined in ExerciseLog.swift.
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
EditExerciseLogView are coded similarly to
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.
Slightly different than the others,
CreateExerciseLog not only contains state data in properties with
@Published annotations, but also has functions for making API calls.
updateExerciseLog() make API calls to create and update exercise logs, respectively.
As previously mentioned, both
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.
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.
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 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
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 -
EditExerciseLogViewController. Both are shown below and are available in CreateExerciseLogViewController.swift and EditExerciseLogViewController.swift files, respectively.
Within both the
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
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
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
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.