Pulling from UserDefaults for a Picker - ios

I feel like I must be doing something stupid. All I'm trying to do if pull from UserDefaults to set a Picker value, then save to UserDefaults when the Picker changes. The issue I'm running into comes when I try and make the code work for iOS14 and 15.
struct SettingsView: View {
#State var rangeSelection: Int = UserDefaults.standard.integer(forKey: "vocalRange")
#State var rangeOptions = ["Soprano", "Mezzo-Soprano/Alto", "Extended Range"]
var body: some View {
VStack{
Form {
Section {
Picker(selection: $rangeSelection, label: Text("Range")) {
ForEach(0 ..< rangeOptions.count, id: \.self) { range in
Text(rangeOptions[range])
.tag(range)
}
}
.onChange(of: rangeSelection, perform: { value in
UserDefaults.standard.setValue(rangeSelection, forKey: "vocalRange")
})
}
}
}
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.inline)
}
}
This seems to work fine, and the 'onChange' fires great. Only weird thing is, if you leave the page and immediately go back in, the 'rangeSelection' variable resets to what it was originally. Once you restart the app however, the change is reflected correctly.
As an alternative, I tried the following
#State var rangeSelection: Int = 0
...
.onAppear(perform:{
rangeSelection = UserDefaults.standard.integer(forKey: "vocalRange")
})
This creates another fun issue. In iOS15 it works fine, but in iOS14, once you've changed the picker, the OnAppear fires before the OnChange, and reverts the Picker back to its previous value. This only happens because I'm using the picker in a Form/List, so it changes pages when you select the Picker.
I feel like there is an obvious solution I should be using.

You can set delay in .OnAppear method to work it
.onAppear(perform:{
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
rangeSelection = UserDefaults.standard.integer(forKey: "vocalRange")
}
})

Thanks to #loremipsum I used AppStorage. I declared the variable like this, and no 'onChange' was needed!
#AppStorage("vocalRange") var rangeSelection: Int = UserDefaults.standard.integer(forKey: "vocalRange")

Related

UserDefaults .set not being picked up by state variable in SwiftUI

I am attempting to to detect whether a user has onboarded or not e.g. launched the app. When I access the UserDefaults.standard.bool directly I can see the change but when I attempt to access it through the #State it retains the original false value.
As a workaround I directly change the value to true, but I have a feeling this isn't the way to do this.
How can I get the #State variable to pick up the change when changing the UserDefaults object without having to close and restart the app? I want the user to onboard, click a button, then switch immediately to the main app view.
#State var userOnboarded: Bool = UserDefaults.standard.bool(forKey: "userOnboarded")
var body: some Scene {
WindowGroup {
if userOnboarded {
Text("PLACEHOLDER: Main App View")
} else {
Button(action: {
UserDefaults.standard.set(true, forKey: "userOnboarded")
userOnboarded = true
print(UserDefaults.standard.bool(forKey: "userOnboarded"))
}) {
VStack {
Text("Click circle to onboard.")
Circle()
.foregroundColor(Color.red)
}
}
}
}
In SwiftUI there is a dedicated wrapper to watch UserDefaults. As previously mentioned in your comments, it's called #AppStorage and it's used like so.
#AppStorage("userOnboarded") var userOnboarded: Bool = false
It functions similarly to a UserDefault but will refresh the view and set the data when the value changes.
Button(action: {
userOnboarded = true
print(UserDefaults.standard.bool(forKey: "userOnboarded"))
}) //....
Notice in the button, we set the userOnboarded to true, this will update and refresh your view as well as update the user default value.

Swift - didSet property not called from binding changes

My problem:
I want to call a function when my customView clicked
So I use #State variable to observe the click action from my custom view. But the problem is my value changed but didSet function not triggered
My code:
Main struct:
#State var buttonClicked : Bool = false {
didSet{
needToCallMyFunction() // not triggered
}
}
Custom struct:(for my customview)
#Binding var isClicked : Bool
someview
.onTapGesture(perform: {
print("custom clicked")
isClicked.toggle()
})
Use instead in main view .onChange(of:) modifier, like
SomeView()
.onChange(of: buttonClicked) { _ in
self.needToCallMyFunction()
}
Update: variant for SwiftUI 1.0 / iOS 13+
import Combine // needed to use Just
...
SomeView()
.onReceive(Just(buttonClicked)) { _ in
self.needToCallMyFunction()
}
Asperi solution working fine.
But it have one bug in lower versions. It called multiple times
I found another solution
SomeView(buttonClicked:
Binding(get: { self.buttonClicked },
set: { self.buttonClicked = $0
print("your action here ")
}))

