Why I can not use Equatable function in my View when the View can use it in SwiftUI?

Viewed 837

I am trying use EquatableView for my View but SwiftUI does not use my == function, it seems I done everything right, but does not work, need help. I want stop SwiftUI from unnecessary rendering also using my == function, but it renders every time and does not use the given function, why?

import SwiftUI

extension Int { var isEven: Bool { return self % 2 == 0 } }

struct ContentView: View {
    
    @State private var number = 3
    
    var body: some View {
        VStack {
            
            // EquatableView ( content: NumberView(number: number) )  // <<: Here
            // NumberView(number: number).equatable()   // <<: Or Here
            

            Button("New Random Number") {
                number = Int.random(in: 1...100)
            }
            
            Text(number.description).bold().padding()
        }
    }
}

struct NumberView: View, Equatable {
    
    let number: Int
    
    init(number: Int) {
        
        print("initializing")
        
        self.number = number
    }

    var body: some View {
        
        print("rendering")

        return VStack {
            
            if number.isEven {
                Text("EVEN").bold().foregroundColor(Color.green)
            } else {
                Text("ODD").bold().foregroundColor(Color.red)
            }
            
        }
        .padding()
  
    }

    static func == (lhs: NumberView, rhs: NumberView) -> Bool {
        
        print("Equatable used!")
        
        return lhs.number.isEven == rhs.number.isEven
    }
  
}
1 Answers

(Xcode 12.4, macOS 11.2.3)

The mechanics of equatable() are a little bit opaque. In fact, all the headers have is this (certainly not detailed) definition:

Prevents the view from updating its child view when its new value is the same as its old value.

For example, one would assume that with your original example, that your == would be called to evaluate if the views are equal. But, clearly that doesn't happen.

It can be forced to happen in a number of (sometimes suprising) ways. For example (this one is the un-surprising one), if you add an additional property on NumberView that is a @State property, your == is called each time:


struct NumberView: View, Equatable {
    
    var number: Int = 0
    @State var additionalProp: Int = 0
    
    var body: some View {
        print("rendering \(Date()): \(number)")
        return VStack {
            Text("\(number)")
            if number.isEven {
                Text("EVEN").bold().foregroundColor(Color.green)
            } else {
                Text("ODD").bold().foregroundColor(Color.red)
            }
        }
        .padding()
    }

    static func == (lhs: NumberView, rhs: NumberView) -> Bool {
        print("Equatable used!")
        return lhs.number.isEven == rhs.number.isEven
    }
}

which behaves differently than the following (in which == is not called):

var number: Int = 0
var additionalProp: Int = 0

From this, I think we can infer a couple of things:

  1. With a View without other properties, SwiftUI will do an equality check of the props and if they're not equal, it won't even bother calling your equatable function
  2. If there are @State properties, SwiftUI will call your equatable function.

Here's the more interesting case to me:

struct NumberView: View, Equatable {
    
    var number: Int = 0
    var additionalProp: String = ""
    
    var body: some View {
        print("rendering \(Date()): \(number)")
        return VStack {
            Text("\(number)")
            if number.isEven {
                Text("EVEN").bold().foregroundColor(Color.green)
            } else {
                Text("ODD").bold().foregroundColor(Color.red)
            }
        }
        .padding()
    }

    static func == (lhs: NumberView, rhs: NumberView) -> Bool {
        print("Equatable used!")
        return lhs.number.isEven == rhs.number.isEven
    }
}

In the above case, the equatable function will get used again, even though String is a value type (and Equatable) in Swift and not using @State. I don't have a great explanation for that part.


This page (https://swiftui-lab.com/equatableview/) has a quote from an Apple engineer about when `==` is used:

SwiftUI assumes any Equatable.== is a true equality check, so for POD views it compares each field directly instead (via reflection). For non-POD views it prefers the view’s == but falls back to its own field compare if no ==. EqView is a way to force the use of ==. When it does the per-field comparison the same rules are applied recursively to each field (to choose direct comparison or == if defined). (POD = plain data, see Swift’s _isPOD() function.)

Related