Best way to use UserDefaults with ObservableObject in Swiftui - swift

I'm trying to save user basic's data in UserDefaults.
My goal is to be able to consume data from UserDefaults and to update them each time the user do some changes.
I'm using an ObservableObject class to set and get these data
class SessionData : ObservableObject {
#Published var loggedInUser: User = User(first_name: "", last_name: "", email: "")
static let shared = SessionData()
func setLoggedInUser (user: User) {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(user) {
UserDefaults.standard.set(encoded, forKey: "User")
self.loggedInUser = currentUser
}
}
and also
struct ProfileView: View {
#ObservedObject var sessionData: SessionData = SessionData.shared
var body: some View {
VStack{
Text(self.sessionData.loggedInUser.first_name)
}
}
}
This way the changes are updated. But if I leave the app I will lose the data.
Solution 2:
I also tried to rely on reading the data from UserDefault like this
class SessionData : ObservableObject {
func getLoggedInUser() -> User? {
if let currentUser = UserDefaults.standard.object(forKey: "User") as? Data {
let decoder = JSONDecoder()
if let loadedUser = try? decoder.decode(User.self, from: currentUser) {
return loadedUser
}
}
return nil
}
}
Problem: I don't get the updates once a user change something :/
I don't find a nice solution to use both UserDefaults and ObservableObject

in "getLoggedInUser()" you are not updating the published var "loggedInUser".
Try this to do the update whenever you use the function:
func getLoggedInUser() -> User? {
if let currentUser = UserDefaults.standard.object(forKey: "User") as? Data {
let decoder = JSONDecoder()
if let loadedUser = try? decoder.decode(User.self, from: currentUser) {
loggedInUser = loadedUser // <--- here
return loadedUser
}
}
return nil
}
or just simply this:
func getLoggedInUser2() {
if let currentUser = UserDefaults.standard.object(forKey: "User") as? Data {
let decoder = JSONDecoder()
if let loadedUser = try? decoder.decode(User.self, from: currentUser) {
loggedInUser = loadedUser // <--- here
}
}
}
You could also do this to automatically save your User when it changes (instead of using setLoggedInUser):
#Published var loggedInUser: User = User(first_name: "", last_name: "", email: "") {
didSet {
if let encoded = try? JSONEncoder().encode(loggedInUser) {
UserDefaults.standard.set(encoded, forKey: "User")
}
}
}
and use this as init(), so you get back what you saved when you leave the app:
init() {
getLoggedInUser2()
}

Related

SwiftUI: keep data even after closing app

i have been trying to make that when a user adds a page to favorites or removes the page it saves it, so when a user closes the app it remembers it. I can't figure out how i can save the mushrooms table. I want to save it locally and is it done by using Prospects ?
class Favorites: ObservableObject {
public var mushrooms: Set<String>
public let saveKey = "Favorites"
init() {
mushrooms = []
}
func contains(_ mushroom: Mushroom) -> Bool {
mushrooms.contains(mushroom.id)
}
func add (_ mushroom: Mushroom) {
objectWillChange.send()
mushrooms.insert(mushroom.id)
save()
}
func remove(_ mushroom: Mushroom) {
objectWillChange.send()
mushrooms.remove(mushroom.id)
save()
}
func save() {
}
}
I was able to figure it out. Here is the code i did if someone else is struggling with this.
I added this to the save function
func save() {
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(mushrooms) {
defaults.set(encoded, forKey: "Favorites")
}
}
And to the init() :
let decoder = JSONDecoder()
if let data = defaults.data(forKey: "Favorites") {
let mushroomData = try? decoder.decode(Set<String>.self, from: data)
self.mushrooms = mushroomData ?? []
} else {
self.mushrooms = []
}
EDIT:
and of course add the defaults
let defaults = UserDefaults.standard

How to grab the current users "firstname" from firebase store. Swift 5

