Daveyon Mayne 😻
Posted on February 14, 2021
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)
}
}
}
}
}
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"]
}
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)
}
})
[...]
}
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)
}
}
[...]
}
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
[...]
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)")
}
}
}
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)")
}
}
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)
}
}
[...]
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.
Posted on February 14, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.