Views dependant on UserDefaults not updating on change - ios

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 = ""

Related

SwiftUI - How to save custom class data to UserDefaults?

I have created a custom class (as an ObservedObject since I need to share this data).
Everything works but when I force close the app the data resets and I have not found any way to save it to UserDefaults. (Trying to save normally as an object crashed the app).
How can I save this data? (When pressing the button in SettingsView)
First time asking here.
---MainView---
import SwiftUI
class ProgressData: ObservableObject {
#Published var licenseDate: Date = Date()
#Published var dayProgressValue: Float = 0.00
#Published var nightProgressValue: Float = 0.00
#Published var newDriverProgressValue: Float = 0.00
}
struct MainView: View {
let defualts = UserDefaults.standard
#ObservedObject var data = ProgressData()
var body: some View {
// App Code Here!
}
--- SettingsView ---
import SwiftUI
struct SettingsView: View {
#ObservedObject var data: ProgressData
var body: some View {
Button(action: {
// What to do in order to save it here?
})
}
Thanks.
In order to do so, all you have to do is make sure your custom class is comforting the Codable protocol.
class ProgressData: ObservableObject, Codable {
#Published var licenseDate: Date = Date()
#Published var dayProgressValue: Float = 0.00
#Published var nightProgressValue: Float = 0.00
#Published var newDriverProgressValue: Float = 0.00
}
Next, you need to save it to UserDefault like that:
import SwiftUI
struct SettingsView: View {
#ObservedObject var data: ProgressData
var body: some View {
Button(action: {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(data) {
UserDefaults.standard.set(encoded, forKey: "saved_data")
}
})
}
}
When you want to pull your saved data from UserDefaults, all you have to do it:
if let data = defaults.object(forKey: "saved_data") as? Data {
let decoder = JSONDecoder()
if let savedData = try? decoder.decode(ProgressData.self, from: data) {
// Do wantever you want with `savedData`
}
}
You can read a bit more about it, in this wonderful article

How to pass data from an observed class to another?

This post is related to this post that I made. While there is no initialization error anymore now, it seems that there's one problem here: when you change the username in the textfield, the url and payment detail will not get updated still? Any idea how to solve this?
struct passingData: View {
#ObservedObject var userData: UserData
#ObservedObject var images: ImageURL
#ObservedObject var payment: Payment
init() {
let data = UserData()
self.userData = data
self.images = ImageURL(userData: data)
self.payment = Payment(userData: data)
}
var body: some View {
VStack{
TextField("Enter userName", text: $userData.userName)
Text("url is \(images.imageURL)")
Text("Payment detail: \(payment.paymentDate)")
}
}
}
class Payment: ObservableObject{
#Published var paymentDate = ""
#ObservedObject var userData: UserData
init(userData: UserData){
self.userData = userData
loadPaymentDate()
}
func loadPaymentDate(){
self.paymentDate = "last payment date from \(userData.userName) is 12.12.22 "
}
}
class ImageURL: ObservableObject{
#Published var imageURL = ""
#ObservedObject var userData: UserData
init(userData: UserData){
self.userData = userData
loadImageURL()
}
func loadImageURL(){
self.imageURL = "123_\(userData.userName).com"
}
}
class UserData: ObservableObject{
#Published var userName = ""
}
You cannot use #ObservedObject property wrapper in class, it is designed for View only.
Here is a demo of solution for one class. Tested with Xcode 12 / iOS 14
import Combine
class ImageURL: ObservableObject{
#Published var imageURL = ""
private var userData: UserData // << reference type
private var observer: AnyCancellable?
init(userData: UserData){
self.userData = userData
// observe changes of userName via publisher explicitly
self.observer = userData.$userName.sink(receiveValue: {[weak self] _ in
self?.loadImageURL()
})
loadImageURL()
}
func loadImageURL(){
self.imageURL = "123_\(userData.userName).com"
}
}

SwiftUI: How can I catch changing value from observed object when I execute function

I have a problem with observed object in SwiftUI.
I can see changing values of observed object on the View struct.
However in class or function, even if I change text value of TextField(which is observable object) but "self.codeTwo.text still did not have changed.
here's my code sample (this is my ObservableObject)
class settingCodeTwo: ObservableObject {
private static let userDefaultTextKey = "textKey2"
#Published var text: String = UserDefaults.standard.string(forKey: settingCodeTwo.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: settingCodeTwo.userDefaultTextKey)
}
}
deinit {
canc.cancel()
}
}
and the main problem is... "self.codeTwo.text" never changed!
class NetworkManager: ObservableObject {
#ObservedObject var codeTwo = settingCodeTwo()
#Published var posts = [Post]()
func fetchData() {
var urlComponents = URLComponents()
urlComponents.scheme = "http"
urlComponents.host = "\(self.codeTwo.text)" //This one I want to use observable object
urlComponents.path = "/mob_json/mob_json.aspx"
urlComponents.queryItems = [
URLQueryItem(name: "nm_sp", value: "UP_MOB_CHECK_LOGIN"),
URLQueryItem(name: "param", value: "1000|1000|\(Gpass.hahaha)")
]
if let url = urlComponents.url {
print(url)
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil {
let decoder = JSONDecoder()
if let safeData = data {
do {
let results = try decoder.decode(Results.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.Table
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
and this is view, I can catch change of the value in this one
import SwiftUI
import Combine
struct SettingView: View {
#ObservedObject var codeTwo = settingCodeTwo()
var body: some View {
ZStack {
Rectangle().foregroundColor(Color.white).edgesIgnoringSafeArea(.all).background(Color.white)
VStack {
TextField("test", text: $codeTwo.text).textFieldStyle(BottomLineTextFieldStyle()).foregroundColor(.blue)
Text(codeTwo.text)
}
}
}
}
Help me please.
Non-SwiftUI Code
Use ObservedObject only for SwiftUI, your function / other non-SwiftUI code will not react to the changes.
Use a subscriber like Sink to observe changes to any publisher. (Every #Published variable has a publisher as a wrapped value, you can use it by prefixing with $ sign.
Reason for SwiftUI View not reacting to class property changes:
struct is a value type so when any of it's properties change then the value of the struct has changed
class is a reference type, when any of it's properties change, the underlying class instance is still the same.
If you assign a new class instance then you will notice that the view reacts to the change.
Approach:
Use a separate view and that accepts codeTwoText as #Binding that way when the codeTwoText changes the view would update to reflect the new value.
You can keep the model as a class so no changes there.
Example
class Model : ObservableObject {
#Published var name : String //Ensure the property is `Published`.
init(name: String) {
self.name = name
}
}
struct NameView : View {
#Binding var name : String
var body: some View {
return Text(name)
}
}
struct ContentView: View {
#ObservedObject var model : Model
var body: some View {
VStack {
Text("Hello, World!")
NameView(name: $model.name) //Passing the Binding to name
}
}
}
Testing
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let model = Model(name: "aaa")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
model.name = "bbb"
}
return ContentView(model: model)
}
}
It is used two different instances of SettingCodeTwo - one in NetworkNamager another in SettingsView, so they are not synchronised if created at same time.
Here is an approach to keep those two instances self-synchronised (it is possible because they use same storage - UserDefaults)
Tested with Xcode 11.4 / iOS 13.4
Modified code below (see also important comments inline)
extension UserDefaults {
#objc dynamic var textKey2: String { // helper keypath
return string(forKey: "textKey2") ?? ""
}
}
class SettingCodeTwo: ObservableObject { // use capitalised name for class !!!
private static let userDefaultTextKey = "textKey2"
#Published var text: String = UserDefaults.standard.string(forKey: SettingCodeTwo.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
private var observer: NSKeyValueObservation!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: SettingCodeTwo.userDefaultTextKey)
}
observer = UserDefaults.standard.observe(\.textKey2, options: [.new]) { _, value in
if let newValue = value.newValue, self.text != newValue { // << avoid cycling on changed self
self.text = newValue
}
}
}
}
class NetworkManager: ObservableObject {
var codeTwo = SettingCodeTwo() // no #ObservedObject needed here
...

Changing #State variable to value from UserDefaults on load not updating Picker

I'm trying to update a #State variable upon loading my app with a value stored in UserDefaults but my Picker is not updating. This is what I tried:
struct ContentView: View {
#State var selectedCanteen = 1
init() {
let previousSelectedCanteen = UserDefaults.standard.string(forKey: "selectedCanteen")
if let exist = previousSelectedCanteen {
self.selectedCanteen = Int(exist) ?? 1
}
}
var body: some View {
Picker(selection: $selectedCanteen, label: Text("Testing...")) {
Text("Stuff").tag(1)
Text("Stuff 2").tag(2)
}
}
}
I'm coming from react-native so might I might have missed some basic concepts in Swift/SwiftUI. Hope somebody can lead me in the right direction.
Try to use the following approach
#State var selectedCanteen: Int
init() {
let previousSelectedCanteen = UserDefaults.standard.string(forKey: "selectedCanteen")
var initialValue = 1
if let exist = previousSelectedCanteen {
initialValue = Int(exist) ?? 1
}
_selectedCanteen = State<Int>(initialValue: initialValue)
}

How to observe a TextField value with SwiftUI and Combine?

I'm trying to execute an action every time a textField's value is changed.
#Published var value: String = ""
var body: some View {
$value.sink { (val) in
print(val)
}
return TextField($value)
}
But I get below error.
Cannot convert value of type 'Published' to expected argument type 'Binding'
This should be a non-fragile way of doing it:
class MyData: ObservableObject {
var value: String = "" {
willSet(newValue) {
print(newValue)
}
}
}
struct ContentView: View {
#ObservedObject var data = MyData()
var body: some View {
TextField("Input:", text: $data.value)
}
}
In your code, $value is a publisher, while TextField requires a binding. While you can change from #Published to #State or even #Binding, that can't observe the event when the value is changed.
It seems like there is no way to observe a binding.
An alternative is to use ObservableObject to wrap your value type, then observe the publisher ($value).
class MyValue: ObservableObject {
#Published var value: String = ""
init() {
$value.sink { ... }
}
}
Then in your view, you have have the binding $viewModel.value.
struct ContentView: View {
#ObservedObject var viewModel = MyValue()
var body: some View {
TextField($viewModel.value)
}
}
I don't use combine for this. This it's working for me:
TextField("write your answer here...",
text: Binding(
get: {
return self.query
},
set: { (newValue) in
self.fetch(query: newValue) // any action you need
return self.query = newValue
}
)
)
I have to say it's not my idea, I read it in this blog: SwiftUI binding: A very simple trick
If you want to observe value then it should be a State
#State var value: String = ""
You can observe TextField value by using ways,
import SwiftUI
import Combine
struct ContentView: View {
#State private var Text1 = ""
#State private var Text2 = ""
#ObservedObject var viewModel = ObserveTextFieldValue()
var body: some View {
//MARK: TextField with Closures
TextField("Enter text1", text: $Text1){
editing in
print(editing)
}onCommit: {
print("Committed")
}
//MARK: .onChange Modifier
TextField("Enter text2", text: $Text2).onChange(of: Text2){
text in
print(text)
}
//MARK: ViewModel & Publisher(Combine)
TextField("Enter text3", text: $viewModel.value)
}
}
class ObserveTextFieldValue: ObservableObject {
#Published var value: String = ""
private var cancellables = Set<AnyCancellable>()
init() {
$value.sink(receiveValue: {
val in
print(val)
}).store(in: &cancellables)
}
}
#Published is one of the most useful property wrappers in SwiftUI, allowing us to create observable objects that automatically announce when changes occur that means whenever an object with a property marked #Published is changed, all views using that object will be reloaded to reflect those changes.
import SwiftUI
struct ContentView: View {
#ObservedObject var textfieldData = TextfieldData()
var body: some View {
TextField("Input:", text: $textfieldData.data)
}
}
class TextfieldData: ObservableObject{
#Published var data: String = ""
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

Resources