Luke
Luke

Reputation: 9700

How can I detect changes in a struct, including any of its properties, or their properties, etc.?

Let's say I have the following set up:

struct Sandwich {
    var bread: Bread
    var fillings: [Filling]
    var slices: Int
}

enum Bread: String {
    case white
    case wholemeal
}

struct Filling {
    let name: String
    let calories: Int
}

So, we have a parent struct which has some other properties, enums, other structs, etc. inside as children. They may have their own children too.

What I'm trying to do is detect any change to my Sandwich, and post a notification or use Combine to publish a change, etc.

So, here's my sandwich:

var sandwich = Sandwich(bread: .white, fillings: [Filling(name: "Chicken", calories: 100)], slices: 2)

Now, let's say I come along a few minutes later and decide to add another filling:

sandwich.fillings.append(Filling(name: "Lettuce", calories: 10)]

Or, perhaps they decide to increase their order:

sandwich.slices += 2

What I'm trying to do, is stay in the loop. I want a notification, something, to let me know if my Sandwich changed in any way, whether one of its child properties changed, or one of its grandchild's properties, or even further down the tree.

I'd also like to use these changed versions of my Sandwich to build an undo/redo queue, so the state at each change would ideally be loggable / storable.

I realise I could write a load of delegates and stuff to do this, but I want something that will automatically work and keep working if the model changes (i.e. if I add a new property, etc.)

Upvotes: 1

Views: 1094

Answers (1)

dengST30
dengST30

Reputation: 4037

Both KVO and Combine works for class inherited from NSObject.

the automatically change needs runtime.


Use class, instead of struct

Model Part:

class Sandwich: NSObject {
    var bread = Bread.white
    @objc dynamic var fillings = [Filling]()
    @objc dynamic var slices: Int = 0
}

enum Bread: String {
    case white
    case wholemeal
}

class Filling: NSObject {
    @objc dynamic var name: String = ""
    @objc dynamic var calories: Int = 0
}

Use KVO:

class ViewController: UIViewController {

    @objc var food = Sandwich()
    var observationFilling: NSKeyValueObservation?
    var observationSlice: NSKeyValueObservation?

    override func viewDidLoad() {
        super.viewDidLoad()
        // use KVO
        observationFilling = observe(\.food.fillings, options: [.new]) { object, change in
            if let val = change.newValue{
                print ("food.fillings now \(val).")
            }
        }
        observationSlice = observe(\.food.slices, options: [.new]) { object, change in
            if let val = change.newValue{
                print ("food.slices now \(val).")
            }
        }
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        food.fillings.append(Filling())
        food.slices += 1
    }

}

Use Combine:

class ViewController: UIViewController {

    @objc var food = Sandwich()

    var cancellableFilling: Cancellable?
    var cancellableSlice: Cancellable?

    override func viewDidLoad() {
        super.viewDidLoad()
        // use Combine
        cancellableFilling = food.publisher(for: \.fillings)
                                    .sink(){
                                        val in
                                        print("food.fillings now \(val).")
                                    }


        cancellableSlice = food.publisher(for: \.slices)
                                    .sink(){
                                        val in
                                        print("food.slices now \(val).")
                                    }

    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        food.fillings.append(Filling())
        food.slices += 1
    }

}

Upvotes: 1

Related Questions