#Appstorage is not updating based on my picker selection - SwiftUI - WatchApp - swiftui

I have a picker that updates an variable bmr.
the picker in not updating the value. I put a test Text on the second screen to see if I can call the new value, its always showing the default.
import SwiftUI
struct ProfileView: View {
#AppStorage("ProfileView") var profileView:Bool = false
#AppStorage("ContentView") var contentView:Bool = false
#AppStorage("TimerView") var timerView:Bool = false
#AppStorage("Name") var userName: String?
#AppStorage("BMR") var bmr: Int?
var body: some View {
VStack {
Picker(selection: $bmr, label: Text("Enter your BMR:")) {
Text("50").tag(50)
Text("60").tag(60)
Text("70").tag(70)
Text("80").tag(80)
}
NavigationLink(destination: ContentView()) {
Text("Save")
}
}
}
}
struct ContentView: View {
#AppStorage("ProfileView") var profileView:Bool = false
#AppStorage("ContentView") var contentView:Bool = false
#AppStorage("TimerView") var timerView:Bool = false
#AppStorage("Name") var userName: String?
#AppStorage("BMR") var bmr: Int?
var body: some View {
VStack {
Text("Your BMR: \(bmr ?? 50)")
}
}
}
I am learning, so I don't know what to really try. I tried binding and unbinding, its not really working.
I tried to store variables and pass through views previously to test if it would update, and it worked fine. But passing info through views doesn't work with my app as I need this data to to be stored even if I exit.

The issue is bmr is an Int? while your tags are Int. Since they are not the same thing, the selection won't update it. The trick is to cast your tag as an Int? like this:
struct ProfileView: View {
#AppStorage("ProfileView") var profileView:Bool = false
#AppStorage("ContentView") var contentView:Bool = false
#AppStorage("TimerView") var timerView:Bool = false
#AppStorage("Name") var userName: String?
#AppStorage("BMR") var bmr: Int?
var body: some View {
VStack {
Picker(selection: $bmr, label: Text("Enter your BMR:")) {
Text("50").tag(50 as Int?)
Text("60").tag(60 as Int?)
Text("70").tag(70 as Int?)
Text("80").tag(80 as Int?)
}
NavigationLink(destination: ContentView()) {
Text("Save")
}
}
}
}
The tag and the selection types must EXACTLY match.

Related

Issue in Setting Value to #State variable in SwiftUI [duplicate]

