SwiftUI is a modern way of building user interfaces. It appeared in iOS 13, macOS 10.15, tvOS 13, and watchOS 6. Nowadays, most apps don’t need to support iOS 12, so it’s safe to use SwiftUI in your project.
On the other hand, there’s huge code base written in UIKit. Hundreds of popular libraries appeared before SwiftUI or support old versions of iOS. That’s why in this chapter, we’ll talk not only about SwiftUI but also about mixing two frameworks.
What is SwiftUI? Is it a wrapper around UIKit or an independent platform? It’s both. Some SwiftUI controls are built on top of UIKit; others are completely new.
Basic Overview of SwiftUI
SwiftUI uses the conception of building blocks. Each block is a View. Blocks can represent the whole screen, or part of it. Blocks can contain other blocks, and this structure can be deep. A similar architecture is used in other platforms like Flutter or React Native.
Basic App
SwiftUI apps contain two initial controls, App and View, shown in Recipes 8-1 and 8-2, respectively.
import SwiftUI
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Recipe 8-1
SwiftUI App
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, world!")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Recipe 8-2
SwiftUI View
These two structs are the only pieces of code you need to run an app. It will have only one text label in the middle, but it will run.
Building Blocks
When you create a new SwiftUI project, you see two controls
:
Text – The closest control from UIKit is UILabel. Of course, it has SwiftUI specifics.
ContentView – This View is the layout of the screen. It has size of phone screen because it’s referred in your App struct.
From a SwiftUI point of view, there’s no difference between Text and ContentView. For example, this code is totally valid:
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
Text("I'm the only element on the screen :)")
}
}
}
At the same time, ContentView can be used inside another View.
The most common controls are as follows:
Text – A block of text similar to UILabel
Image – A control displaying an image, similar to UIImageView
HStack and VStack – Controls displaying a row or column of elements, one after another, similar to UIStackView
List – A scrollable list of items, similar to UITableView or UICollectionView
ZStack – A frame, showing child controls on top of each other, like UIView in UIKit
Spacer – Used in stacks to make a dynamic gap between controls
Button – A tappable element, allowing the user to control the app. Like UIButton
The preceding list is not a full list, but these controls are the most common
.
Modifying Controls
You can make small modifications
to controls without overriding them or creating new ones. For example, you may want to change the font, color, padding, background, and other properties.
In SwiftUI, it’s done by calling methods following controls constructors. For example:
Text("Hello, world!")
.font(.system(size: 14))
.foregroundColor(.blue)
A modifier can be applied not only to components, but to a group of controls. For example, if you apply a foreground color to HStack, all the inner controls will get this color automatically, unless it’s overridden.
Compositions
When you create a new control, you usually build it from existing blocks. This is a composition
. SwiftUI has three main controls for this purpose:
HStack – It shows controls horizontally one after another.
VStack – It shows controls vertically one after another.
ZStack – It shows controls on top of each other.
Recipe 8-3 is an example of such composition. It’s a custom control for a guest list shown in Figure 8-1. You have a list of contacts with photos and names. On the right side, there’s a switch. When it’s on, the contact is invited; otherwise, they are not in our guest list.
Photos will be loaded from the Web using the URL provided in pictureURL. SwiftUI has a native AsyncImage control, but it’s available only since iOS 15, so we’ll use KFImage.
Here’s an interesting part in this line: @State private var isInvited = false
It’s a state of your component, or a part of it. When toggle (in UIKit, it’s called Switch) state is changed, the variable is automatically changed too. You can access your state and take the necessary action.
How to handle state change? In an ideal case, we should have a view model that handles all the changes and stores the result in a repository. For this, we use method .onChange. Full signature looks this way:
@inlinable public func onChange<V>(of value: V, perform action: @escaping (_ newValue: V) -> Void) -> some View where V : Equatable
In a provided closure, we change the value in the internal storage or send a request to the back end. In this example, we just print it.
Note
onChange should be called right after the Toggle declaration, before styling. Styling methods return simple View, not Toggle, which won’t recognize the onChange method.
To wrap the SwiftUI basics up, Recipe 8-4 shows how to use it.
LazyVStack is a variation of VStack that doesn’t render invisible components. LazyVStack inside ScrollView is like UITableView. SwiftUI has its own List, but when you have a static set of items, it may be faster to use this composition.
Inserting UIKit Components
SwiftUI
is a relatively new framework. Most of the available libraries, including the popular ones, still don’t support it. Fortunately, there’s an easy way to insert standard UIKit UIView (or any subclass) to a SwiftUI component tree.
Creating a SwiftUI Component from a UIKit Component
Let’s say you have a complicated custom
UIView subclass MyCustomOldView that you’re not ready to port to SwiftUI. You need to make a wrapper around it, which conforms to the UIViewRepresentable protocol.
The second step is to add properties. You can add them by adding variables to your struct.
Steps 3 and 4 are implementing two functions declared in the UIViewRepresentable protocol:
This function creates an instance of your MyCustomOldView and returns it. If there are any constant settings of that component, you can set them up in this function as well. It may be helpful if you’re not using your own custom class, but standard UIKit class or class from an external library, and you need some adjustments.
This function needs to update the state of your UIKit component. You shouldn’t (re)create your UIView in this function; only apply all the changeable properties. Don’t return anything.
Example with Custom Label
In Recipe 8-5, we’ll wrap a UILabel subclass into a SwiftUI structure. Many apps have a predefined list of components with integrated styles and formatting. When the app starts working with SwiftUI, there are two options:
To add all these components in SwiftUI using Text
To wrap existing components into SwiftUI ones
The second way has an advantage if part of your app migrated to SwiftUI but another part didn’t.
If your properties can be changed dynamically, you can add the @Binding keyword.
Applying Styles with ViewModifier
We already applied several styles with modifiers. We used font, foregroundColor, frame, and others.
When you create your own component, you don’t have to provide all the adjustable properties in a constructor. It may be reasonable if you have two to three of them. But when you create a more complicated View, there can be dozens of them, and it’s very uncomfortable to use them all in a constructor.
For this purpose, you can add extensions changing your custom View and view modifiers changing the properties of View more globally.
Creating a ViewModifier
To create your own modifier, you need to add a struct conforming to the ViewModifierprotocol
.
In this struct, you need to implement one function:
func body(content: Content) -> some View
In this function, you need to set up your View and return the result.
struct ModifierName: ViewModifier {
func body(content: Content) -> some View {
content
... // Do modifications here
}
}
Chaining Modifiers in SwiftUI
To make your modifier
look like the standard SwiftUI modifiers, you need to create an extension. Depending on your modifier, you can make it globally available by extending View, or make it more targeted to your own view or some tree of components (subclasses of Text, for example).
extension View {
func applyMyStyle() -> some View {
modifier(ModifierName())
}
}
HeaderText without UIKit
Recipe 8-6 shows how we can make a modifier for Text to make a header with style matching the one from Recipe 8-5.
import SwiftUI
struct Header: ViewModifier {
func body(content: Content) -> some View {
content
.font(.system(size: 20, weight: .bold))
.foregroundColor(Color(UIColor.darkText))
.fixedSize(horizontal: false, vertical: true)
.lineLimit(1)
.frame(alignment: .center)
}
}
extension Text {
func styleAsHeader() -> some View {
modifier(Header())
}
}
Recipe 8-6
HeaderText Using ViewModifier
Use it this way:
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.styleAsHeader()
}
}
If there are properties specific to your View that can’t be applied to Context, apply them in an extension directly, before calling the modifier method. For example:
func changeMyView() -> some View {
doSomethingSpecific().modifier(SomeModifier())
}
Using ViewModifiers, you can create your own style library without subclassing Views.
Note
If you apply a modifier on a container (e.g., HStack or VStack), it applies to all of its children. That’s why it’s usually better to use pure modifiers without calling methods of the View subclass and to extend View globally.
Creating Custom Views
One of the most useful features of SwiftUI is creating custom reusable components
. Creating small adjustable building blocks gives endless possibilities in UI creation.
There are three main ways of creating custom views:
Composition of existing SwiftUI components adjusted for your needs
Wrapping UIKit components
Using Canvas for drawing your own views using lines, circles, and other primitives
Custom views can have changeable data. There are two key property wrappers:
@State
@Binding
We already discussed composition and wrapping UIKit components earlier in this chapter. Let’s review Canvas drawing and property wrappers in details.
Drawing on a Canvas
Canvas
is a SwiftUI control allowing to get access directly to CoreGraphics’ CGContext. CGContext has a number of methods for drawing, such as follows:
func addRect(CGRect) – Draws a rectangle
func addEllipse(in: CGRect) – Draws an ellipse
func addArc(center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool) – Draws an arc (part of an ellipse)
And others
If we draw a filled circle with dashed border and spin it around its center, it can look like a loading indicator (see Figure 8-2). Let’s review an example in Recipe 8-7.
import SwiftUI
let loaderTimer = Timer.publish(every: 0.1, on: .main, in: .common).autoconnect()
struct LoaderView: View {
let circleColor: UIColor
let spinnerColor: UIColor
@State private var phase: CGFloat = 0
@State private var length: CGFloat = Double.pi*12
var body: some View {
Canvas { context, size in
context.withCGContext { cgContext in
let rect = CGRect(origin: .zero, size: size).insetBy(dx: 4, dy: 4)
let path = CGPath(ellipseIn: rect, transform: nil)
You can see many magic numbers here. Try to play with them to see how the result changes.
Component State
We already used the @State keyword in several recipes. What is @State and how does it change our View?
@State is a property wrapper
, which means it generates some code behind the scenes. It creates a storage and moves our variable out of struct as it’s a value type. It’s recommended to make @State variables private and initialize them right after the declaration, without a constructor.
@State variables are used to keep the View state. Depending on component logic, it can be a text string, a Boolean value, a number, or a structure. @State variables can be directly used in layout, and even more, the layout can change them in response to the user’s actions.
@State variables shouldn’t be shared between objects. They’re not observable, and their changes can’t be handled by willSet or didSet. To run some code when the @State variable is changed, you can use three ways:
Use UI control callbacks, like onChange or onEditingChanged.
Use bindings.
Use observable objects (e.g., your view model can conform to ObservableObject).
To see the @State wrapper in action, please review Recipes 8-3 and 8-7.
Data Binding
Using @Binding instead of @State allows you to have a more powerful connection between variables and UI elements using it and share state between objects
.
If you use a view model and declare some variable there, you can set up a connection with UI elements passing the binding itself, not the value.
The advantages of this approach are as follows:
When you change Toggle or type text, it will change the value directly in your view model. It’s much easier to use it later.
If a variable changes several UI components, they’ll be automatically updated when the variable is changed.
For example, if some text field should appear only when Toggle is on, @State won’t solve the problem, but @Binding will (Recipe 8-8).
In this recipe, we use both bindings and observable objects. By using a publishable view model and bindings, we can control the Login button (it becomes inactive when one of the fields is empty) with minimum code. We can also access the entered values from the Login button callback without referencing the text fields themselves.
These features show the power of SwiftUI and its advantages compared to UIKit (Figure 8-3).
Summary
SwiftUI is a future of iOS UI development. The purpose of this chapter is not to give a comprehensive SwiftUI course, it’s too big topic for one chapter. But we talked about several interesting concepts showing power of SwiftUI, such as building user interface from building blocks, using UIKit components in SwiftUI, using ViewModifier, Canvas and Binding.