Cannot get UserDefautls updated values in realtime - ios

I'm new to swift and I cannot get the UserDefaults updated values in the same session. Only after the application restarts.
Here is what I mean in some code:
//This is where I have the userdefaults
#ObservedObject var userSettingsController = UserSettingsController
//These are the auxiliar vars I created to help me achieve the conditional renders I need. I'm looking to get rid of these and use the usersettings updated values
#State private var showMap = false
#State private var showTutorial = true
//Partial code of my view, where I'm using the variables
if(!self.userSettingsController.showActionSheet && self.showMap) {
showMapView()
.onTapGesture {
if (self.userSettingsController.showNextDeparturesTutorial && self.showTutorial {
self.showTutorial = false
self.userSettingsController.showNextDeparturesTutorial.toggle()
} else {
//code that has nothing to do with the question
No, here is my UserSettings and UserSettingsController classes:
UserSettings
import Foundation
struct UserSettings {
var settings: UserDefaults
init() {
self.settings = UserDefaults.standard
self.settings.register(
defaults: [
"userCity": "",
"showUserCityActionSheet": true,
"showNextDeparturesTutorial": true,
])
}
}
UserSettingsController
import Foundation
import Combine
import SwiftUI
class UserSettingsController: ObservableObject {
#Published var userSettings = UserSettings()
var userCity : String {
get {
return self.userSettings.settings.string(forKey: "userCity") ?? ""
}
set {
self.userSettings.settings.set(newValue, forKey: "userCity")
}
}
var showUserCityActionSheet: Bool {
get {
return self.userSettings.settings.bool(forKey: "showUserCityActionSheet")
}
set {
self.userSettings.settings.set(newValue, forKey: "showUserCityActionSheet")
}
}
var showNextDeparturesTutorial: Bool {
get {
return self.userSettings.settings.bool(forKey: "showNextDeparturesTutorial")
}
set {
self.userSettings.settings.set(newValue, forKey: "showNextDeparturesTutorial")
}
}
}
My question is, how can I get the updated values of the UserDefault values showNextDeparturesTutorial and showActionSheet in realtime? I've already tried to store them in other variables but to no avail.
Thanks.
EDIT
I accepted #Asperi answer because it was the most efficient one considering my project.
However, #pawello2222 answer would also solve my problem.
Thanks, all.

The possible solution is to activate ObservableObject publisher explicitly in every setter, like below
var userCity : String {
get {
return self.userSettings.settings.string(forKey: "userCity") ?? ""
}
set {
self.userSettings.settings.set(newValue, forKey: "userCity")
self.objectWillChange.send() // << this one !!
}
}

You can make your variables #Published. This way their changes will be detected by your View.
Every time you modify some of these variables it will be saved to UserDefaults as well. And when you init your UserSettingsController you have to load values from UserDefaults first:
class UserSettingsController: ObservableObject {
private let userSettings = UserDefaults.standard
#Published var showNextDeparturesTutorial: Bool {
didSet {
self.userSettings.set(showNextDeparturesTutorial, forKey: "showNextDeparturesTutorial")
}
}
init() {
_showNextDeparturesTutorial = .init(initialValue: userSettings.bool(forKey: "showNextDeparturesTutorial"))
}
}
struct ContentView: View {
#ObservedObject var userSettingsController = UserSettingsController()
...
}

The problem is not the UserDefaults, it is that SwiftUI is not detecting any changes in the data since your data resides in the UserDefaults database and SwiftUI can’t see the changes.
The #Published on the userSettings variable is no use here since it is an object, and in the current version of SwiftUI/Combine, it only detects changes of the object being referenced, instead of changes within the object. E.g. if you assigned a different defaults object to UserDefaults it would fire its ObjectWillChange publisher.
You would be better off storing your settings values in actual variables, and using didSet to persist them to the User Defaults Database on each change. You would then only need to load them on startup to get the data back from User Defaults.

Related

SwiftUI Pass EnvironmentObject to a class

I'm struggling with passing an variable to a class.
I have a class with different settings I store. The data is available and might be changed on different views.
class UserData: ObservableObject {
#Published var ZipCode = "DK6700"
}
The class is on my main view initialised as StateObject:
struct ContentView: View {
#StateObject var SavedData = UserData()
var body: some View {
NavigationView {
ChartView()
}
.edgesIgnoringSafeArea(.bottom)
.environmentObject(SavedData)
}
}
I call the Struct ChartView() where UserData is initialised as
#EnvironmentObject var SavedData: UserData
#ObservedObject var dataModel = DataModel()
and from the corresponding View, i can access the stored ZipCode.
So far so good.
The ChartView calls another class, where I download data in JSON format. This works as well, but I need the stored ZIP code in this class, and I can't figure out how to pass it.
Currently ZIP is hardcoded in the DataModel, and works, but it should be the stored value instead.
My DataModel():
class DataModel: ObservableObject {
#EnvironmentObject var SavedData: UserData
#MainActor #Published var Data: [MyData] = []
var ZIP:String = "3000"
#MainActor func reload() async {
let url = URL(string: "https://MyURL?zip=\(ZIP)")!
let urlSession = URLSession.shared
do {
...
} catch {
...
}
}
}
Any suggestions to get the stored ZIP code to my DataModel Class?
How about changing your function signature to
#MainActor func reload(for zipCode: String) async {
and passing it in when you call the function?

#Published Realm object not triggering view redrawing in SwiftUI

In our app we use a UserService that is a ObservableObject and passed as environment. A synced realm is opened and the app user (a RealmObject) is obtained using flexible sync.
When updating the users properties, such as his username, the view does not get redrawn. This is against my expectations since UserService contains a #Published property where the user (that is being edited) is stored. On the database it clearly shows the property being edited, however the view does not get redrawn, only when restarting the app the new properties are shown.
What would be the best way to have a UserService objects taking care of all user related logic (storing a user object (reference to it), containing functions to update, ...) and use this to display the active data of this user throughout the views?
Here is a MRE (the login logic is left out to reduce complexity):
import SwiftUI
import RealmSwift
class UserService2: ObservableObject {
var realm: Realm
#Published var ownUser: User
var userNotificationToken: NotificationToken?
init(realm: Realm, ownUser: User) {
self.realm = realm
self.ownUser = ownUser
userNotificationToken = ownUser.observe { change in
print(change) // just to see that the user object is actually live and being updated...
}
}
func changeName(newName: String) {
do {
try self.realm.write {
self.ownUser.userName = newName
}
} catch {
print("error")
}
}
}
struct TestView: View {
#EnvironmentObject var userService: UserService2
var body: some View {
VStack {
Text(userService.ownUser.userName ?? "no name")
Button {
userService.changeName(newName: Date().description)
} label: {
Text("change name")
}
}
}
}
struct ContentView: View {
var realm: Realm? = nil
init() {
let flexSyncConfig = app.currentUser!.flexibleSyncConfiguration(initialSubscriptions: { subs in
subs.append(
QuerySubscription<User>(name: "ownUserQuery") {
$0._id == "123"
})
})
do {
let realm = try Realm(configuration: flexSyncConfig)
self.realm = realm
} catch {
print("sth went wrong")
}
}
var body: some View {
if let realm = realm, let ownUser = realm.objects(User.self).where( { $0._id == "123" } ).first {
TestView()
.environmentObject(UserService2(realm: realm, ownUser: ownUser))
} else {
ProgressView()
}
}
}
The User Object looks like this
import Foundation
import RealmSwift
class User: Object, ObjectKeyIdentifiable {
#Persisted(primaryKey: true) var _id = UUID().uuidString
#Persisted var userName: String?
convenience init(_id: String? = nil, userName: String? = nil) {
self.init()
if let _id = _id {
self._id = _id
}
self.userName = userName
}
}
P.S. I assume I could observe changes on the object using realm and somehow force a view refresh, but I would find it much more clean using the already existing way to watch for changes and redraw views when needed using #Published...
P.P.S. This user object is created on the server using a trigger when someone authenticates. However, I assume this is not really relevant to this problem.
The issue here is the usage of a reference type as "Source of truth".
ObservableObject and SwiftUI Views use Combine Publishers to know when to refresh.
The #Published value sends the .objectWillChange publisher of the ObservableObject only when its wrapped value "changes". "changes" in this context means it gets replaced. So value types are preferred here, because if you change one of the properties the whole object will be replaced. This does not happen for reference types.
Multiple possible solutions here:
change the User class to a struct (Probably not wanted here, because this object implements Realm)
use the .objectWillChange.send() method yourself before altering the user
instead of altering the ownUservar replace it with a new one that contains the new information.
func changeName(newName: String) {
do {
self.objectWillChange.send() //add this
try self.realm.write {
self.ownUser.userName = newName
}
} catch {
print("error")
}
}

Views dependant on UserDefaults not updating on change

So I have a class that records the state of a toggle and a selection of a picker into UserDefaults
import Foundation
import Combine
class UserSettings: ObservableObject {
#Published var shouldSort: Bool {
didSet {
UserDefaults.standard.set(shouldSort, forKey: "shouldSort")
}
}
#Published var sortKey: String {
didSet {
UserDefaults.standard.set(sortKey, forKey: "sortKey")
}
}
public var sortKeys = ["alphabetical", "length", "newest", "oldest"]
init() {
self.shouldSort = UserDefaults.standard.object(forKey: "shouldSort") as? Bool ?? true
self.sortKey = UserDefaults.standard.object(forKey: "sortKey") as? String ?? "Name"
}
}
On my settings page I use the following code
#ObservedObject var userSettings = UserSettings()
...
Toggle(isOn: $userSettings.shouldSort, label: {Text("Sort Books")})
Picker(selection: $userSettings.sortKey, label: Text("Sort By"), content: {
ForEach(userSettings.sortKeys, id: \.self){ key in
Text(key)
}
})
This code changes the value just fine because if I close and open the app, the views update based on the data. I am reading the data with
#State var sorted = UserDefaults.standard.bool(forKey: "shouldSort")
#State var sort = UserDefaults.standard.string(forKey: "sortKey")
in my content view. (shouldSort calls a function to sort if true and sortKey determines how the data is sorted)
Am I reading the data wrong with the #State variable (can #State even detect changes in state of UserDefaults)?
Forget all what you learnt about UserDefaults in UIKit and say Hello to AppStorage in SwiftUI, use this Codes:
#AppStorage("shouldSort") var sorted: Bool = false
#AppStorage("sortKey") var sort: String = ""

