DEV Community

Maarek
Maarek

Posted on • Updated on

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
 }]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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)
            }
     }
Enter fullscreen mode Exit fullscreen mode

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()
    }
}
Enter fullscreen mode Exit fullscreen mode

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]()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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")
    })
)
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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()
        }
Enter fullscreen mode Exit fullscreen mode

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 πŸ˜„

Top comments (4)

Collapse
 
codger profile image
codger

Hi I am getting the following error when running app on simulator

error: module importing failed: invalid token (rlm_lldb.py, line 37)
File "temp.py", line 1, in

I am using Beta 4 and the only changes I made was to alter the DidChange to WillChange and the DidSet to WillSet in the "TodoListViewModel"

Thanks in advance

Codger

Collapse
 
kevinmaarek profile image
Maarek

Hey,
I need to update this for the beta 4...
Looks like a realm error (?). Can I see your code ?

Collapse
 
codger profile image
codger

Same as your sample except TodoListViewModel: changed didChange to willChange per Version 4 beta requirements

import Foundation
import SwiftUI
import Combine

public class TodoListViewModel: BindableObject {
public let willChange = PassthroughSubject()

var todos: Todos = [Todo]() {
    willSet {
        willChange.send(self)
    }
}

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

func sort() {
    self.todos = self.todos.sorted(by: { ($0.pinned ?? false) && (!($1.pinned ?? false)) })
}

func markDone(id: Int) {
    self.todos.first(where: { $0.id == id })?.completed.toggle()
    self.willChange.send(self)
}

func pin(id: Int) {
    self.todos.first(where: { $0.id == id })?.pinned?.toggle()
    self.sort()
}

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()
}

}

Thread Thread
 
kevinmaarek profile image
Maarek

Replacing didChange to willChange did not raise any error, but I noticed you did not added the satisfying types for output and failures. In my case :

public let willChange = PassthroughSubject<TodoViewModel, Never>()

I have no idea of how you could get that kind of error, but check if that's what you missed.