Solved : How do I reset state of an ObservableObject without messing things up? - swift

enter image description here
I have finally managed to resolve my issue. It was caused by depending on .onappear to set ObservableObject properties, rather than using an explicit prior user action with a button (for example).
This was the issue....
I created a StateObject in the "App" file that is an instance of a class called "Controller". Controller contains structs, functions etc that are shared across a number of views.
All views get access to the StateObject through an EnvironmentObject. Views are navigated in sequence using NavigationViews and NavigationLinks. All views call functions and read values of the Controller. Any value changes are conducted entirely within the Controller itself.
My problem is that; When I complete this sequence of Views, I want to reset the state of the Controller. Doing this will enable the user to cycle through the process as many times as needed.
However, whenever I reset state of the Controller using a class level function (called from .onappear in the ContentView), the app immediately navigates to View 2 and ignores the state reset I've invoked. It preserves the previous values that belonged to the Controller. I can see through debugging that it does briefly use the reset values but then instantly reverts to the previously held values.
Can anyone provide any insight as to what might be happening here?
Many thanks.....
I tried to reset the state of a StateObject so that I could re-cycle through my app. Unfortunately, the reset does not work and views with EnvironmentObjects seem to want to preserve the previous state!
This is the code for the view that insists I cannot change then StateObject...
import SwiftUI
struct InPlay: View {
var myCourseId:Int = -1
#EnvironmentObject var golfController: GolfController
var body: some View {
VStack (spacing: 5){
VStack{
Text("\(golfController.golfData.course[golfController.courseIndex].name)").offset(x:0, y:0)
.font(.system(size: 20))
.foregroundColor(golfController.roundFinished ? .gray : GolfColor.golfBlue)
.frame(maxWidth: .infinity, alignment: .leading).offset(x:12, y:-5)
}
HStack(spacing:35){
HStack(spacing: 0){
Text("\(golfController.thisRound.holes[golfController.holeIndex].holeStrokes)")
.gesture(tapGesture)
.font(.system(size: 65))
.fontWeight(.thin)
.frame(width: 200, height: 100)
.frame(maxWidth: .infinity, alignment: .center)
.foregroundColor(golfController.roundFinished ? .gray : golfController.editMode ? GolfColor.golfGreen : GolfColor.golfBlue)
.position(x: 35, y: 0).disabled(golfController.roundFinished)
}
}.offset(y:0)
}.environmentObject(golfController)
.onAppear{
golfController.resetGolfController()
}
.navigationBarTitle("")
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
}
struct InPlay_Previews: PreviewProvider {
static var previews: some View {
InPlay(myCourseId: 0)
}
}
}
And this is the reset function within the controller...
func resetGolfController()
{
calculatedPar = ""
parColor = Color.black
selectedCoursename = ""
courseId = 0
courseIndex = 0
holeIndex = 0 // - a problem
showingPinConfirmation = false
showingDropConfirmation = false
showingQuitConfirmation = false
currentHole = 0
editMode = false
scorecardScore = 0
roundFinished = false
storePin = false
holeParPrefix = ""
storePin = false
holeParPrefix = ""
thisRound = Round(startTimestamp: "", endTimestamp: "", roundScore: 0, roundPar: "0", holes: [RoundHole(holeNum: 1, holeStrokes: 0, holePar: "-", strokes: [])])
golfData.readFromFile()
}

Related

Is there a way to solve the no-data when I click on the view for the first time? In Swift

