View not updating after #state variable changes - swiftui

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.

Related

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

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

How to use Binding<Color> in SwiftUI?

How to change a color dynamically in SwiftUI, like in this example
#Binding var randomColor: Color
public var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 20)
.opacity(0.5)
.foregroundColor($randomColor)
}
}
This does'nt work because .foregroundColor only takes Color. Is there any way of using #Binding with colors or is this just not the way of doing it in SwiftUI?
You only need to use the $variableName form of a binding variable in subviews that need to change, as well as read, the variable’s value.
If all your subview is doing is reading the currently set value, you would access variableName directly. So your body in the example you provided would be
public var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 20)
.opacity(0.5)
.foregroundColor(randomColor)
}
}
Furthermore, using #Binding says that your view is going to need to edit the value of a state variable that is owned by the parent’s view. If you’re not making any changes to it, there is no point declaring it as a binding at all.
On the other hand, if you are going to be changing the value of the variable, but this view “owns" the variable, you’d use #State instead.
For example:
struct ContentView: View {
// owned by this view (and with a starting value defined)
#State var myColor: Color = .accessColor
var body: some View {
VStack {
// This subview is allowed to change the value, so it takes a binding
ColorPicker("Color", selection: $myColor)
// This subview is only going to display the color, so it takes a read-only property
ExampleColorView(color: myColor)
}
}
}
struct ExampleColorView: View {
var color: Color
var body: some View {
ZStack {
Circle()
.stroke(lineWidth: 20)
.opacity(0.5)
.foregroundColor(color)
}
}
}
Whenever the parent view’s myColor variable changes, the SwiftUI system creates a new ExampleColorView struct with the new color defined – and it spots the difference between the old view and the new one, and redraws the screen.

How to initialize a binding Bool value in a SwiftUI view

Revised Example
Based on the thoughtful response from #pawello2222 I received in the comments below I have revised an example to demonstrate the issue I am wrestling with.
In order to demonstrate the issue I am having I am using 2 views a parent and a child. In my code code the parent view executes multiple steps but sometimes the animation subview is not visible in the first step. However when it does become visible the animation has already taken on the appearance of the end state. You can see this behavior in the following example.
Parent View
struct ContentView: View {
#State var firstTime = true
#State var breath = false
var body: some View {
VStack {
// This subview is not displayed until after the first time
if !firstTime {
SecondView(breath: $breath)
}
Spacer()
// A button click simulates the steps in my App by toggling the #Binding var
Button("Breath") {
withAnimation {
self.breath.toggle()
self.firstTime = false
}
}
// This vies shows what happens when the subview is being displayed with an intial state of false for the #Binding var
Spacer()
SecondView(breath: $breath)
}
}
}
Here is the subview containing the animation and using a #Binding var to control the animation appearance.
struct SecondView: View {
#Binding var breath: Bool
var body: some View {
Image(systemName: "flame")
.resizable()
.rotationEffect(.degrees(breath ? 360 : 0), anchor: .center)
.scaleEffect(breath ? 1 : 0.2)
.opacity(breath ? 1 : 0.75)
.animation(.easeInOut(duration: 2))
.foregroundColor(Color.red)
}
}
When you execute this the first time thru the top subview is not being displayed and when you click the button the lower subview executes the expected animation and then toggles the firstTime var so that the top subview becomes visible. Notice that the animation is fully expanded and if I was to then have another step (click) with the same value of true for the #Binding property the view would not change at all. This is the issue I am wrestling with. I would like to keep the subview from being at the end state if the first step is one that has toggled the Bool value even if the subview was not being displayed. In other words I would like to only initialize the subview when it is actually being displayed with a value of true so that the animation will always start out small.
This is why I was hoping to have the subview initialized the Binding var to false until it actually gets invoked for the first time (or to reset its state to the shrunk version of the animation) whichever is more feasible.
It looks like you may want to initialise _breath with the provided parameter:
struct ContentView: View {
#Binding var breath: Bool
init(breath: Binding<Bool>) {
_breath = breath
}
}
However, if you want to use a constant value (in your example false) you can do:
struct ContentView: View {
#Binding var breath: Bool
init(breath: Binding<Bool>) {
_breath = .constant(false)
}
}
But then, why do you need the breath: Binding<Bool> parameter?
EDIT
Here is an example how to control an animation of a child view using a #Binding variable:
struct ContentView: View {
#State var breath = false
var body: some View {
VStack {
Button("Breath") {
withAnimation {
self.breath.toggle()
}
}
SecondView(breath: $breath)
}
}
}
struct SecondView: View {
#Binding var breath: Bool
var body: some View {
Image(systemName: "flame")
.imageScale(.large)
.rotationEffect(.degrees(breath ? 360 : 0), anchor: .center)
.scaleEffect(breath ? 1 : 0.2)
.opacity(breath ? 1 : 0.75)
.animation(.easeInOut(duration: 2))
}
}

set #State var inside geometryReader

