Refreshing UserDefaults Variable not working - swift

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
}
}

Related

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

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()
}

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)
}

Timer crashing screens in SwiftUI

I have an app that is using a timer to scroll programmatically between views on the onboarding screen. From the onboarding screen, I can go to login then signup screen (or profile then my account screen). In both ways, when opening the login screen and clicking on signup, the signup screen appears but then disappears and takes me back to login (The same happens when I try to enter My Account screen from the profile screen). Check please the gif attached.
I've searched all around the web without finding any similar issue.
Note: I am using NavigationLink to navigate between screens, and I tried also to use both Timer, and Timer.TimerPublisher leading me to the same result, the screens will crash once the timer fires.
Note: When removing the timer (or the code inside onReceive) everything works fine. I also suspect that the issue could be related to menuIndex
struct OnboardingView: View {
//MARK: - PROPERTIES
#State var menuIndex = 0
#State var openLogin = false
let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect()
//MARK: - BODY
var body: some View {
NavigationLink(destination: LoginView(), isActive: $openLogin) {}
VStack (spacing: 22) {
ProfileButton(openProfile: $openLogin)
.frame(maxWidth: .infinity, alignment: .trailing)
.padding(.horizontal, 22)
OnboardingMenuItem(text: onboardingMenu[menuIndex].title, image: onboardingMenu[menuIndex].image)
.frame(height: 80)
.animation(.easeInOut, value: menuIndex)
.onReceive(onboardingVM.timer) { input in
if menuIndex < onboardingMenu.count - 1 {
menuIndex = menuIndex + 1
} else {
menuIndex = 0
}
}
...
}
}
}
You can only have one detail NavigationLink, if you want more you have to set isDetailLink(false), this is why you are seeing the glitch. Alternatively if you don't require the split view behaviour you can use .navigationViewStyle(.stack). Note in iOS 16 this is replaced with NavigationStackView.
FYI a timer needs to be #State or it will start from zero every time that View is init, e.g. on a state change in the parent View causing its body to be invoked. Even better is to group the related properties into their own struct, e.g.
struct OnboardingViewConfig {
var menuIndex = 0
var openLogin = false
let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect()
// mutating func exampleMethod(){}
}
struct OnboardingView: View {
#State var config = OnboardingViewConfig()
...

How to animate the removal of a view created with a ForEach loop getting its data from an ObservableObject in SwiftUI

The app has the following setup:
My main view creates a tag cloud using a SwiftUI ForEach loop.
The ForEach gets its data from the #Published array of an ObservableObject called TagModel. Using a Timer, every three seconds the ObservableObject adds a new tag to the array.
By adding a tag the ForEach gets triggered again and creates another TagView. Once more than three tags have been added to the array, the ObservableObject removes the first (oldest) tag from the array and the ForEach removes that particular TagView.
With the following problem:
The creation of the TagViews works perfect. It also animates the way it's supposed to with the animations and .onAppear modifiers of the TagView. However when the oldest tag is removed it does not animate its removal. The code in .onDisappear executes but the TagView is removed immediately.
I tried the following to solve the issue:
I tried to have the whole appearing and disappearing animations of TagView run inside the .onAppear by using animations that repeat and then autoreverse.
It sort of works but this way there are two issues.
First, if the animation is timed too short, once the animation finishes and the TagView is removed, will show up for a short moment without any modifiers applied, then it will be removed.
Second, if I set the animation duration longer the TagView will be removed before the animation has finished.
In order for this to work I'd need to time the removal and the duration of the animation very precisely, which would make the TagView very dependent on the Timer and this doesn't seem to be a good solution.
Another solution I tried was finding something similar to self.presentationMode.wrappedValue.dismiss() using the #Environment(\.presentationMode) variable and somehow have the TagView remove itself after the .onAppear animation has finished. But this only works if the view has been created in an navigation stack and I couldn't find any other way to have a view destroy itself. Also I assume that would again cause issue as soon as TagModel updates its array.
I read several other S.O. solution that pointed towards the enumeration of the data in the ForEach loop. But I'm creating each TagView as its own object, I'd assume this should not be the issue and I'm not sure how I'd have to implement this if this is part of the issue.
Here is the simplified code of my app that can be run in an iOS single view SwiftUI project.
import SwiftUI
struct ContentView: View {
private let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
#ObservedObject var tagModel = TagModel()
var body: some View {
ZStack {
ForEach(tagModel.tags, id: \.self) { label in
TagView(label: label)
}
.onReceive(timer) { _ in
self.tagModel.addNextTag()
if tagModel.tags.count > 3 {
self.tagModel.removeOldestTag()
}
}
}
}
}
class TagModel: ObservableObject {
#Published var tags = [String]()
func addNextTag() {
tags.append(String( Date().timeIntervalSince1970 ))
}
func removeOldestTag() {
tags.remove(at: 0)
}
}
struct TagView: View {
#State private var show: Bool = false
#State private var position: CGPoint = CGPoint(x: Int.random(in: 50..<250), y: Int.random(in: 10..<25))
#State private var offsetY: CGFloat = .zero
let label: String
var body: some View {
let text = Text(label)
.opacity(show ? 1.0 : 0.0)
.scaleEffect(show ? 1.0 : 0.0)
.animation(Animation.easeInOut(duration: 6))
.position(position)
.offset(y: offsetY)
.animation(Animation.easeInOut(duration: 6))
.onAppear() {
show = true
offsetY = 100
}
.onDisappear() {
show = false
offsetY = 0
}
return text
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
It is not clear which effect do you try to achieve, but on remove you should animate not view internals, but view itself, ie. in parent, because view remove there and as-a-whole.
Something like (just direction where to experiment):
var body: some View {
ZStack {
ForEach(tagModel.tags, id: \.self) { label in
TagView(label: label)
.transition(.move(edge: .leading)) // << here !! (maybe asymmetric needed)
}
.onReceive(timer) { _ in
self.tagModel.addNextTag()
if tagModel.tags.count > 3 {
self.tagModel.removeOldestTag()
}
}
}
.animation(Animation.easeInOut(duration: 1)) // << here !! (parent animates subview removing)

Accessing class instance in XCTest using SwiftUI and #EnvironmentObject

I have a SwiftUI View hierarchy with an instance of a custom class injected using .environment() similar to the following:
struct ContentView: View {
// Pointer to the AppStateController passed in .environment()
#EnvironmentObject var appStateController: AppStateController
var body: some View {
VStack(spacing: 0) {
TitleView()
.modifier(TitleStyle())
.environmentObject(appStateController)
Spacer()
}
}
}
struct TitleView: View {
#EnvironmentObject var appStateController: AppStateController
var body: some View {
Button(action: {
self.appStateController.isPlaying.toggle()
}, label: {
if self.appStateController.isPlaying {
Image(systemName: "stop.circle")
.opacity(self.appStateController.isPlayable ? 1.0 : 0.5)
.accessibility(label: Text("stop"))
}
else {
Image(systemName: "play.circle")
.opacity(self.appStateController.isPlayable ? 1.0 : 0.5)
.accessibility(label: Text("play"))
}
})
}
}
On the TitleView there are a bunch of buttons whose actions change #Published values in the appStateController. The buttons also change their label (icon) when tapped.
I am just getting started with UI unit testing and I've got as far as testing a button tap changes the icon (by searching for the button and inspecting it's accessibility label) and that works fine, but I'd also like to assert that the action actually does something by checking the appStateController.isPlaying boolean - effectively testing that my action: {} closure does what I need it to.
I can't seem to locate any documentation that tells me how, given a running app, I can find a reference, through the view hierarchy, to the injected appStateController and inspect the attributes therein. Is this possible and if so, does anyone know where I can find some documentation/blog articles on doing this?
In UI testing you test UI flows, not internal engine states, so you can verify that UI represents correct state (ie. engine finished correctly and UI updated correspondingly). For that purpose it needs to identify required UI elements and then check if desired present in runtime.
Here is possible approach. Tested with Xcode 11.4.
1) in code add accessibility identifiers
if self.appStateController.isPlaying {
Image(systemName: "stop.circle")
.opacity(self.appStateController.isPlayable ? 1.0 : 0.5)
.accessibility(identifier: "playing") // identify state !!
.accessibility(label: Text("stop"))
}
else {
Image(systemName: "play.circle")
.opacity(self.appStateController.isPlayable ? 1.0 : 0.5)
.accessibility(identifier: "idle")
.accessibility(label: Text("play")) // identify state !!
}
2) in XC test
func testPlaying() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
// UI manipulations to activate playback
XCTAssertTrue(app.images["playing"].exists)
}