Search code examples
swiftfoundation

How to implement `isEqual` method conveniently using Mirror in Swift 3


like

class A :NSObject {
    let a :Int
    let b :UIColor
}

I don't want to implement isEqual by comparing all properties one by one. If that, When I add another property, I should modify isEqual's implement again.

When using Mirror in swift, I could print all properties conveniently. How do I implement isEqual method conveniently by using Mirror.


Solution

  • You shouldn't use runtime introspection other than for diagnostics, and certainly not to avoid small amounts of "boilerplate" code or to avoid updating existing code.

    Below follows, however, some comments on the subject, but note that these should be considered hacks and should not be used in any kind of production code. They can, however, show some example usage of runtime introspection in Swift.

    Equatable class/struct "wrapper", using runtime introspection for property-by-property equality testing

    You could implement an Equatable-container for holding values of equatable types, which can (different from Equatable itself) be casted to, which we will make use of to compare properties of say, a class or a struct.

    /*  Heterogeneous protocol acts as castable Equatable container used for
        property-by-property equality testing in EquatableConstruct   */
    protocol PseudoEquatableType {
        func isEqual(to other: PseudoEquatableType) -> Bool
    }
    
    extension PseudoEquatableType where Self : Equatable {
        func isEqual(to other: PseudoEquatableType) -> Bool {
            if let o = other as? Self { return self == o }
            return false
        }
    }
    

    with the class/struct equatable "wrapper" and its conformance to Equatable implemented (ab)using runtime introspection:

    /*  EquatableConstruct and its conformance to Equatable  */
    protocol EquatableConstruct : Equatable { }
    func ==<T: EquatableConstruct>(lhs: T, rhs: T) -> Bool {
    
        let mirrorLhs = Mirror(reflecting: lhs)
        let mirrorRhs = Mirror(reflecting: rhs)
    
        guard let displayStyle = mirrorLhs.displayStyle,
            (displayStyle == .struct || displayStyle == .class) else {
    
                print("Invalid use: type is not a construct.")
                return false
        }
    
        let childrenLhs = mirrorLhs.children.filter { $0.label != nil }
        let childrenRhs = mirrorRhs.children.filter { $0.label != nil }
    
        guard childrenLhs.count == childrenRhs.count else { return false }
    
        guard !childrenLhs.contains(where: { !($0.value is PseudoEquatableType) }) else {
            print("Invalid use: not all members have types that conforms to PseudoEquatableType.")
            return false
        }
    
        return zip(
            childrenLhs.flatMap { $0.value as? PseudoEquatableType },
            childrenRhs.flatMap { $0.value as? PseudoEquatableType })
            .reduce(true) { $0 && $1.0.isEqual(to: $1.1) }
    }
    

    Example usage

    We setup some non-native types to use in the example:

    struct MyStruct {
        var myInt: Int = 0
        var myString: String = ""
    }
    
    class MyClass {
        var myInt: Int
        var myString: String
        var myStruct: MyStruct
        var myColor: UIColor
    
        init(myInt: Int, myString: String,
             myStruct: MyStruct, myColor: UIColor) {
            self.myInt = myInt
            self.myString = myString
            self.myStruct = myStruct
            self.myColor = myColor
        }
    }
    

    For some given type, e.g. MyClass, the EquatableConstruct "equatable wrapper" may be used only if all types of different properties in the type itself (here, in MyClass) themselves conform to PseudoEquatableType:

    /* Extend (some/all) fundamental (equatable) Swift types to PseudoEquatableType  */
    extension Bool : PseudoEquatableType {}    
    extension Int : PseudoEquatableType {}
    // ... Int8, UInt8, ..., Double, Float, ... and so on
    
    extension String : PseudoEquatableType {}
    extension UIColor: PseudoEquatableType {}
    
    /* As a MyStruct instance is contained in MyClass, extend MyStruct to PseudoEquatableType
     to add the type to allowed property types in EquatableConstruct    */
    extension MyStruct : PseudoEquatableType {}
    
    /* Conformance to EquatableConstruct implies conformance to Equatable */
    extension MyStruct : EquatableConstruct {}
    extension MyClass : EquatableConstruct {}
    

    Testing the automatic Equatable conformance of MyStruct and MyClass, given by their conformance to EquatableConstruct:

    /* Example */
    var aa = MyStruct()
    var bb = MyStruct()
    
    aa == bb            // true
    aa.myInt = 1
    aa == bb            // false
    
    var a = MyClass(myInt: 10, myString: "foo",
                    myStruct: aa, myColor: UIColor(white: 1.0, alpha: 1.0))
    var b = MyClass(myInt: 10, myString: "foo",
                    myStruct: aa, myColor: UIColor(white: 1.0, alpha: 1.0))
    
    a == b              // true
    a.myInt = 2
    a == b              // false
    b.myInt = 2
    b.myString = "Foo"
    a.myString = "Foo"
    a == b              // true
    a.myStruct.myInt = 2
    a == b              // false
    a.myStruct.myInt = 1
    a == b              // true
    a.myColor = UIColor(white: 0.5, alpha: 1.0)
    a == b              // false