Trying to save an array into UserDefaults - ios

In my code, data is an array of format [String : Any], I'm trying to store it in userdefaults
let userDefaults = UserDefaults.standard
let encodedData: Data = NSKeyedArchiver.archivedData(withRootObject: data)
userDefaults.set(encodedData, forKey: "data")
userDefaults.synchronize()
but I get this error:
-[Features encodeWithCoder:]: unrecognized selector sent to instance
what I'm doing wrong?

You can only store several types of objects in UserDefaults like Int, String, Float, Double, Bool, URL, Data or collection of these types. However according to apple:
If you want to store any other type of object, you should typically
archive it to create an instance of NSData.
Luckily Apple released Codable so any class or struct conforming to Codable can be converted to and from Data.
First conform your dataModel to Codable
struct Features : Codable {
var feature1:String?
var feature2:String?
}
then after you create you objects and add them to the array
let feature1 = Features(feature1: "awesome", feature2: "World")
let feature2 = Features(feature1: "Hello", feature2: "World")
arrayOfFeatures = [feature1,feature2]
you can use save them like this:
func saveInUserDefault(with arrayOfFeatures:[Features]){
do{
UserDefaults.standard.set(try PropertyListEncoder().encode(arrayOfFeatures), forKey: "data")
UserDefaults.standard.synchronize()
}
catch
{
print(error.localizedDescription)
}
}
and get them with like this:
func getDataFromArray(){
if let savedData: Data = UserDefaults.standard.data(forKey: "data"){
do
{
arrayOfFeatures = try PropertyListDecoder().decode([Features].self, from: savedData)
for feature in arrayOfFeatures
{
print(feature.feature1)
print(feature.feature2)
}
}
catch
{
print(error.localizedDescription)
}
}
}
saveInUserDefault(with: arrayOfFeatures)

Related

How to archive data in swift?

I am trying to archive data and want to store it in userdefault but app getting crash.
Also tried this
let encodedData = try NSKeyedArchiver.archivedData(withRootObject: selectedPoductDetails, requiringSecureCoding: false)
selectedPoductDetails is dict of type [String: SelectedProductDetail]
import Foundation
class SelectedProductDetail {
let product: String
var amount: Double
var title: String
init(product: String, amount: Double, title: String ) {
self.product = product
self.amount = amount
self.title = title
}
}
May i know why its not working and possible solution for same?
For this case you can use UserDefaults
struct ProductDetail: Codable {
//...
}
let encoder = JSONEncoder()
let selectedProductDetails = ProductDetail()
// Set
if let data = try? encoder.encode(selectedProductDetails) {
UserDefaults.standard.set(data, forKey: "selectedProductDetails")
}
// Get
if let selectedProductDetailsData = UserDefaults.standard.object(forKey: "selectedProductDetails") as? Data {
let selectedProductDetails = try? JSONDecoder().decode(ProductDetail.self, from: selectedProductDetailsData)
}
As mentioned in the comments to use NSKeyedArchiver the class must adopt NSSecureCoding and implement the two required methods.
The types in your class are JSON compatible, so adopt Codable and archive the data with JSONEncoder (or PropertyListEncoder). You could even use a struct and delete the init method
struct SelectedProductDetail: Codable {
let product: String
var amount: Double
var title: String
}
var productDetails = [String: SelectedProductDetail]()
// populate the dictionary
do {
let data = try JSONEncoder().encode(productDetails)
UserDefaults.standard.set(data, forKey: "productDetails")
} catch {
print(error)
}
And load it
do {
guard let data = UserDefaults.standard.data(forKey: "productDetails") else { return }
productDetails = try JSONDecoder().decode([String: SelectedProductDetail].self, from: data)
} catch {
print(error)
}
Note:
UserDefaults is the wrong place for user data. It's better to save the data in the Documents folder

Convert Codable object to Dictionary without Bool turning to NSNumber