SwiftUI NavigationLink push in onAppear immediately pops the view when using #ObservableObject

I want to programmatically be able to navigate to a link within a List of NavigationLinks when the view appears (building deep linking from push notification). I have a string -> Bool dictionary which is bound to a custom Binding<Bool> inside my view. When the view appears, I set the bool property, navigation happens, however, it immediately pops back. I followed the answer in SwiftUI NavigationLink immediately navigates back and made sure that each item in the List has a unique identifier, but the issue still persists.
Two questions:
Is my binding logic here correct?
How come the view pops back immediately?
import SwiftUI
class ContentViewModel: ObservableObject {
#Published var isLinkActive:[String: Bool] = [:]
}
struct ContentViewTwo: View {
#ObservedObject var contentViewModel = ContentViewModel()
#State var data = ["1", "2", "3"]
#State var shouldPushPage3: Bool = true
var page3: some View {
Text("Page 3")
.onAppear() {
print("Page 3 Appeared!")
}
}
func binding(chatId: String) -> Binding<Bool> {
return .init(get: { () -> Bool in
return self.contentViewModel.isLinkActive[chatId, default: false]
}) { (value) in
self.contentViewModel.isLinkActive[chatId] = value
}
}
var body: some View {
return
List(data, id: \.self) { data in
NavigationLink(destination: self.page3, isActive: self.binding(chatId: data)) {
Text("Page 3 Link with Data: \(data)")
}.onAppear() {
print("link appeared")
}
}.onAppear() {
print ("ContentViewTwo Appeared")
if (self.shouldPushPage3) {
self.shouldPushPage3 = false
self.contentViewModel.isLinkActive["3"] = true
print("Activating link to page 3")
}
}
}
}
struct ContentView: View {
var body: some View {
return NavigationView() {
VStack {
Text("Page 1")
NavigationLink(destination: ContentViewTwo()) {
Text("Page 2 Link")
}
}
}
}
}
The error is due to the lifecycle of the ViewModel, and is a limitation with SwiftUI NavigationLink itself at the moment, will have to wait to see if Apple updates the pending issues in the next release.
Update for SwiftUI 2.0:
Change:
#ObservedObject var contentViewModel = ContentViewModel()
to:
#StateObject var contentViewModel = ContentViewModel()
#StateObject means that changes in the state of the view model do not trigger a redraw of the whole body.
You also need to store the shouldPushPage3 variable outside the View as the view will get recreated every time you pop back to the root View.
enum DeepLinking {
static var shouldPushPage3 = true
}
And reference it as follows:
if (DeepLinking.shouldPushPage3) {
DeepLinking.shouldPushPage3 = false
self.contentViewModel.isLinkActive["3"] = true
print("Activating link to page 3")
}
The bug got fixed with the latest SwiftUI release. But to use this code at the moment, you will need to use the beta version of Xcode and iOS 14 - it will be live in a month or so with the next GM Xcode release.
I was coming up against this problem, with a standard (not using 'isActive') NavigationLink - for me the problem turned out to be the use of the view modifiers: .onAppear{code} and .onDisappear{code} in the destination view. I think it was causing a re-draw loop or something which caused the view to pop back to my list view (after approx 1 second).
I solved it by moving the modifiers onto a part of the destination view that's not affected by the code in those modifiers.

Why is it in SwiftUI, when presenting modal view with .sheet, init() is called twice

