leavez leavez - 1 month ago 13
Swift Question

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
.

Answer

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