iosswiftui

How to make SwiftUI realise NSObjects are the same?


I have a rather simple premise: I have a ForEach with data items inside that are NSObjects (actually they inherit and are more specific) let's call them TestClass. When a new item appears (or disappears) from the data, I want to use a scale transition to scale the view in-/or out.

However, here is the issue: whenever the array changes in any way (even if it is beign set to the same value as it already has, all items are being scaled for a short time, as if dissappearing and appearing rapidly.

How can I achieve my desired effect?

Code to reproduce:

class TestClass: NSObject {
    var id: String
    
    init(id: String) {
        self.id = id
    }
    
    /* does not help...
     static func == (lhs: TestClass, rhs: TestClass) -> Bool {
        return lhs.id == rhs.id
    }
    
    override public var hash: Int {
        return id.hash
    }*/
}

struct TestView: View {
    
    @State var test2: [TestClass] = [
        TestClass(id: "1")
    ]
    
    var body: some View {
        HStack(spacing: 20) {
            ForEach(test2, id: \.self) { _ in
                Color.red.frame(width: 100, height: 100).transition(.scale)
            }
            .animation(.bouncy(duration: 0.4), value: test2)
            .onAppear {
                Task {
                    try? await Task.sleep(seconds: 5)
                    print("RESET")
                    print("before: \(test2)")
                    withAnimation {
                        self.test2 = [
                            TestClass(id: "1"),
                            TestClass(id: "2"),
                        ]
                    }
                    print("after: \(test2)")
                    try? await Task.sleep(seconds: 5)
                    print("RESET2")
                    withAnimation {
                        self.test2 = [
                            TestClass(id: "1"),
                            TestClass(id: "2"),
                            TestClass(id: "3"),
                        ]
                    }
         }
}

Solution

  • This problem is all about identity.

    If TestClass could be changed to a struct then it works:

    struct TestClass: Hashable {
        // ...
    }
    

    Alternatively, if TestClass must be a class then just change the ForEach to use id: \.id:

    ForEach(test2, id: \.id) { _ in
    

    Better still, TestClass could conform to Identifiable:

    class TestClass: NSObject, Identifiable {
        // ...
    }
    

    Then the ForEach can be simplified:

    ForEach(test2) { _ in