I did more trial and error and a bit of online research and this is what I came back with:
func presentWelcomeMessage() {
//Get specific document from current user
let docRef = Firestore.firestore()
.collection("users")
.whereField("uid", isEqualTo: Auth.auth().currentUser?.uid ?? "")
// Get data
docRef.getDocuments { (querySnapshot, err) in
if let err = err {
print(err.localizedDescription)
} else if querySnapshot!.documents.count != 1 {
print("More than one document or none")
} else {
let document = querySnapshot!.documents.first
let dataDescription = document?.data()
guard let firstname = dataDescription?["firstname"] else { return }
self.welcomeLabel.text = "Hey, \(firstname) welcome!"
}
}
It works, but am not sure if it is the most optimal solution.
First I should say firstname is not really the best way to store a var. I would recommend using firstName instead for readability. I also recommend getting single documents like I am, rather than using a whereField.
An important thing to note is you should create a data model like I have that can hold all of the information you get.
Here is a full structure of how I would get the data, display it, and hold it.
struct UserModel: Identifiable, Codable {
var id: String
var firstName: String
private enum CodingKeys: String, CodingKey {
case id
case firstName
}
}
import SwiftUI
import FirebaseAuth
import FirebaseFirestore
import FirebaseFirestoreSwift
class UserDataManager: ObservableObject {
private lazy var authRef = Auth.auth()
private lazy var userInfoCollection = Firestore.firestore().collection("users")
public func getCurrentUIDData(completion: #escaping (_ currentUserData: UserModel) -> Void) {
if let currentUID = self.authRef.currentUser?.uid {
self.userInfoCollection.document(currentUID).getDocument { (document, error) in
if let document = document {
if let userData = try? document.data(as: UserModel.self) {
completion(userData)
}
} else if let error = error {
print("Error getting current UID data: \(error)")
}
}
} else {
print("No current UID")
}
}
}
struct ContentView: View {
#State private var userData: UserModel? = nil
private let
var body: some View {
ZStack {
if let userData = self.userData { <-- safely unwrap data
Text("Hey, \(userData.firstName) welcome!")
}
}
.onAppear {
if self.userData == nil { <-- onAppear can call more than once
self.udm.getCurrentUIDData { userData in
self.userData = userData <-- pass data from func to view
}
}
}
}
}
Hopefully this can point you in a better direction of how you should be getting and displaying data. Let me know if you have any further questions or issues.

Save complex relational data in CoreData

I'm back on a learning course in SwiftUI using CoreData. I have three entities:
User // Has many Customers
Customer // Belongs to User and has many PartExchanges
PartExchange // Belongs to customer
When user first installs the app after they logged in, I fetch some initial data to be saved (the above: customers, part exchnages etc...):
struct AuthResponse: Decodable {
let error: String?
let token: String?
let userData: UserObject?
let customers: [Customers]?
struct UserObject: Decodable {
let FirstName: String?
let Surname: String?
let EmailAddress: String?
let UserID: String
}
struct Customers: Decodable {
let FirstName: String?
let Surname: String?
let EmailAddress: String?
let Customer_ID: String
let PartExchanges: [PartExchangeData]?
}
}
// In another file and not inside AuthResponse
struct PartExchangeData: Decodable {
let Registration: String?
let Customer_ID: String?
let PartExchange_ID: String?
let Variant: String?
let Colour: String?
}
AuthResponse is only used when user first logs in or reinstalls the app to get the initial data from our API:
// The exact data I have
import SwiftUI
class AuthController {
var emailUsername: String = ""
var password: String = ""
func login() -> Void {
guard let url = URL(string: "http://localhost:4000/api/auth") else {
print("Invalid URL")
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
let body: [String: AnyHashable] = [
"emailUsername": emailUsername,
"password": password
]
request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .fragmentsAllowed)
// Make the request
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
if let decodedResponse = try?
decoder.decode(AuthResponse.self, from: data) {
DispatchQueue.main.async {
if decodedResponse.error != nil {
// Tell user?
return
}
let userObject = UserModel()
userObject.createUser(authObject: decodedResponse)
}
return
}
}
print("Fetch failed: \(error?.localizedDescription ?? "Unknown error")")
}.resume()
}
}
Last, the UserModel:
class UserModel: ObservableObject {
private let fetchRequest: NSFetchRequest<User> = User.fetchRequest()
private let viewContext = PersistenceController.shared.container.viewContext
#Published var saved: Bool = false
var firstName: String = ""
var surname: String = ""
var emailAddress: String = ""
var token: String = ""
var userId: String = ""
init() {...}
public func createUser(authObject: AuthResponse) -> Void {
do {
// Create a user on first login
let user = User(context: viewContext)
let customer = Customer(context: viewContext)
let partExchange = PartExchange(context: viewContext)
//let userCustomers: [AuthResponse.Customers]
user.firstName = authObject.userData!.FirstName
user.surname = authObject.userData!.Surname
user.emailAddress = authObject.userData!.EmailAddress
user.token = authObject.token!
user.userId = authObject.userData!.UserID
// Save customers
for cus in authObject.customers! {
customer.firstName = cus.FirstName
customer.surname = cus.Surname
user.addToCustomers(customer)
// save part exchanges
for px in cus.PartExchanges! {
partExchange.registration = px.Registration
partExchange.partExchangeId = px.PartExchange_ID
partExchange.variant = px.Variant
customer.addToPartExchanges(partExchange)
}
}
try viewContext.save()
saved = true
print("ALL SAVED!!")
} catch {
let error = error as NSError
// If any issues, rollback? viewContext.rollback()
fatalError("Could not save user: \(error)")
}
}
public func logOut() {
// Only remove the token....
}
}
The issue I'm having with this approach is when saving; it's saving the last customer in the loop.
Xcode generated some extensions for User, Customer and PartExchnage and inside User, I see a function: #NSManaged public func addToCustomers(_ values: NSSet):
[..]
user.addToCustomers(<what-goes-here>)
My User entity saves correctly. Customer only has the last data from the api array. How to correctly save the user with many customers, where the each customer has many part exchanges?
You need to create a new object for each iteration in each of your loops since each object created will be stored as a separate item in Core Data
So change createUser like this
public func createUser(authObject: AuthResponse) -> Void {
do {
let user = User(context: viewContext)
user.firstName = authObject.userData!.FirstName
// more properties ...
for cus in authObject.customers! {
let customer = Customer(context: viewContext)
customer.firstName = cus.FirstName
customer.surname = cus.Surname
user.addToCustomers(customer)
for px in cus.PartExchanges! {
let partExchange = PartExchange(context: viewContext)
partExchange.registration = px.Registration
partExchange.partExchangeId = px.PartExchange_ID
partExchange.variant = px.Variant
customer.addToPartExchanges(partExchange)
}
}
try viewContext.save()
saved = true
print("ALL SAVED!!")
} catch let error = error as NSError {
//Either log the error and return some status or throw it
//FatalError is a bit to much in this situation
fatalError("Could not save user: \(error)")
}
}

