A complete SwiftUI app !

In this post, we'll take the sample App we made on my Getting started with SwiftUI and Combine article and try to improve it and implement real life features.

This post could serves as part 2 of the previous, as we will continue to explore Apple's SwiftUI and Combine Framework possibilities.

Before rushing into XCode, I wanted to make a quick checklist of the features I wanted to add. This is what I got :

  • ✅ Mark/Unmark a ToDo item as DONE
  • 📌 The ability to pin an item on top of the list
  • 👨‍🎨 Improve the UI (to make it deserve an Apple Design Awards)

Ok now download and open the base project (available here) and we are good to go.

We'll start by an easy one. We need to add the pinned attribute to our model, so we know if a Todo item is pinned or not.

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

Then, we will create a cell viewModel. As we will add more feature and display stuff, I like to keep things clean.

public class TodoCellViewModel {

    private var todo: Todo

    public init(todo: Todo) {
        self.todo = todo
    }

    public func isPinned() -> Bool {
        return self.todo.pinned ?? false
    }

    public func isComleted() -> Bool {
        return self.todo.completed
    }

    public func getTitle() -> String {
        return self.todo.title
    }

    public func getId() -> Int {
        return self.todo.id
    }
}

It will just prevent the cell to handle actual models. Not that much useful for now, but as I said : It feels cleaner. 😇

Now, we are going to modify the cell's UI to add a "pin" button :
Basically add a Spacer and a pin image right after in the HStack.
We'll also add 2 blocks : one for the mark as Done action and another for the pin action. And we'll call these on the right target.

struct TodoCell : View {
    var todoCellViewModel: TodoCellViewModel
    var markDoneAction: ((_ id: Int) -> Void)
    var pinAction: ((_ id: Int) -> Void)

    var body: some View {
        HStack {
            // NOT USING BUTTONS BUT IMAGE + TAP ACTION
            // BUG W/ Buttons in List

            Image(systemName: (self.todoCellViewModel.isComleted() ? "checkmark.square" : "square")).tapAction {
                self.markDoneAction(self.todoCellViewModel.getId())
            }

            Text(self.todoCellViewModel.getTitle())
            Spacer()

            Image(systemName: (self.todoCellViewModel.isPinned() ? "pin.fill" : "pin")).tapAction {
                self.pinAction(self.todoCellViewModel.getId())
            }
        }
        .padding()
    }
}

⚠️ I am not using Buttons, but images with tapAction. There is currently an Issue with multiple buttons in list's cell.

Let's also clean the content view, isolating the List, creating a independent List View, it will also look way cleaner, in which we'll call our new Cell. You'll need to create the same callbacks as in the cell. These will call the viewModel methods we'll create right after.

struct TodoList : View {
    var todos: Todos
    var markDoneAction: ((_ id: Int) -> Void)
    var pinAction: ((_ id: Int) -> Void)

    var body: some View {
        List(self.todos) { todo in
            TodoCell(todoCellViewModel: TodoCellViewModel(todo: todo), markDoneAction: { (id) in
                self.markDoneAction(id)
            }) { (id) in
                self.pinAction(id)
            }
        }
    }
}

Great, now we just have to add new methods to our viewModel to pin and mark as done. This is how I did it :

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

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

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

Finally, call the TodoList in the ContentView :

TodoList(todos: self.viewModel.todos, markDoneAction: {(id) in
    self.viewModel.markDone(id: id)
}, pinAction: {(id) in
    self.viewModel.pin(id: id)
})

And we're done!
I'd like to clean up a bit the file hierarchy.

To go further with this project, in another post, I would like to try to present a modal on tap on the todo text that will show the details of the item and choose a color tag, you know, like in the macOS Finder.
In the list, adding filters, like Show Pin Only or Done Only.

For now, here is the project's files you can download and play with.

Hope you enjoyed it, you can download the full project files here.

Happy Coding!

Leave a Comment