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 😄
Top comments (4)
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
Hey,
I need to update this for the beta 4...
Looks like a realm error (?). Can I see your code ?
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()
}
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 :
I have no idea of how you could get that kind of error, but check if that's what you missed.