Back to blog

Get Started with SwiftUI for iOS


Aasif Khan
By Aasif Khan | Last Updated on January 24th, 2024 6:42 am | 5-min read

SwiftUI is a framework to build User Interfaces (UI) for iOS apps. With SwiftUI, you create the UIs of your iOS apps entirely with Swift code, using a novel declarative approach.

SwiftUI was introduced during WWDC 2019. It’s a completely new paradigm and it changes how we think about building User Interfaces for iOS apps. Right now it’s an alternative to UIKit/Storyboards, but SwiftUI has the potential to become the standard for building UIs on iOS and beyond.

In this tutorial, you’ll learn how to use SwiftUI to build UIs for iOS. We’ll focus on the following topics:

  • What’s SwiftUI, and how is it different than UIKit?
  • How can you work with SwiftUI and its views?
  • The fundamental component in SwiftUI: views
  • Working with stacks, such as the VStack and HStack
  • Table view and navigation controller equivalents in SwiftUI
  • Working with List, NavigationView and NavigationLink
  • The beauty of declarative programming
  • The Builder pattern, and modifiers and chaining
  • Composability, and how it affects building UIs with SwiftUI
  • Answers to Frequently Asked Questions about SwiftUI!

What Is SwiftUI?

SwiftUI is a declarative framework that you can use to build User Interfaces (UIs) for iOS, macOS, Mac Catalyst, tvOS, watchOS and iPadOS.

It’s a completely new take on building UIs with Swift code, and it’s entirely different from the view controllers, XIBs and Storyboards you’re used to. SwiftUI was first announced during Apple’s Worldwide Developer Conference in 2019, and it’s widely regarded as the most exciting thing that’s happened to Swift since Swift!

Let’s take a look at an example. The snippet below declares a simple view with a text label that reads “Hello world!”

struct ContentView: View {
var body: some View {
Text(“Hello World”)
}
}

The above code declares a View with a body that includes some text. You don’t see any functions or any control flow, just declarations of UI components in a tree-like structure.

This is shockingly different from what you may be used to with UIViewController, UILabel and viewDidLoad(). In fact, SwiftUI isn’t built on top of UIKit. It replaces building UIs with UIKit entirely!

SwiftUI can use UI components from UIKit, such as a web view, thanks to UIViewRepresentable and UIViewControllerRepresentable. Conversely, UIKit-based UIs can use SwiftUI-based UIs via UIHostingController. In short, SwiftUI and UIKit-based UIs can be used interchangeably.

Before we dive in deeper, let’s discuss what “declarative” means. It’s an important concept! We can divide modern programming languages into two broad categories:

  • Imperative: With imperative programming, we’re directly telling the program (or app) what to do and how to do it. You’re coding: “Put this button here, then download that piece of data, make a decision with if _, and finally assign that value to a text label.”_
  • Declarative: With declarative programming, we’re merely telling the program (or app) what to do, but not how. We’re building the logic of a program, without describing its control flow. The actual implementation is up to the program or its frameworks.

The biggest difference between imperative and declarative programming, is that declarative programming doesn’t involve control flow. A declarative programming language merely describes what needs to happen, a desired outcome called state, and leaves the implementation up to another program or framework.

A good example of a declarative language is HTML and CSS. They’re used to build web pages by providing so-called markup and styling rules. You don’t use HTML and CSS to draw pixels on a screen, or to tell a browser how to render your web page exactly. Instead, you tell it what components to put on screen, and what they should look like. You leave the rest up to the web browser.

SwiftUI uses a declarative programming approach. Instead of initializing UI components programmatically, and adding them to a view, you’re merely describing what the view looks like. When you’re building a UI with SwiftUI, it’s helpful to think not about how you want to accomplish a result, but instead about what the UI looks like in it’s new state.

SwiftUI is closely related to modern development tools and paradigms, such as React, React Native, Flutter, and reactive programming. Platforms like React Native and Flutter have always been one step ahead when it comes to rapid application development (RAD), but SwiftUI puts app development for iOS and co. right back at the forefront.

You can use Live Previews with SwiftUI in Xcode. It’s an awesome feature, because it’ll let you reload your app’s UI right in Xcode, without running the app on Simulator or iPhone first! This feature is similar to Hot Reload for Flutter, and Live Reload for React Native. Live Previews only work on Xcode 11+ in macOS Catalina (10.15).

Fundamentals: Views in SwiftUI

Building UIs with SwiftUI revolves around a fundamental UI component: the View. It’s a Swift protocol, and you can see how it’s used in that snippet we looked at earlier.

