I am trying to store user info into userDefaults and assign those to singleton. I am saving the info after successful API call. I know for sure that it saves all those values. But for some reason the singleton returns nil.
This is my User class:
class User {
var id: Int
var name: String
var email: String
var profile_image_url: String
var token: String
init(id: Int, name: String, email: String, profile_image_url: String, token: String) {
self.id = id
self.name = name
self.email = email
self.profile_image_url = profile_image_url
self.token = token
}
init?(dict: [String: JSON]) {
guard let id = dict["id"]?.intValue,
let name = dict["name"]?.stringValue,
let email = dict["email"]?.stringValue,
let profile_image_url = dict["profile_image_url"]?.stringValue,
let token = dict["token"]?.stringValue
else{ return nil }
self.id = id
self.name = name
self.email = email
self.profile_image_url = profile_image_url
self.token = token
}
class var sharedInstance: User {
struct Singleton {
static var instance : User = {
let userDefaults = UserDefaults.standard
let keychain = KeychainSwift()
let user = User(
id: userDefaults.integer(forKey: "user_id"),
name: userDefaults.string(forKey: "name")!,
email: userDefaults.string(forKey: "email")!,
profile_image_url: userDefaults.string(forKey: "profile_image_url")!,
token: keychain.get("token")!
)
return user
}()
}
return Singleton.instance
}
}
And this is how I save values after API call:
class func saveUserData(user_id: Int, name: String, email: String, profile_image_url: String, token:String) {
let userDefaults = UserDefaults.standard
let keychain = KeychainSwift()
keychain.clear()
userDefaults.set(user_id, forKey: "user_id")
userDefaults.set(name, forKey: "name")
userDefaults.set(email, forKey: "email")
userDefaults.set(profile_image_url, forKey: "profile_image_url")
keychain.set(token, forKey: "token")
}
What I am doing wrong, is it even right approach?
As I have mentioned, if you are coding in Swift 4 you should take advantage of the Codable protocol. Make your user a struct that conforms to Codable. You can provide your User struct custom coding keys if needed (not required). Add a method to save your user data to user defaults and create a fallible initializer to read your user data from it:
struct User: Codable {
let id: Int
let name: String
let email: String
let profileImageUrl: String
let token: String
init(id: Int, name: String, email: String, profileImageUrl: String, token: String) {
self.id = id
self.name = name
self.email = email
self.profileImageUrl = profileImageUrl
self.token = token
}
func saveToDefaults() -> Bool {
let encoder = JSONEncoder()
do {
let json = try encoder.encode(self)
UserDefaults.standard.set(json, forKey: "User\(id)Key")
return true
} catch {
print(error)
return false
}
}
init?(data: Data) {
do {
self = try JSONDecoder().decode(User.self, from: data)
} catch {
print(error)
return nil
}
}
init?(id: Int) {
guard let data = UserDefaults.standard.data(forKey: "User\(id)Key") else { return nil }
self.init(data: data)
}
}
Playground testing
let user = User(id: 1, name: "a name", email: "name#email.com", profileImageUrl: "https://i.stack.imgur.com/Xs4RX.jpg", token: "anyTokenString")
if user.saveToDefaults() {
print("success")
if let user = User(id: 1) {
print(user.id)
print(user.name)
print(user.email)
print(user.profileImageUrl)
print(user.token)
} else {
print("there is no user with id 1")
}
}
This will print
success
1
a name
name#email.com
https://i.stack.imgur.com/Xs4RX.jpg
anyTokenString
Related
I have a struct set up for Messages and each time I got to load messages in app, I receive an unexpectedly found nil error on this line of code -->
var chatPartnerId: String {
return isFromCurrentUser ? toID! : fromID! // where I get the error
}
I can't figure out what Im doing wrong here at all.
Here's the class setup:
struct Message: MessageType {
let id: String?
var messageId: String {
return id ?? UUID().uuidString
}
var content: String?
var toID: String?
var fromID: String?
var isFromCurrentUser = Bool()
var chatPartnerId: String {
return isFromCurrentUser ? toID! : fromID!
}
let sentDate: Date
let sender: SenderType
var image: UIImage?
var downloadURL: URL?
var kind: MessageKind {
if let image = image {
let mediaItem = ImageMediaItem(image: image)
return .photo(mediaItem)
} else {
return .text(content ?? "")
}
}
init(user: User, content: String, fromID: String, toID: String) {
sender = Sender(senderId: user.uid!, displayName: user.name!)
self.content = content
self.fromID = fromID
self.toID = toID
sentDate = Date()
id = nil
}
init(user: User, image: UIImage) {
sender = Sender(senderId: user.uid!, displayName: user.name!)
self.image = image
content = ""
fromID = ""
toID = ""
sentDate = Date()
id = nil
}
init?(document: QueryDocumentSnapshot) {
let data = document.data()
guard
let sentDate = data["created"] as? Timestamp,
let senderId = data["senderId"] as? String,
let fromID = data["fromID"] as? String,
let toID = data["toID"] as? String,
let senderName = data["senderName"] as? String
else {
return nil
}
id = document.documentID
self.sentDate = sentDate.dateValue()
sender = Sender(senderId: senderId, displayName: senderName)
self.isFromCurrentUser = fromID == Auth.auth().currentUser?.uid
if let content = data["content"] as? String {
self.content = content
downloadURL = nil
} else if let urlString = data["url"] as? String, let url = URL(string: urlString) {
downloadURL = url
content = ""
} else {
return nil
}
}
}
// MARK: - DatabaseRepresentation
extension Message: DatabaseRepresentation {
var representation: [String: Any] {
var rep: [String: Any] = [
"created": sentDate,
"senderId": sender.senderId,
"fromID": fromID,
"toID": toID,
"senderName": sender.displayName
]
if let url = downloadURL {
rep["url"] = url.absoluteString
} else {
rep["content"] = content
}
return rep
}
}
// MARK: - Comparable
extension Message: Comparable {
static func == (lhs: Message, rhs: Message) -> Bool {
return lhs.id == rhs.id
}
static func < (lhs: Message, rhs: Message) -> Bool {
return lhs.sentDate < rhs.sentDate
}
}
Both toID and fromID are optionals and may be nil. Avoid force unwrapping the optional (and actually avoid force unwrapping anything else, with very rare exceptions), like you do in the problematic statement.
Instead, you can:
Don't be afraid to return an optional:
var chatPartnerId: String? { // <-- returns optional
return isFromCurrentUser ? toID : fromID
}
In many cases it's much better to deal with the nil as a condition that helps you understand the state of the app. For example nil may mean you should skip the processing of such message.
You can return a default bogus ID, or an empty string:
var chatPartnerId: String {
guard let id = isFromCurrentUser ? toID : fromID else {
return "" // <-- returns bogus ID
}
return id
}
You can change the property to be required:
var toID: String // <-- not optional
var fromID: String // <-- not optional
Looking at all of your inits I see none of them allows these paramters to be nil. So you don't need to make them optional.
I have an App and also a Share Extension. Between them I share data via UserDefaults. But it stopped working all of a sudden. Only bools or Strings can now be retrieved inside the Share Extension but when trying to retrieve a Custom Struct it is always returning nil.
Custom Struct getter/setter in UserDefaults:
//MARK: dataSourceArray
func setDataSourceArray(data: [Wishlist]?){
set(try? PropertyListEncoder().encode(data), forKey: Keys.dataSourceKey)
synchronize()
}
func getDataSourceArray() -> [Wishlist]? {
if let data = self.value(forKey: Keys.dataSourceKey) as? Data {
do {
_ = try PropertyListDecoder().decode(Array < Wishlist > .self, from: data) as [Wishlist]
} catch let error {
print(error)
}
if let dataSourceArray =
try? PropertyListDecoder().decode(Array < Wishlist > .self, from: data) as[Wishlist] {
return dataSourceArray
}
}
return nil
}
I am calling it like this inside my Extension as well as in my Main App:
if let defaults = UserDefaults(suiteName: UserDefaults.Keys.groupKey) {
if let data = defaults.getDataSourceArray() {
print("working")
} else {
print("error getting datasourceArray")
}
}
This is printing "working" in the Main App but "error getting datasourceArray" in my Extension. I don't understand the issue, especially because simple Bool-Getter are working also from my Share Extension, the issue is only with the Custom Struct.
What am I missing here?
Wishlist Struct:
import UIKit
enum PublicState: String, Codable {
case PUBLIC
case PUBLIC_FOR_FRIENDS
case NOT_PUBLIC
}
struct Wishlist: Codable {
var id: String
var name: String
var image: UIImage
var wishes: [Wish]
var color: UIColor
var textColor: UIColor
var index: Int
var publicSate: PublicState
enum CodingKeys: String, CodingKey {
case id, name, image, wishData, color, textColor, index, isPublic, isPublicForFriends, publicSate
}
init(id: String, name: String, image: UIImage, wishes: [Wish], color: UIColor, textColor: UIColor, index: Int, publicSate: PublicState) {
self.id = id
self.name = name
self.image = image
self.wishes = wishes
self.color = color
self.textColor = textColor
self.index = index
self.publicSate = publicSate
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decode(String.self, forKey: .id)
name = try values.decode(String.self, forKey: .name)
wishes = try values.decode([Wish].self, forKey: .wishData)
color = try values.decode(Color.self, forKey: .color).uiColor
textColor = try values.decode(Color.self, forKey: .textColor).uiColor
index = try values.decode(Int.self, forKey: .index)
publicSate = try values.decode(PublicState.self, forKey: .publicSate)
let data = try values.decode(Data.self, forKey: .image)
guard let image = UIImage(data: data) else {
throw DecodingError.dataCorruptedError(forKey: .image, in: values, debugDescription: "Invalid image data")
}
self.image = image
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encode(wishes, forKey: .wishData)
try container.encode(Color(uiColor: color), forKey: .color)
try container.encode(Color(uiColor: textColor), forKey: .textColor)
try container.encode(index, forKey: .index)
try container.encode(image.pngData(), forKey: .image)
try container.encode(publicSate, forKey: .publicSate)
}
}
Update
This is the part where it fails:
if let data = self.value(forKey: Keys.dataSourceKey) as? Data
Is there any way to catch an error?
I also found out that this feature is actually working for other users. The app is live: https://apps.apple.com/de/app/wishlists-einfach-w%C3%BCnschen/id1503912334
But it is not working for me? I deinstalled the app, downloaded it from the App Store but it is still not working.
I had the same problem but with another type of extension. Hope it works for you too.
Create a file you share between the two targets and put the following code there:
//MARK: - Model
struct WishlistStruct: Codable {
//your wishlist struct, I'll assume you'll have a name and some items
var name : String
var items : [String]
}
typealias Wishlist = WishlistStruct
//MARK: - Defaults
let sharedUserdefaults = UserDefaults(suiteName: SharedDefault.suitName)
struct SharedDefault {
static let suitName = "yourAppGroupHere"
struct Keys{
static let WishlistKey = "WishlistKey"
}
}
var myWishlist: [Wishlist] {
get {
if let data = sharedUserdefaults?.data(forKey: SharedDefault.Keys.WishlistKey) {
let array = try! PropertyListDecoder().decode([Wishlist].self, from: data)
return array
} else{
//Here you should return an error but I didn't find any way to do that so I put this code which hopefully will never be executed
return sharedUserdefaults?.array(forKey: SharedDefault.Keys.WishlistKey) as? [Wishlist] ?? [Wishlist]()
}
} set {
}
}
Now, whenever you need to retrieve the struct, both in app and extension, use the following code:
var wishlist : [Wishlist] = []
var currentWishlist = myWishlist
//In your viewDidLoad call
wishlist.append(contentsOf: myWishlist)
To edit the data inside of your wishlist use the following code
wishlist.append(Wishlist(name: "wishlist", items: ["aaa","bbb","ccc"]))
currentWishlist.append(Wishlist(name: "wishlist", items: items: ["aaa","bbb","ccc"]))
if let data = try? PropertyListEncoder().encode(currentWishlist) {
sharedUserdefaults?.set(data, forKey: SharedDefault.Keys.WishlistKey)
}
Let me know if you need more clarifications
Updated code to your struct. You should change some type of properties to yours(i remove some field for test).
import UIKit
enum PublicState: String, Codable {
case PUBLIC
case PUBLIC_FOR_FRIENDS
case NOT_PUBLIC
}
struct Wishlist: Codable {
var id: String = ""
var name: String = ""
var image: Data = Data()//TODO: use Data type
var color: String = ""//TODO: change it to your class
// var wish: //TODO: add this filed, i don't have it
var textColor: String = "" //TODO: change it to your class
var index: Int = 0
var publicSate: PublicState = .PUBLIC
enum CodingKeys: String, CodingKey {
case id, name, image, color, textColor, index, publicSate
}
init() {}
init(id: String, name: String, image: Data, color: String, textColor: String, index: Int, publicSate: PublicState) {
self.id = id
self.name = name
self.image = image
self.color = color
self.textColor = textColor
self.index = index
self.publicSate = publicSate
}
}
struct WishlistContainer: Codable {
var list: [Wishlist] = []
enum CodingKeys: String, CodingKey {
case list
}
}
class UserDefaultsManager {
//be sure your correctly setup your app groups
private var currentDefaults: UserDefaults = UserDefaults(suiteName: "put here your app group ID")!
private func getFromLocalStorage<T: Codable>(model: T.Type, key: String) -> T? {
if let decoded = currentDefaults.object(forKey: key) as? String {
guard let data = decoded.data(using: .utf8) else { return nil }
if let product = try? JSONDecoder().decode(model.self, from: data) {
return product
}
}
return nil
}
private func saveToLocalStorage(key: String, encodedData: String) {
currentDefaults.set(encodedData, forKey: key)
}
private func removeObject(key: String) {
currentDefaults.removeObject(forKey: key)
}
var wishList: WishlistContainer? {
set {
guard let value = newValue else {
removeObject(key: "wishList")
return
}
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
guard let jsonData = try? encoder.encode(value) else { return }
guard let jsonString = String(data: jsonData, encoding: .utf8) else { return }
saveToLocalStorage(key: "wishList", encodedData: jsonString)
}
get {
guard let value = getFromLocalStorage(model: WishlistContainer.self, key: "wishList") else {
return nil
}
return value
}
}
}
//MARK: - Usage
let list: [Wishlist] = [Wishlist()]
let container: WishlistContainer = WishlistContainer(list: list)
UserDefaultsManager().wishList = container //set
UserDefaultsManager().wishList // get
I'm trying to save a custom class array to UserDefaults but it doesn't work. I get nil back on if let. I looked everywhere online. I'm using Swift 4.2
extension UserDefaults {
func saveReciters(_ reciters: [Reciter]) {
do {
let encodedData = try NSKeyedArchiver.archivedData(withRootObject: reciters, requiringSecureCoding: false)
self.set(encodedData, forKey: UD_RECITERS)
} catch {
debugPrint(error)
return
}
}
func getReciters() -> [Reciter] {
if let reciters = self.object(forKey: UD_RECITERS) as? Data {
return NSKeyedUnarchiver.unarchiveObject(with: reciters) as! [Reciter]
} else {
print("EMPTY RECITERS")
return [Reciter]()
}
}
}
UserInfo={NSDebugDescription=Caught exception during archival: -[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x600001babcc0
Thats my class:
class Reciter: NSCoding {
private(set) public var name: String
private(set) public var image: UIImage?
private(set) public var surahs: [Surah]
private(set) public var documentID: String
private let quranData = QuranData()
init(name: String, image: UIImage?, surahCount: Int?, documentID: String) {
self.name = name
self.image = image
self.documentID = documentID
if let surahCount = surahCount {
surahs = Array(quranData.getAllSurahs().prefix(surahCount))
} else {
surahs = quranData.getAllSurahs()
}
}
func encode(with aCoder: NSCoder) {
}
required init?(coder aDecoder: NSCoder) {
}
}
On my Surah class i get nil back. All other properties i get back succesfully
Most often I see developer's use codeable, here I am using user as an example:
YourDataModel.swift
struct User: Codable {
var userId: String = ""
var name: String = ""
var profileImageData: Data? }
UserDefaults.swift
import Foundation
extension UserDefaults {
/// The current user of the application, see `./Models/User.swift`
var currentUser: User? {
get {
guard let userData = self.object(forKey: #function) as? Data else { return nil }
return try? JSONDecoder().decode(User.self, from: userData)
}
set {
guard let newuser = newValue else { return }
if let userData = try? JSONEncoder().encode(newuser) {
self.set(userData, forKey: #function)
}
}
}
}
Transform the data into json data... #function is the function or value name i.e.
// For the case the user doesn't yet exist.
if ( UserDefaults.standard.currentUser == nil ) {
// Create a new user
user = User()
// Generate an id for the user, using a uuid.
user?.userId = UUID().uuidString
} else {
// otherwise, fetch the user from user defaults.
user = UserDefaults.standard.currentUser
}
I want to create one func which i can used with various struct.
I have several struct and I want use one func with all my struct.
I work with Firestore and want use this one func to access the Firestore.
My first struct:
struct Profile {
var name = ""
var surname = ""
var email = ""
var dictionary: [String: Any] {
return [
"name": name,
"surname": surname,
"email": email
]
}
}
extension Profile: DocumentSerializable {
init?(dictionary: [String: Any], id: String) {
let name = dictionary["name"] as? String ?? ""
let surname = dictionary["surname"] as? String ?? ""
let email = dictionary["email"] as? String ?? ""
self.init(name: name,
surname: surname,
email: email)
}
}
My second struct:
struct FavoriteList {
var favoriteList: [String]
var id: String
var dictionary: [String: Any] {
return [
"favoriteList": favoriteList,
"id": id
]
}
}
extension FavoriteList: DocumentSerializable {
init?(dictionary: [String : Any], id: String) {
let favoriteList = dictionary["favorite"] as? [String] ?? [""]
let id = id
self.init(favoriteList: favoriteList, id: id)
}
}
And my func which I used now to load data from firestore:
func observeQuery() {
guard let query = query else { return }
let time = DispatchTime.now() + 0.5
listener = query.addSnapshotListener { [unowned self] (snapshot, error) in
if let snapshot = snapshot {
DispatchQueue.main.asyncAfter(deadline: time) {
let profileModels = snapshot.documents.map { (document) -> Profile in
if let profileModel = Profile(dictionary: document.data(), id: document.documentID) {
return profileModel
} else {
fatalError("Error!")
}
}
self.profile = profileModels
self.document = snapshot.documents
self.tableView.reloadData()
}
}
}
}
So how I can make func observeQuery to use my structs Profile or FavouriteList?
You can use Generic Functions :
func observeQuery<T>(someObject: T) {
if someObject is Profile {
//do something
} else if someObject is FavouriteList {
//do something
}
}
I know I can create failable initializer for my model class to initialize from JSON data by :
struct Person {
let firstName: String
let middleName: String?
init?(JSONData data:[String:AnyObject]) {
guard let firstName = data["firstName"] as? String else { return nil }
self.firstName = firstName
self.middleName = data["middleName"] as? String
}
}
But what if I have another attribute in Person which is another model class type? For example:
struct Person {
let firstName: String
let car: Car
init?(JSONData data:[String: AnyObject]) {
guard let firstName = data["firstName"] as! String,
let car = data["car"] as! Car // this line doesn't work I guess
else {return nil}
self.firstName = firstName
self.car = car
}
}
Car looks like this:
struct Car {
let year: Int
let brand: String
}
What is the proper way to make the failable initializer above work with custom type Car for JSON data parsing?
e.g. JSON:
{“firstName”: “John”,
“car”: {
“year”: 2009,
“brand”: “BMW”
}}
Firstly, you are not returning optionals in your guard statement. Change ! to ? in them.
Secondly, you should pass the data to the initializer and check if it is nil.
guard let firstName = data["firstName"] as? String,
let carData = data["car"] as? [String: AnyObject],
let car = Car(JSONData: carData)
else {return nil}
Change Car struct with this code-
struct Car {
let year: Int
let brand: String
init?(JSONData data:[String: AnyObject]) {
guard let brand = data["brand"] as! String,
let year = data["year"] as! Int
else {return nil}
self.brand = brand
self.year = year
}
}
And change Person struct to this-
struct Person {
let firstName: String
let car: Car
init?(JSONData data:[String: AnyObject]) {
guard let firstName = data["firstName"] as! String,
let car = Car(JSONData:data["car"])
else {return nil}
self.firstName = firstName
self.car = car
}
}
you can try like
struct Person {
let firstName: String
let car: Car
init?(JSONData data:[String: AnyObject]) {
guard let firstName = data["firstName"] as? String,
let carJSON = data["car"] as? [String: AnyObject?],
let car = Car(data: carJSON)
else {return nil}
self.firstName = firstName
self.car = car
}
}
and in Car
struct Car {
let year: Int
let brand: String
init?(data: [String: AnyObject?]) {
guard let brand = data["brand"] as? String,
let year = data["year"] as? Int
else {return nil}
self.brand = brand
self.year = year
}
}
As Swift 4+ introduces Codable For Encoding & decoding with external representation(JSON,Plist)
JsonData -
let jsonExample = """
{
"firstName": "John",
"car": {
"year": 2009,
"brand": "BMW"
}
}
""".data(using: .utf8)!
Struct with Codable protocol-
struct UserData: Codable{
var firstName: String
var car: CarData
}
struct CarData: Codable{
var year: Int
var brand: String
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
year = try values.decode(Int.self, forKey: .year)
brand = try values.decode(String.self, forKey: .brand)
}
}
Usage-
let jsonDecoder = JSONDecoder()
do {
let modelResult = try jsonDecoder.decode(UserData.self,from: jsonExample)
print("firstName is \(modelResult.firstName)")//prints John
print("car brand is \(modelResult.car.brand)")//Prints BMW
} catch {
print(error)
}
If of Json is with diffrent keys respective of your struct then use CodingKeys
Example-
let jsonExample = """
{
"firstNameOfBuyer": "John",
"car": {
"year-of-made": 2009,
"brand-name": "BMW"
}
}
""".data(using: .utf8)!
struct UserData: Codable {
var firstName: String
var car: CarData
private enum CodingKeys: String, CodingKey {
case firstName = "firstNameOfBuyer"
case car
}
}
struct CarData: Codable{
var year: Int
var brand: String
private enum CodingKeys: String, CodingKey {
case year = "year-of-made"
case brand = "brand-name"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
year = try values.decode(Int.self, forKey: .year)
brand = try values.decode(String.self, forKey: .brand)
}
}