Storing array of custom objects in UserDefaults

I'm having a heck of a time trying to figure out how to store an array of my custom struct in UserDefaults.
Here is my code:
struct DomainSchema: Codable {
var domain: String
var schema: String
}
var domainSchemas: [DomainSchema] {
get {
if UserDefaults.standard.object(forKey: "domainSchemas") != nil {
let data = UserDefaults.standard.value(forKey: "domainSchemas") as! Data
let domainSchema = try? PropertyListDecoder().decode(DomainSchema.self, from: data)
return domainSchema!
}
return nil
}
set {
UserDefaults.standard.set(try? PropertyListEncoder().encode(newValue), forKey: "domainSchemas")
}
}
struct SettingsView: View {
var body: some View {
VStack {
ForEach(domainSchemas, id: \.domain) { domainSchema in
HStack {
Text(domainSchema.domain)
Text(domainSchema.schema)
}
}
// clear history button
}
.onAppear {
if (domainSchemas.isEmpty) {
domainSchemas.append(DomainSchema(domain: "reddit.com", schema: "apollo://"))
}
}
}
}
It is giving me these errors:
Cannot convert return expression of type 'DomainSchema' to return type '[DomainSchema]'
'nil' is incompatible with return type '[DomainSchema]'
I'm not really sure how to get an array of the objects instead of just a single object, or how to resolve the nil incompatibility error...
If you really want to persist your data using UserDefaults the easiest way would be to use a class and conform it to NSCoding. Regarding your global var domainSchemas I would recommend using a singleton or extend UserDefaults and create a computed property for it there:
class DomainSchema: NSObject, NSCoding {
var domain: String
var schema: String
init(domain: String, schema: String) {
self.domain = domain
self.schema = schema
}
required init(coder decoder: NSCoder) {
self.domain = decoder.decodeObject(forKey: "domain") as? String ?? ""
self.schema = decoder.decodeObject(forKey: "schema") as? String ?? ""
}
func encode(with coder: NSCoder) {
coder.encode(domain, forKey: "domain")
coder.encode(schema, forKey: "schema")
}
}
extension UserDefaults {
var domainSchemas: [DomainSchema] {
get {
guard let data = UserDefaults.standard.data(forKey: "domainSchemas") else { return [] }
return (try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)) as? [DomainSchema] ?? []
}
set {
UserDefaults.standard.set(try? NSKeyedArchiver.archivedData(withRootObject: newValue, requiringSecureCoding: false), forKey: "domainSchemas")
}
}
}
Usage:
UserDefaults.standard.domainSchemas = [.init(domain: "a", schema: "b"), .init(domain: "c", schema: "d")]
UserDefaults.standard.domainSchemas // [{NSObject, domain "a", schema "b"}, {NSObject, domain "c", schema "d"}]
If you prefer the Codable approach persisting the Data using UserDefaults as well:
struct DomainSchema: Codable {
var domain: String
var schema: String
init(domain: String, schema: String) {
self.domain = domain
self.schema = schema
}
}
extension UserDefaults {
var domainSchemas: [DomainSchema] {
get {
guard let data = UserDefaults.standard.data(forKey: "domainSchemas") else { return [] }
return (try? PropertyListDecoder().decode([DomainSchema].self, from: data)) ?? []
}
set {
UserDefaults.standard.set(try? PropertyListEncoder().encode(newValue), forKey: "domainSchemas")
}
}
}
Usage:
UserDefaults.standard.domainSchemas = [.init(domain: "a", schema: "b"), .init(domain: "c", schema: "d")]
UserDefaults.standard.domainSchemas // [{domain "a", schema "b"}, {domain "c", schema "d"}]
I think the best option would be to do not use UserDefaults, create a singleton "shared instance", declare a domainSchemas property there and save your json Data inside a subdirectory of you application support directory:
extension URL {
static var domainSchemas: URL {
let applicationSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
let bundleID = Bundle.main.bundleIdentifier ?? "company name"
let subDirectory = applicationSupport.appendingPathComponent(bundleID, isDirectory: true)
try? FileManager.default.createDirectory(at: subDirectory, withIntermediateDirectories: true, attributes: nil)
return subDirectory.appendingPathComponent("domainSchemas.json")
}
}
class Shared {
static let instance = Shared()
private init() { }
var domainSchemas: [DomainSchema] {
get {
guard let data = try? Data(contentsOf: .domainSchemas) else { return [] }
return (try? JSONDecoder().decode([DomainSchema].self, from: data)) ?? []
}
set {
try? JSONEncoder().encode(newValue).write(to: .domainSchemas)
}
}
}
Usage:
Shared.instance.domainSchemas = [.init(domain: "a", schema: "b"), .init(domain: "c", schema: "d")]
Shared.instance.domainSchemas // [{domain "a", schema "b"}, {domain "c", schema "d"}]
You don't need to use NSKeyedArchiver to save custom objects into UserDefaults Because you have to change your struct into a class.
There is an easier solution and That's JSONDecoder and JSONEncoder.
Whenever you want to save a custom object into UserDefaults first convert it into Data by using JSONEncoder and when you want to retrieve an object from Userdefaults you do it by using JSONDecoder. Along with that I highly recommend you to write a separate class or struct to manage your data so that being said you can do:
struct DomainSchema: Codable {
var domain: String
var schema: String
}
struct PersistenceMangaer{
static let defaults = UserDefaults.standard
private init(){}
// save Data method
static func saveDomainSchema(domainSchema: [DomainSchema]){
do{
let encoder = JSONEncoder()
let domainsSchema = try encoder.encode(domainSchema)
defaults.setValue(followers, forKey: "yourKeyName")
}catch let err{
print(err)
}
}
//retrieve data method
static func getDomains() -> [DomainSchema]{
guard let domainSchemaData = defaults.object(forKey: "yourKeyName") as? Data else{return}
do{
let decoder = JSONDecoder()
let domainsSchema = try decoder.decode([DomainSchema].self, from: domainSchemaData)
return domainsSchema
}catch let err{
return([])
}
}
}
Usage:
let domains = PersistenceMangaer.standard.getDomains()
PersistenceMangaer.standard.saveDomainSchema(domainsTosave)