Custom property wrapper not accurately reflecting the state of my TextField in SwiftUI, any idea why?

I've created a property wrapper that I want to insert some logic into and the "set" value is doing the right thing, but the textfield isn't updating with all uppercase text. Shouldn't the text field be showing all uppercase text or am I misunderstanding how this is working?
Also this is a contrived example, my end goal is to insert a lot more logic into a property wrapper, I'm just using the uppercase example to get it working. I've searched all over the internet and haven't found a working solution.
import SwiftUI
import Combine
struct ContentView: View {
#StateObject var vm = FormDataViewModel()
var body: some View {
Form {
TextField("Name", text: $vm.name)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class FormDataViewModel: ObservableObject {
#Capitalized var name: String = ""
}
#propertyWrapper
public class Capitalized {
#Published var value: String
public var wrappedValue: String {
get { value }
set { value = newValue.uppercased() } //Printing this shows all caps
}
public var projectedValue: AnyPublisher<String, Never> {
return $value
.eraseToAnyPublisher()
}
public init(wrappedValue: String) {
value = wrappedValue
}
}
SwiftUI watches #Published properties in #StateObject or #ObservedObject and triggers UI update on changes of them.
But it does not go deep inside the ObservableObject. Your FormDataViewModel does not have any #Published properties.
One thing you can do is that simulating what #Published will do on value changes.
class FormDataViewModel: ObservableObject {
#Capitalized var name: String = ""
private var nameObserver: AnyCancellable?
init() {
nameObserver = _name.$value.sink {_ in
self.objectWillChange.send()
}
}
}
Please try.
This can be done with standard #Published that seems simpler and more reliable.
Here is a solution. Tested with Xcode 12 / iOS 14.
class FormDataViewModel: ObservableObject {
#Published var name: String = "" {
didSet {
let capitalized = name.uppercased()
if name != capitalized {
name = capitalized
objectWillChange.send()
}
}
}
}

Passing an ObservableObject model through another ObObject?

I feel like I can sort of understand why what I'm doing isn't working but I'm still trying to wrap my head around Combine and SwiftUI so any help here would be welcome.
Consider this example:
Single view app that stores some strings in UserDefaults, and uses those strings to display some Text labels. There are three buttons, one to update the title, and one each to update the two UserDefaults-stored strings to a random string.
The view is a dumb renderer view and the title string is stored directly in an ObservableObject view model. The view model has a published property that holds a reference to a UserSettings class that implements property wrappers to store the user defined strings to UserDefaults.
Observations:
• Tapping "Set A New Title" correctly updates the view to show the new value
• Tapping either of the "Set User Value" buttons does change the value internally, however the view does not refresh.
If "Set A New Title" is tapped after one of these buttons, the new values are shown when the view body rebuilds for the title change.
View:
import SwiftUI
struct ContentView: View {
#ObservedObject var model = ViewModel()
var body: some View {
VStack {
Text(model.title).font(.largeTitle)
Form {
Section {
Text(model.settings.UserValue1)
Text(model.settings.UserValue2)
}
Section {
Button(action: {
self.model.title = "Updated Title"
}) { Text("Set A New Title") }
Button(action: {
self.model.settings.UserValue1 = "\(Int.random(in: 1...100))"
}) { Text("Set User Value 1 to Random Integer") }
Button(action: {
self.model.settings.UserValue2 = "\(Int.random(in: 1...100))"
}) { Text("Set User Value 2 to Random Integer") }
}
Section {
Button(action: {
self.model.settings.UserValue1 = "Initial Value One"
self.model.settings.UserValue2 = "Initial Value Two"
self.model.title = "Initial Title"
}) { Text("Reset All") }
}
}
}
}
}
ViewModel:
import Combine
class ViewModel: ObservableObject {
#Published var title = "Initial Title"
#Published var settings = UserSettings()
}
UserSettings model:
import Foundation
import Combine
#propertyWrapper struct DefaultsWritable<T> {
let key: String
let value: T
init(key: String, initialValue: T) {
self.key = key
self.value = initialValue
}
var wrappedValue: T {
get { return UserDefaults.standard.object(forKey: key) as? T ?? value }
set { return UserDefaults.standard.set(newValue, forKey: key) }
}
}
final class UserSettings: NSObject, ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
#DefaultsWritable(key: "UserValue", initialValue: "Initial Value One") var UserValue1: String {
willSet {
objectWillChange.send()
}
}
#DefaultsWritable(key: "UserBeacon2", initialValue: "Initial Value Two") var UserValue2: String {
willSet {
objectWillChange.send()
}
}
}
When I put a breakpoint on willSet { objectWillChange.send() } in UserSettings I see that the objectWillChange message is going to the publisher when I would expect it to so that tells me that the issue is likely that the view or the view model is not properly subscribing to it. I know that if I had UserSettings as an #ObservedObject on the view this would work, but I feel like this should be done in the view model with Combine.
What am I missing here? I'm sure it's really obvious...
ObsesrvedObject listens for changes in #Published property, but not the deeper internal publishers, so the below idea is to join internal publisher, which is PassthroughSubject, with #Published var settings, to indicate that the latter has updated.
Tested with Xcode 11.2 / iOS 13.2
The only needed changes is in ViewModel...
class ViewModel: ObservableObject {
#Published var title = "Initial Title"
#Published var settings = UserSettings()
private var cancellables = Set<AnyCancellable>()
init() {
self.settings.objectWillChange
.sink { _ in
self.objectWillChange.send()
}
.store(in: &cancellables)
}
}

Resources