In this article

As developers, we have all had to do some CRUDy work. And by CRUDy, I mean developing data management applications that are really just front-ends for Create-Read-Update-Delete operations on application data. They are not sexy, but they help make our data-driven world go 'round. 

In this article, I will walk through how I and my team tackled one simple aspect of managing updates to existing data — whether or not a piece of data is "dirty," or has pending changes that are ready to be saved. Our solution utilizes a relatively new feature in Swift and allowed us to track that state alongside the model types, all without modifying these shared types or the syntax to use them.

Depending on your desired UX, sometimes when working through the design for managing updates to data, it can be important for your application to manage whether or not a piece of data is "dirty." That is, whether a piece of data has been modified from its original state and those pending changes need to be persisted to a back-end data store. For instance, on a recent project, we only wanted to show the user a Save button when there were changes to the data to be saved. In this article, we will recreate the solution we used to create a generic reusable means of tracking the dirty state of any model instance.

The app we were working on was a Mac app and was the admin tool for another iOS app that ultimately consumed the data from the app's back-end. We were able to share and reuse the same data model types and data access code between the admin tool app and the core iOS app in the same Xcode project, which was very nice. What we needed was a "Swifty" way for our Admin Tool to manage the dirty state of instances of those existing model structs.

Start simple

The first pass at this did the job, but it resulted in a LOT of boilerplate code to accomplish it. So given a simple model type like:

struct Person {
    var name: String
    var age: Double
}

We simply added a Boolean isDirty property to our model structs, and made sure that property was true with didSet closures on each data property we would be persisting to the back-end data store.

struct Person {
    var isDirty = false
    
    var name: String {
        didSet {
            isDirty = true
        }
    }
    
    var age: Double {
        didSet {
            isDirty = true
        }
    }
}

We also did not like that all that boilerplate was on the model structs themselves. The concept of managing the dirty state was only relevant in the admin tool app, so we wanted to find a way of solving the problem and still keeping the model types thin, simple data containers. 

Property wrappers are super Swifty!

In an effort to try to find a Swiftier way of working away at that boilerplate, our first inclination was to look into creating a Property Wrapper we could apply to each of the tracked data properties, similar to the @Published property wrapper on an ObservableObject. We would still need to have an .isDirty property on our model struct, but ideally, we would be able to point our property wrapper at that property with a keyPath to set to true when the value is mutated.

struct Person {
	var isDirty = false
	
    @TrackDirty(\.isDirty) var name: String
    @TrackDirty(\.isDirty) var age: Double
}

However, property wrappers typically operate in total isolation from their enclosing types. There is a somewhat hidden API to accomplish this type of technique, but unfortunately, it's only available when being used on classes, and not structs, so that wouldn't help in our case.

Custom wrapper type

What we needed was a wrapper type that encapsulated all of the aspects of managing the dirtiness of its wrapped instance, but still made the underlying instance available for us to modify. 

We can create our wrapper to manage an instance of this like this:

struct Dirtyable<T> {
    var original: T
    
    var isDirty: Bool = false
    
    init(_ initialValue: T) {
        original = initialValue
    }
}

Then, to use it, we can create a person instance and wrap it with our Dirtyable type:

let me = Person(name: "Matt", age: 45)
var newMe = Dirtyable(me)

newMe.original.age = 30

Clearly, the wrapped person is mutable, but we're not yet doing anything to track those changes to the underlying person. We'll come back to that part.

Improving the call site API with @dynamicMemberLookup

In order to access or change the person's properties, we have to access through the original property. In order to streamline access to the wrapped type's properties, let's use Swift's @dynamicMemberLookup capabilities to remove the .original bit from our property accesses. Using this flavor of @dynamicMemberLookup, we can essentially surface all of the properties of our wrapped objects as if they were our own.

@dynamicMemberLookup
struct Dirtyable<T> {
    …
    
    subscript<LocalValue>(dynamicMember keyPath: WritableKeyPath<T, LocalValue>) -> LocalValue {
        get {
            return original[keyPath: keyPath]
        }
        set {
            original[keyPath: keyPath] = newValue
        }
    }
}

Now we can directly access the wrapped instance's properties without specifying the .original property.

newMe.age = 25

print(newMe.original.age) // prints 25
print(newMe.age)          // prints 25

Great! And now the setter in the dynamicMember subscript allow us a seam to also manage an .isDirty property since changes are coming in.

  set {
      original[keyPath: keyPath] = newValue
      isDirty = true
  }

With that in place, we can use our Dirtyable<Person> instance just like was an original Person instance, but with the additional dirty tracking as an added bonus.

var newMe = Dirtyable(me)

print(newMe.isDirty)    // false
newMe.age = 21
print(newMe.isDirty)    // true

Then, when the user taps the Save button, we can trigger the actual update and reset the .isDirty property to be ready for more changes:

// Save to database
newMe.isDirty = false

