Getting started with SwiftUI and Combine

EDIT : Updated for XCode 11.1.

So, I've been developing for iOS for quite a while now and I decided ton get my hands into that new super fresh frameworks, introduced few days ago by Apple at the WWDC 2019 : SwiftUI and Combine.

If you missed it, SwiftUI is a new way for making you UI in a declarative way. Combine works along with SwiftUI and provides a declarative Swift API for processing values such as UI or Network events.

So let's get our hand dirty and try it out !

First, let's find an API to call and play with. I discovered JSONPlaceholder, they provide a todo API I chose to implement : it returns 200 todo items that looks like :

 [{
    "userId": 1,
    "id": 1,
    "title": "delectus aut autem",
    "completed": false
 }]

It'll do the job. I'll use Quicktype to automatically make my Codable structs for my Todo Model.

Now, let's start a new XCode project. Don't forget that "Use SwiftUI" checkbox 😇.
We're going to add our todo model, a copy/past from the Quicktype result, and add the Identifiable conformance, we already have an id and we'll need that later.

public class Todo: Codable, Identifiable {
    public let userID: Int
    public let id: Int
    public let title: String
    public let completed: Bool

    enum CodingKeys: String, CodingKey {
        case userID = "userId"
        case id = "id"
        case title = "title"
        case completed = "completed"
    }

    public init(userID: Int, id: Int, title: String, completed: Bool) {
        self.userID = userID
        self.id = id
        self.title = title
        self.completed = completed
    }
}

public typealias Todos = [Todo]

Identifiable is a protocol (that comes with the SwiftUI Framework) that serves to compare and identify elements. It requires and id and an identifiedValue which, by default, returns Self, and we'll keep it that way.

That way, we let know that every Todo object is unique.
Note that it is required for working with a List or a ForEach.

Now let's continue by creating our view, a simple List and cell, starting by the todo cell.
A Horizontal Stack will work just fine :


(you can download the full project at the end of the article)

Then, the List, very basic and don't forget the Todos as stored a property (for now) :

    var todos: Todos
    var body: some View {
        NavigationView {
            List(self.todos) { todo in
                TodoCell(todo: todo)
            }
     }

Now we should work on our View Model. Create a class called TodoViewModel and let's add some functions to it. Let's say we'll need to (obviously) download the todo list, and make another function to shuffle the list (because why not, I'm short for Ideas when it comes to features for a todo list).

This is what you should have by now :

public class TodoViewModel {
    var todos: Todos = [Todo]()

    func shuffle() {
        self.todos = self.todos.shuffled()
    }

    func load() {
        guard let url = URL(string: "https://jsonplaceholder.typicode.com/todos/") else { return }
        URLSession.shared.dataTask(with: url) { (data, response, error) in
            do {
                guard let data = data else { return }
                let todos = try JSONDecoder().decode(Todos.self, from: data)
                DispatchQueue.main.async {
                    self.todos = todos
                }
            } catch {
                print("Failed To decode: ", error)
            }
        }.resume()
    }
}

Now comes the tricky part.
We need to make the ViewModel conforms to ObservableObject. This is a protocol that will help us to automatically notify any subscriber when a value has changed.
It will allow us to mark any property that will require an update to the subscribers with a property wrapper (more about property wrappers in this post) @Published.

The @Published property wrapper works by adding a Publisher to the property.

    @Published var todos: Todos = [Todo]()

That way every time the todos property will be set, anything that will be observing our view model will be notified. Therefore if we're talking about a SwiftUI View, it will refresh automatically (with some smooth SwiftUI magic).

Now, instead of having a stored list of Todos in our view, we'll replace the todos property with a viewModel instance.

To tell our view to observe the VieModel, we also have a property wrapper.

@ObservedObject var viewModel: TodoViewModel = TodoViewModel()

To our NavigationView, let's add a navigationBar buttons like so :

.navigationBarItems(leading:
    Button(action: {
        self.viewModel.shuffle()
    }, label: {
        Text("Shuffle")
    }),
trailing:
    Button(action: {
        self.viewModel.load()
    }, label: {
        Image(systemName: "arrow.2.circlepath")
    })
)

A shuffle button and a Reload button that calls the corresponding method to our ViewModel.

And for our list, we'll now iterate on the viewModel's todos array.

List(self.viewModel.todos) { todo in
    TodoCell(todo: todo)
}

And we're done!

On the onAppear block, don't forget to call the load function to start downloading the todo list :

        NavigationView {
            // ...
        }.onAppear {
            self.viewModel.load()
        }

Now, when you hit the refresh button, it will call the load function, and update the todo list. When the todo list changes, it will call the send function and the UI will automatically update.

Hope you enjoyed this little intro on what SwiftUI+Combine could do.
You can download the project files here on my github.

Happy coding 😄

Étiquettes :

Leave a Comment