UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

SOLVED: Having trouble grasping the concept of a view changing when the content of a class changes

Forums > SwiftUI

I took a sample from Paul's samples to simply display a list of game scores. I understand that a struct is "real data" and a class is a reference to data. I changed the definition of the SimpleGameResult from a struct to a class.

In the example below, a score is changed in the .onAppear.

The new value is not displayed unless something else on the view is changed. The myCounter += 1 is commented out in this code, and the .onAppear score = 99 does not show in the view. Uncomment the myCounter += 1 and the 99 shows correctly since the view has changed.

if the SimpleGameResult is a struct, the .onAppear score of 99 appears correctly without the nei'cessity if changing something else in the view.

o.k. so the results isn't really changing since the score in the class is changed, but the value that should be displayed has changed, so why would the view not consider itself changed? I'm doing this in a macOS app thus the .frame for sizing

it's the concept that is escaping me and I really need an explanation please.

import SwiftUI

class SimpleGameResult {
    internal init(score: Int) {
        self.score = score
    }
    let id = UUID()
    var score: Int
}

struct ContentView: View {
    @State var results = [
        SimpleGameResult(score: 8),
        SimpleGameResult(score: 5),
        SimpleGameResult(score: 10)
    ]
    @State private var myCounter = 0
    var body: some View {
        VStack {
            Text("myCounter is \(myCounter)")
            ForEach(results, id: \.id) { result in
                Text("Result: \(result.score)")
            }
        }
        .frame(width: 200, height: 200, alignment: .center)
        .onAppear(perform: {
            results[0].score = 99
          //  myCounter += 1
        })
    }
}

3      

Because class is a reference type. When you declare a variable to be a reference type, you are basically storing a pointer to a location in memory where the object resides. Changing a property of that object doesn't change its location in memory.

struct, on the other hand, is a value type, meaning that a variable that is a value type stores the actual item itself. When you make a change to a value type, the entire value changes. SwiftUI observes these changes with the @State property wrapper.

This is why reference types need to be ObservableObjects; SwiftUI can't tell they have changed until/unless the ObservableObject tells it they have changed via the objectWillChange publisher (either explicitly or through the use of the @Published property wrapper).

This is also why @State only works with value types. Making a change to a reference type doesn't trigger a state change because the referenced object hasn't changed at all, just one of its properties. @State watches the value itself, not any of the value's properties, but changing a property of a value type creates a whole new value, which triggers the state change.

Your particular case is a bit more complicated since the results array is a value type but it is storing a bunch of SimpleGameResult instance and that's a reference type. Same principle still applies, though. When you call results[0].score = 99 that changes a property of a particular SimpleGameResult object but not the object itself because of reference semantics. So @State doesn't see the change and the View does not get refreshed. Note, however, that the property does get changed, it just doesn't trigger a refresh of the View.

The reason why you will see the changes if you also update myCounter is because myCounter is a value type and so changing it causes the View to be refreshed. When the View is refreshed, it looks at the current values of everything and picks up the changed value for the SimpleGameResult object that didn't trigger a refresh.

Make sense?

3      

Thanks @roosterboy, that is exactly what I thought was happening and I did not want to accept my understanding as a fact.

Now the problem is how do you make an array an observedobject?

I had forgotten about the observableobject since I have not used it lately, but, how do you make an array of observableobjects. I changed the previous example to the following, but I am getting the same results of not updating until something is changed on the view, ie myCounter += 1 refreshes the view, the .onappear does not.

I did read in passing one solution that @observedobject does not work on an array. Maybe that is this latest problem?

import SwiftUI

class SimpleGameResult: ObservableObject {
    internal init(score: Int) {
        self.score = score
    }
    let id = UUID()
    @Published var score: Int
}
class SimpleClass: ObservableObject {
    @Published var  scores = [
        SimpleGameResult(score: 8),
        SimpleGameResult(score: 5),
        SimpleGameResult(score: 10)
    ]
}
struct ContentView: View {
    @ObservedObject var results = SimpleClass()
    @State private var myCounter = 0
    var body: some View {
        VStack {
            Text("myCounter is \(myCounter)")
            ForEach(results.scores.indices, id: \.self) { ind in
                Text("Result: \(results.scores[ind].score)")
            }
        }
        .frame(width: 200, height: 200, alignment: .center)
        .onAppear(perform: {
            results.scores[0].score = 99
           // myCounter += 1
        })
    }
}

3      

The easiest way to do it is to extract the code within the ForEach into its own View and pass a single SimpleGameResult object into it. Like so:

            ForEach(results.scores.indices, id: \.self) { ind in
                ReferenceRow(rowObject: results.scores[ind])
            }
struct ReferenceRow: View {
    @ObservedObject var rowObject: SimpleGameResult

    var body: some View {
        Text("Result: \(rowObject.score)")
    }
}

(I would also change @ObservedObject var results = SimpleClass() to use @StateObject unless you still need to work on iOS 13.)

But I would first ask if SimpleGameResult needs to be a class instead of a struct? Swift (and SwiftUI) encourages us to use value types as much as possible. You should start with value types and only change them to reference types if you need to.

3      

@roosterboy interesting reading and I have a supplementary question about a class being a reference.

Imagine if I have a swift file defining a class which has variables and methods. This is instantiated in ContentView.

If I also have another view and declare a variable of the class when I navigate to that view is it acceptable to pass a reference to the view to give the functionality of the class to the new view

Example below and sorry if this is in the wrong thread.

import SwiftUI

class TestClass {
    func displayMessage() {
        print("Message")
    }
}

struct ContentView: View {

    var testClass = TestClass()

    var body: some View {
        NavigationView {
            VStack {
                NavigationLink("New View", destination: NewView(testClass: testClass))

            }
            .navigationBarTitle("Class Test")
        }
        .onAppear(perform: testClass.displayMessage)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
import SwiftUI

struct NewView: View {

    var testClass: TestClass

    var body: some View {
        VStack {
            Text("New View")
        }
        .onAppear(perform: testClass.displayMessage)
    }
}

struct NewView_Previews: PreviewProvider {
    static var previews: some View {
        NewView(testClass: TestClass())
    }
}

3      

@roosterboy ... that is the old "keep you views small and simple" trick. Thanks. When you only have one thing that changes, rather than an array of things that change, the world(view) is a happy thing.

This problem popped up while I was putting two classes together with inheritance. Structs work just fine if life was so simple.

@Quindownunder ... you got the concept. That is exactly what is going on here. Sure saves a lot of coding when the class functionality is just passed on. Your can do the same with a struct, they are so interchangable if you do not need one of the class features.

4      

@Quindownunder, you could do that but you'd have to watch out for your class object being deallocated out from under you. This danger was one of the reasons why @StateObject was introduced in SwiftUI 2.0; it makes sure that a reference to the class is retained outside the view hierarchy so that it won't get deallocated and cause mysterious problems. So you would use @StateObject in whatever View "owns" that data and it will be retained even if the View struct itself is removed from memory.

@rbquick:

This problem popped up while I was putting two classes together with inheritance. Structs work just fine if life was so simple.

That's one of the great things about Swift; you can usually use structs and protocols to handle what is done with inheritance in OO languages. Really, there are far fewer places in Swift and SwiftUI where a class is needed. Working with Apple's built-in objects is the main one, e.g. subclassing UIButton or UIView to make custom buttons and views, etc.

3      

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.