SwiftUI - How to save custom class data to UserDefaults? - ios

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

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?

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

How to pass observed properties to other classes?

I created an instance of UserData so that other classes can observe this instance and show the necessary information by using the username. What I am trying to do here is, when a user is logged in, different classes with user related stored properties will be updated ( i.e. by calling the api) from time to time according to the user activity in the app.
However, it shows the error 'Cannot use instance member 'userData' within property initializer; property initializers run before 'self' is available'. Any ideas how to solve this?
I am not sure how to pass the data from a single ObservedObject to another.
struct passingData: View {
#ObservedObject var userData = UserData()
#ObservedObject var images = ImageURL(userData: userData)
#ObservedObject var payment = Payment(userData: userData)
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 someDate "
}
}
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 = ""
}
Here is possible solution:
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)
}
// ... other code

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
...

Resources