I have a Codable object that I need to convert to Dictionary so I first encode it to convert it to Data and then use JSONSerialization to convert that Data to Any and then use as? [String: Any] to get the dictionary.
The conversion is successful but due to use of JSONSerialisation, Bool types are being converted to NSNumber but I want to retain the Bool value inside the Dictionary
import Foundation
struct Employee: Codable {
let employeeID: Int?
let meta: Meta?
}
struct Meta: Codable {
let demo: Bool?
}
let employeeObject = Employee(employeeID: 1, meta: Meta(demo: true))
do {
let encodedObject = try JSONEncoder().encode(employeeObject)
let dictionary = try JSONSerialization.jsonObject(with: encodedObject, options: .fragmentsAllowed) as? [String: Any]
print(dictionary ?? [:])
} catch {
print(error)
}
OUTPUT
["meta": {
demo = 1; }, "employeeID": 1]
demo property is being converted to NSNumber but I want to retain the Bool value
The print output is misleading. That's related to internal implicit bridging to NSDictionary
Although the value is printed as 1 the type is a boolean, see
let meta = dictionary!["meta"] as! [String:Any]
print(type(of: meta["demo"]! )) // __NSCFBoolean
Your concern is causeless because to get the value from a [String:Any] dictionary you have to cast the type anyway
let demo = meta[demo] as! Bool
Or you have to cast the dictionary to [String:Bool]
let meta = dictionary!["meta"] as! [String:Bool]
let demo = meta["demo"]!
Side note:
Declare struct members non-optional as much as possible, not the opposite.
This should do the trick:
struct Meta: Codable {
let demo: Bool?
public init(demo : Bool) {
self.demo = demo
}
enum CodingKeys: String, CodingKey {
case demo = "demo"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
demo = try container.decode(Bool.self, forKey: .demo)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.demo, forKey: .demo)
}
}
The JSON representation is still a number, but it decodes correctly back into Bool.

How to save a Array with (Multiple Types) in NSUserDefaults