struct ContentView: View {
var body: some View {
Text(“Hello World”)
}
}

The struct ContentView conforms to the View protocol, and so does its body property. Every View must to declare a body property of type View. The body property is a read-only computed property and must return only one instance of View. This enables a tree-like view hierarchy – a fundamental concept of SwiftUI – because you can wrap views in views in views, creating a complex hierarchy of views.

If you’re familiar with Model-View-Controller (MVC) and view controllers, you know that a view has two responsibilities:

  • A view is responsible for presenting the User Interface(s) of an app
  • It’s responsible for responding to user behavior and interaction

A view controller includes the business logic, i.e., what happens when, that connects the UI and data models with each other. Consider a to-do list app, for example. If you tap the checkbox for a to-do item, the view propagates that interaction to the controller, which will prompt the model to save its isCompleted property.

With SwiftUI, the controller part of Model-View-Controller is taken out of the equation (almost) entirely! A new concept, called bindings, is used to connect views to models, and vice versa. It’s reminiscent of Observer-Observable, NotificationCenter and MVVM, but it propagates data changes much more elegantly.

This emphasis on views and models is a superpower in SwiftUI. SwiftUI underscores that an app doesn’t do much else than display UIs and manipulate data. The view controller is written off, and doesn’t get in your way any more. Its job is replaced by the glue that makes SwiftUI’s views, and models, work.

Let’s get back to that view example once more:

struct ContentView: View {
var body: some View {
Text(“Hello World”)
}
}

In the above code, the some keyword denotes an opaque type, which is part of the generics feature group of Swift 5.1.

An opaque type can be used with protocols, such as in var body: some View. It means that the value of body conforms to protocol View, but we’re unaware of its concrete type. The concrete type is abstracted away; it is “opaque”. It’s like saying: “Look, this value is a View _, but we’re not telling you exactly what type of_ View it is.”

This has a few advantages, such as simplifying the public interface of a type. As a developer, you can deal with the value as a View, without knowing its specific, concrete type.

What’s also interesting about the implementation of the body property in the above code, is the use of return. But wait, the function doesn’t return anything!? That’s right – the code for body returns the value of type Text implicitly, without making use of return.

Here’s an example, that uses return explicitly:

var body: some View {
return Text(“Hello World”)
}

Since Swift 5.1, single-line functions, such as the one above, may omit the return type. This makes for some pretty concise, readable code! And it’s a feature that SwiftUI uses extensively.

Enough about views, let’s move on to building some UIs!

In my opinion, SwiftUI places iOS’s default architecture in the Model-View-Whatever category. Instead of using Model-View-Controller, in which the controller has an important role, Model-View-Whatever merely defines the use of User Interfaces (views) and data representations (models). What(ever) you put between models and views is up to you! On iOS, the business logic of an app, beyond using bindings, is likely to be assigned to the manager, helper, builder, coordinator, delegate, etc. roles of architectural design patterns. The Combine framework also plays a role here.

Building and Modifying Views in SwiftUI

Let’s riff on the previous example some more, to discover more of the features of SwiftUI. We’ve discussed views and the view hierarchy, but what if you want to change the style of a view?

In SwiftUI, you change views with functions called modifiers, like this:

struct ContentView: View {
var body: some View {
Text(“Hello World”)
.font(.title)
.color(.blue)
}
}

In the above example, we’ve chained the Text value to a call to function font(_:), and chained that to a call to function color(_:). This will first change the font of the text to iOS’s default title font, and then sets its color to blue. These functions are called modifiers in SwiftUI lingo.

This approach is called the Builder pattern. You start with a basic view, and by tacking on modifiers you change the attributes of that view. What’s interesting is that every modifier function call returns a new View instance. That’s why you can “chain” and combine multiple modifiers to get to a specific result.

But wait, is the code for body still a single-line expression? Yes it is! We might as well write the entire chain on one line, like this:

Text(“Hello World”).font(.title).color(.blue)

Your code is easier to read if you stack the modifiers vertically, indented with one tab, but the actual implementation is still considered a single-line expression. No return statement needed!

Now, there’s something else going on with these builders. Those .title and .blue constants, where do they come from? Why don’t we just choose the text’s RGB color and pick the default system font?

SwiftUI uses sensible defaults that take into account many aspects of iOS, such as a user’s preferences. This means Dark Mode is supported out-of-the-box if you use SwiftUI.

