Global Updates With UserDefaults In SwiftUI - swiftui

I've been using #AppStorage and UserDefaults for updates in SwiftUI. If I make a change to the vie that has the #AppStorage wrapper all works well. I'm confused with how to make this work globally.
I'm using a struct that has computed properties and formatters associated. The idea is to check user defaults and convert items to lbs or kg. The issue is that the views using the computed properties do not update when UserDefaults is updated. Is there a way to create a global change that would update weightFormatted in SecondaryView below?
// Weight Struct
struct Weight {
var weight: Double
var weightFormatted: String {
return weightDecimalLbsOrKgFormatted2(weight)
}
// Formatting Method
func weightDecimalLbsOrKgFormatted2(_ lbs: Double) -> String {
if (!UserDefaults.standard.bool(forKey: "weightInKilograms")) {
let weightString = decimalFormatterDecimal2(lbs)
return weightString + "lbs"
} else {
let kg = toKg(lbs)
let weightString = decimalFormatterDecimal2(kg)
return weightString + "kg"
}
}
// Where weightInKilograms Is Set
struct AccountView: View {
#AppStorage("weightInKilograms") var weightInKilograms = false
let weight = Weight(weight: 9.0))
var body: some View {
VStack {
Text(weight.weightFormatted)
Toggle(isOn: $weightInKilograms) {
Text("Kilograms")
}
}
}
}
// Secondary View Not Updating
struct SecondaryView: View {
let weight = Weight(weight: 9.0))
var body: some View {
Text(weight.weightFormatted)
}
}

Your problem is that weight isn't wrapped by any state.
In your AccountView, give weight a #State wrapper:
struct AccountView: View {
#AppStorage("weightInKilograms") var weightInKilograms = false
#State var weight = Weight(weight: 9.0))
var body: some View {
//...
}
}
In SecondaryView, ensure that weight is wrapped with #Binding:
struct SecondaryView: View {
#Binding var weight: Weight
var body: some View {
// ...
}
}
Then, pass weight as a Binding<Weight> variable to SecondaryView within your first View:
SecondaryView(weight: $weight)
Is there a way to create a global change that would update weightFormatted in SecondaryView below?
If you're looking to make a global change, you should consider setting up a global EnvironmentObject:
class MyGlobalClass: ObservableObject {
// Published variables will update view states when changed.
#Published var weightInKilograms: Bool
{ get {
// Get UserDefaults here
} set {
// Set UserDefaults here
}}
#Published var weight: Weight
}
If you pass an instance of MyGlobalClass as an EnvironmentObject to your main view, then to your secondary view, any changes made to properties in the global instance will update the views' state via the #Published wrapper:
let global = MyGlobalClass()
/* ... */
// In your app's lifecycle, or where AccountView is instantiated
AccountView().environmentObject(global)
struct AccountView: View {
#EnvironmentObject var global: MyGlobalClass
var body: some View {
// ...
Text(global.weight.weightFormatted)
// ...
SecondaryView().environmentObject(global)
}
}

Related

Trouble Passing Data From Form Entry

This is a module where the user enters transaction data and then it is saved to coreData. EntryView calls getFormData for entry of form data then calls the function saveButton() for saving the data to coreData.
Things have been working great until I recently added two additional parameters gotCountry and gotHome. These parameters are defined in EntryView. The data I want is found in getFormData but I don't want to make it available to other parts of the app until the save button is pressed (func saveButton()) hence I need to pass the data from getFormData to saveButton().
One of the two warnings is Initialization of immutable value 'gotCountry' was never used; consider replacing with assignment to '_' or removing it if I place let in front of the parameters gotCountry and gotHome in getFormData. These are variables so shouldn't have let in front of them. Removing 'let' results in the error Type '()' cannot conform to 'View'
The parameters entryDT and entryPT are coming from form input data while gotCountry and gotHome are coming from calculated data available at time of entry.
Note that I have stripped out some of the code to see the passing of data better.
struct EntryView: View {
#EnvironmentObject var ctTotals: CountryTotals
#State private var entryDT = Date()
#State private var entryPT: Int = 0
#State private var gotCountry: String = ""
#State private var gotHome: Double = 0.0
var body: some View {
VStack (alignment: .leading){
ShowTabTitle(g: g, title: "Enter Transaction")
getFormData(entryDT: $entryDT, entryPT: $entryPT, gotCountry: $gotCountry, gotHome: $gotHome)
Button {
self.saveButton() // button pressed
} label: {
Text ("Save")
}
}
}
func saveButton() {
// save entry to core data
let newEntry = CurrTrans(context: viewContext)
// entry id
newEntry.id = UUID()
// entry date
newEntry.entryDT = entryDT
// entry payment type
newEntry.entryPT = Int64(entryPT)
ctTotals.sendTotals(gotCountry: gotCountry, gotHome: gotHome)
do {
try viewContext.save()
} catch {
}
// reset parameters for next entry
self.entryDT = Date()
self.entryPT = 0
}
}
struct getFormData: View {
#Binding var entryDT: Date
#Binding var entryPT: Int
#Binding var gotCountry: String
#Binding var gotHome: Double
var body: some View {
// get entry date and time
DatePicker("", selection: $entryDT, in: ...Date())
// select payment type
Picker(selection: $entryPT, label: Text("")) {}
// copy data to totals by country
gotCountry = currencies.curItem[userData.entryCur].cunName
gotHome = totalValue
}
}
struct CtModel: Codable, Identifiable, Hashable {
var id = UUID()
var ctName: String
var ctHome: Double
}
class CountryTotals: ObservableObject {
#Published var ctItem: [CtModel] {
didSet {
if let encoded = try? JSONEncoder().encode(ctItem) {
UserDefaults.standard.set(encoded, forKey: StorageKeys.ctTotals.rawValue)
}
}
}
init() {
}
func sendTotals(gotCountry: String, gotHome: Double) -> () {
let item = CtModel(ctName: gotCountry, ctHome: gotHome)
ctItem.append(item)
}
}