This is pretty simple but can't seem to find the correct information to solve saving an array like this in User Defaults.
It says it's not a property that NSUser Defaults Excepts.
Code:
var notificationList: [(type: String,imageName: String, text: String, date: String, seen: Bool)] = [(type: "Default",imageName: "ClearPartioned", text: "", date: "", seen: true)]
if (UserDefaults.standard.object(forKey: "notificationList")) == nil { // first time launching
print("making notification list")
UserDefaults.standard.set(notificationList, forKey: "notificationList")
UserDefaults.standard.synchronize()
print("\(notificationList)")
} else {
print("getting saved array")
notificationList = (UserDefaults.standard.object(forKey: "notificationList") as! [(type: String, imageName: String, text: String, date: String, seen: Bool)])
print("\(notificationList)")
}
Update:
This is closer but gives error found in this question here. These are the closet answers I have been able to find and there either out dated or crash the system.
Code:
if (UserDefaults.standard.object(forKey: "notificationList")) == nil { // first time launching
print("making notification list")
let encodedData = NSKeyedArchiver.archivedData(withRootObject: notificationList)
UserDefaults.standard.set(encodedData, forKey: "notificationList")
UserDefaults.standard.synchronize()
} else {
print("getting saved array")
notificationList = (UserDefaults.standard.object(forKey: "notificationList") as! [(type: String, imageName: String, text: String, date: String, seen: Bool)])
print("\(notificationList)")
}
Update 2: This is best answer implementation From Dhiru
Code:
if (UserDefaults.standard.object(forKey: "notificationList")) == nil { // first time launching
print("making notification list")
let notificationData = NSKeyedArchiver.archivedData(withRootObject: notificationList)
UserDefaults.standard.set(notificationData, forKey: "notificationList")
UserDefaults.standard.synchronize()
} else {
print("getting saved array")
let decodedData = UserDefaults.standard.object(forKey: "notificationList") as! Data
let notificationList = NSKeyedUnarchiver.unarchiveObject(with: decodedData) as AnyObject
print("\(notificationList)")
}
Its giving me an error that crashes system
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[_SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x1c011f380'
libc++abi.dylib: terminating with uncaught exception of type NSException
Im sure this code would fix it but this is horribly implemented with multiple errors below because I have no clue how to use this code.
Code:
func (coder aDecoder: NSCoder) {
if let notificationList = aDecoder.decodeObjectForKey("notificationList") {
self.notificationList = notificationList
}
}
func encodeWithCoder(aCoder: NSCoder) {
if let notificationList = notificationList {
aCoder.encodeObject(notificationList, forKey: "notificationList")
}
}
You have to store your Object in form of Data
Convert into data using
NSKeyedArchiver.archivedData(withRootObject:)
Convert back to Object using
NSKeyedUnarchiver.unarchiveObject(with:)
Saving Data for UserDefaults
let notificationData = NSKeyedArchiver.archivedData(withRootObject: notificationList)
UserDefaults.standard.set(notificationData, forKey: "notificationList")
Retrive Data from User UserDefaults
let decodedData = UserDefaults.standard.object(forKey: "notificationList") as! Data
let notificationList = NSKeyedUnarchiver.unarchiveObject(with: decodedData) as! AnyObject
This is how I actually save a Custom Object created in the app in Swift 4.
First, we create 3 protocols for our purpose of saving the custom object in UserDefaults. The logic behind is to convert the Custom Object into a normalized Dictionary/Array form.
This can be applied to any kind of Object which you have created.
The 3 protocols are:
Decoder (Used to decode the dictionary into custom object)
Encoder (Used to encode the custom object into dictionary)
UserDefaultsProtocol (Used to save, delete, update & retrieve the custom object from UserDefault)
Decoder Protocol
protocol Decoder {
associatedtype T
static func decode(dictionary: [String: Any]) -> T
}
Encoder Protocol
protocol Encoder {
func encode() -> [String: Any]
}
UserDefaultsProtocol
protocol UserDefaultsDelegate: class {
associatedtype T
func saveToUserDefaults()
static func removeFromUserDefaults()
static func retrieveFromUserDefaults() -> T?
}
As per your question, NotificationList Object would look like this
class NotificationList {
var type: String = ""
var imageName: String = ""
var text: String = ""
var date: String = ""
var seen: Bool = false
}
Now, you need to confirm all the 3 mentioned protocols to NotificationList. (Swift Best Practice: Use of Extensions & Protocols)
class NotificationList {
private struct Constants {
static let RootKey = "notification_list"
static let TypeKey = "type"
static let ImageNameKey = "image_name"
static let TextKey = "text"
static let DateKey = "date"
static let SeenKey = "seen"
}
var type: String = ""
var imageName: String = ""
var text: String = ""
var date: String = ""
var seen: Bool = false
typealias T = NotificationList
}
extension NotificationList: Encoder {
func encode() -> [String : Any] {
return [
Constants.TypeKey: type,
Constants.ImageNameKey: imageName,
Constants.TextKey: text,
Constants.DateKey: date,
Constants.SeenKey: seen
]
}
}
extension NotificationList: Decoder {
static func decode(dictionary: [String: Any]) -> NotificationList {
let type = dictionary[Constants.TypeKey] as! String
let imageName = dictionary[Constants.ImageNameKey] as! String
let text = dictionary[Constants.TextKey] as! String
let date = dictionary[Constants.DateKey] as! String
let seen = dictionary[Constants.SeenKey] as! Bool
let notificationList = NotificationList()
notificationList.type = type
notificationList.imageName = imageName
notificationList.text = text
notificationList.date = date
notificationList.seen = seen
return notificationList
}
}
extension NotificationList: UserDefaultsDelegate {
func saveToUserDefaults() {
UserDefaults.standard.setValue(encode(), forKey: Constants.RootKey)
}
static func retrieveFromUserDefaults() -> NotificationList? {
guard let encodedNotificationList = UserDefaults.standard.dictionary(forKey: Constants.RootKey) else {
return nil
}
return NotificationList.decode(dictionary: encodedNotificationList)
}
static func removeFromUserDefaults() {
UserDefaults.standard.removeObject(forKey: Constants.RootKey)
}
}
How to save NotificationList to UserDefaults?
var notificationList = NotificationList()
notificationList.type = "Default"
notificationList.imageName = "ClearPartioned"
notificationList.text = ""
notificationList.date = ""
notificationList.seen = true
Save to UserDefaults
notificationList.saveToUserDefaults()
Retrieve from UserDefaults
if let notificationList = NotificationList.retrieveFromUserDefaults() {
// You will get the instance of notification list saved in UserDefaults
}
HOW TO SAVE ARRAY OF NOTIFICATION LIST?
Say notificationLists contains the array of notificationList objects.
var notificationListsArray = [[String: Any]]()
notificationLists.forEach {
notificationListsArray.append($0.encode())
}
Save that array of dictionary to UserDefaults
UserDefaults.standard.setValue(notificationListsArray, forValue: "notificationLists")

Is it possible to send complex Arrays from iPhone to AppleWatch with watch connectivity?

I have a problem passing data from iOS to WatchOS 2.0
I want to send an ArrayList to WatchOS but my ArrayList has no type like String, Int but an Object that I generated.
// here I fetch my Lists with Users
var friendList: [UserProfile] = Utils().loadUsers("friendList")
var blackList: [UserProfile] = Utils().loadUsers("blackList")
var users: [UserProfile] = Utils().loadUsers("UsersList")
// here I put the Lists in the Dictionary in order to send this Dictionary to Watch
let dictionary: [String: AnyObject]=[
"UsersList" : self.users,
"BlackList" : self.blackList,
"FriendList" : self.friendList
]
WCSession.defaultSession().sendMessage(dictionary, replyHandler: { (data) -> Void in
// handle the response from the device
}) { (error) -> Void in
print("error: \(error.localizedDescription)")
}
In my WatchApp Class I try to get the Data but there is following error:
error: Payload contains unsupported type.
This is how I want to get the Data. If I send Bools, Integers or String this works, but not for Arrays like mine:
let userList: [UserProfile] = applicationContext["UsersList"] as! [UserProfile]
let blackList: [UserProfile] = applicationContext["BlackList"] as! [UserProfile]
let friendList: [UserProfile] = applicationContext["FriendList"] as! [UserProfile]
Hope anyone can help me with this Problem.
Step 1: You need to have NSCoding properly working.
import Foundation
class UserProfile : NSObject, NSCoding {
/// The name of the activity.
var name: String
/**
The constructor
:param: name The name of the user.
*/
init(name: String, start: Int, end: Int) {
self.name = name
}
required init(coder aDecoder: NSCoder) {
name = aDecoder.decodeObjectForKey("Name") as! String
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(name, forKey: "Name")
}
}
Step 2: Set the function that will be receiving your data:
func session(session: WCSession, didReceiveMessageData messageData: NSData, replyHandler: (NSData) -> Void) {
// Set the same class to avoid the name change for every target.
NSKeyedUnarchiver.setClass(UserProfile.self, forClassName: "UserProfile")
// Unarchive the activity object passed from the paired device.
guard let data = NSKeyedUnarchiver.unarchiveObjectWithData(messageData) else {
return
}
let userProfiles = data as! [UserProfile]
//Send the replyHandler you might need
let response: NSData = //...
replyHandler(response)
}
}
Step 3: Set the function that will be sending your data:
let userProfiles: [UserProfile] = //some of your UserProfiles...
// Set the same class to avoid the name change for every target.
NSKeyedArchiver.setClassName("UserProfile", forClass: UserProfile.self)
// Archive the object to NSData.
let data = NSKeyedArchiver.archivedDataWithRootObject(userProfiles)
session.sendMessageData(data, replyHandler: { (data) -> Void in
// handle the response from the device
}) { (error) -> Void in
print("error: \(error.localizedDescription)")
}
You are allowed to use any types that are allowed in property lists:
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/PropertyLists/AboutPropertyLists/AboutPropertyLists.html
As NSString is one of those data types, you can use anything that can be serialized into Strings.
Example: you could serialize your objects into a JSON String, send it to the watch as a String and use JSON to create your objects from it. That's the way I chose.
UserProfile objects friendList, blackList, users are not serialised yet, and cannot be directly send to Apple Watch.
You can convert them to dictionaries before sending them to the Apple Watch.
Better to use UserDefaults if Watch is not updating the ArrayList of the user :-
You can share your "ArrayListObj" by enabling the Capabilites of group on both application and watch target and sharing by the Userdefaults among the Targets(iPhone and Watch).
//iPhone sharing Userinfo
func sharedUserInfo() {
if let userDefaults = UserDefaults(suiteName: "group.watch.app.com" ) {
userDefaults.set( UserProfileObj as AnyObject, forKey: "UserInfo")
userDefaults.synchronize()
}
}
//Watch extracting the info
func sharedInfo() {
if let userDefaults = UserDefaults(suiteName: "group.watch.app.com") {
let userInfo = userDefaults.string(forKey: "UserInfo")
}
}