And there’s more! Accessibility features, such as a user’s preference for larger fonts, are automatically taken into account. Likewise, the .blue color isn’t the same as the RGB color #0000FF – a hard blue. Instead, it’s iOS 13’s take on a shade of blue, which makes it easier for your app to fit in with iOS’s styling.

Stacks: HStack, VStack, ZStack

Now that we’ve discussed views and builders, let’s take a look at building a more complex UI. What if you want to compose a UI that consists of multiple views? That’s where stacks, lists and groups come in.

SwiftUI’s views are composable. Just like Mozart and Beethoven use composition to turn many distinct musical notes into one coherent piece of music, you use many individual views to compose your app’s complete User Interface. The View building blocks can be combined and nested in many, many ways.

As we’ve discussed before, the body property of a View instance needs to return exactly one view. If you want to compose a view that consists of an image and some text, how are you going to do that if a view can only have one subview? That’s where stacks come in.

SwiftUI provides 3 distinct stacks:

  • HStack, which arranges its children (i.e., subviews) in a horizontal line, next to each other
  • VStack, which arranges its children in a vertical line, i.e. above and below each other
  • ZStack, which overlays its children, i.e. places them on top of each other, back-to-front, along the z depth axis

Here’s an example of the VStack in action:

struct ContentView: View {
var body: some View {
VStack {
Text(“A beautiful landscape”)
.font(.title)
Image(“landscape”)
}
}
}

In the above code, we’re composing a VStack view that consists of two children: a Text view and an Image view. The VStack itself is a View instance, of course, that accepts an unnamed parameter content. In short, this content parameter is a closure that uses the ViewBuilder type to construct views. Differently said, we can put subviews in the VStack and have them show up vertically in the UI.

You can also combine a horizontal and vertical stack, like this:

struct ContentView: View {
var body: some View {
VStack(alignment: .leading) {
Text(“John Appleseed”)
.font(.title)
HStack {
Text(“iOS developer at Acme Inc.”)
.font(.subheadline)
Spacer()
Text(“+1-202-5385-1234”)
.font(.subheadline)
}
}
.padding()
}
}

What’s going on in this block of code?

  • The top level element is a VStack with its alignment set to .leading, which means that its subviews are aligned to the left of the view. On the last line, the VStack is also given some space around its edges with .padding().
  • The vertical stack consists of 2 child views: some text and a horizontal stack. The text’s font is set to .title.
  • The horizontal stack has 3 children: text, spacer, text. The spacer presses both text items to the left and the right of the view.

The previous snippet is inspired by one of SwiftUI’s official tutorials, called Creating and Combining Views. You’re building a Landmarks app in that tuturial, and I recommend you check it out!

Working With Lists in SwiftUI

In SwiftUI, you can show a list of rows with the List component. The List view is the equivalent of a table view in UIKit. Let’s take a look at how lists work! In the next few steps, we’ll build a simple Contacts list app.

First, we’re going to define a model called Contact. Here it is:

struct Contact {
var name:String
var jobTitle:String
var phone:String
}

Simple, right? It’s an ordinary struct with 3 properties of type String. We’ll now fill up an array contacts with some instances of this new struct. This array will serve as the data source for the list view.

let contacts = [
Contact(name: “Marvin”, jobTitle: “Paranoid Android”, phone: “+1-200-8261-0817”),
Contact(name: “Arthur Dent”, jobTitle: “BBC Radio employee”, phone: “+1-200-1234-5678”),
Contact(name: “Zaphod Beeblebrox”, jobTitle: “President of Universe”, phone: “+1-200-7162-2715”),
Contact(name: “Ford Prefect”, jobTitle: “Alien journalist”, phone: “+1-200-8162-7651”),
Contact(name: “Trillian Astra”, jobTitle: “Mathematician”, phone: “+1-200-9876-5432”),
]

If you’re following along in Xcode, make sure to add the Contact struct to its own Swift file. Add the contacts array outside the struct, so you can use it globally in your project.

The next step is creating the ContactRow view. Xcode has a neat template that you can use to build views with SwiftUI. Here’s how:

  1. Choose the File → New → File… menu
  2. Find the SwiftUI View template below User Interface and click Next
  3. Give the Swift file a sensible name, such as ContactRow.swift and click Create

This will automatically create the default code that’s needed to get started with a SwiftUI view. Great! Here’s the view that we’re going to use:

struct ContactRow : View {
var contact: Contact

var body: some View {
VStack(alignment: .leading) {
Text(contact.name).font(.title)
HStack {
Text(contact.jobTitle).font(.subheadline)
Spacer()
Text(contact.phone).font(.subheadline)
}
}
}
}