I want to display the content of a Card with a Modal View that appears when I click on the Custom Card, but when I click only one card (not different card) , it displays a Model View with no data in it. After that, it works good for every card.
This is the code where the error is:
import SwiftUI
struct GoalsView: View {
#Binding var rootActive: Bool
#State private var showSheet = false
#Binding var addGoalIsClicked: Bool
#State private var selectedGoal : Goal = Goal(title: "", description: "")
var body: some View {
ScrollView {
VStack(){
HStack{
Text("Add goals")
.fontWeight(.bold)
.font(.system(size: 28))
Spacer()
}
.padding()
ForEach(goalDB) {goal in
Button() {
selectedGoal = goal
showSheet = true
}label: {
Label(goal.title, systemImage: "")
.frame(maxWidth: .infinity)
.foregroundColor(.black)
}
.padding()
.fullScreenCover(isPresented: $showSheet) {
ModalView(rootActive: $rootActive, goal: selectedGoal)
}
.background {
RoundedRectangle(cornerRadius: 10)
.frame(maxWidth: .infinity)
.foregroundColor(.white)
}
}
.padding(4)
}
}.background(Color.gray.opacity(0.15))
}
}
I tried to find a solution on the Internet (someone has my same problem), reading the documentation of the FullScreenCover but I can not find out how to do it
Instead of using a Boolean to control whether or not to show a sheet and/or full screen cover (I'm going to use "sheet" throughout, the principle is identical for both), you can instead use an optional object.
In this scenario, you start with an optional value that is nil. When you want your sheet to display, you set the object to an actual value. Dismissing the sheet will return your state object to nil, just as dismissing a sheet managed by a Boolean resets the state to false.
In your code, you can remove your showSheet state value, and convert selectedGoal to an optional:
#State private var selectedGoal: Goal?
Your button action now only needs to set this value:
Button {
selectedGoal = goal
} label: {
// ...
}
And then in your modifier, you use the item: form instead. The closure receives the unwrapped version of the state object – i.e., it'll always get a Goal, rather than a Goal?. This is the value you should pass into the view being presented:
.fullScreenCover(item: $selectedGoal) { goal in
ModalView(rootActive: $rootActive, goal: goal)
}

Refreshing UserDefaults Variable not working

I have a navigation view that uses User defaults, when I am on a different page I have a button that adds += 1 to the userDefualts variable. When I go back there, it doesn't refresh unless I swipe the app. That is super weird, what I want to happen is it automatically refreshes when I enter that page.
Nav View that I need refreshed:
struct Analytics: View {
#ObservedObject var viewModel = VariableViewModel()
HStack {
// Problems compleated
Text(" Amount of problems compleated:")
.font(.system(size: 20))
.foregroundColor(.black)
Spacer()
Text(String(viewModel.problemsCompleated))
.font(.system(size: 20))
.foregroundColor(.yellow)
.bold()
.offset(x: -5, y: 0)
}.offset(x: 0, y: 5)
}
Nav View were the variables sit and are being changed:
class VariableViewModel: ObservableObject {
// analytics
#Published var problemsCompleated = UserDefaults.standard.integer(forKey: "problemsCompleated")
problemsCompleated += 1
UserDefaults.standard.set(problemsCompleated, forKey: "problemsCompleated")
}
With your current code, if you're using different instances of VariableViewModel, you won't see it refresh because it only ever pulls from UserDefaults on instantiation. So, when you go 'back' to the other page, it has no way of knowing that variable has been updated.
If you were just within your view, I'd recommend using #AppStorage, since it does all the lifting of checking and updating UserDefaults for you. In the ObservableObject, it's slightly more complicated, because #AppStorage won't trigger an update like #Published. But, it looks like by manually calling objectWillChange when the #AppStorage is set, we can get the best of both worlds:
class VariableViewModel: ObservableObject {
#AppStorage("problemsCompleted") var problemsCompleted = 0 {
didSet {
self.objectWillChange.send()
}
}
func increment() {
problemsCompleted += 1
}
}

SwiftUI - triggered breakpoints twice

I am trying to figure out why a breakpoint triggered just once for the first time I used my app and when I did the same steps one more time the breakpoints triggered twice for the same line of code, nothing changed on runtime I just used navigation forward and back. I demo this issue with a comment on my video below. It's 5 minutes video with explanation, but you will see the issue in 2 minutes:
here is a link to a video I made
https://youtu.be/s3fqJ4YhQEc
If you don't want to watch a video I will add a code at the bottom of the question:
here is a link to a repo I used to record a video: https://github.com/matrosovDev/SwiftUINavigatioPopBackIssue
I am also use Apple Feedback Assistant, so guys help me with this as well. I assumed that there is something with deallocating view that was not deallocated in a right way but they answered with this:
There is no notion of “deallocating” structs in Swift. SwiftUI manages
view lifetimes. If you’re writing code that somehow depends on the
existance or non-existance of a particular View at a particular point
in time, that code is wrong.
What leads you to believe that the SecondView isn’t “deallocated”?
The code that leads you to that conclusion is making incorrect
assumptions about the SwiftUI view lifecycle.
Anyway you may see on the video breakpoints triggered twice and even before I actually showing SecondView.
Code example:
struct FirstView: View {
#EnvironmentObject var simpleService: SimpleService
#State var showSecondView = false
var body: some View {
NavigationView {
ZStack {
Button(action: {
self.downloadImageFromURL()
}) {
Text("Next")
.fontWeight(.bold)
.font(.title)
.padding(EdgeInsets(top: 20, leading: 40, bottom: 20, trailing: 40))
.background(Color.blue)
.cornerRadius(8)
.foregroundColor(.white)
}
NavigationLink(destination: SecondView(), isActive: $showSecondView) { EmptyView() }
}
}
}
func downloadImageFromURL() {
simpleService.image = nil
if let url = URL(string: "https://placebear.com/200/300") {
simpleService.downloadImage(from: url) { (image) in
self.showSecondView = true
}
}
}
}
struct CircleImage: View {
var image: Image
var body: some View {
image
.resizable()
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 10)
.aspectRatio(contentMode: .fill)
}
}
struct SecondView: View {
#EnvironmentObject var simpleService: SimpleService
var body: some View {
NavigationView {
CircleImage(image: simpleService.image!) // Called twice next time I visit SecondView
.frame(height: 140)
.frame(width: 140)
}.onAppear(perform: fetch)
}
private func fetch() {
print(Date())
}
}

How to keep the animation running on TabView View change?

