import Foundation protocol KeyPathListable { var allKeyPaths: [String: PartialKeyPath] { get } var keyPathsList: [PartialKeyPath] { get } } extension KeyPathListable { private subscript(checkedMirrorDescendant key: String) -> Any { Mirror(reflecting: self).descendant(key)! } var keyPathsList: [PartialKeyPath] { var list = [PartialKeyPath]() let mirror = Mirror(reflecting: self) for case (let key?, _) in mirror.children { list.append(\Self.[checkedMirrorDescendant: key] as PartialKeyPath) } return list } var allKeyPaths: [String: PartialKeyPath] { var membersTokeyPaths = [String: PartialKeyPath]() let mirror = Mirror(reflecting: self) for case (let key?, _) in mirror.children { membersTokeyPaths[key] = \Self.[checkedMirrorDescendant: key] as PartialKeyPath } return membersTokeyPaths } } struct DataSubstruct: Hashable, KeyPathListable { let foo: String let num: Double } struct DataStruct: Hashable, KeyPathListable { let num: Int let str: String let arrOfStrings: [String] let substruct: DataSubstruct let arrOfSubstructs: [DataSubstruct] } extension Hashable where Self: KeyPathListable { // WARNING: Non-transitive! func diff(against updated: Self) -> [PartialKeyPath: AnyHashable] { guard self.keyPathsList.elementsEqual(updated.keyPathsList) else { fatalError("Not implemented!") } var diff = [PartialKeyPath: AnyHashable]() dump(Self.self) for (_, kp) in self.allKeyPaths { if let currentValue = self[keyPath: kp] as? AnyHashable, let updatedValue = updated[keyPath: kp] as? AnyHashable { if currentValue != updatedValue { diff[kp] = updatedValue } } } return diff } static func == (lhs: Self, rhs: Self) -> Bool { guard lhs.keyPathsList.elementsEqual(rhs.keyPathsList) else { fatalError("Not implemented!") } for kp in lhs.keyPathsList { guard let lhsValue = lhs[keyPath: kp] as? AnyHashable, let rhsValue = rhs[keyPath: kp] as? AnyHashable, lhsValue == rhsValue else { return false } } return true } func hash(into hasher: inout Hasher) { for kp in keyPathsList { guard let value = self[keyPath: kp] as? AnyHashable else { continue } value.hash(into: &hasher) } } } let a = DataStruct( num: 1, str: "One", arrOfStrings: ["foo", "bar", "baz"], substruct: DataSubstruct(foo: "Foo", num: 1), arrOfSubstructs: [ DataSubstruct(foo: "Bar", num: 1.0), DataSubstruct(foo: "Baz", num: 2.0) ] ) let b = DataStruct( num: 2, str: "Two", arrOfStrings: ["foo", "bar", "baz"], substruct: DataSubstruct(foo: "Foo", num: 1), arrOfSubstructs: [ DataSubstruct(foo: "Bar", num: 1.0), DataSubstruct(foo: "Quix", num: 2.0) ] ) let difference = a.diff(against: b) difference.keys.forEach { print(\DataStruct.num == $0) print(a[keyPath: $0], "->", b[keyPath: $0]) }