A few things stand out here:

  • The view is basically the same as the view from the previous chapter, with 3 text elements in a vertical and horizontal stack.
  • The ContactRow view has a contact property of type Contact. That’s the data model we defined earlier. In this app, it’ll represent the data that’s going into the view.
  • Instead of hard-coded strings, we’re using the properties of contact to fill the views with the text. The name property is used for the top text, and the jobTitle and phone properties are used for the left and right text at the bottom of the row.

Take a step back now, and look at your code from a wider perspective. What’s going on here? First, we created the data model. Then, we created the row that goes into the list. The row has a property contact that accommodates one instace of the Contact model. It uses the properties of the model to populate the row.

If you’re familiar with table views, see if you can match what you know about table views with what you know know about List. Where do the data source, the cells, etc., fit in?

Finally, we can declare the main view of the app. It includes that List element and some logic to deal with assigning model instances to the list’s rows. Here we go:

struct ContentView: View {
var body: some View {
List(contacts, id: \.name) { contact in
ContactRow(contact: contact)
}
}
}

See the List element in the above snippet? By default, this view accepts subviews, similar to a VStack. Like this:

List {
ContactRow(contact: contacts[0])
ContactRow(contact: contacts[1])
ContactRow(contact: contacts[2])

}

The above example hard-codes ContactRow views in the List element, by using the items in the global contacts array. However, this approach doesn’t load the rows dynamically!

If we want to load rows dynamically, we’ll have to provide those to the List element. The default initializer of the List element accepts a @ViewBuilder closure, which constructs view elements from closures. The initializer we’ll use takes an array, a keypath, and a closure that provides the views for the list.

Like this:

List(contacts, id: \.name) { contact in
ContactRow(contact: contact)
}

The above code looks simple, but there’s a lot going on. Here’s what:

  • The initializer List(_:id:rowContent:) is used to populate the list. It’ll accept an array, a keypath, and a closure. The first argument is simple, that’s the contacts array we want to use. The second parameter id contains a keypath, which is used to uniquely identify rows in the list. We’ll use the name property of a Contact item. The last argument is a closure, which gets an item from the contacts array as an argument, and needs to return a value of RowContent to its caller. In short, the List takes an array and a keypath, and needs to return a view for every item in the array.
  • The closure is invoked to provide views for every item in the collection (from contacts). This essentially passes us one contact item, for which we’ll have to return a View instance.
  • Inside the closure, we’re initializing an instance of type ContactRow, using the contact value. The ContactRow(contact:) initializer is automatically synthesized for us.

Differently said, for each item in the contacts array, a ContactRow view is initialized, and added to the List view. What you end up with, is a list of rows with contact information. Neat!

SwiftUI together with Realm Database is an exceptionally powerful combo. With just a few lines of code you can build a model-view app, backed by an offline-first database.

The next step is adding navigation to our app. You can do this with the NavigationView component. Adding navigation to your app is incredibly simple.

Here’s how we’re adding a NavigationView element to the list UI we built earlier:

struct ContentView: View {
var body: some View {
NavigationView {
List(contacts, id: \.name) { contact in
ContactRow(contact: contact)
}
.navigationBarTitle(Text(“Contacts”))
}
}
}
In the above code, we’ve wrapped the List view in a NavigationView element. This is essentially all that’s needed start working with navigation!

The .navigationBarTitle(_:) modifier is used to display a text view in the navigation bar, as a title. It’s important to note here that the modifier is inside the NavigationView element. Why? Because it’s the navigation title of the List element – the navigation view merely wraps that list.

Let’s take a look at how we can actually navigate to a next view. Here’s the SwiftUI code we’ll work with:

NavigationView {
List(contacts, id: \.name) { contact in
NavigationLink(destination: ContactDetail()) {
ContactRow(contact: contact)
}
}
.navigationBarTitle(Text(“Contacts”))
}
This is what happens in the above code:

  • The NavigationView wraps the List view, but unlike before, the items in the list are now NavigationLink elements. This is what’s used to facilitate side-by-side navigation.
  • The NavigationLink view has two parameters. A destination, which is another View element, and an unnamed label parameter. That’s the ContactRow view. It’s easiest to see this as the navigation destination and the navigation button itself. You tap the button to go to the destination, i.e. you tap the ContactRow to go to the ContactDetail view.

