Saving Firebase snapshot array to NSUserDefaults - swift

I am using Swift to retrieve data from my Firebase database. When the user first logs in, I'd like to save the 'places' from my Firebase snapshot as a UserDefault.
static func getAllPlaces(){
databaseRef = Database.database().reference()
databaseRef.database.reference().child("places").observe(.childAdded) { (snapshot: DataSnapshot) in
if let value = snapshot.value as? NSDictionary {
let place = Place()
let id = value["id"] as? String ?? "ID not found"
let title = value["title"] as? String ?? "Title not found"
let type = value["type"] as? String ?? ""
place.id = id
place.title = title
place.type = type
DispatchQueue.global().async {
// Something here to append place data to UserDefaults?
places.append(place) // appends to NSObject for later use
}
}
}
}
The current code works fine - I just need to add something to get it stored so I can grab it later.
Bonus question: I am storing a good few hundred snapshots in the Firebase database. The reason I want to store them on the device is so that the user doesn't have to keep downloading the data. Is this a good idea? Would it take up a lot of memory?
Any help would be appreciated.

One way to save custom classes/data to UserDefaults is to encode them like this:
let encodedData: Data = NSKeyedArchiver.archivedData(withRootObject: place)
UserDefaults.standard.set(encodedData, forKey: "place")
UserDefaults.standard.synchronize()
Then you can decode it like this:
if UserDefaults.standard.object(forKey: "place") != nil{
let decodedData = UserDefaults.standard.object(forKey: "place") as! Data
let decodedPlace = NSKeyedUnarchiver.unarchiveObject(with: decodedData) as! Place
}
Updated for swift 4 and iOS 12:
do {
let encodedData: Data = try NSKeyedArchiver.archivedData(withRootObject: place, requiringSecureCoding: false)
UserDefaults.standard.set(encodedData, forKey: "place")
UserDefaults.standard.synchronize()
} catch {
//Handle Error
}
do {
if UserDefaults.standard.object(forKey: "place") != nil{
let decodedData = UserDefaults.standard.object(forKey: "place") as! Data
if let decodedPlace = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(decodedData) as? Place {
//Do Something with decodedPlace
}
}
}
catch {
//Handle Error
}

Related

Saving nested dictionary into UserDefaults using Xcode and Swift

I'm trying to save a nested dictionary in userDefaults.
The app crashes when I try to save it the usual way, i.e.
defaults.set(totalBuy, forKey: "test")
and I get this error:
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object
And when I try convert it to NSData and then try retrieve it, it always comes back as nil.
Here is the code:
var buyData = [Int : String]()
var buyingTotal = [String : [Int: String]]()
var totalBuy = [Int : [String : [Int: String]]]()
let buyerDict = defaults.dictionary(forKey: "buyerDict")
let test = defaults.dictionary(forKey: "test")
func userDefaultSave(){
buyData[0] = value
buyData[1] = value
buyData[2] = value
buyData[3] = value
buyingTotal["skuu"] = buyData
totalBuy[0] = buyingTotal
let data: Data = NSKeyedArchiver.archivedData(withRootObject: totalBuy) /// converts to NS Data
defaults.set(data, forKey: "buyerDict")
defaults.set(totalBuy, forKey: "test")
if let dic = defaults.dictionary(forKey: "test") as? [Int : [String : [Int: String]]] {
print(dic)
}
let retrieved = defaults.object(forKey: "buyerDict") as! Data
let dictionary: Dictionary? = NSKeyedUnarchiver.unarchiveObject(with: retrieved) as? [String : Any]
print("dictionary--->", dictionary as Any)
}
Can anyone help me?
There are 2 ways you can get this working.
1. You can use JSONEncoder() and JSONDecoder() to get the data to and from the Dictionary object, i.e.
To get the data from totalBuy,
if let data = try? JSONEncoder().encode(totalBuy) {
defaults.set(data, forKey: "buyerDict")
}
To get the Dictionary from data,
if let data = defaults.data(forKey: "buyerDict"), let dict = try? JSONDecoder().decode([Int:[String:[Int:String]]].self, from: data) {
print(dict)
}
2. In case you still want to use NSKeyedArchiver and NSKeyedArchiver, here you go
To get the data from totalBuy,
let data = try? NSKeyedArchiver.archivedData(withRootObject: totalBuy)
defaults.set(data, forKey: "buyerDict")
To get the Dictionary from data,
if let data = defaults.data(forKey: "buyerDict") {
let dict = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)
print(dict)
}
Use data(forKey:) instead of object(forKey:) when retrieving the data from UserDefaults.
your dictionary need to change key Int to String Or convert dictionary to Data and retrieve it.
try this method to set and get your value from userdefault
let USERDEFAULT = UserDefaults.standard
class func setUserValueArchiver(value:Any, key :String) -> Void{
guard !(value is NSNull) else {
return
}
do{
let archive = try NSKeyedArchiver.archivedData(withRootObject: value, requiringSecureCoding: true)
USERDEFAULT.set(archive, forKey: key)
USERDEFAULT.synchronize()
}catch let error{
Log("Error Save in UserDefault \(error.localizedDescription)")
}
}
class func getUserUnArchiveData(key : String) -> Any?{
if let userdata = USERDEFAULT.object(forKey: key) as? Data{
do{
let unarchived = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(userdata)
return unarchived
}catch let error{
Log("Error At Get UserDATA :\(error.localizedDescription)")
}
}
return nil
}