Hello I am running into a problem here and I do not have a consistent behavior between my .sheet() view when running on ios13 or ios14
I got a view like this :
#State private var label: String = ""
#State private var sheetDisplayed = false
///Some code
var body: some View {
VStack {
Button(action: {
self.label = "A label"
self.isDisplayed = true
}) {
Text("test")
}
}.sheet(isPresented: $sheetDisplayed, onDismiss: {
self.label = ""
}) {
Text(self.label)
}
}
On ios 13 this work as expected btn click -> set label -> call sheet -> display "A label" in a Text view.
On ios14 I got an empty string in self.label when in sheet closure, hence it does not display anything.
Did I missed something ? Is it an iOS 14 bug or did I had it wrong on ios13 and that got corrected.
PS: I have a couple of other variables that are passed in the closure I simplified it.
Your code have expectation of view update/creation order, but in general it is undefined (and probably changed in iOS 14).
There is explicit way to pass information inside sheet - use different sheet creator, ie. .sheet(item:...
Here is working reliable example. Tested with Xcode 12 / iOS 14
struct ContentView: View {
#State private var item: Item?
struct Item: Identifiable {
let id = UUID()
var label: String = ""
}
var body: some View {
VStack {
Button(action: {
self.item = Item(label: "A label")
}) {
Text("test")
}
}.sheet(item: $item, onDismiss: {
self.item = nil
}) {
Text($0.label)
}
}
}
This is some really strange behaviour in iOS 14, which doesn't appear to be documented.
Using the other answer here and the comment on this thread, I used #Binding to solve the issue as it seemed the cleanest and most SwiftUI-esq solution.
I have no idea why this behaviour has changed, and it seems less intuitive than before, so I'm assuming its a bug!
An example:
struct MainView: View {
#State private var message = ""
#State private var showSheet = false
var body: some View {
Button(action: {
self.message = "This will display the correct message"
self.showSheet = true
}, label: {
Text("Test Button")
})
.sheet(isPresented: self.$showSheet) {
SheetView(message: self.$message)
}
}
}
struct SheetView: View {
#Binding var message: Int
var body: some View {
Text(self.message)
}
}
The behaviour changed with SwiftUI 2.0, so it affects MacOS 11 as well, just adding a binding to the view fixes it even when that binding is never used, which makes me think this is an implementation bug.
Additionally just using the details state variable in a Text() within the body of the view also fixes it.
struct MyViewController : View {
#State var details: String?
#State var showDetails = false
// #Binding var havingAbindingFixesIt: String?
var body: some View {
VStack {
// Text(details ?? "")
Text("Tap here for details")
.onTapGesture {
self.details = "These are the details"
self.showDetails.toggle()
}
.sheet(isPresented: $showDetails) { Text(details ?? "") }
}
}
}

SwiftUI - Picker value not changing when accessing data from UserDefaults

I am making an app where I am showing different views based of user's selection by a picker. The binding value of the picker is initially set by UserDefaults in a viewModel. The problem is when I choose a picker value in my app, The picker automatically go back to initial state, as if someone forcing the picker not the change the values.
Settings ViewModel :
import Foundation
class SettingsViewModel:ObservableObject{
#Published var showSettings = false
//Here is the problem
#Published var choosenUserType = UserDefaults.standard.string(forKey: "userType"){
didSet{
UserDefaults.standard.set(self.choosenUserType, forKey: "userType")
}
}
static var userTypes = ["Client", "Worker"]
}
Home View:
import SwiftUI
struct HomeView: View {
#StateObject var settingsVM = SettingsViewModel()
var body: some View {
VStack{
switch settingsVM.choosenUserType{
case "Client":
Text("This is client")
case "Worker":
Text("This is worker")
default:
Text("This is default")
}
}.navigationTitle("Tanvirgeek Co")
.navigationBarItems(trailing: Button(action: {
settingsVM.showSettings.toggle()
}, label: {
Text("Settings")
}))
.sheet(isPresented: $settingsVM.showSettings, content: {
SettingsView(dissmiss: $settingsVM.showSettings)
.environmentObject(settingsVM)
})
}
}
Settings View:
import SwiftUI
struct SettingsView: View {
#EnvironmentObject var settingVM:SettingsViewModel
#Binding var dissmiss:Bool
var body: some View {
VStack{
Picker(selection: $settingVM.choosenUserType, label: Text("Choose User Type"), content: {
ForEach(SettingsViewModel.userTypes, id: \.self) { userType in
Text("\(userType)")
}
})
Button(action: {
dissmiss.toggle()
}, label: {
Text("Dismiss")
})
}
}
}
What I am doing wrong? How to change the picker's binding variable value through the picked value here?
Your choosenUserType ends up with an inferred type of String? because that's what UserDefaults.string(forKey:) returns.
The Picker's selection type needs to match exactly with the tag type. The tags (which are inferred in this case as well) are of type String.
I've solved this by giving a default value to choosenUserType so that it can be a String (not String?):
class SettingsViewModel:ObservableObject{
#Published var showSettings = false
#Published var choosenUserType : String = UserDefaults.standard.string(forKey: "userType") ?? SettingsViewModel.userTypes[0] {
didSet{
UserDefaults.standard.set(self.choosenUserType, forKey: "userType")
}
}
static var userTypes = ["Client", "Worker"]
}
Also, in your SettingsView, you don't have to interpolate the userType in the Text -- you can just provide it directly:
struct SettingsView: View {
#EnvironmentObject var settingVM:SettingsViewModel
#Binding var dissmiss:Bool
var body: some View {
VStack{
Picker(selection: $settingVM.choosenUserType, label: Text("Choose User Type")) {
ForEach(SettingsViewModel.userTypes, id: \.self) { userType in
Text(userType)
}
}
Button(action: {
dissmiss.toggle()
}, label: {
Text("Dismiss")
})
}
}
}

Presenting a Sheet modally, #State value is not changing

I have tried much time on it but I couldn't figure out why it is not working.
The problem is when I tap the button, the new value inside the sheet is not updated. It always show the same value which is set up in start.
#State var value:String = "empty"
#State var explorePageIsEnabled:Bool = false
VStack{
Button("tap me"){
value = "the new one"
exploreStatusIsEnabled.toggle()
}
.sheet(isPresented: $exploreStatusIsEnabled, content: {
Text(value)
})
}
Deployment target is IOS 14+
Create a separate struct view for text and use Binding.
struct SheetView: View {
#Binding var value: String
var body: some View {
Text(value)
}
}
struct ContentView: View {
#State private var value: String = "empty"
#State private var explorePageIsEnabled: Bool = false
var body: some View {
VStack{
Button("tap me"){
value = "the new one"
explorePageIsEnabled.toggle()
}
.sheet(isPresented: $explorePageIsEnabled, content: {
SheetView(value: $value)
})
}
}
}

