Persisting Data in CoreData using SwiftUI.

mirmayne

Daveyon Mayne 😻

Posted on February 14, 2021

Persisting Data in CoreData using SwiftUI.

Alt Text

I've tasked myself with learning Swift, Swift 5 to be exact, though I've tried to replicate Apple's Calendar app using Swift 2 many moons ago which did worked. Things has changed since and now I wanted to add another programming language to my list of programming stack.

Reintroduction

My name is Daveyon Mayne and I'm a software engineer at RTC Automotive Solutions. My core work is working on our in-house software called CloseIt for car dealers/dealerships.

Imagine you have a form where your customer fills out long form by taking the information from their customer face-to-face or over the phone or via any other medium and your user accidentally closes the form. All data will be lost where your user now use the phrase "erm… could I take your name, age etc again please?" They've lost the data entered before saving. As good programmers we should always persist the data when any data is entered or removed. Ever seen few apps present you with "Saving draft…"? Yes, as you type, your data is saved into a database somewhere. This could be locally or remotely. I'll be skipping the boring stuff as code if life. 

I'll assume you've created a new project with CoreData selected. If not, "youtube" some videos on how to enable CoreData. I'm using Xcode 12.

import SwiftUI

struct CreateCustomerView: View {
    @StateObject private var customerForm = CustomerDraftFormFields()

        // We could move this in CustomerDraftFormFields class 
        // and keep our view... clean? 🤷🏽
        let titles = ["Mr", "Miss", "Mrs", "Dr"]


        var body: some View {
            // I'm not using a NavigationView here. You can.
            // You can use Form or List here. I use List because of styling
            List {
                // I use a Group because my List has more than 10 items
                // For this example, you do not need it
                Group {
                    Section {
                        Picker("Title", selection: $customerForm.title, content: {
                            ForEach(titles, id: \.self) {title in
                                Text(title).tag(title)
                            }
                        })

                        TextField("First Name", text: $customerForm.firstName)
                            .disableAutocorrection(true)
                        TextField("Last Name", text: $customerForm.surname)
                            .disableAutocorrection(true)
                    }
                }
            }
        }
}
Enter fullscreen mode Exit fullscreen mode

With that little amount of setup, you should have a basic view to work with. You will have some errors so let's fix that. We need a CustomerDraftFormFields class:

import Foundation

class CustomerDraftFormFields: ObservableObject {
    @Published var title: String = ""
    @Published var firstName: String = ""
    @Published var surname: String = ""

    // It looks much better to keep this here.
    let titles = ["Mr", "Miss", "Mrs", "Dr"]
}
Enter fullscreen mode Exit fullscreen mode

Let's make a slight update to how we loop the titles:

[...]

Section {
    Picker("Title", selection: $customerForm.title, content: {
        ForEach(self.customerForm.titles, id: \.self) {title in
            Text(title).tag(title)
        }
    })

    [...]
}
Enter fullscreen mode Exit fullscreen mode

Listen For State Change

There needs to be a way to listen for some sort of events when any of our form fields change. To do this, we add didSet on each of our fields we are listening for data change.

class CustomerDraftFormFields: ObservableObject {
    @Published var title: String = "" {
        didSet {
            print(title)
        }
    }
    @Published var firstName: String = "" {
        didSet {
            print(firstName)
        }
    }
    @Published var surname: String = "" {
        didSet {
            print(surname)
        }
    }

    [...]
}
Enter fullscreen mode Exit fullscreen mode

Making a change to either fields, we get a print of the value. didSet runs after the property was set and willSet is.... yes, it runs before the property was set.

CoreData

I won't be going through the data setup. I'll assume that's already been taken cared of and you have an entity called DraftCustomers. Call this what ever you like but for this example, we'll use DraftCustomers. Update your class file to import CoreData:

import Foundation
import CoreData

[...]
Enter fullscreen mode Exit fullscreen mode

Now we need to pre-populate the saved values from CoreData to our form fields, if any:

import Foundation
import CoreData

class CustomerDraftFormFields: ObservableObject {
    // Make reference to our context
    private let viewContext = PersistenceController.shared.container.viewContext
    private let fetchRequest: NSFetchRequest<DraftCustomers> = DraftCustomers.fetchRequest()

    [...]

    // Pre-populate the values on first load.
    init() {
        // Only include the first record in the array.
        // fetchRequest will be an array containing only one record (the first)
        fetchRequest.fetchLimit = 1

        do {
            // If there's an error, catch will active
            let customers = try viewContext.fetch(fetchRequest)

            if customers.count == 1 {
                title = customers.first?.title ?? ""
                firstName = customers.first?.firstName ?? ""
                surname = customers.first?.surname ?? ""
            }

        } catch {
            let error = error as NSError
            fatalError("Could not fetch draft customer: \(error)")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

With just that amount of setup, our form fields should be pre-populated. We haven't saved anything as yet so nothing will be shown. Let's save some data. Still in our class:

[...]

private func saveOrUpdateDraft(column: String, value: Any) {
    fetchRequest.fetchLimit = 1

    do {
        let customers = try viewContext.fetch(fetchRequest)

        if customers.count == 1 {
            let customer = customers[0] as NSManagedObject
            customer.setValue(value, forKey: column)

            // Update the entity record
            try viewContext.save()
        } else {
            let customer = DraftCustomers(context: viewContext)
            customer.setValue(value, forKey: column)

            // Create a new entity record
            try viewContext.save()
        }
    } catch {
        let error = error as NSError
        fatalError("Could not save customer as draft: \(error)")
    }
}
Enter fullscreen mode Exit fullscreen mode

We've defined a function called saveOrUpdateDraft(...). The sole purpose of this function is to update a record or create a new record if not found. When you first run this function, it will (should) save the data to our database, but not just yet. Let's update our didSet:

@Published var title: String = "" {
    didSet {
        saveOrUpdateDraft(column: "title", value: title)
    }
}

@Published var firstName: String = "" {
    didSet {
        saveOrUpdateDraft(column: "firstName", value: firstName)
    }
}

@Published var surname: String = "" {
    didSet {
        saveOrUpdateDraft(column: "surname", value: surname)
    }
}

[...]
Enter fullscreen mode Exit fullscreen mode

Every time we update our form fields, saveOrUpdateDraft will run. In the code we specified the column and its value to be entered (created or updated).

You may have a more complex setup but you get a basic idea on how to save or updating a data if already exists in the database.

Improvements

It's still my early days in learning Xcode + Swift. Could this be refactored or refactor for less performance hit? Let me know and I'll update where needed.

💖 💪 🙅 🚩
mirmayne
Daveyon Mayne 😻

Posted on February 14, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related