DEV Community

Cover image for Building a ToDo app with SwiftUI and Cosmic
Karl
Karl

Posted on

Building a ToDo app with SwiftUI and Cosmic

Introduction

In this article, we're going to dive deep into a particularly powerful combination of technologies, SwiftUI and the Cosmic CMS, to build a cross-platform ToDo list app.

SwiftUI, a brainchild of Apple since 2019, is an innovative and intuitive UI toolkit that lets developers write UIs from one platform and share the code across all platforms. It's powering a new age of app development with its simplicity and efficiency in design.

But what's a great UI if we can't manage and deliver the content seamlessly? That's where our next tool, the Cosmic CMS, steps in. Cosmic, known as a headless CMS (content management system), enables us to manage content across various platforms with ease.

The task at hand is a cross-platform ToDo list app, a fundamental tool for productivity that helps us keep track of what needs to be done. Throughout this journey, we will explore the key features and benefits of a ToDo list app, such as managing tasks, organizing work, and improving productivity. It's an excellent way to understand CRUD (Create, Read, Update, Delete) operations and appreciate how SwiftUI and Cosmic can power such an application.

This unique blend of SwiftUI's cross-platform capabilities and Cosmic's efficient content management is going to provide us with an efficient and effective way to build a truly robust ToDo list app. So buckle up and let's enjoy the ride together!

Prerequisites

Before we dive into creating our application, it's crucial to have some necessary tools installed and configurations in place. Here are the steps you'll need to follow:

Installation Requirements and Setups

  1. Xcode:

    Xcode is an Integrated Development Environment (IDE) for writing, compiling, and debugging Swift code. You can download it directly from the Mac App Store. Make sure you have the most recent version installed for optimal performance and the latest features.

  2. iOS SDK:

    The iOS SDK (Software Development Kit) comes bundled with Xcode and enables developers to develop, test, and debug apps for iOS. Ensure the bundled iOS SDK is updated within Xcode's preferences settings.

  3. Swift:

    Swift is a robust and intuitive programming language created by Apple for iOS development. By installing the latest version of Xcode, you should inherently have the most recent stable version of Swift.

  4. Cosmic SDK Swift:
    The Cosmic SDK for Swift is a simple and elegant way to manage CRUD operations from your Swift or SwiftUI app.

Setting Up a New Cosmic CMS Account and Creating a New Project

Cosmic is the CMS that we'll use for data storage and management of our ToDo list app. Here are the steps to setup a new account:

  1. Go to the Cosmic official website.
  2. Click on the Sign Up button.
  3. You can sign up with your GitHub, Google account, or manually by entering your details.
  4. Upon signing up and logging in, create an empty project and Bucket (our 'data container').
  5. Save the Bucket slug and API read/write key found in Bucket > Settings > API keys.

Now you're set up and ready to start building the ToDo list app!

Overview of SwiftUI

SwiftUI is a modern, cross-platform, declarative UI framework from Apple introduced in 2019. It is used for developing UIs for Apple's platforms – iOS, iPadOS, macOS, watchOS and tvOS. SwiftUI uses Swift language features like generics and type inference to ensure your UI works effectively. Here are some highlighted features:

  1. Declarative Syntax: SwiftUI uses a declarative syntax, which means you just need to state what you want in the UI, and SwiftUI ensures your interface is in that state.

  2. Design Tools: SwiftUI was built with a reciprocal relationship between code and design. It provides a live preview of the UI which updates as the code is changed. This powerful feature allows developers to see real-time changes while crafting their user interface.

  3. Cross-platform: With SwiftUI, you're able to develop once and deploy on all Apple platforms.

  4. Accessibility: SwiftUI comes with built-in accessibility features like voice-over, larger type, and contrast ratio. It carries out the heavy lifting allowing developers to create accessible apps without too much boilerplate.

In our task of creating a ToDo list app, SwiftUI will be handling all the UI aspects, making the design of our app more streamlined and intuitive.

