inexcitus
inexcitus

Reputation: 2649

@State property never updates the View

I am learning SwiftUI and right now I have problems understanding all those property wrappers. I made this very simple progress view:

import Foundation
import SwiftUI

public struct VKProgressView : View
{
    private var color: Color = Color.green
    
    @State private var progress: CGFloat = 0.0
    
    public init(value: Float)
    {
        self.progress = CGFloat(value)
    }
    
    public func color(_ color: Color) -> VKProgressView
    {
        var newView = self
        newView.color = color
        
        return newView
    }
    
    public var body: some View
    {
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                Rectangle()
                    .frame(width: geometry.size.width, height: geometry.size.height)
                    .foregroundColor(Color.gray)
                    .opacity(0.30)
                
                Rectangle()
                    .frame(width: geometry.size.width * self.progress, height: geometry.size.height)
                    .foregroundColor(self.color)
            }
        }
    }
}

#if DEBUG
public struct VKProgressView_Previews: PreviewProvider
{
    @State static var progress: Float = 0.75 // This is the value that changes in my real app.
    
    public static var previews: some View
    {
        VKProgressView(value: self.progress)
            .color(Color.accentColor)
    }
}
#endif

However, when passing in some value, changing the value never updates the view. The property that is passed in has the @Published wrapper.

My workaround was to create a new ViewModel class that is instantiated in this progress view. The instance of the ViewModel has the ObservedObject and both properties have the @Published property wrapper. Although this works, I am thinking...this can't be right.

What am I missing here?

This is the working code (you can ignore the color property here):

import Foundation
import SwiftUI

public struct VKProgressView : View
{
    @ObservedObject var viewModel: VKProgressViewViewModel
    
    public init(value: Float, color: Color)
    {
        self.viewModel = VKProgressViewViewModel(progress: value, color: color)
    }
    
    public init(value: CGFloat, color: Color)
    {
        self.viewModel = VKProgressViewViewModel(progress: Float(value), color: color)
    }
    
    public init(value: Double, color: Color)
    {
        self.viewModel = VKProgressViewViewModel(progress: Float(value), color: color)
    }
    
    public var body: some View
    {
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                Rectangle()
                    .frame(width: geometry.size.width, height: geometry.size.height)
                    .foregroundColor(Color.gray)
                    .opacity(0.30)
                
                Rectangle()
                    .frame(width: geometry.size.width * self.viewModel.progress, height: geometry.size.height)
                    .foregroundColor(self.viewModel.color)
            }
        }
    }
}

#if DEBUG
public struct VKProgressView_Previews: PreviewProvider
{
    public static var previews: some View
    {
        VKProgressView(value: Float(0.5), color: Color.green)
    }
}
#endif

And the ViewModel:

import Foundation
import SwiftUI

public class VKProgressViewViewModel : ObservableObject
{
    @Published var progress: CGFloat = 0.0
    @Published var color: Color = Color.accentColor
    
    internal init(progress: Float, color: Color)
    {
        self.progress = CGFloat(progress)
        self.color = color
    }
}

In the second example, every time the "original" value changes, that was passed in, the view updates accordingly. I am experiencing this issue with every single View I have created, so I think that I am simply missing something (fundamental).

Any help is appreciated.

Upvotes: 0

Views: 53

Answers (1)

jrturton
jrturton

Reputation: 119292

@State is for internally managed properties of the view, that would trigger a redraw of that view. It is for value types, so when you pass in a value, the value is copied. SwiftUI maintains the value of @State independently of the specific instance of the view struct, because those structs are created and re-created frequently.

Your progress view is not likely to be updating the value of the progress amount, since it is simply reporting on the value it is given. It should just be let progress: CGFloat. Think of this as like a Text - you just give it a string to display, and it displays it.

Redrawing the view would be the responsibility of the next level up, which would own the progress state, and pass in the current value to your view:

@ObservedObject var model: SomeModelThatOwnsProgressAsAPublishedProperty
...
VKPRogressView(progress: model.progress)

or

@State var progress: CGFloat = 0
...
VKPRogressView(progress: progress)

In either case, changes to the progress would trigger a view redraw.

You haven't shown the code in your app where you are trying to pass in a value that isn't updated, so I can't comment on what exactly is going wrong, but a general rule of thumb is that a view with an @-something property will re-evaluate the body of itself (and therefore its subviews) when that property updates.

  • Views that don't own or update values should have them as let properties.
  • Views that own and update values should have them as @State properties.
  • Views that don't own but can update properties should have them as @Binding

"own" here refers to the source of truth for the property.

Upvotes: 1

Related Questions