Hands-on code to help you get moving fast.
SwiftUI is Apple's incredible new user interface framework for building apps for iOS, macOS, tvOS, and even watchOS, and brings with it a new, declarative way of building apps that makes it faster and safer to build software.
If you're already using UIKit there's a bit of a speed bump at first because we’re all so programmed to think in terms of UIKit’s flow, but once you’re past that – once you start thinking in the SwiftUI mindset – then everything becomes much clearer.
At the launch of SwiftUI at WWDC19, Apple described it as being four things:
Having now written tens of thousands of lines of code with SwiftUI I can tell you they missed one important point off: concise. Your SwiftUI code will be maybe 10-20% of what your UIKit code was – almost all of it disappears because we no longer repeat ourselves, no longer need to handle so many lifecycle methods, and more.
Let’s dig in to how SwiftUI works…
SPONSORED Transform your career with the iOS Lead Essentials. Unlock over 40 hours of expert training, mentorship, and community support to secure your place among the best devs. Click for early access to this limited offer and a FREE crash course.
Sponsor Hacking with Swift and reach the world's largest Swift community!
In SwiftUI, View
is more or less what we had with UIView
, with two major differences:
Apple’s default sample code gives us this view:
struct ContentView: View {
var body: some View {
Text("Hello World")
}
}
You’ll notice a few things there:
some View
, which is an opaque type – watch my video on opaque return types in Swift 5.1 if you haven’t seen these before.Text
view with the text “Hello World” – that’s equivalent to a UILabel
in UIKit terms.return
keyword – that’s another Swift 5.1 change, so you might want to watch my video for that too.Remember, our views must return precisely one thing inside them – not 0, 2, or 200. So, if you want to show multiple labels you need to place them inside a container such as a HStack
, a VStack
, or a ZStack
– effectively horizontal stack views, vertical stack views, or a stack view that positions one thing on top of another.
So, if you wanted to place three labels above each other you’d write this:
var body: some View {
VStack {
Text("Hello World")
Text("Hello World")
Text("Hello World")
}
}
By default these stacks take up only the space they need, so you’ll see those three text views bunched up in the center of your screen. If you want to force them apart, you can insert a flexible space control using Spacer
, like this:
var body: some View {
VStack {
Text("Hello World")
Text("Hello World")
Spacer()
Text("Hello World")
}
}
Now you’ll see the first two text views at the top of your screen, then a large gap, then the final text view at the bottom of your screen. For a more subtle effect, use Divider
rather than Spacer
– this creates a horizontal rule between elements, giving a little visual space between them without pushing them far apart.
Everyone who has used UIKit knows the monotony of creating table views: we need to register cell prototypes, tell UIKit how many items we have, dequeue and configure cells, and more – it’s tedious, and it’s repetitive.
And it’s all gone with SwiftUI.
Tables in SwiftUI use a new List
view, which can show static or dynamic content. For example, we might have a struct that defines users, like this:
struct User {
var firstName: String
var lastName: String
}
If we wanted to show that inside rows in a list, we’d start by defining what one row looks like. We know it needs to work with a User
, so we would add that as a property. And we know it needs to show their names, so we’d make our view body return a text view.
In SwiftUI, it’s this:
struct UserRow: View {
var user: User
var body: some View {
Text("\(user.firstName) \(user.lastName)")
}
}
Finally, we can update our ContentView
struct so that it creates a couple of users then puts them in a list:
struct ContentView: View {
var body: some View {
let user1 = User(firstName: "Piper", lastName: "Chapman")
let user2 = User(firstName: "Gloria", lastName: "Mendoza")
return List {
UserRow(user: user1)
UserRow(user: user2)
}
}
}
While that works fine for simple lists, for anything more advanced you’re going to want to use dynamic content – to pass in an array of data and have SwiftUI figure out how many rows it needs, create them all, and show them all.
To make that happen, we must make User
conform to the Identifiable
protocol so that SwiftUI knows which person is which uniquely. Although it could check for this by querying every single property, that’s slow and likely to lead to false positives. So, instead we need to give our structs a unique id
value that we know won’t be duplicated by other items in the same list.
So, we could update User
to have a id
property like this:
struct User: Identifiable {
var id: Int
var firstName: String
var lastName: String
}
You can use anything you like for your identifier – strings, UUIDs, or whatever, are all fine.
Now that we have that we can create an array of our users – which could just as easily come from some Codable
input you’ve downloaded from the internet – and pass that into the list as we create it. We then pass a closure to run that configures individual rows in the list, like this:
struct ContentView: View {
var body: some View {
// create some example data
let user1 = User(id: 1, firstName: "Piper", lastName: "Chapman")
let user2 = User(id: 2, firstName: "Gloria", lastName: "Mendoza")
let users = [user1, user2]
// show that data
return List(users) { user in
UserRow(user: user)
}
}
}
Just to be clear: about half that code is just me creating some example data – the actual SwiftUI part to create and show many list rows is only three lines of code, and that’s only if you include the closing brace on a line by itself.
In fact, if you’re just showing rows like that, you can collapse it down even further:
return List(users, rowContent: UserRow.init)
Boom.
Seriously, SwiftUI will demolish so much of your code.
Before we’re done with lists, there’s one more thing: we can change UserRow
freely without worrying about how it’s used elsewhere. Remember, SwiftUI is designed for composability: we pass a user into UserRow
and let it figure out how it’s displayed, rather than always having our main views control everything like we did with UIKit.
So, if we wanted UserRow
to show two labels stack vertically, with one large and one small, and with both labels aligned to their leading edge, we’d write this:
struct UserRow: View {
var user: User
var body: some View {
VStack(alignment: .leading) {
Text(user.firstName)
.font(.largeTitle)
Text(user.lastName)
}
}
}
Beautiful.
Showing just one screen isn’t very interesting, so let’s create another.
First, we’ll say this screen should show just the last name of the user that was selected – nice and big, and in a red color:
struct DetailView: View {
var selectedUser: User
var body: some View {
Text(selectedUser.lastName)
.font(.largeTitle)
.foregroundColor(.red)
}
}
Next we want to embed the list in ContentView
inside a navigation view, which is equivalent to a navigation controller in UIKit:
struct ContentView: View {
var body: some View {
let user1 = User(id: 1, firstName: "Piper", lastName: "Chapman Yay")
let user2 = User(id: 2, firstName: "Gloria", lastName: "Mendoza")
let users = [user1, user2]
return NavigationView {
List(users, rowContent: UserRow.init)
}
}
}
By default navigation bars don’t have a title, so you should attach a title to your list like this:
List(users, rowContent: UserRow.init)
.navigationBarTitle("Users")
Of course, what we really want is for tapping items in our cells to show our detail screen, passing in whichever user was tapped.
To make that happen we need to wrap our user rows inside a NavigationLink
. This gets created with a handful of parameters, but here we’re going to use only two: where to send the user when the item is tapped, and a trailing closure specifying what to put inside the navigation item. In our case, that’s our user row with whatever user is being shown.
So, replace the navigation view with this:
return NavigationView {
List(users) { user in
NavigationLink(destination: DetailView(selectedUser: user)) {
UserRow(user: user)
}
}.navigationBarTitle("Users")
}
Now two things have happened. First, if you click the small play button at the bottom-right corner of the preview area, you’ll find you can now tap on list rows to show the detail view we made. Second, you might also have noticed that our list rows have disclosure indicators – that’s what Apple meant about SwiftUI being automatic.
Once you've spent some time with SwiftUI it's fair to say that going back to UIKit feels like going back to Objective-C – methods like viewDidLoad()
that we took for granted now seem just a bit alien, and you start to resent writing boilerplate code that effectively disappears in SwiftUI.
If you'd like to learn much more about SwiftUI, I published a massive, free SwiftUI tutorial called SwiftUI By Example, which walks you through a complete project from scratch then dives into well over 100 solutions for common problems you'll face – how do you create text fields? How do you work with Core Data? How do you animate changes? All that and more are covered, and it's online for free.
I’d love to hear your views on SwiftUI – send me a tweet at @twostraws and let me know what you think!
SPONSORED Transform your career with the iOS Lead Essentials. Unlock over 40 hours of expert training, mentorship, and community support to secure your place among the best devs. Click for early access to this limited offer and a FREE crash course.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.