SwiftUI Task App
This tutorial provides step-by-step instructions for using SwiftUI to create a task app in Xcode, which consists of the following high-level steps:
This article provides reference information for the following:
Prequisites
- Ditto account and access credentials
- iOS 14 (or later)
- macOS 11 (or later)
- tvOS 14 (or later)
- Xcode 15 (or later)
For instructions on creating your account and obtaining your credentials, see Get Started.
Creating a New App in Xcode
Click File,** and then select New Project**.
In the **Choose a template for your new project **modal, select App and then click Next.
In the **Choose options for your new project **modal, enter the following information as appropriate, and then click Next.
The following are merely suggestions; enter any information in the form that you desire.
The following steps provide suggested values for the form; however, you can enter any information you desire:
- For Product Name, type “ToDo”.
- For Team, select from Apple developer account teams.
- For Organization Identifier, enter your org identifier, typically reverse domain, e.g. “live.ditto”.
- For Interface, select SwiftUI. (iOS app only)
- For Life Cycle, select Swift UI App. (iOS app only)
Add tvOS as a destination (tvOS only)
- From the PROJECT list in the editor, click to select ToDo, and then under Supported Destinations click the + icon to add tvOS.
Adding Ditto SDK Package
From the top level of the project navigator, select your project by clicking ToDo, and then, from the modal that appears, do the following:
From the PROJECT list in the editor, click to select ToDo, and then select the Package Dependencies tab.
From the bottom of the Packages list, click the **+ **to add the package dependency.
From the search bar in the Add Package modal that appears, enter the following URL:
github.com/getditto/DittoSwiftPackage.
Click Dependency Rule > select Up to Next Major Version from the list > and then, in the field located on the right, type “4.8.0
”.
From Choose Package Products for DittoSwiftPackage, click to select both DittoObjC and DittoSwift libraries:
Click Add Package.
ToDo tasks
Create a ToDo Struct
Ditto is a document database, which represents all its rows in the database as JSON-like structures with key/value properties. In this tutorial, we will define each ToDo
task like so::
Ditto documents have a flexible structure, like JSON, and in Swift, DQL queries return a DittoQueryResult
, which has an items
array property containing a DittoQueryResultItem
for each match. We can access the key/value pairs of those items with the item.value
property.
It is a common practice to create a data model structure to more conveniently work with the app’s business logic and to leverage Swift’s type safety. Let’s do that now. Create a new Swift file called ToDo.swift in your project.
- Add
import DittoSwift
to the top of the file. - Create a ToDo struct and add the matching properties
let _id: String
,let body: String
, andlet isCompleted: Bool
to the struct. - The type of
DittoQueryResultItem.value
is[String: Any?]
, which contains the matching key/value pairs we will use to initialize the struct. Add theinit(value: [String: Any?])
constructor which will take an argument of the result item.value
type . - Add an extension to the struct declaring conformance with the
Identifiable
protocol, and implementvar id: String
to return the_id
key string. TheIdentifiable
protocol is required to uniquely identify ToDo instances in theForEach
component in SwiftUI’sList
view. It may seem confusing to implement both_id
andid
in the same struct. To clarify,_id
is a property of the underlyingDittoDocument
uniquely identifying it in the Ditto database, and the struct’sid
property uniquely identifies the struct instance for the SwiftUI view.
Later in the tutorial we will see how to register a Ditto store observer that will return the result of a query whenever there is a change in the database documents matching the query.
Create a TasksListScreen view
When we generated the project, Xcode created a default ContentView which we will delete, and then create the TasksListScreen to replace it, which will show the list of the views.
- Create a new SwiftUI View View by clicking File > New > SwiftUI View named “TasksListScreen”, and import
DittoSwift
at the top of the file. - Create a
ditto
property of typeDitto
. - In the
body
block, add aNavigationView
with aList
child view. We will fill out the contents of theList
in the next section. - Add a
.navigationTitle
modifier on the end of theList
to display a title on the navigation bar. - Then add a trailing navigation “plus” button in a
.navigationBarItems
modifier. We will implement the button action later. - And finally, stub in a
.sheet
modifier that we will use to present anEditScreen
, which we will create later.
Delete ContentView.swift
- Right-click ContentView.swift in the Xcode Navigator pane.
- Select “Delete” from the menu.
- Click “Move to Trash” from the action sheet.
Set TaskListScreen as main view
- In ToDoApp.swift replace ContentView with the TaskListScreen in the WindowGroup.
In the last part of the tutorial we implemented the start of a TasksListScreen
view to display a List
of ToDo
tasks.
Create a TaskRow view
Each row of the tasks list will be represented by a SwiftUI View
called ToDoRow
which takes in a ToDo
task instance and two action closures which we will use later.
- If
task.isCompleted
istrue
, we will show a filled circle icon and a strikethrough style for thebody
text. - If
task.isCompleted
isfalse
, we will show an open circle icon. - When the user taps the circle icon, we will call the
onToggle: ((_ task: Task) -> Void)?
, we will reverse theisCompleted
fromtrue
tofalse
orfalse
totrue
- If the user taps the
Text
, we will call aonClickBody: ((_ task: Task) -> Void)?
. We will use this to navigate anEditScreen
(we will create this later)
For brevity, we will skip discussions on styling as it’s best to see the code snippet below:
We’ve also included a TaskRow_Previews
that allows you to see the end result with some test data quickly.
Create a TasksListScreenViewModel
In the world of SwiftUI, the most important design pattern is the MVVM, which stands for Model-View-ViewModel. MVVM strives to separate all data manipulation (Model and ViewModel) and data presentation (UI or View) into distinct areas of concern. When it comes to Ditto, we recommend that you never include references to edit ditto
in View.body
. All interactions with ditto
for upsert
, update
, find
, remove
and observe
should be within a ViewModel
. The View should only render data from observable variables from the ViewModel
and only the ViewModel
should make direct edits to these variables.
Typically we create a ViewModel
per screen or per page of an application. For the TasksListScreen
we need some functionality like:
- Showing a realtime list of
Task
objects - Triggering an intention to edit a
Task
- Triggering an intention to create a
Task
- Clicking an icon to toggle the icon from
true
tofalse
orfalse
totrue
In SwiftUI we create a view model by inheriting the ObservableObject
. The ObservableObject
allows SwiftUI to watch changes to certain variables to trigger view updates intelligently. To learn more about ObservableObject
we recommend this excellent tutorial from Hacking with Swift.
- Create a file called TasksListScreenViewModel.swift in your project
- Add an
init
constructor to pass in aditto: Ditto
instance and store it in a local variable. - Create two
@Published
variables fortasks
and isPresentingEditScreen
.@Published
variables are special variables of anObservableObject
. If these variables change, SwiftUI will update the view accordingly. Any variables that are not decorated with@Published
can change but will be ignored by SwiftUI. - We also add a normal variable,
private(set) var taskToEdit: Task? = nil
. When a user is attempting to edit a task, we need to tell the view model which task the user would like to edit. This does not need to trigger a view reload, so it’s a simple variable. - Here’s where the magic happens. As soon as the
TasksListScreenViewModel
is initialized, we need to.observe
all the tasks by creating a live query. To prevent theliveQuery
from being prematurely deallocated, we store it as a variable. In the observe callback, we convert all the documents intoTask
objects and set it to the@Published tasks
variable. Every time to.observe
fires, SwiftUI will pick up the changes and tell the view to render the list of tasks. - We will add an eviction call to the initializer that will remove any deleted documents from the collection
- Add a function called
toggle()
. When a user clicks on a task’s image icon, we need to trigger reversing theisCompleted
state. In the function body we add a standard call to find the task by its_id
and attempt to mutate theisCompleted
property. - Add a function called
clickedBody
. When the user taps theTaskRow
’sText
field, we need to store that task and change theisPresentingEditScreen
to true. This will give us enough information to present a.sheet
in theTasksListScreenViewModel
to feed to theEditScreen
- In the previous setup of the
TasksListScreen
, we added anavigationBarItem
with a plus icon. When the user clicks this button we need to tell the view model that it should show theEditScreen
. So we’ve set theisPresentingEditScreen
property totrue
. However, because we are attempting to create aTask
, we need to set thetaskToEdit
tonil
because we don’t yet have a task.
Render TaskRow in a ForEach within the TasksListScreen
Now we need to update our TasksListScreen
to properly bind any callbacks, events, and data to the TasksListScreenViewModel
.
- Back in the
TasksListScreen
view, we need to construct ourTasksListScreenViewModel
and store it as an@ObservedObject
. This@ObservedObject
tells the view to watch for specific changes in theviewModel
variable. - We will need to store our
ditto
object to pass to theEditScreen
later. - In our
body
variable, find theList
and add:
This will tell the list to iterate over all the viewModel.tasks
and render a TaskRow
. In each of the TaskRow
children, we need to bind the onToggle
and onClick
callbacks to the viewModel methods.
- Bind the plus button to the
viewModel.clickedPlus
event - Now we need to present a
.sheet
which will activate based on the$viewModel.isPresentingEditScreen
variable. Notice how we added the$
beforeviewModel
..sheet
can edit theisPresentingEditScreen
once it’s dismissed, so we need to treat the variable as a bidirectional binding. - We’ve also included a
TasksListScreen_Previews
so that you can add some test data and see the result in a live view.
Notice that we DO NOT HAVE TO manipulate the tasks
value directly. Executing the UPDATE
query on dittoStore
will automatically fire the storeObserver
to update the @Published var tasks
with changes. You can always trust DittoStoreObserver
to immediately update the @Published var tasks
with changes. There is no reason to poll or force reload. Ditto will automatically handle the state changes and SwiftUI will pick these changes up automatically.
Editing Tasks
Our final screen will be the EditScreen and its ViewModel. The EditScreen will be in charge of 3 functions:
- Editing an existing
- Creating a
- Deleting an existing
Creating the EditScreenViewModel
Like before, we need to create an EditScreenViewModel
for the EditScreen
. Since we’ve already gone over the concepts of MVVM, we will go a bit faster.
- The
EditScreenViewModel
needs to be initialized withditto
and an optionaltask: Task?
value. If the task value isnil
, we need to set thecanDelete
variable tofalse
. This means that the user is attempting create a newTask
. We will use this value to show a deleteButton
in theEditScreen
later. We will store the_id: String?
from thetask
parameter and use it later in thesave()
function. - We need two
@Published
variables to bind to aTextField
andToggle
SwiftUI views for the task’sisCompleted
andbody
values. If thetask == nil
, we will set some default values like an empty string and a falseisCompleted
value. - When the user wants to click a save
Button
, we need tosave()
and handle either an.upsert
or.update
function appropriately. If the local_id
variable isnil
, we assume the user is attempting to create aTask
and will call ditto’s.upsert
function. Otherwise, we will attempt to.update
an existing task with a known_id
. - Finally if a delete button is clicked, we attempt to find the document and call
.remove
Create the EditScreen
Like the TasksListScreen.swift
in the previous section, we will create an EditScreen.swift
.
This screen will use SwiftUI’s Form and Section wrapper.
- An
TextField
which we use to edit theTask.body
- A
Switch
which is used to edit theTask.isCompleted
- A
Button
for saving a task. - A
Button
for deleting a task
- In the
EditScreen
we need to add a@Environment(\.presentationMode) private var presentationMode
. In SwiftUI views house some environment variables. Because theTasksListScreen
presened theEditScreen
as a.sheet
, we need a way to dismiss the current screen if the user taps any of the buttons. To learn more aboutEnvironment
, read Apple’s official documentation.. To dismiss the current screen we can callself.presentationMode.wrappedValue.dismiss()
- Like before, store the
EditScreenViewModel
as anObservedObject
. Pass thetask: Task?
and theditto
instance to properly initialize theEditScreenViewModel
. Now the ViewModel should know if the user is attempting a creation or update flow. - We now can bind the
TextField
for the$viewModel.body
andToggle
to the$viewModel.isCompleted
. Notice the$
, this allows SwiftUI fields to bi-directionally edit these@Published
values and trigger efficient view reloading. - Bind the save button’s
action:
handler to theviewModel.save()
function and dismiss the view. Whenever the user clicks the save button, they will save the current data and return back to theTasksListScreen
- If the
viewModel.canDelete
istrue
, we can show a deleteButton
. Notice how we don’t need the$
since we are only reading the value once. Moreover, we do not need to tell SwiftUI to re-render oncanDelete
since it will never change during theEditScreen
’s life cycle. - Bind the delete button’s
action:
to theviewModel.delete()
function and dismiss the view. - Finally we add a
EditScreen_Previews
so that you can easily watch the view’s final rendering as you develop.
Run the App!
Congratulations you have successfully created a task app using Ditto!
Was this page helpful?