Reputation: 2649
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
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.
let
properties.@State
properties.@Binding
"own" here refers to the source of truth for the property.
Upvotes: 1