Rethink iOS Programming with SwiftUI and Combine

The recent additions of SwiftUI and Combine offers a brand new way to construct UI across all Apple platforms.

SwiftUI is a platform-agnostic UI toolkit introduced by Apple during WWDC 2019. It provides views, controls, and layout structures to let us design and develop apps in a declarative manner. If this sounds an awful lot like React to you, you're not wrong! SwiftUI's design seems to be heavily inspired by libraries like React, as declarative UI frameworks have enabled developers to create dynamic, data-driven UIs without explicitly programming instructions as to how each view should be rendered. As a result, Xcode is starting to move in a direction where designers and developers can collaborate on the same canvas.

SwiftUI Basics

Let's pretend that we're building an iOS client for a social media app. Here's a PostView that shows information regarding a post within the app's feed. We declare SwiftUI components by declaring a struct that conforms to the View protocol, which returns all its subviews through a computed property called body.

import SwiftUI

struct PostView: View {
    var post: Post

    var body: some View {
        VStack {
            Image(uiImage: post.avatarImage)
            Text(post.author)
            Text(post.body)
        }
    }
}

Note: The some keyword is indicating an opaque return type of View. You can think of it as the variable body having a return value of type T which conforms to the View protocol. This is what makes SwiftUI components composable.

Historically, UI programming on iOS has been done in an imperative style. iOS devs had two options:

  • Wire up UI elements using the Interface Builder and hook'em up in code
  • Programmatically create them in the View Controller using the AutoLayout API.

A lot of the rendering logic had to be explicitly handled, and all cases and states must be accounted for beforehand. This approach tends to get buggy if not handled with care, and the iOS community devised many approaches to mitigate issues that an imperative UI programming model brought with it.

SwiftUI lets us describe, or declare what the views look like, attach handlers for data and user events, and let the framework take care of the actual rendering work for us. Instead of rendering just images and text, we want to explore a more interesting layout for our PostView component. Let's pretend that we've got a highlight post in our feed, which has a big colorful gradient behind the text:

struct PostView: View {
    var post: Post

    var body: some View {
        ZStack {
            LinearGradient(
                gradient: Gradient(colors: [.yellow, .purple]),
                startPoint: .top,
                endPoint: .bottom
            )

            HStack {
                Image(uiImage: post.avatarImage)
                    .padding(.right, 12)

                VStack {
                    Text(post.author)
                        .font(.headline)
                        .fontWeight(.bold)
                        .foregroundColor(.blue)
                    Text(post.body)
                        .font(.subheadline)
                        .foregroundColor(.gray)
                }
            }
        }
    }
}

See those methods chained alongside the views? Those are SwiftUI modifiers, which allow us to configure our views and their attributes. There is an enormous list of modifiers to play around with, ranging from a plethora of visual effects to layout styles, or any other UI related settings you could think of.

Additionally, the Xcode Preview Canvas lets us add these modifiers visually to our components. This is a really powerful tool! It means that designers and developers can both collaborate on the same canvas, with a developer focusing on the data / interactive layer while the designer quickly builds out the UI.

Before we move forward, let's clean the code up. As we inline more modifiers and subviews, it can become difficult to understand what the code is doing at a glance, and SwiftUI lets us approach our UI in a highly modular manner. Breaking this PostView component down into smaller parts could look something like:

struct PostContent: View {
    var post: Post

    var body: some View {
        HStack {
            Image(uiImage: post.avatarImage)
                .padding(.right, 12)

            VStack {
                Text(post.author)
                    .font(.headline)
                    .fontWeight(.bold)
                    .foregroundColor(.blue)
                Text(post.body)
                    .font(.subheadline)
                    .foregroundColor(.gray)
            }
        }
    }
}

struct PostBackgroundView: View {
    var body: some View {
        LinearGradient(
            gradient: Gradient(colors: [.yellow, .purple]),
            startPoint: .top,
            endPoint: .bottom
        )
    }
}

By breaking down the subviews, we can compose the final view for a post, making our code much cleaner and easier to read!

struct PostView: View {
    var post: Post

    var body: some View {
        ZStack {
            PostBackgroundView()
            PostContent(post: post)
        }
    }
}

Stateful Components

Our UI isn't really interactive, so we'll want to add state to it. Adding state to our SwiftUI components is painless. Let's add a button to our PostView component, which will toggle the display of some additional info for each post.

struct PostView: View {
    var post: Post

    @State private var showInfo = false

    var body: some View {
        ZStack {
            PostBackgroundView()
            PostContent(post: post)

            if showInfo {
                Text(post.info)
            }

            Button(action: {
                self.showInfo.toggle()
            }) {
                Image(systemName: "info.circle")
            }
        }
    }
}

As you can see, this is a powerful pattern that has been heavily in use on the Web since React's rise in popularity. Letting the framework do the actual rendering work lets us focus on building our application without getting caught up in the low-level details. We just need to focus on one primary axiom of declarative UI tools: UI is a function of state.

Combining SwiftUI and Combine

So far, we've just been exploring the UI portion, but what about data? Let's say we want to fetch a collection of posts from an API and display them using the PostView component we just created. We could encapsulate the data fetching inside of a special object called a view-model.

import SwiftUI
import Combine

final class PostsViewModel: ObservableObject {
    @Published var posts: [Post] = [Post]()

    func fetch() {
        guard let url = URL(string: API_ENDPOINT) else { return }

        URLSession.shared.dataTask(with: url) { (data, response, error) in
            do {
                guard let data = data else { return }

                let payload = try JSONDecoder().decode(Post.self, from: data)

                DispatchQueue.main.async {
                    self.posts = Array(payload.data)
                }
            } catch {
                print("Failed To decode: ", error)
            }
        }.resume()
    }
}

Combine gives us the ability to create observable objects, which allows our UI to subscribe to our data models and get notified whenever there is a change. Actually, it does a lot more than just give us observables. It's a full featured framework for reactive programming (similar to RxSwift), but let's stick to the basics for now. By marking a property as @Published, the views that consume it will automatically be notified of new changes, causing SwiftUI to invoke the respective view's computed property body again, effectively causing a re-render.

Now that we have our data, rendering a collection in SwiftUI is very convenient using the built-in ForEach struct.

struct PostsIndex: View {
    @ObservedObject var viewModel = PostsViewModel()

    var body: some View {
        NavigationView {
            ForEach(viewModel.posts) { post in
                NavigationLink(destination: PostDetail(post: post)) {
                    PostView(post: post)
                }
            }
        }.onAppear {
            self.viewModel.fetch()
        }.onDisappear {}
    }
}

Hopefully you're starting to get an idea of how this makes UI programming on iOS much easier. Not to mention, this is a more sustainable model for scaling to bigger apps, and it's quite fun too! Part of React's success on the Web can be attributed to the fact that it introduced a declarative model for creating UIs, and SwiftUI is following the same path.

We've merely scratched the surface here, as both SwiftUI and Combine offer rich APIs for doing many things beyond fetching data and rendering components on the screen. I urge you to dive deeper into some of Apple's documentation and interactive tutorials, as they get into much more of the nitty gritty goodness.