The code we’ve written so far also exposes another problem. You can see how these hierarchies of nested views can get huge! If you wrap views in views in views – i.e., a complete UI – you can easily get lost. When you combine those nested views with 5-10 modifiers for every view, it gets cluttered quickly. And that’s a PITA to debug!

How can you deal with this issue effectively? First and foremost: SwiftUI is a new technique, so it’s a great idea to make lots of mistakes. At this point, no one knows how SwiftUI will behave in production apps. Many developers and engineers will give you recommendations, but it’s a great idea to figure out what works for you. Make the technology your own, so to speak.

The official SwiftUI tutorials make use of subclassing and composition. Take for example the ContactRow view that we’ve built ourselves. If you feel that your view hierarchy gets nested too deeply, you can decide to abstract away some prototypical views into their own structs. You can then replace the code with an instance of that new struct.

Likewise, you can abstract away view modifiers that you’re using for common views. If your text views always use padding, you might as well create a view called PaddedText, that returns a text view with Text().padding().

FAQ: Migrating Your App to SwiftUI

Let’s take a look at a few Frequently Asked Questions about SwiftUI, or, better said, Commonly Held Confusions.

Should I use SwiftUI in my apps?

That depends on your app. SwiftUI is part of iOS 13 and later, and can’t be used on previous versions of iOS. You’ll need Xcode 11 or later to work with SwiftUI, too. Most app publishers want their apps to be backwards-compatible with the before-last iOS version (i.e., iOS 12). Differently said, SwiftUI is a problem if you want your apps to run on iOS 12 and lower.

What consequences does “iOS 13 and later” have for SwiftUI?

In short, it means that you can’t use SwiftUI easily if your app needs to support iOS versions prior to iOS 13. According to Apple, 77% of all devices introduced in the last four years use iOS 13. It’s up to you if your app can afford to give 23% of iOS users the cold shoulder. It’s likely that SwiftUI will see a moderately slow adoption phase, just like Swift in 2014 and 2015. Whatever you do, don’t shy away from experimenting with SwiftUI though!

What happened to UIKit? Is everything I know about view controllers obsolete now?

That’s an interesting question. SwiftUI is not built on top of UIKit. This means that SwiftUI has the potential to replace everything you know about UILabel, UIButton, UIViewController, table views, collection views, Auto Layout, and much, much more. Yikes! The reality of SwiftUI is probably not going to be that extreme, though. Think about Swift vs. Objective-C: plenty of apps these days still use Objective-C. In the coming years, many apps will keep using UIKit – because it works fine. Until then, it’s a smart idea to learn more about SwiftUI and adopt it as you see fit. It’s yet another argument to keep learning more about iOS development!

Can I use SwiftUI together with UIKit?

Yes! SwiftUI and UIKit are interoperable, which will greatly help SwiftUI’s adoption in existing iOS apps. Interfacing with UIKit from SwiftUI revolves around the UIHostingController, UIViewRepresentable and UIViewControllerRepresentable protocols. In short, you can wrap views and view controllers from UIKit in SwiftUI views, and vice versa.

Can SwiftUI handle complex views?

What about complex UIs, can SwiftUI deal with that? Well, the most pragmatic answer here is: “No one knows, until someone has the guts to find out!” The best thing you can do here is to try out if your complex UI can be built with SwiftUI, and to share your insights with other developers. That said, my gut feeling tells me we’re going to see a whole lot more of SwiftUI in the coming years. SwiftUI is awesome today, but it’s going to be way more awesome if SwiftUI takes the same path as Swift 1.0 to 5.1.

Is SwiftUI merely a prototyping tool?

I personally don’t think so, but SwiftUI does put iOS and co. back on the map as a Rapid App Development (RAD) tool. A fair share of app designers with coding skills used Storyboards to mock up functional app prototypes, and that’s definitely something you can do more easily with SwiftUI. That said, I don’t think SwiftUI is merely a prototyping tool.

Further Reading

Pfew! SwiftUI is pretty awesome, right? Building UIs with SwiftUI is so elegant. This tutorial barely touches the surface of what’s possible with SwiftUI. It’s up to you what you’re going to build, try out, and experiment with!

It’s recommended you check out some of the official documentation around SwiftUI. These docs are exceptionally readable, and in particular, the SwiftUI Tutorials are worth checking out.

  • SwiftUI on developer.apple.com
  • SwiftUI Documentation
  • SwiftUI Tutorials (recommended!)


Aasif Khan

Head of SEO at Appy Pie

App Builder

Most Popular Posts