SwiftUI #State and .sheet() ios13 vs ios14

Hello I am running into a problem here and I do not have a consistent behavior between my .sheet() view when running on ios13 or ios14
I got a view like this :
#State private var label: String = ""
#State private var sheetDisplayed = false
///Some code
var body: some View {
VStack {
Button(action: {
self.label = "A label"
self.isDisplayed = true
}) {
Text("test")
}
}.sheet(isPresented: $sheetDisplayed, onDismiss: {
self.label = ""
}) {
Text(self.label)
}
}
On ios 13 this work as expected btn click -> set label -> call sheet -> display "A label" in a Text view.
On ios14 I got an empty string in self.label when in sheet closure, hence it does not display anything.
Did I missed something ? Is it an iOS 14 bug or did I had it wrong on ios13 and that got corrected.
PS: I have a couple of other variables that are passed in the closure I simplified it.
Your code have expectation of view update/creation order, but in general it is undefined (and probably changed in iOS 14).
There is explicit way to pass information inside sheet - use different sheet creator, ie. .sheet(item:...
Here is working reliable example. Tested with Xcode 12 / iOS 14
struct ContentView: View {
#State private var item: Item?
struct Item: Identifiable {
let id = UUID()
var label: String = ""
}
var body: some View {
VStack {
Button(action: {
self.item = Item(label: "A label")
}) {
Text("test")
}
}.sheet(item: $item, onDismiss: {
self.item = nil
}) {
Text($0.label)
}
}
}
This is some really strange behaviour in iOS 14, which doesn't appear to be documented.
Using the other answer here and the comment on this thread, I used #Binding to solve the issue as it seemed the cleanest and most SwiftUI-esq solution.
I have no idea why this behaviour has changed, and it seems less intuitive than before, so I'm assuming its a bug!
An example:
struct MainView: View {
#State private var message = ""
#State private var showSheet = false
var body: some View {
Button(action: {
self.message = "This will display the correct message"
self.showSheet = true
}, label: {
Text("Test Button")
})
.sheet(isPresented: self.$showSheet) {
SheetView(message: self.$message)
}
}
}
struct SheetView: View {
#Binding var message: Int
var body: some View {
Text(self.message)
}
}
The behaviour changed with SwiftUI 2.0, so it affects MacOS 11 as well, just adding a binding to the view fixes it even when that binding is never used, which makes me think this is an implementation bug.
Additionally just using the details state variable in a Text() within the body of the view also fixes it.
struct MyViewController : View {
#State var details: String?
#State var showDetails = false
// #Binding var havingAbindingFixesIt: String?
var body: some View {
VStack {
// Text(details ?? "")
Text("Tap here for details")
.onTapGesture {
self.details = "These are the details"
self.showDetails.toggle()
}
.sheet(isPresented: $showDetails) { Text(details ?? "") }
}
}
}

How to add an observable property when other properties change

I have the following model object that I use to populate a List with a Toggle for each row, which is bound to measurement.isSelected
final class Model: ObservableObject {
struct Measurement: Identifiable {
var id = UUID()
let name: String
var isSelected: Binding<Bool>
var selected: Bool = false
init(name: String) {
self.name = name
let selected = CurrentValueSubject<Bool, Never>(false)
self.isSelected = Binding<Bool>(get: { selected.value }, set: { selected.value = $0 })
}
}
#Published var measurements: [Measurement]
#Published var hasSelection: Bool = false // How to set this?
init(measurements: [Measurement]) {
self.measurements = measurements
}
}
I'd like the hasSelection property to be true whenever any measurement.isSelected is true. I'm guessing somehow Model needs to observe changes in measurements and then update its hasSelection property… but I've no idea where to start!
The idea is that hasSelection will be bound to a Button to enable or disable it.
Model is used as follows…
struct MeasurementsView: View {
#ObservedObject var model: Model
var body: some View {
NavigationView {
List(model.measurements) { measurement in
MeasurementView(measurement: measurement)
}
.navigationBarTitle("Select Measurements")
.navigationBarItems(trailing: NavigationLink(destination: NextView(), isActive: $model.hasSelection, label: {
Text("Next")
}))
}
}
}
struct MeasurementView: View {
let measurement: Model.Measurement
var body: some View {
HStack {
Text(measurement.name)
.font(.subheadline)
Spacer()
Toggle(measurement.name, isOn: measurement.isSelected)
.labelsHidden()
}
}
}
For info, here's a screenshot of what I'm trying to achieve. A list of selectable items, with a navigation link that is enabled when one or more is selected, and disabled when no items are selected.
#user3441734 hasSelection should ideally be a get only property, that
is true if any of measurement.isSelected is true
struct Data {
var bool: Bool
}
class Model: ObservableObject {
#Published var arr: [Data] = []
var anyTrue: Bool {
arr.map{$0.bool}.contains(true)
}
}
example (as before) copy - paste - run
import SwiftUI
struct Data: Identifiable {
let id = UUID()
var name: String
var on_off: Bool
}
class Model: ObservableObject {
#Published var data = [Data(name: "alfa", on_off: false), Data(name: "beta", on_off: false), Data(name: "gama", on_off: false)]
var bool: Bool {
data.map {$0.on_off} .contains(true)
}
}
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
VStack {
List(0 ..< model.data.count) { idx in
HStack {
Text(verbatim: self.model.data[idx].name)
Toggle(isOn: self.$model.data[idx].on_off) {
EmptyView()
}
}
}
Text("\(model.bool.description)").font(.largeTitle).padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
When the model.data is updated
#Published var data ....
its publisher calls objectWillChange on ObservableObject.
Next SwiftUI recognize that ObservedObject needs the View to be "updated". The View is recreated, and that will force the model.bool.description will have fresh value.
LAST UPDATE
change this part of code
struct ContentView: View {
#ObservedObject var model = Model()
var body: some View {
NavigationView {
List(0 ..< model.data.count) { idx in
HStack {
Text(verbatim: self.model.data[idx].name)
Toggle(isOn: self.$model.data[idx].on_off) {
EmptyView()
}
}
}.navigationBarTitle("List")
.navigationBarItems(trailing:
NavigationLink(destination: Text("next"), label: {
Text("Next")
}).disabled(!model.bool)
)
}
}
}
and it is EXACTLY, WHAT YOU HAVE in your updated question
Try it on real device, otherwise the NavigationLink is usable only once (this is well known simulator bug in current Xcode 11.3.1 (11C504)).
The problem with your code at the moment is that even if you observe the changes to measurements, they will not get updated when the selection updates, because you declared the var isSelected: Binding<Bool> as a Binding. This means that SwiftUI is storing it outside of your struct, and the struct itself doesn't update (stays immutable).
What you could try instead is declaring #Published var selectedMeasurementId: UUID? = nil on your model So your code would be something like this:
import SwiftUI
import Combine
struct NextView: View {
var body: some View {
Text("Next View")
}
}
struct MeasurementsView: View {
#ObservedObject var model: Model
var body: some View {
let hasSelection = Binding<Bool> (
get: {
self.model.selectedMeasurementId != nil
},
set: { value in
self.model.selectedMeasurementId = nil
}
)
return NavigationView {
List(model.measurements) { measurement in
MeasurementView(measurement: measurement, selectedMeasurementId: self.$model.selectedMeasurementId)
}
.navigationBarTitle("Select Measurements")
.navigationBarItems(trailing: NavigationLink(destination: NextView(), isActive: hasSelection, label: {
Text("Next")
}))
}
}
}
struct MeasurementView: View {
let measurement: Model.Measurement
#Binding var selectedMeasurementId: UUID?
var body: some View {
let isSelected = Binding<Bool>(
get: {
self.selectedMeasurementId == self.measurement.id
},
set: { value in
if value {
self.selectedMeasurementId = self.measurement.id
} else {
self.selectedMeasurementId = nil
}
}
)
return HStack {
Text(measurement.name)
.font(.subheadline)
Spacer()
Toggle(measurement.name, isOn: isSelected)
.labelsHidden()
}
}
}
final class Model: ObservableObject {
#Published var selectedMeasurementId: UUID? = nil
struct Measurement: Identifiable {
var id = UUID()
let name: String
init(name: String) {
self.name = name
}
}
#Published var measurements: [Measurement]
init(measurements: [Measurement]) {
self.measurements = measurements
}
}
I'm not sure exactly how you want the navigation button in the navbar to behave. For now I just set the selection to nil when it's tapped. You can modify it depending on what you want to do.
If you want to support multi-selection, you can use a Set of selected ids instead.
Also, seems like the iOS simulator has some problems with navigation, but I tested on a physical device and it worked.