How is it possible to set a #State var inside a geometryReader?
This is my code:
#State var isTest:Int = 0
var body: some View {
VStack{
ForEach(self.test, id: \.id) { Test in
VStack{
GeometryReader { geometry in
self.isTest = 1
I try with a function but it doesn't work.
#State var isTest: Int = 0
func testValue() {
self.isTest = 1
}
var body: some View {
VStack{
ForEach(self.test, id: \.id) { Test in
VStack{
GeometryReader { geometry in
testValue()
Any idea? Thanks!
I also had a similar problem yesterday. But I was trying to pass a value from inside the GeometryReader.
I tried a couple of ways but it didn't work.
When I use #State var to declare the variable, the compiler again complained in a purple line saying that Modifying the view during update will make it become Undefined.
When I tried to declare a variable using var only, the compiler just told me that it's immutable.
And then, I tried storing it onto my #EnvironmentObject. And I just got a dead loop.
So, my last hope was using the notification way and some how it worked. But I don't know if it's the standard way of implementation.
#State private var viewPositionY:CGFloat = 0
First, post the value frame.origin.y via notification.
GeometryReader{ geometry -> Text in
let frame = geometry.frame(in: CoordinateSpace.global)
NotificationCenter.default.post(name: Notification.Name("channelBlahblahblah"), object:nil, userInfo:["dict":frame.origin.y])
return Text("My View Title")
}
And then declare a publisher to receive the notification.
private let myPublisher = NotificationCenter.default.publisher(for: Notification.Name("channelBlahblahblah"))
Finally, use the the .onReceive modifier to receive the notification.
.onReceive(myPublisher) { (output) in
viewPositionY = output.userInfo!["dict"] as! CGFloat
//you can do you business here
}
While putting code into function is a nice touch, there may arrive another problem and that is altering the #State variable during update phase:
[SwiftUI] Modifying state during view update, this will cause undefined behavior
Using NotificationCenter to move #State variable update after view update phase can help, but one could use much more simple solution like performing variable update right after render phase by using DispatchQueue.
#State var windowSize = CGSize()
func useProxy(_ geometry: GeometryProxy) -> some View {
DispatchQueue.main.async {
self.windowSize = geometry.size
}
return EmptyView()
}
var body: some View {
return GeometryReader { geometry in
self.useProxy(geometry)
Text("Hello SwiftUI")
}
}
You can update #State variables in the onAppear method if you need the initial geometry values
#State var windowSize = CGSize()
var body: some View {
return GeometryReader { geometry in
VStack {
Text("Hello SwiftUI")
}
.onAppear {
windowSize = geometry.size
}
}
}
You can use onAppear(perform:) to update #State variables with the initial view size and onChange(of:perform:) to update the variables when the view size changes:
struct MyView: View {
#State private var size: CGSize = .zero
var body: some View {
GeometryReader { geometry in
ZStack {
Text("Hello World")
}.onAppear {
size = geometry.size
}.onChange(of: geometry.size) { newSize in
size = newSize
}
}
}
}
Try this
#State private var viewSize: CGSize = .zero
var body: some View {
VStack {
// ...
}
.background(GeometryReader { proxy in
Color.clear.preference(
key: ViewSizePreferenceKey.self,
value: proxy.size
)
})
.onPreferenceChange(ViewSizePreferenceKey.self) { size in
viewSize = size
}
}
private struct ViewSizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = value.width + value.height > nextValue().width + nextValue().height ? value : nextValue()
}
}
So it's totally possible to update a #State inside a GeometryReader. The solution is simple. However, there's a caveat:
you might end up with an infinite loop
(nothing too troublesome, I'll present a solution here)
You'll just need a DispatchQueue.main.async and explicitly declare the type of the view inside GeometryReader. If you execute the View below (don't forget to stop it) you'll see that it never stops updating the value of the Text.
NOT THE FINAL SOLUTION:
struct GenericList: View {
#State var timesCalled = 0
var body: some View {
GeometryReader { geometry -> Text in
DispatchQueue.main.async {
timesCalled += 1 // infinite loop
}
return Text("\(timesCalled)")
}
}
}
This happens because the View will "draw" the GeometryReader, which will update a #State of the View. Thus, the new #State invalidates the View causing the View to be redrawn. Consequently going back to the first step (drawing the GeometryReader and updating the state).
To solve this you need to put some constraints in the draw of the GeometryReader. Instead of returning your View inside the GeometryReader, draw it then add the GeometryReader as a transparent overlay or background. This will have the same effect but you'll be able to put constraints in the presentation.
Personally, I'd rather use an overlay because you can add as many as you want. Note that an overlay does not permit an if else inside of it, but it is possible to call a function. That's why there's the func geometryReader() below. Another thing is that in order to return different types of Views you'll need to add #ViewBuilder before it.
In this code, the GeometryReader is called only once and you get the #State var timesCalled updated.
FINAL SOLUTION:
struct GenericList: View {
#State var timesCalled = 0
#ViewBuilder
func geometryReader() -> some View {
if timesCalled < 1 {
GeometryReader { geometry -> Color in
DispatchQueue.main.async {
timesCalled += 1
}
return Color.clear
}
} else {
EmptyView()
}
}
var body: some View {
Text("\(timesCalled)")
.overlay(geometryReader())
}
}
Note: you don't need to put the same constraints, for example, in my case, I wanted to move the view with the drag gesture. So, I've put the constraints to start when the user touches down and to stop when the user ends the drag gesture.