Our app uses SwiftUI, so let's use that .isDirty property to determine whether or not our view should show the Save button to the user:

struct EditPersonView: View {
    @State private var newMe = Dirtyable(Person(name: "Matt", age: 45))
    
    var body: some View {
        Form {
            TextField("name", text: $newMe.name)
            
            HStack {
                Text("\(Int(newMe.age))") 
               
                Slider(value: $newMe.age, in: 0...150, step: 1.0)
            }
            
            if newMe.isDirty {
                Button("Save", action: saveToDatabase)
            }
        }
    }
    
    private func saveToBackend() {
        // Save to database
		newMe.isDirty = false
    }
}

Undoing changes

But I think we can add even more utility with our Dirtyable wrapper. What if, in addition to a Save button, we also wanted to provide a Cancel button that would effectively roll back any pending updates to their original state? Since structs are lightweight, in addition to holding the original instance of our wrapped value, let's also hold another instance that represents our current pending values. 

We'll create a copy of the original upon the first property change and mutate that copy. Our dynamicMember getter then will return the potentially-updated value from the copy, or if it's accessed before the copy is made, return the property from our original. Next, we'll also provide a rollback() method to ditch the copy and just go back to using the original instance.

@dynamicMemberLookup
struct Dirtyable<T> {
    var original: T
    var updated: T?
    
    var isDirty: Bool = false
    
    init(_ initialValue: T) {
        original = initialValue
    }
    
    subscript<LocalValue>(dynamicMember keyPath: WritableKeyPath<T, LocalValue>) -> LocalValue {
        get {
            if let dirty = updated {
                return dirty[keyPath: keyPath]
            }
            return original[keyPath: keyPath]
        }
        set {
            if updated == nil {
                updated = original
            }
            updated![keyPath: keyPath] = newValue
            isDirty = true
        }
    }
    
    mutating func rollback() {
        updated = nil
    }
}

Then in our view, we can add our Reset button:

    if newMe.isDirty {
        Button("Save", action: saveToBackend)
        
        Button("Reset") {
            newMe.rollback()
        }
    }

Next, we add a corollary to our rollback() function called commit() to clean up after a successful save of our model and at the same time add some more smarts to our .isDirty property such that we will not need to manually reset it at all.

@dynamicMemberLookup
struct Dirtyable<T> {
    …
    
    var isDirty: Bool { updated != nil }
    var current: T { updated ?? original }
    
    …
    
    mutating func commit() {
        if let updated = updated {
            original = updated
            self.updated = nil
        }
    }   
    
    …    
}    

Now after our saveToBackend button action persists the new values, we can reset the view's state with the saved version set as the new baseline.

private func saveToBackend() {
    guard newMe.isDirty else { return }
    
    save(newMe.current)
    
    newMe.commit()
}

Smarter 'isDirty' with Equatable

This is looking pretty good, but there's still a simple improvement we can make. When we make changes to our model, our .isDirty property is getting set to true auto-magically for us. However, if a user were to set the properties back to their original states, our .isDirty flag stays true, and the user could then tap the Save button to trigger what would be an unnecessary save operation in our backend.

Let's use a conditional extension on Dirtyable to take advantage of the case when our generic wrapped type happens to conform to Equatable. In such cases, we'll override our .isDirty property to only return true if updated exists and it is indeed different from the original.

extension Dirtyable where T: Equatable {
    var isDirty: Bool {
        guard let updated = updated else { return false }
        return updated != original
    }
}

With that in place, if Person conformed to the Equatable protocol and the user moves our slider from its original value and then back to its original value, we'll see our Save and Reset buttons appear and disappear accordingly!

The final product

At the end of the day, our Dirtyable wrapper looks like this.

@dynamicMemberLookup
struct Dirtyable<T> {
    var original: T
    var updated: T?
    
    var isDirty: Bool {
        return updated != nil
    }
    var current: T { updated ?? original }
    
    init(_ initialValue: T) {
        original = initialValue
    }
    
    subscript<LocalValue>(dynamicMember keyPath: WritableKeyPath<T, LocalValue>) -> LocalValue {
        get {
            if let dirty = updated {
                return dirty[keyPath: keyPath]
            }
            return original[keyPath: keyPath]
        }
        set {
            if updated == nil {
                updated = original
            }
            updated![keyPath: keyPath] = newValue
        }
    }
    
    mutating func commit() {
        if let updated = updated {
            original = updated
            self.updated = nil
        }
    }
    
    mutating func rollback() {
        updated = nil
    }
}

extension Dirtyable where T: Equatable {
    var isDirty: Bool {
        guard let updated = updated else { return false }
        return updated != original
    }
}

With just under 50 lines of library code, we've got a reusable means of encapsulating the management of the dirty state of any model type that doesn't involve making changes to those model types at all.

We can help

Do you or your organization have an application development need? Reach out to us to talk about how we can be of assistance.