How to maintain UICollectionView Data Cache after app close and open using Swift?

In my scenario, I am trying to maintain cache data for UICollectionView. Here, VC2 to VC1 I am passing array data and VC1 I am loading passed data into UICollectionView. Now, If I close and reopen app then I can’t able to see UICollectionView data Its all removed but I have to maintain cache. How to do it?
Collection View Data Load From VC2 passed array
func pass(data: [TeamListData]) {
print("ARRAY DATA RECEIVED:\(data)")
participantsData = data
self.collectionView.reloadData()
}
My Array Data
ARRAY DATA RECEIVED:[TeamListData(userid: Optional("1"), firstname: Optional(“abc”), designation: Optional("Analyst"), profileimage: Optional(“url.jpg"), isSelected: true), TeamListData(userid: Optional(“2”), firstname: Optional(“def”), designation: Optional("Executive"), profileimage: Optional(“url.jpg"), isSelected: true)]
Saved your data after getting callback in VC1
Code
func pass(data: [TeamListData]) {
print("ARRAY DATA RECEIVED:\(data)")
participantsData = data
self.collectionView.reloadData()
UserDefaults.standard.setValue( try? PropertyListEncoder().encode(data), forKey: "sessiondata")
}
Inside viewDidLoad in VC1
func storeValidaion(){
// Retrive Array Values
if participantsData == nil {
if let data = UserDefaults.standard.value(forKey:"sessiondata") as? Data {
guard let sessionData = try? PropertyListDecoder().decode(Array<TeamListData>.self, from: data) else {
return
}
print("ARRAY VALUES: \(sessionData)")
self.participantsData = sessionData
self.collectionView.reloadData()
}
}
}
You're not looking for a cache, but for a persistent storage. Depending on where your data is coming from and how good solution you need, you can either use the disk, UserDefaults or a database approach such as CoreData, Realm or others.
There's a handy tutorial with a lot of code here for storing custom objects in UserDefaults with NSCoding: https://developer.apple.com/library/archive/referencelibrary/GettingStarted/DevelopiOSAppsSwift/PersistData.html
E.g.
Conforming to the NSCoding:
struct PropertyKey {
static let name = "name"
static let photo = "photo"
static let rating = "rating"
}
class Meal: NSObject, NSCoding {
let name: String
let photo: UIImage
let rating: Int
required convenience init?(coder aDecoder: NSCoder) {
// The name is required. If we cannot decode a name string, the initializer should fail.
guard let name = aDecoder.decodeObject(forKey: PropertyKey.name) as? String else {
return nil
}
// Because photo is an optional property of Meal, just use conditional cast.
let photo = aDecoder.decodeObject(forKey: PropertyKey.photo) as? UIImage
let rating = aDecoder.decodeInteger(forKey: PropertyKey.rating)
// Must call designated initializer.
self.init(name: name, photo: photo, rating: rating)
}
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: PropertyKey.name)
aCoder.encode(photo, forKey: PropertyKey.photo)
aCoder.encode(rating, forKey: PropertyKey.rating)
}
}
Saving data:
private func saveMeals() {
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(meals, toFile: Meal.ArchiveURL.path)
if isSuccessfulSave {
os_log("Meals successfully saved.", log: OSLog.default, type: .debug)
} else {
os_log("Failed to save meals...", log: OSLog.default, type: .error)
}
}
Loading data:
private func loadMeals() -> [Meal]? {
return NSKeyedUnarchiver.unarchiveObject(withFile: Meal.ArchiveURL.path) as? [Meal]
}
Realm on the other hand offers a lot of flexibility for a bit more time investment in learning a third party lib: https://realm.io/docs/swift/latest/#models

Resources