It was really exciting to hear about Apples new framework called SwiftUI at WWDC 2019. This post is a quick guide to get started with SwiftUI.
At first, we will take a look at how views in SwiftUI are created, layouted and presented. Then, we will examine how state is being managed with SwiftUI. And finally, we will take a look at how SwiftUI and UIKit play together.
With UIKit we had different ways to build user interfaces. Many discussions were held whether to build UI programmatically or by using the Interface Builder. With SwiftUI those problems are gone. We finally have a declarative way to write UI in code and to preview it side-by-side with the code.
A SwiftUI view is just a usual .swift
file with two structures.
The first structure, that is usually a struct
, is the view itself. All views in SwiftUI conform to the View
protocol which requires a computed property called body
that contains the layout for the view. The second structure declares a preview for that view.
In the example, the view returned by the body property is a VStack
, a vertical stack which contains two other views, an Image
and a Text
. On the right side, Xcode generates a preview, so we can directly check our written code.
When implementing SwiftUI views, we do not have to set up constraints like with UIKit views. Instead, SwiftUI figures out the intrinsic size of each view and positions it by default in the center of it's parent view without resizing it.
The core concept for aligning views in SwiftUI is by using stacks, the VStack
for vertical or the HStack
for horizontal alignment.
As shown in the example, we can nest different stacks to achieve the desired result, the concept is really intuitive.
Additionally, there are different modifiers we can use on each view to align and position them. For example, the frame(width:height:alignment:)
wraps the view into a container of the given size and aligns the view within this container.
Other modifiers like position(x:y:)
and offset(x:y:)
can also be used to position the child in its parent’s coordinate space.
Furthermore, we can use Spacer
to align views. It is a flexible space that expands as long as there is space.
As we can see, the Spacer
helped us to move the text to the right side of the parent view.
The view's appearance can be modified by using so called modifiers. Most modifiers provided by SwiftUI are self-explanatory. We have already seen some of them in action in the examples above like font()
, background()
, padding()
etc.
It is also possible to create our own modifiers, for example if we want to reuse styles.
In the example above, we define a custom modifier named PrimaryButtonModifier
that we can now use to style our buttons. As shown in the preview, to apply the custom modifier to a view, we can use the .modifier()
modifier.
Creating a list of views is a common task when building an application. It is really simple achieved in SwiftUI with the List
view.
We just need to pass our data and an id into the List
view and then configure the rows with this data.
In our simple example, we can use the hashValue
property of String
as the id. In a real world application, we are more likely to work with structs or classes for our data definitions. In this case, we can make our objects conform to the Identifiable
protocol, then SwiftUI will automatically use it's id property.
Navigation between views is another common task that is also easy to achieve with SwiftUI.
To add navigation that is similar to a UINavigationController
in UIKit, we can embed the view in a NavigationView
to add a navigation bar and then use a NavigationLink
to set up a transition to a destination view.
In the example, we wrap a list into a NavigationView
and the rows of the list into a NavigationLink
. So when tapping a row, the user will be directed to the FruitDetailView
. Of course, instead of a list we could use any other views to setup a navigation behaviour like this by wrapping them in the same way.
A big change in SwiftUI compared to UIKit is how state is being handled. State represents the data associated with a view. With SwiftUI, when state properties change, the views are updated automatically.
To be able to achieve those automatic updates, Apple provides different property wrappers like @State
, @Binding
, @StateObject
and more. Let’s take a closer look at those property wrappers and how they relate to each other.
State
and Binding
The State
and Binding
property wrappers are used inside of View
objects and allow the view to respond to any changes made to the state properties.
Let's take a look at the following view that shows a text and a text field. We want the Text
view to always show the current input from the TextField
view.
struct ExampleView: View {
@State private var username: String = "" // 1
var body: some View {
VStack {
Text(username) // 2
TextField("username", text: $username) // 3
}
}
}
To achieve this, we
State
-wrapped property called username
.Text
view with this property. From now on, every time username
changes, the Text
view will update automatically.username
property to the text field. We recognise the binding by the fact that the property is passed in with an $
prefix. From now on, every time the text field's input changes, our username
property will also be updated.The binding in the last step is possible, because the text
property of TextField
has the type Binding<String>
. That means, it is wrapped with Binding
internally.
The difference between Binding
and State
is, that State
is used for the view's private state, whereas Binding
creates a two-way connection between a view and a state property defined outside of that view.
StateObject
and ObservedObject
While State
and Binding
are used for value types, StateObject
and ObservedObject
can be used for reference types. The reference type needs to conform to ObservableObject
.
Let's look at an example.
class User: ObservableObject { // 1
@Published var firstName: String = "" // 2
@Published var lastName: String = ""
}
struct ExampleView: View {
@ObservedObject var user: User // 3
...
}
To setup the binding between the object and the view, we
User
object conform to the ObservableObject
protocol.@Published
.user
property in our view with StateObject
or @ObservedObject
.From now on, SwiftUI will automatically update the view, when the firstName
or the lastName
of the User
object change.
The difference between StateObject
and ObservedObject
is the same as between State
and Binding
. While StateObject
is used for the view's private state, ObservedObject
creates a two-way connection between a view and a state property defined outside of that view. The view should not create the instance of the ObservedObject
itself.
EnvironmentObject
EnvironmentObject
is used in the same situations as ObservedObject
with the difference, that we don't need to pass it in through the whole view hierarchy.
It is useful for complex view hierarchies where we would have to pass the ObservableObject
through several view's initializers before it reaches the view where it's needed. With EnvironmentObject
, we only need supply the environment object within one of the view’s parents. SwiftUI will take care of the rest.
struct ExampleView: View {
@EnvironmentObject var user: User
}
SomeParentViewOfExampleView()
.environmentObject(user)
We could even make an EnvironmentObject
available in the whole app by passing it into the root view of the app.
Until SwiftUI came out, UIKit was the way to build views in iOS. So now, when we start using SwiftUI in existing projects, we need a way to combine those two worlds. The good news is, Apple provides tools to place UIKit views and view controllers inside SwiftUI views, and vice versa.
To use a UIKit view in a SwiftUI view, we wrap the UIKit view in a SwiftUI view that conforms to the UIViewRepresentable
protocol.
struct Label: UIViewRepresentable {
@Binding var title: String
func makeUIView(context: Context) -> UILabel {
let label = UILabel()
return label
}
func updateUIView(_ label: UILabel, context: Context) {
label.text = title
}
}
When implementing the UIViewRepresentable
protocol, we need to implement the two methods makeUIView
and updateUIView
. In the makeUIView
method, we can create and return our UIKit view. The updateUIView
gets called from SwiftUI every time there are state changes, so we can update our UIKit view manually.
In SwiftUI, there is no concept of a view controller, everything is a view. So we can use the same approach from above for using UIKit view controllers in SwiftUI views. Only this time, the SwiftUI view needs to conform to the UIViewControllerRepresentable
protocol which has equivalent methods as the UIViewRepresentable
protocol.
For using SwiftUI views inside UIKit, Apple provides a class named UIHostingViewController
. We simply initialize a UIHostingController
with the SwiftUI view and then use it like a standard view controller.
let vc = UIHostingController(rootView: Text("Hi"))
Just like the introduction of Swift back then, SwiftUI brings a whole new exciting world into iOS development. It might seem overwhelming at first, but in the end, SwiftUI delivers an innovative and simple way to build user interfaces taking advantage of the entire power of Swift.