Could not cast value of type __NSCFDictionary to NSData only when app is not fresh installed

When this code runs on a fresh app install, it works perfectly fine. However, when there is no data previously saved on the device, this function causes the app to crash.
I get the error Could not cast value of type __NSCFDictionary to NSData and it returns a thread zero error on the following line:
playlists = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(playlistsData as! Data) as! [String:[Song]]
Here is my full function code below:
func getPlaylists() -> [String:[Song]] {
var playlists: [String:[Song]] = [:]
let playlistsData = defaults.object(forKey: "user_playlists")
if playlistsData != nil {
playlists = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(playlistsData as! Data) as! [String:[Song]]
}
return playlists
}
To get the data safely change the method to
func getPlaylists() -> [String:[Song]] {
guard let playlistsData = defaults.data(forKey: "user_playlists"),
let playlists = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(playlistsData) as? [String:[Song]] else { return [:] }
return playlists
}
By the way the error occurs because you previously saved a dictionary to UserDefaults rather than Data.
You probably mean However, when there is data previously saved...
First time works as playlistsData is nil in the beginning
let playlistsData = defaults.object(forKey: "user_playlists")
>>>>>> hereis the crash >>>>> playlistsData as! Data
you should store the object in Data not as a Dictionary with
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: somethingToSave, requiringSecureCoding: false) else { return }
defaults.set(data,forKey:"user_playlists")

How to stop observe

I have this code for retrieving data from Firebase database. The code works fine, but it retrieves the same data multiple times.
How do I stop observe so that the data will only be called once
My code
self.eventGalleryImage.child("event")
.child(self.eventName.text!)
.child("EventImages")
.observeSingleEvent(of: .value, with: { (snapshot) in
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
for child in snapshots {
if let dict = child.value as? Dictionary<String, Any> {
if let userGallery = dict["Text"] as? String {
self.postsFindGallery.insert(galleryStruct(gallery: userGallery), at: 0)
self.collectionView.reloadData()
}
}
}
}
})
A lesson to be learnt. Do all you can to avoid using for loops when observing data. This is what you'll need to do to get all of the data inside that database path ONCE:
self.eventGalleryImage.child("event")
.child(self.eventName.text!)
.child("EventImages")
.observe(.childAdded, with: { (snapshot) in
guard let dictionary = snapshot.value as? [String: AnyObject] else { return }
if let userGallery = dictionary["Text"] as? String {
self.postsFindGallery.insert(galleryStruct(gallery: userGallery), at: 0)
self.collectionView.reloadData()
}
}, withCancel: nil)
You may also want to try:
DispatchQueue.main.async {
self.collectionView.reloadData()
}

Codable to CKRecord