How to store user data locally in SwiftUI

I try to accomplish having an observable object with a published value training. On every change it should save the custom struct to the user defaults. On every load (AppState init) it should load the data:
class AppState: ObservableObject {
var trainings: [Training] {
willSet {
if let encoded = try? JSONEncoder().encode(trainings) {
let defaults = UserDefaults.standard
defaults.set(encoded, forKey: "trainings")
}
objectWillChange.send()
}
}
init() {
self.trainings = []
if let savedTrainings = UserDefaults.standard.object(forKey: "trainings") as? Data {
if let loadedTraining = try? JSONDecoder().decode([Training].self, from: savedTrainings) {
self.trainings = loadedTraining
}
}
}
}
I know if this is best practice, but I want to save the data locally.
The code I wrote is not working and I can't figure out why.
I'm a beginner and I never stored data to a device.
Each time you call the init method the first line resets the value stored in UserDefaults and in-turn returns the empty array instead of the value that was previously stored. Try this modification to your init method to fix it:
init() {
if let savedTrainings = UserDefaults.standard.object(forKey: "trainings") as? Data,
let loadedTraining = try? JSONDecoder().decode([Training].self, from: savedTrainings) {
self.trainings = loadedTraining
} else {
self.trainings = []
}
}
Better Approach: A much better approach would to modify your trainings property to have a get and set instead of the current setup. Here is an example:
var trainings: [Training] {
set {
if let encoded = try? JSONEncoder().encode(newValue) {
let defaults = UserDefaults.standard
defaults.set(encoded, forKey: "trainings")
}
objectWillChange.send()
}
get {
if let savedTrainings = UserDefaults.standard.object(forKey: "trainings") as? Data,
let loadedTraining = try? JSONDecoder().decode([Training].self, from: savedTrainings) {
return loadedTraining
}
return []
}
}
Note: This can again be improved using Swift 5.1's #PropertyWrapper. Let me know in the comments if anyone wants me to include that as well in the answer.
Update: Here's the solution that makes it simpler to use UserDefaults using Swift's #PropertyWrapper as you have requested for:-
#propertyWrapper struct UserDefault<T: Codable> {
var key: String
var wrappedValue: T? {
get {
if let data = UserDefaults.standard.object(forKey: key) as? Data {
return try? JSONDecoder().decode(T.self, from: data)
}
return nil
}
set {
if let encoded = try? JSONEncoder().encode(newValue) {
UserDefaults.standard.set(encoded, forKey: key)
}
}
}
}
class AppState: ObservableObject {
#UserDefault(key: "trainings") var trainings: [Training]?
#UserDefault(key: "anotherProperty") var anotherPropertyInUserDefault: AnotherType?
}