Cosmic CMS

Cosmic is a cloud-based, Content Management System (CMS) that enables developers to manage and maintain content for their applications more efficiently. It allows you to create and manage the content for your application in a simple and intuitive way.

RESTful API: Cosmic provides a RESTful API allowing for easy integration with any web or mobile applications. The Cosmic SDK for Swift abstracts the REST API to manage the CRUD operations for our app, this is what we’ll be using for our app.

For our ToDo list app, we will be using Cosmic as the backend to handle all CRUD operations where our data will be stored and managed.

Building The ToDo List App

Setting Up Xcode

Start by building out a basic Xcode app project.

Xcode project setup
Xcode project setup
Xcode initial view

Once you’re done, you should have a simple ‘Hello, world!’ View. You may find that it defaults to macOS as we picked a Multiplatform app project. So you can just pick an iPhone target from the Targets list.

Xcode targets dropdown

Integrating Cosmic

So firstly, we’re going to set up Cosmic by putting together our content model. This model is what we’ll also map to inside our SwiftUI project when we move over to there. I like to set up my model in Cosmic first so I can pre-fill with some placeholder data to help with response testing and scaffolding our UI.

During your initial setup, you’ll be navigated to create your first content type, stick with the ‘Multiple’ option as we’ll want to have multiple ToDos stored. Call it whatever you want, but ToDo is probably safest.

Content model

Our basic structure will be the following:

|— title
    [Metadata]
    |— is_completed
    |— due_date
    |— priority
Enter fullscreen mode Exit fullscreen mode

The title of each object will be what we fill in when we create a new ToDo item, we’ll then have an option to mark it as completed or not (which will default to not, or false), set a due date, and finally a priority (low, medium or high).

This gives us lots of nice things we can do in our UI to help with sorting and styling.

Let’s create a couple of example todos to populate a little data.

Example filled out ToDo

We can even view our ToDo list in the Cosmic dashboard, almost as we will in our app

ToDo list

Fetching our data

We know we’ll have multiple todo items eventually, and therefore we’ll need to loop over them and present a list. Thankfully, the Cosmic SDK for Swift and SwiftUI is already set up in a way to handle this for us.

Now, we can write our API call to fetch our data.

Note: I’ve put my specific Cosmic credentials inside a ‘Secrets’ file so’s not to expose them during this tutorial, but note this is not a safe way to store them and shouldn’t be committed to GitHub.

Firstly let's start by creating a new ToDoViewModel.swift file. Pick a standard Swift type as we don't need access to SwiftUI here.

Now we need to add the SDK. Start by visiting the File menu and select 'Add package dependencies..."

Then, in the window, paste in the GitHub url to the search bar.

https://github.com/cosmicjs/CosmicSDKSwift
Enter fullscreen mode Exit fullscreen mode

Once added, we just need to import it in the files we're working in. In this case, we should include it at the top of our file.

import CosmicSDK
Enter fullscreen mode Exit fullscreen mode

Once we've done this, create a class for our model and make it Observable.

class ToDoViewModel: ObservableObject {
    private let BUCKET = CONST_BUCKET
    private let READ_KEY = CONST_READ_KEY
    private let WRITE_KEY = CONST_WRITE_KEY
    private let TYPE = CONST_TYPE
Enter fullscreen mode Exit fullscreen mode

We're also going to declare some new local constants that pull from our secrets. This is just to keep things simpler and file private.

We can then initiate each of our global secrets directly within the Cosmic createBucketClient method. This is because we need to initiate the bucket client before our variables are available to the app.

private let cosmic = CosmicSDKSwift(
    .createBucketClient(
        bucketSlug: CONST_BUCKET,
        readKey: CONST_READ_KEY,
        writeKey: CONST_WRITE_KEY
    )
)
Enter fullscreen mode Exit fullscreen mode

We then declare two published variables, this means that our app listens to changes from these as part of their observability