I have several codable structs and I'd like to create a universal protocol to code them to CKRecord for CloudKit and decode back.
I have an extension for Encodable to create a dictionary:
extension Encodable {
var dictionary: [String: Any] {
return (try? JSONSerialization.jsonObject(with: JSONEncoder().encode(self), options: .allowFragments)) as? [String: Any] ?? [:]
}
}
Then in a protocol extension, I create the record as a property and I try to create a CKAsset if the type is Data.
var ckEncoded: CKRecord? {
// Convert self.id to CKRecord.name (CKRecordID)
guard let idString = self.id?.uuidString else { return nil }
let record = CKRecord(recordType: Self.entityType.rawValue,
recordID: CKRecordID(recordName: idString))
self.dictionary.forEach {
if let data = $0.value as? Data {
if let asset: CKAsset = try? ckAsset(from: data, id: idString) { record[$0.key] = asset }
} else {
record[$0.key] = $0.value as? CKRecordValue
}
}
return record
}
To decode:
func decode(_ ckRecord: CKRecord) throws {
let keyIntersection = Set(self.dtoEncoded.dictionary.keys).intersection(ckRecord.allKeys())
var dictionary: [String: Any?] = [:]
keyIntersection.forEach {
if let asset = ckRecord[$0] as? CKAsset {
dictionary[$0] = try? self.data(from: asset)
} else {
dictionary[$0] = ckRecord[$0]
}
}
guard let data = try? JSONSerialization.data(withJSONObject: dictionary) else { throw Errors.LocalData.isCorrupted }
guard let dto = try? JSONDecoder().decode(self.DTO, from: data) else { throw Errors.LocalData.isCorrupted }
do { try decode(dto) }
catch { throw error }
}
Everything works forth and back except the Data type. It can't be recognized from the dictionary. So, I can't convert it to CKAsset. Thank you in advance.
I have also found there is no clean support for this by Apple so far.
My solution has been to manually encode/decode: On my Codable subclass I added two methods:
/// Returns CKRecord
func ckRecord() -> CKRecord {
let record = CKRecord(recordType: "MyClassType")
record["title"] = title as CKRecordValue
record["color"] = color as CKRecordValue
return record
}
init(withRecord record: CKRecord) {
title = record["title"] as? String ?? ""
color = record["color"] as? String ?? kDefaultColor
}
Another solution for more complex cases is use some 3rd party lib, one I came across was: https://github.com/insidegui/CloudKitCodable
So I had this problem as well, and wasn't happy with any of the solutions. Then I found this, its somewhat helpful, doesn't handle partial decodes very well though https://github.com/ggirotto/NestedCloudkitCodable

Swift loading images from firebase

Does someone know why this is not working? The tableView is empty and not showing anything even though there are items in the database and storage. This worked fine before I implemented the loading of the images from storage which you will see at the bottom of this code that I have pasted in. The food.append() statement used to be outside the storageRef.getData() closure (since it didn't exist) however if I take it out now it won't be able to access recipeImage since it's declared within the closure. Is it not working because it's in the closure? If so how do I fix it?
let parentRef = Database.database().reference().child("Recipes")
let storage = Storage.storage()
parentRef.observe(.value, with: { snapshot in
//Processes values received from server
if ( snapshot.value is NSNull ) {
// DATA WAS NOT FOUND
print("– – – Data was not found – – –")
} else {
//Clears array so that it does not load duplicates
food = []
// DATA WAS FOUND
for user_child in (snapshot.children) {
let user_snap = user_child as! DataSnapshot
let dict = user_snap.value as! [String: String?]
//Defines variables for labels
let recipeName = dict["Name"] as? String
let recipeDescription = dict["Description"] as? String
let downloadURL = dict["Image"] as? String
let storageRef = storage.reference(forURL: downloadURL!)
storageRef.getData(maxSize: 1 * 1024 * 1024) { (data, error) -> Void in
let recipeImage = UIImage(data: data!)
food.append(Element(name: recipeName!, description: recipeDescription!, image: recipeImage!))
}
}
self.tableView.reloadData()
}
})
Move
self.tableView.reloadData()
after
food.append(Element(name: recipeName!, description: recipeDescription!, image: recipeImage!))