Read value of #ObservedObject only once without listening to changes

Suppose I have some environmental object that stores the states of a user being signed in or not;
class Account: ObservableObject{
#Published var isSignedIn: Bool = false
}
I want to conditional display a view if the user is signed in. So,
app loads -> RootView(). If user is signed In -> go to Profile, otherwise go to LoginScreen
struct RootView: View {
#EnvironmentObject private var account: Account
var body: some View {
NavigationStackViews{
// This is just a custom class to help display certain views with a transition animation
if account.isSignedIn{ // I only want to read this ONCE. Otherwise, when this value changes, my view will abruptly change views
ProfileView()
} else { SignInView() }
}
}
}
Set a #State variable in onAppear(perform:) of the current isSignedIn value. Then use that in the view body instead.
Code:
struct RootView: View {
#EnvironmentObject private var account: Account
#State private var isSignedIn: Bool?
var body: some View {
NavigationStackViews {
if let isSignedIn = isSignedIn {
if isSignedIn {
ProfileView()
} else {
SignInView()
}
}
}
.onAppear {
isSignedIn = account.isSignedIn
}
}
}

#Published struct in SwiftUI does not published

See my code below. My problem is if i change accessToken via UserService.shared.currentStore.accessToken = xxx, SwiftUI doesn't publish, and there's no update on StoreBanner at all.
//SceneDelegate
let contentView = ContentView()
.environmentObject(UserService.shared)
//Define
class UserService: ObservableObject {
#Published var currentStore = Store.defaultValues()
static let shared = UserService()
}
struct Store: Codable, Hashable {
var storeName: String = ""
var accessToken: String = ""
}
//Use it
struct StoreBanner: View {
var body: some View {
Group {
if UserService.shared.currentStore.accessToken.isNotEmpty {
ShopifyLinkedBanner()
} else {
ShopifyLinkBanner()
}
}
}
}
You're trying to use UserService inside StoreBanner without using a property wrapper to tell the view to respond to updates. Without the #ObservedObject property wrapper, the View doesn't have a mechanism to know that any of the #Published properties have been updated.
Try this:
struct StoreBanner: View {
#ObservedObject private var userService = UserService.shared
var body: some View {
Group {
if userService.currentStore.accessToken.isNotEmpty {
ShopifyLinkedBanner()
} else {
ShopifyLinkBanner()
}
}
}
}
This should work assuming you set accessToken somewhere in your code on the same instance of UserService.

SwiftUI Combine Why can't bind data in init?

I am trying a very simple test to just combine a simple Just("JustValue") to a property.
But it did not work.
↓ This is my code
struct ResponseView: View {
private var contentCancellable: AnyCancellable? = nil
#State var content: String = "InitialValue"
var body: some View {
Text(content)
}
init() {
contentCancellable = Just("JustValue").assign(to: \.content, on: self)
}
}
Is there anyone know why the Text shows "InitialValue" instead "JustValue"
This is specific of state property wrapper initialization pass... the external state storage is created later so only one initialisation is applied.
If you want to update it, do it later, when state be already created and linked to view, like
struct ResponseView: View {
#State var content: String = "InitialValue"
var body: some View {
Text(content)
.onAppear {
_ = Just("JustValue").assign(to: \.content, on: self)
}
}
}
the gives UI which you expected.

SwiftUI - Updating #State when Global changes

I'd like to update an UI element on an overview view when data on another view is changed.
I looked into #EnvironmentalObject and #Binding. However, an update to either object does not appear to force a view reload. Only changes to #State force renders.
Also, in the case described below, the ChangeView is not a child of OverviewView. Therefore #Binding is not an option anyway.
Data.swift
struct ExampleData : Hashable {
var id: UUID
var name: String
}
var globalVar: ExampleData = ExampleData(id: UUID(), name:"")
OverviewView.swift
struct OverviewView: View {
#State private var data: ExampleData = globalVar
var body: some View {
Text(data.name)
}
}
ChangeView.swift
struct ChangeView: View {
#State private var data: ExampleData = globalVar
var body: some View {
TextField("Name", text: $data.name, onEditingChanged: { _ in
globalVar = data }, onCommit: { globalVar = data })
}
}
Changes within the ChangeView TextField will update the globalVar. However, this will not update the Text on the OverviewView when switching back to the view.
I am aware that using global variables is "ugly" coding. How do I handle data that will be used in a multitude of unrelated views?
Please advise on how to better handle such a situation.
OverviewView and ChangeView hold different copies of the ExampleData struct in their data variables (When assigning a struct to another variable, you're effectively copying it instead of referencing it like an object.) so changing one won't affect the other.
#EnvironmentObject suits your requirements.
Here's an example:
Since, we're using #EnvironmentObject, you need to either convert ExampleData to
a class, or use a class to store it. I'll use the latter.
class ExampleDataHolder: ObservableObject {
#Published var data: ExampleData = ExampleData(id: UUID(), name:"")
}
struct CommonAncestorOfTheViews: View {
var body: some View {
CommonAncestorView()
.environmentObject(ExampleDataHolder())
}
}
struct OverviewView: View {
#EnvironmentObject var dataHolder: ExampleDataHolder
var body: some View {
Text(dataHolder.data.name)
}
}
struct ChangeView: View {
#EnvironmentObject var dataHolder: ExampleDataHolder
var body: some View {
TextField("Name", text: $dataHolder.data.name)
}
}