I am producing the situation on WatchOS with the following code
struct Modal : View {
#Binding var showingModal : Bool
init(showingModal : Binding<Bool>){
self._showingModal = showingModal
print("init modal")
}
var body: some View {
Button(action: {
self.showingModal.toggle()
}, label: {
Text("TTTT")
})
}
}
struct ContentView: View {
#State var showingModal = false
var body: some View {
Button(action: {
self.showingModal.toggle()
}, label: {
Text("AAAA")
}).sheet(isPresented: $showingModal, content: {Modal(showingModal: self.$showingModal)})
}
}
Every time I press the button in the master view to summon the modal with .sheet, Two instances of the modal view are created.
Could someone explain this phenomenon?
I tracked this down in my code to having the following line in my View:
#Environment(\.presentationMode) var presentation
I had been doing it due to https://stackoverflow.com/a/61311279/155186, but for some reason that problem seems to have disappeared for me so I guess I no longer need it.
I've filed Feedback FB7723767 with Apple about this.
It is probably a bug, as of Xcode 11.4.1 (11E503a).
Beware, that if for example initializing view models (or anything else for that matter) like so:
.sheet(isPresented: $isEditingModalPresented) {
LEditView(vm: LEditViewModel(), isPresented: self.$isEditingModalPresented)
}
the VM will be initialized twice.
Comment out/remove the init() method from Modal with everything else the same. You should be able to solve the issue of two instances of Modal being created, its because you are explicitly initializing the binding (showingModal) in the init() of Modal. Hope this makes sense.
private let uniqueId: String = "uniqueId"
Button(action: {
self.showingModal.toggle()
}, label: {
Text("AAAA")
})
.sheet(isPresented: $showingModal) {
Modal(showingModal: self.$showingModal)
.id("some-unique-id")
}
Ex:
.id(self.uniqueId)
Add unique id to your .sheet and not't worry :)
But, do not use UUID(), because sheet view will be represented on every touch event

SwiftUI: How can I get the selected row in a List before pushing another view

Another SwiftUI struggle!
I have a view that contains a list. When user taps on a row, I want to first save the selected item in my VM then push another view.
The only way I can think of to solve that issue is to first save the selected row and have another button to push the next view. It seems impossible to do this with only one tap.
Anyone have a clue?
Here's the code by the way:
struct AnotherView : View {
#State var viewModel = AnotherViewModel()
var body: some View {
NavigationView {
VStack {
List(viewModel.items.identified(by: \.id)) { item in
NavigationLink(destination: DestinationView()) {
Text(item)
}
// Before the new view is open, I want to save the selected item in my VM, which will write to a global store.
self.viewModel.selectedItem = item
}
}
}
}
}
Thank you!
Alright, I found a not too shady solution.
I used this article https://ryanashcraft.me/swiftui-programmatic-navigation shout out to him!
Instead of using a NavigationLink button, I use a regular button, save the selected item when the user tap then use NavigationDestinationLink to push the new view as is self.link.presented?.value = true.
Works like a charm as of beta 3!
I'll update my post if something change in the next betas.
Here's how it could look like:
struct AnotherView : View {
private let link: NavigationDestinationLink<AnotherView2>
#State var viewModel = AnotherViewModel()
init() {
self.link = NavigationDestinationLink(
AnotherView2(),
isDetail: true
)
}
var body: some View {
NavigationView {
VStack {
List(viewModel.items.identified(by: \.id)) { item in
Button(action: {
// Save the object into a global store to be used later on
self.viewModel.selectedItem = item
// Present new view
self.link.presented?.value = true
}) {
Text(value: item)
}
}
}
}
}
}
You can add simple TapGesture
NavigationLink(destination: ContentView() ) {
Text("Row")
.gesture(TapGesture()
.onEnded({ _ in
//your action here
}))
}
This can also be accomplished by using a Publisher and the OnReceive hook in any View inheritor.
Swift 5.1
In case you would like to apply it to a static List. You may try this.
NavigationView{
List{
NavigationLink("YourView1Description", destination: YourView1())
NavigationLink("YourView2Description", destination: YourView2())
NavigationLink("YourView3Description", destination: YourView3())
NavigationLink("YourView4Description", destination: YourView4())
}
.navigationBarTitle(Text("Details"))
}

Resources