I've recently found that when an animation is running forever and the View is switched through the TabView, the animation state is lost. While this is expected behaviour, I'd like to understand if there is a way to keep the animation running when back to the animated View, after the TabView View change?
Here's a quick example, showing the behaviour in SwiftUI:
import SwiftUI
class MyState: ObservableObject {
#Published var animate: Bool = false
}
struct ContentView: View {
var body: some View {
TabView {
AnimationView()
.tabItem {
Image(systemName: "eye")
}
Text("Some View")
.tabItem {
Image(systemName: "play")
}
}
}
}
struct AnimationView: View {
#EnvironmentObject var myState: MyState
var body: some View {
VStack {
Text(self.myState.animate ? "Animation running" : "Animation stopped")
.padding()
ZStack {
Circle()
.stroke(style: StrokeStyle(lineWidth: 12.0))
.foregroundColor(Color.black)
.opacity(0.3)
Circle()
.trim(
from: 0,
to: self.myState.animate ? 1.0 : 0.0
)
.stroke(
style: StrokeStyle(lineWidth: 12.0, lineCap: .round, lineJoin: .round)
)
.animation(
Animation
.linear(duration: self.myState.animate ? 1.0 : 0.0)
.repeatForever(autoreverses: false)
)
}.frame(width: 200, height: 200)
Button(action: {
self.myState.animate.toggle()
}) {
Text("Toggle animation")
.padding()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(MyState())
}
}
Below you can see the behaviour:
What I'd expect to control, is get back to the View where the animation runs and see the state it'd be at the current time - this is, imagining that the animation is looping when switching Views and back to the View find it in the correct point in "running time" or "real-time".
Obs: The State is handled in MyState an EnvironmentObject and it's not local (#State) in the View
So I added an extra parameter to your MyState to store the current animation status when the user switches between tabs. Below is the updated code. I have also added comments in the code for better understanding.
class MyState: ObservableObject {
#Published var animate: Bool = false
var isAnimationActive = false // Store animate status when user switches between tabs
}
struct AnimationView: View {
#EnvironmentObject var myState: MyState
var body: some View {
VStack {
Text(self.myState.animate ? "Animation running" : "Animation stopped")
.padding()
ZStack {
Circle()
.stroke(style: StrokeStyle(lineWidth: 12.0))
.foregroundColor(Color.black)
.opacity(0.3)
Circle()
.trim(
from: 0,
to: self.myState.animate ? 1.0 : 0.0
)
.stroke(
style: StrokeStyle(lineWidth: 12.0, lineCap: .round, lineJoin: .round)
)
.animation(
Animation
.linear(duration: self.myState.animate ? 1.0 : 0.0)
.repeatForever(autoreverses: false)
)
}.frame(width: 200, height: 200)
Button(action: {
self.myState.animate.toggle()
}) {
Text("Toggle animation")
.padding()
}
}
.onAppear(perform: {
if self.myState.isAnimationActive {
/*switching the animation inside withAnimation block is important*/
withAnimation {
self.myState.animate = true
}
}
})
.onDisappear {
self.myState.isAnimationActive = self.myState.animate
/*The property needs to be set to false as SwiftUI compare the
views before re-rendering them*/
self.myState.animate = false
}
}
}
After some tests, the best solution I found this far is to follow a few rules I've stated below. Have in mind that this is only useful if you can track the exact animation position in time. So please check the process bellow:
1) Stop the animation .onDisappear
The reason is that state changes between views that have animated elements will cause unexpected behaviour, for example, the element positioning be completely off and so on.
self.myState.animate = false
2) Keep track of progress
My use-case is a long computation that starts from 0 and ends in X and MyState has methods that provide me with the current position in the computation. This value is then is calculated in the range* 0 to 1, so that it's possible to put it into the SwiftUI Animation.
3) Jumpstart to a specific time
This part is not supported yet in SwiftUI Animation, but let's say that you have an animation that takes 5 seconds, you're at 50% playtime, so you'd jump to 2.5 seconds where you'd want to start play.
An implementation for this, if you've experienced Javascript and way before, ActionScript is GSAP doc for seek (https://greensock.com/docs/v2/Core/Animation/seek())
4) Re-trigger the animation
We started by stopping the animation by changing the state, to make SwiftUI start the animation all we need to do at this stage is to change the state once again
self.myState.animate = true
There might be other ways to keep the animation running, but at the time of writing haven't found it documented, so there was a need to "start animation from any point".

How to make a picture appear in a cell after pressing a button

In SwiftUI I have a button in a View:
Button(action: {self.isMatrimonioOn.toggle()}) {
Image(!isMatrimonioOn ? "imageA" : "imageA_color")
}
When the user presses the button it changes color (imageA-> imageA_color) and at the same time an image (imageA_Color itself) must appear in the cell in another view. How should I set the function?
Thanks but I still haven't succeeded. I put it, I think, but when the button is pressed it does not change the image.
#Binding var isMatrimonioOn : Bool
var body: some View {
ZStack{
Image("sfondo")
.resizable()
.frame(width: 350, height: 180)
CellBackground()
.offset(x: 93, y: 29)
HStack {
VStack(alignment: .leading) {
if isNataleOn == true {
Image("imageA").offset(x: 3, y: 15)
} else {
Image("none")
}
Assuming "another view" here is a child view of this view and isMatrimonioOn is a #State
The child view can have a var with attribute #Binding to which you can pass $isMatrimonioOn.
You can then use logic like in your question or if statement wrapping the image.