#State property never updates the View - swiftui

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.

#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.

Related

referenced binding param not triggering animation

I have a custom view
struct CustomView: View {
#Binding var text: String
...
var body: some View {
VStack {
...
Text(SomeText)
.offset(y: text.isEmpty ? 0 : -25)
.scaleEffect(text.isEmpty ? 1 : 0.5, anchor: .leading)
.animation(.spring(), value: text.isEmpty)
...
}
the scale effect and offset animation never trigger if text is referenced from an object.
For example, I have a ViewModel such as
class SomeViewModel: ObservableObject {
#Published var text: String = ""
}
And the parent View such as
struct ParentView: View {
#State private var vm = SomeViewModel()
#State private var text = "" //this one works!
...
var body: some View {
...
CustomView(..., text: $vm.text) // no animation, but the value of v.name is updated
CustomView(..., text: $text)) // everything works, including animation
For SwiftUI to properly update based on changes of an ObservableObject, you will need to use a different property wrapper. Usually this is either #ObservedObject or #StateObject dependent on the context you are using it in.
Try using #StateObject if this is where you are first initialising the class.
StateObject Documentation

SwiftUI gesture state is reset between gestures

I have the following code for a simple square to which I attach a MagnificationGesture to update its state with a pinch to zoom gesture.
import SwiftUI
struct ContentView2: View {
var scale: CGFloat = 1
var magnificationGesture: some Gesture {
MagnificationGesture()
.onChanged { value in
scale = value
}
}
var body: some View {
VStack {
Text("\(scale)")
Spacer()
Rectangle()
.fill(Color.red)
.scaleEffect(self.scale)
.gesture(
magnificationGesture
)
Spacer()
}
}
}
struct ContentView2_Previews: PreviewProvider {
static var previews: some View {
ContentView2()
}
}
However this simple view behaves weird. When I perform the gesture, the scale #State property is succesfully modified. However when I then do another gesture with my hands, the scale property is reset to its initial state, instead of starting from its current value.
Here is a video of what happens. For example, when the red view is very small, performing the gesture, I would expect that it stays small, instead of completely resetting.
How can I get the desired behaviour - that is - the scale property is not reset
I was able to get it working by adding a bit to the code. Check it out and let me know if this works for your use case:
import SwiftUI
struct ContentView2: View {
var magGesture = MagnificationGesture()
#State var magScale: CGFloat = 1
#State var progressingScale: CGFloat = 1
var magnificationGesture: some Gesture {
magGesture
.onChanged { progressingScale = $0 }
.onEnded {
magScale *= $0
progressingScale = 1
}
}
var body: some View {
VStack {
Text("\(magScale)")
Spacer()
Rectangle()
.fill(Color.red)
.scaleEffect(self.magScale * progressingScale)
.gesture(
magnificationGesture
)
Spacer()
}
}
}
struct ContentView2_Previews: PreviewProvider {
static var previews: some View {
ContentView2()
}
}
I solved it by adding another scale and only updating one of them at the end to keep track of the scale
Code
import SwiftUI
struct ContentView2: View {
#State var previousScale: CGFloat = 1
#State var newScale: CGFloat = 1
var magnificationGesture: some Gesture {
MagnificationGesture()
.onChanged { value in
newScale = previousScale * value
}
.onEnded { value in
previousScale *= value
}
}
var body: some View {
VStack {
Text("\(newScale)")
Spacer()
Rectangle()
.fill(Color.red)
.scaleEffect(newScale)
.gesture(
magnificationGesture
)
Spacer()
}
}
}
struct ContentView2_Previews: PreviewProvider {
static var previews: some View {
ContentView2()
}
}

SwiftUI ViewModifier for custom View

Is there a way to create a modifier to update a #State private var in the view being modified?
I have a custom view that returns either a Text with a "dynamic" background color OR a Circle with a "dynamic" foreground color.
struct ChildView: View {
var theText = ""
#State private var color = Color(.purple)
var body: some View {
HStack {
if theText.isEmpty { // If there's no theText, a Circle is created
Circle()
.foregroundColor(color)
.frame(width: 100, height: 100)
} else { // If theText is provided, a Text is created
Text(theText)
.padding()
.background(RoundedRectangle(cornerRadius: 25.0)
.foregroundColor(color))
.foregroundColor(.white)
}
}
}
}
I re-use this view in different parts around my app. As you can see, the only parameter I need to specify is theText. So, the possible ways to create this ChildView are as follows:
struct SomeParentView: View {
var body: some View {
VStack(spacing: 20) {
ChildView() // <- Will create a circle
ChildView(theText: "Hello world!") // <- Will create a text with background
}
}
}
Nothing fancy so far. Now, what I need is to create (maybe) a modifier or the like so that in the parent views I can change the value of that #State private var color from .red to other color if I need more customization on that ChildView. Example of what I'm trying to achieve:
struct SomeOtherParentView: View {
var body: some View {
HStack(spacing: 20) {
ChildView()
ChildView(theText: "Hello world!")
.someModifierOrTheLike(color: Color.green) // <- what I think I need
}
}
}
I know I could just remove the private keyword from that var and pass the color as parameter in the constructor (ex: ChildView(theText: "Hello World", color: .green)), but I don't think that's the way to solve this problem, because if I need more customization on the child view I'd end up with a very large constructor.
So, Any ideas on how to achieve what I'm looking for? Hope I explained myself :)
Thanks!!!
It is your view and modifiers are just functions that generate another, modified, view, so... here is some possible simple way to achieve what you want.
Tested with Xcode 12 / iOS 14
struct ChildView: View {
var theText = ""
#State private var color = Color(.purple)
var body: some View {
HStack {
if theText.isEmpty { // If there's no theText, a Circle is created
Circle()
.foregroundColor(color)
.frame(width: 100, height: 100)
} else { // If theText is provided, a Text is created
Text(theText)
.padding()
.background(RoundedRectangle(cornerRadius: 25.0)
.foregroundColor(color))
.foregroundColor(.white)
}
}
}
// simply modify self, as self is just a value
public func someModifierOrTheLike(color: Color) -> some View {
var view = self
view._color = State(initialValue: color)
return view.id(UUID())
}
}
Using a custom ViewModifier is indeed a way to help expose a simpler interface to users, but the general idea of how to pass customization parameters to a View (other than using an init), is via environment variables with .environment.
struct MyColorKey: EnvironmentKey {
static var defaultValue: Color = .black
}
extension EnvironmentValues {
var myColor: Color {
get { self[MyColorKey] }
set { self[MyColorKey] = newValue }
}
}
Then you could rely on this in your View:
struct ChildView: View {
#Environment(\.myColor) var color: Color
var body: some View {
Circle()
.foregroundColor(color)
.frame(width: 100, height: 100)
}
}
And the usage would be:
ChildView()
.environment(\.myColor, .blue)
You can make it somewhat nicer by using a view modifier:
struct MyColorModifier: ViewModifier {
var color: Color
func body(content: Content) -> some View {
content
.environment(\.myColor, color)
}
}
extension ChildView {
func myColor(_ color: Color) {
self.modifier(MyColorModifier(color: color)
}
}
ChildView()
.myColor(.blue)
Of course, if you have multiple customizations settings or if this is too low-level for the user, you could create a ViewModifier that exposes a subset of them, or create a type that encapsulates a style, like SwiftUI does with a .buttonStyle(_:)
Here's how you can chain methods based on Asperi`s answer:
struct ChildView: View {
#State private var foregroundColor = Color.red
#State private var backgroundColor = Color.blue
var body: some View {
Text("Hello World")
.foregroundColor(foregroundColor)
.background(backgroundColor)
}
func foreground(color: Color) -> ChildView {
var view = self
view._foregroundColor = State(initialValue: color)
return view
}
func background(color: Color) -> ChildView {
var view = self
view._backgroundColor = State(initialValue: color)
return view
}
}
struct ParentView: View {
var body: some View {
ChildView()
.foreground(color: .yellow)
.background(color: .green)
.id(UUID())
}
}

Setting View visibility based on a property value?

When defining a view hierarchy using SwiftUI, is it possible to set the hidden() value of a View in the body of the definition?
For example:
var body: some View {
VStack(alignment: .leading) {
Text(self.name)
.font(.headline)
.hidden()
}
}
would hide the Text object, but I would like to use a boolean property to toggle visibility.
There is a way to do this using a ternary operator and the opacity value of the view, but I was hoping for a less clever solution.
If you don't want to use the opacity modifier this way:
struct ContentView: View {
#State private var showText = true
var body: some View {
VStack(alignment: .leading) {
Text("Hello world")
.font(.headline)
.opacity(showText ? 1 : 0)
}
}
}
you can decide to completely remove the view conditionally:
struct ContentView: View {
#State private var showText = true
var body: some View {
VStack(alignment: .leading) {
if showText {
Text("Hello world")
.font(.headline)
}
}
}
}
Consider that both ways are widely used in SwiftUI. For your specific case I'd honestly use the opacity modifier, but even the removal is fine.
Don't know if its still use useful because it's been a long time and I guess you found a solution since.But for anyone who's interested, we could create a modifier, which switches the visibility of the view according to a binding value :
import SwiftUI
struct IsVisibleModifier : ViewModifier{
var isVisible : Bool
// the transition will add a custom animation while displaying the
// view.
var transition : AnyTransition
func body(content: Content) -> some View {
ZStack{
if isVisible{
content
.transition(transition)
}
}
}
}
extension View {
func isVisible(
isVisible : Bool,
transition : AnyTransition = .scale
) -> some View{
modifier(
IsVisibleModifier(
isVisible: isVisible,
transition: transition
)
)
}
}
In use :
Text("Visible")
.isVisible(isVisible: isVisible)
.animation(.easeOut(duration: 0.3), value: isVisible)

View not updating after #state variable changes

struct Flashcard : View {
#State var frontText = newArray[randomNum].kana
#State var backText = newArray[randomNum].romaji
var body: some View {
let zstack = ZStack {
Frontside(kanatext: frontText)
.background(Color.yellow)
.rotation3DEffect(.degrees(self.showResults ? 180.0 : 0.0), axis: (x: 0.0, y: 1.0, z: 0.0))
.zIndex(self.showResults ? 0 : 1)
.frame(width: 300, alignment: .center)
.cornerRadius(25)
}
}
public struct Frontside: View
{
#State public var kanatext: String = ""
public var body: some View
{
Text(self.kanatext)
.font(.title)
.fontWeight(.black)
.padding(32)
}
}
In my code snippet above, when I update the #State var frontText, I'm expecting my view to refresh and display the frontText. But for some reason it won't show the new frontText when it is used in my Frontside struct. If I just print Text(frontText) in my view, it will always refresh as the variable changes. What am I missing for it to refresh properly whenever frontText is updated? Thanks.
You have to declare kanatext as #Binding:
#Binding public var kanatext: String
and then pass the binding to your state, which is owned by the parent view in the initialiser:
Frontside(kanatext: $frontText)
Basically, when you declare a variable inside a view #State - you indicated that this view owns it and the way it is drawn is dependent on it. When you want this state to influence another view you need to pass it in as binding (the $). IF both reference the same variable as #State each has it's own independent copy as they are all value types.