  1. The first is our array of Objects which we remap to a simple items variable
  2. The second is an error variable, this is so we can publish and capture any View Model errors provided by the SDK
@Published var items: [Object] = []
@Published var error: ViewModelError?

enum ViewModelError: Error {
    case decodingError(Error)
    case cosmicError(Error)
}
Enter fullscreen mode Exit fullscreen mode

Inside of our fetchData() function, we use the find() method to get our data and pass it to the items array

func fetchData() {
    cosmic.find(type: TYPE) { results in
        switch results {
        case .success(let cosmicSDKResult):
            DispatchQueue.main.async {
                self.items = cosmicSDKResult.objects
            }
        case .failure(let error):
            print(error)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Note, you should write proper error handling, but I know we’re safe so I’ve left it simple

// ToDoViewModel.swift

import Foundation
import CosmicSDK

class ToDoViewModel: ObservableObject {
    private let BUCKET = CONST_BUCKET
    private let READ_KEY = CONST_READ_KEY
    private let WRITE_KEY = CONST_WRITE_KEY
    private let TYPE = CONST_TYPE

    private let cosmic = CosmicSDKSwift(
        .createBucketClient(
            bucketSlug: CONST_BUCKET,
            readKey: CONST_READ_KEY,
            writeKey: CONST_WRITE_KEY
        )
    )

    @Published var items: [Object] = []
    @Published var error: ViewModelError?

    enum ViewModelError: Error {
        case decodingError(Error)
        case cosmicError(Error)
    }

    func fetchData() {
        cosmic.find(type: TYPE) { results in
            switch results {
            case .success(let cosmicSDKResult):
                DispatchQueue.main.async {
                    self.items = cosmicSDKResult.objects
                }
            case .failure(let error):
                print(error)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

In order to decode our data correctly, we need to extend the SDK's provided Object to include the specific metadata keys to make them easier to use in our code later. Whilst this is an optional step, it helps to make the output a lot easier to work with.

Inside your ToDoViewModel file, at the very bottom, add the following extension. All this does is tell our code that we can expect specific optional metadata to be returned by the API, and we then map that to new variables that are easier to understand and use.

// Model
extension Object {
    var metadata_due_date: String? {
        return self.metadata?["due_date"]?.value as? String
    }
    var metadata_priority: String? {
        return self.metadata?["priority"]?.value as? String
    }

    var metadata_is_completed: Bool? {
        return self.metadata?["is_completed"]?.value as? Bool
    }   
}
Enter fullscreen mode Exit fullscreen mode

Seeing it in our App

Let's head over to our ContentView.swift file and make some changes. First start by deleting all the boilerplate code and add the below code in.

  1. We need to declare a @StateObject as this view will be our main source of viewing and updating our data
  2. Once we have this, we can loop through our data inside a List
    1. Once in there, we can actually iterate over these objects and get to the data we want
    2. For now, we’ll just get the title of each todo
// ContentView.swift

import SwiftUI

struct ContentView: View {
    @StateObject var viewModel = ToDoViewModel()

    var body: some View {
        VStack {
            List(viewModel.items, id: \.id) { todo in
                Text(todo.title)
            }
        }
        .padding()
        .onAppear {
            viewModel.fetchData()
        }
    }
}

#Preview {
    ContentView()
}
Enter fullscreen mode Exit fullscreen mode

If you build and run this, you should see the following. It doesn’t look terrible but it’s not great either. Also, we’re yet to have proper CRUD functionality, currently we simply have Read.

ToDo List on a phone

Notice that the order reflects what you see in Cosmic. Try re-ording your items and then build and run the app again, it should update to match!

Designing the UI with SwiftUI

Right, now it’s time to add some more things to the UI and make it look a little better. We’re not going to spend forever obsessing over the look, and we’re going to prioritise using SwiftUIs native components.

One thing we do need to do though, is to write a couple of helper functions. These allow us to map over our priority string and return two things:

  1. A replacement for the priority word with an exclamation point
  2. A color tint to help with glanceability
// ContentView.swift

func replaceTextWithValue(priority: String) -> String {
        switch priority {
        case "Low":
            return "!"
        case "Medium":
            return "!!"
        case "High":
            return "!!!"
        default:
            return "?"
        }
    }

    func determineColor(priority: String) -> Color {
        switch priority {
        case "Low":
            return Color.yellow
        case "Medium":
            return Color.orange
        case "High":
            return Color.red
        default:
            return Color.gray
        }
    }
Enter fullscreen mode Exit fullscreen mode

And here’s our fully updated ContentView code

// ContentView.swift

import SwiftUI

struct ContentView: View {
    @StateObject var viewModel = ToDoViewModel()

    var body: some View {
        NavigationStack {
            VStack {
                List(viewModel.items, id: \.id) { todo in
                    VStack {
                        HStack {
                            Text(todo.title)
                            Spacer()
                            Image(systemName: "checkmark.circle")
                                .foregroundStyle(.secondary)
                        }
                        HStack {
                            Text(todo.metadata_due_date)
                                .font(.caption)
                                .foregroundStyle(.secondary)
                            Spacer()
                            Text(replaceTextWithValue(priority: todo.metadata_priority))
                                .fontWeight(.black)
                                .foregroundStyle(determineColor(priority: todo.metadata_priority))
                        }
                    }
                }
            }
            .navigationTitle("Cosmic ToDos")
            .onAppear {
                viewModel.fetchData()
            }
        }
    }

    func replaceTextWithValue(priority: String) -> String {
        switch priority {
        case "Low":
            return "!"
        case "Medium":
            return "!!"
        case "High":
            return "!!!"
        default:
            return "?"
        }
    }

    func determineColor(priority: String) -> Color {
        switch priority {
        case "Low":
            return Color.yellow
        case "Medium":
            return Color.orange
        case "High":
            return Color.red
        default:
            return Color.gray
        }
    }

}


#Preview {
    ContentView()
}
Enter fullscreen mode Exit fullscreen mode

CRUD Operations With Cosmic

Cool, so now we’ve got that in place, we can think about implementing more of the CRUD operations. Let’s start with creating a new todo.

Create Operation

At the top of our file, we’ll first need to declare a new state for showing a sheet that’ll let us add a new todo

Insert @State var showAddNewSheet = false

We can then use this to toggle whether we show a sheet or not by adding the sheet modifier and binding it to our variable.

.sheet(isPresented: $showAddNewSheet) {}
Enter fullscreen mode Exit fullscreen mode

Create a new SwiftUI file called AddNewTodoView

In this view we’ll observe our StateObject by calling an @ObservedObject which we bind to a new variable.

@ObservedObject var viewModel: ToDoViewModel
Enter fullscreen mode Exit fullscreen mode

In this new file we’ll initialise three variables to hold the title, due date and priority for us. I won’t go into lots of detail on how this all works, but it’s a basic form that allows someone to add the relevant elements we need to make our Create submission work.

// AddNewTodoView.swift

struct AddNewTodoView: View {
    @ObservedObject var viewModel: ToDoViewModel
    @Environment(\.dismiss) private var dismiss

    @State private var todoTitle: String = ""
    @State private var todoDueDate: Date = Date()
    @State private var todoDueDateString: String = ""

    enum Priority: String, CaseIterable, Identifiable {
        case low, medium, high
        var id: Self { self }
    }

    @State private var selectedToDo: Priority = .low

#if os(iOS)
    let update = UINotificationFeedbackGenerator()
#endif

    var body: some View {
        NavigationStack {
            Form {
                TextField("Enter a title", text: $todoTitle, axis: .vertical)
                DatePicker("Pick a date", selection: $todoDueDate, displayedComponents: .date)
                Picker("", selection: $todoPriority, content: {
                    ForEach(Priority.allCases) { priority in
                        Text(priority.rawValue.capitalized)
                    }
                })
                .pickerStyle(.segmented)
            }
            .navigationBarTitle("Add new ToDo")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .confirmationAction) {
                    Button {
                        update.notificationOccurred(.success)
                        self.dismiss()
                        DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
                            self.viewModel.createToDo()
                        }
                    } label: {
                        Text("Add")
                            .bold()
                    }
                }
                ToolbarItem(placement: .cancellationAction) {
                    Button {
                        self.dismiss()
                    } label: {
                        Text("Close")
                    }
                }
            }
            .onAppear {
                let dateFormatter = DateFormatter()
                dateFormatter.dateFormat = "YYYY-MM-dd"
                dateFormatter.locale = Locale.init(identifier: "en_GB")
                todoDueDateString = dateFormatter.string(from: todoDueDate)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

So now, when you press the plus button, you should see a nice little form.

Form view

Submitting our data

Now, in order to actually submit our data, we need to make another API call to Cosmic, but this time we’re inserting a new object.

We’ll start by creating a new function in our ToDoViewModel which expects to receive the title, due date and priority from the source where it’s called. We can now rely on another SDK method for insertOne() which just requires the type and title but can take optional parameters too. For our case, we'll rely on the optional metadata.

// ToDoViewModel.swift

func createToDo(title: String, due_date: String, priority: String) {
    cosmic.insertOne(type: TYPE, title: title, metadata: ["due_date": due_date, "priority": priority]) { results in
        switch results {
        case .success(_):
            print("Inserted \(title)")
            DispatchQueue.main.async {
                self.fetchData()
            }
        case .failure(let error):
            print(error)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The provided print statements are not required, but they just help to provide us with feedback whilst developing as we can review the console to see the creation worked.

Now in our AddNewTodoView we need to update the call to createToDo() to include the parameters we defined.

// AddNewToDoView.swift

DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
    self.viewModel.createToDo(title: todoTitle, due_date: todoDueDateString, priority: selectedToDo.rawValue.capitalized)
}
Enter fullscreen mode Exit fullscreen mode

Note that we have to do a little bit of clever stuff. Cosmic, expects the date to be a UTF8 formatted string, so we simply use a dateFormatter function to assign a String value from the Date value which gives us a string value back. For our Priority, we take the currently selected priority and access its raw value (which maps to what Cosmic is expecting) and capitalise it to ensure it matches our backend. If your key isn't capitalized, you can ignore this.

// AddNewToDoView.swift

.onAppear {
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "YYYY-MM-dd"
    dateFormatter.locale = Locale.init(identifier: "en_GB")
    todoDueDateString = dateFormatter.string(from: todoDueDate)
}
Enter fullscreen mode Exit fullscreen mode

Now build and run your app again and things should work as expected and you should be able to add a new Todo! You’ll see it appear in Cosmic once you have, and see it in the UI too. This is because we re-fetch our data from the API on the main thread using a DispatchQueue if the creation was successful.

DispatchQueue.main.async {
    self.fetchData()
}
Enter fullscreen mode Exit fullscreen mode

UI Cleanup

Let’s improve our date formatting on our todos to be a little more readable. I’ve written a small helper function that does this for us. Create a new file called Helpers.swift and add this in there.

// Helpers.swift

import SwiftUI

func parseDate(from dateString: String) -> String? {
    let dateFormatter = ISO8601DateFormatter()
    dateFormatter.formatOptions = [.withFullDate]
    if let date = dateFormatter.date(from: dateString) {
        let outputFormatter = DateFormatter()
        outputFormatter.dateFormat = "dd MMM yyyy"
        return outputFormatter.string(from: date)
    } else {
        return nil
    }
}
Enter fullscreen mode Exit fullscreen mode

One thing I love about SwiftUI is that you don’t have to import files, Xcode is smart enough to see you’ve used it, and so long as it exists somewhere in your project, it auto imports it. Neat!

To use this, we simply call it inside the Text() element and access the description property.

// ContentView.swift

Text(parseDate(from: todo.metadata.due_date)?.description ?? "Invalid Date")
    .font(.caption)
    .foregroundStyle(.secondary)
Enter fullscreen mode Exit fullscreen mode

Updated UI
That looks better!

We’ll also extract our TodoCell into its own component to make things cleaner on our ContentView.

First, move the two functions in ContentView that help parse our Priority information into your Helpers.swift file. Now they’re globally accessible.

Then, copy your individual todo code into a newly created ToDoCell.swift SwiftUI View file.

// ToDoCell.swift

import SwiftUI

struct ToDoCell: View {    
    @State var title: String = ""
    @State var date: String = ""
    @State var priority: String = ""

    var body: some View {
        VStack {
            HStack {
                Text(title)
                Spacer()
                Image(systemName: "checkmark.circle")
                    .foregroundStyle(.secondary)
            }
            HStack {
                Text(parseDate(from: date)?.description ?? "")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                Spacer()
                Text(replaceTextWithValue(priority: priority))
                    .fontWeight(.black)
                    .foregroundStyle(determineColor(priority: priority))
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Update Operation

Now it’s time to make updates. Technically you can update anything here, but we’ll focus on simply completing the task and updating the Cosmic object so that it’s synced between uses.

For now, we’ll make the entire cell a touch target for completion, but, you may want to change this in the future if you want other functionality.

  1. Add a new requirement to your ToDoCell.swift to get the current id of the item
  2. Wrap the entire cell in a Button where the UI is the Button’s label property
  3. Add a new function we’ll create for updating our ToDo in the Button’s action
  4. Add a new variable for is_completed
// ToDoCell.swift

import SwiftUI

struct ToDoCell: View { 
    @State var id: String = ""
    @State var title: String = ""
    @State var date: String = ""
    @State var priority: String = ""
    @State var is_completed: Bool = false

    var body: some View {
        Button {
            viewModel.completeToDo(id: id)
        } label: {
            VStack {
                HStack {
                    Text(title)
                    Spacer()
                    Image(systemName: "checkmark.circle")
                        .foregroundStyle(.secondary)
                }
                HStack {
                    Text(parseDate(from: date)?.description ?? "")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                    Spacer()
                    Text(replaceTextWithValue(priority: priority))
                        .fontWeight(.black)
                        .foregroundStyle(determineColor(priority: priority))
                }
            }
        }
        .buttonStyle(.plain)
    }
}
Enter fullscreen mode Exit fullscreen mode

Now update your ContentView.swift to include this

// ContentView.swift

List(viewModel.items, id: \.id) { todo in
    ToDoCell(viewModel: viewModel, id: todo.id ?? "", title: todo.title, date: todo.metadata_due_date ?? "", priority: todo.metadata_priority ?? "", is_completed: todo.metadata_is_completed ?? false)
}
Enter fullscreen mode Exit fullscreen mode

In order to make this work, we’ll add another function to our ToDoViewModel.swift file. This will allow us to update our is_completed switch to the new state. Now we'll be using the SDK's updateOne() method.

// ToDoViewModel.swift

func completeToDo(id: String, is_completed: Bool) {
    cosmic.updateOne(type: TYPE, id: id, metadata: ["is_completed": is_completed]) { results in
        switch results {
        case .success(_):
            print("Updated \(id)")
            DispatchQueue.main.async {
                self.fetchData()
            }
        case .failure(let error):
            print(error)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we have this, we’ll update our UI to toggle the state properly so we update our UI and our Cosmic data and keep them in sync. Here we use ternary operators to change the UI appearance of the checkmark based on the completion state.

// ToDoCell.swift

Button {
        is_completed.toggle()
        viewModel.completeToDo(id: id, is_completed: is_completed)
        } label: {
            VStack {
                HStack {
                    Text(title)
                    Spacer()
                    Image(systemName: is_completed ? "checkmark.circle.fill" : "checkmark.circle")
                        .foregroundStyle(is_completed ? .green : .secondary)
                }
                HStack {
                    Text(parseDate(from: date)?.description ?? "")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                    Spacer()
                    Text(replaceTextWithValue(priority: priority))
                        .fontWeight(.black)
                        .foregroundStyle(determineColor(priority: priority))
                }
            }
        }
        .buttonStyle(.plain)
Enter fullscreen mode Exit fullscreen mode

Delete Operation

Cool, so now we have Creation, Reading, and Updating working nicely. The final thing is to ensure we can delete any todo as well. Thankfully, because we used SwiftUI’s List API, we can easily utilise the inbuilt Delete method to remove an entry from the list and send that DELETE request to Cosmic.

Let’s create one last function in our ToDoViewModel.swift file. This time, it’ll be a deleteToDo() method.

You’ll see that our entire function is very similar to our update, we pass an id to the method and just tell it that we want to delete that id. In this case, we don’t need to send anything else along with it.

// ToDoViewModel.swift

func deleteToDo(id: String) {
    cosmic.deleteOne(type: TYPE, id: id) { results in
        switch results {
        case .success(_):
            print("Deleted \(id)")
            DispatchQueue.main.async {
                self.fetchData()
            }
        case .failure(let error):
            print(error)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we’ll add a swipeAction() to our cell to enable deletion.

Underneath our button style on the ToDoCell.swift, add a swipe action to include the delete functionality. Because we’re not dealing with Core Data or any other internal library, we can just use a simple button with a call to our delete method inside the action.

// ToDoCell.swift

.swipeActions(edge: .trailing) {
    Button(role: .destructive) {
        withAnimation {
            viewModel.deleteToDo(id: id)
        }
    } label: {
        Label("Delete", systemImage: "trash")
    }
}
Enter fullscreen mode Exit fullscreen mode

Now when you swipe from right to left (or left to right, depending on your country), you’ll get the typical swipe to delete functionality and this will persist again across restarts as our data is always coming from Cosmic.

Deleting an item

Building a Cross-Platform App

This won’t be perfect currently, and macOS will likely encounter errors as we have some specific functionality in there for iOS. But, you can build and run this on iPad without issue. It’s not fully optimised, and it’s not the ideal UI for the larger screen, but, it’s still usable. You can work to improve the iPad UI now that all the functionality is in here.

Conclusion

In this comprehensive guide, we've journeyed together through the process of creating a cross-platform ToDo list app using SwiftUI and Cosmic CMS.

We began with an overview of SwiftUI and Cosmic CMS, familiarizing ourselves with the main features and advantages of these modern technologies. We then proceeded into the nitty-gritty of app development, starting from installing and setting up prerequisites to designing the app UI and integrating Cosmic for seamless data management.

The core section of our journey revolved around executing CRUD operations with Cosmic, where we detailed creating, reading, updating, and deleting tasks. This practical experience showcased the versatility and usefulness of both SwiftUI and Cosmic.

The beauty and efficiency of utilizing SwiftUI and Cosmic have hopefully been thoroughly demonstrated throughout this development process. The ease with which we can create a visually appealing interface with SwiftUI and manage our content effectively with Cosmic makes this combination a highly recommended approach for any aspiring or experienced app developer.

Thanks to the fact we used Cosmic, you could even take this project further and develop a web interface using Next.js, for example, where changes would be reflected across your app too!

Next steps

I hope you found this guide instructive. Sign up to get started using Cosmic to power content for your websites and apps. Build your own mobile apps using the Cosmic Swift SDK.

Top comments (0)