Saving array of [String: Any] to user default crash occasionally? - swift

I try to save an array of [String: Any] to user default, and for some situations it works, but others do not. I use the following to save to the default:
static func savingQueueToDisk(){
let queueDict = App.delegate?.queue?.map({ (track) -> [String: Any] in
return track.dict
})
if let queueDict = queueDict{
UserDefaults.standard.set(queueDict, forKey: App.UserDefaultKey.queue)
UserDefaults.standard.synchronize()
}
}
Queue is an array of Track, which is defined as follows:
class Track {
var dict: [String: Any]!
init(dict: [String: Any]) {
self.dict = dict
}
var album: Album?{
guard let albumDict = self.dict[AlbumKey] as? [String: Any] else{
return nil
}
return Album(dict: albumDict)
}
var artists: [Artist]?{
guard let artistsDict = self.dict[ArtistsKey] as? [[String: Any]] else{
return nil
}
let artists = artistsDict.map { (artistdict) -> Artist in
return Artist(dict: artistdict)
}
return artists
}
var id: String!{
return self.dict[IdKey] as! String
}
var name: String?{
return self.dict[NameKey] as? String
}
var uri: String?{
return self.dict[URIKey] as? String
}
}
I got different output when retrieving from the same API
Crashing output:
http://www.jsoneditoronline.org/?id=cb45af75a79aff64995e01e5efc0e7b6
Valid output:
http://www.jsoneditoronline.org/?id=0939823a4ac261bd4cb088663c092b20

It turns out it's not safe to just store an array of [String: Any] to the user defaults directly, and it might break based on the data it contains, and hence complaining about can't set none-property-list to user defaults. I solve this by first convert the array of [String: Any] to Data using JSONSerializationand now it can be saved correctly.
Solution:
//saving current queue in the app delegate to disk
static func savingQueueToDisk(){
if let queue = App.delegate?.queue{
let queueDict = queue.map({ (track) -> [String: Any] in
return track.dict
})
if let data = try? JSONSerialization.data(withJSONObject: queueDict, options: []){
UserDefaults.standard.set(data, forKey: App.UserDefaultKey.queue)
UserDefaults.standard.synchronize()
}else{
print("data invalid")
}
}
}
//retriving queue form disk
static func retrivingQueueFromDisk() -> [Track]?{
if let queueData = UserDefaults.standard.value(forKey: App.UserDefaultKey.queue) as? Data{
guard let jsonObject = try? JSONSerialization.jsonObject(with: queueData, options: []) else{
return nil
}
guard let queueDicts = jsonObject as? [[String: Any]] else{
return nil
}
let tracks = queueDicts.map({ (trackDict) -> Track in
return Track(dict: trackDict)
})
return tracks
}
return nil
}

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
}

Google maps API Swift; Nil maneuver response

I am trying to get the maneuver data for the given route from Google Maps' API. Running the code gives me nil values.
Here is the code I am running to get the maneuver data:
func getRouteSteps(source: CLLocationCoordinate2D,destination: CLLocationCoordinate2D) {
let session = URLSession.shared
let url = URL(string: "https://maps.googleapis.com/maps/api/directions/json?origin=\(source.latitude),\(source.longitude)&destination=\(destination.latitude),\(destination.longitude)&sensor=false&mode=driving&key=\(APIKey)")!
let task = session.dataTask(with: url, completionHandler: {
(data, response, error) in
guard error == nil else {
print(error!.localizedDescription)
return
}
guard let jsonResult = try? JSONSerialization.jsonObject(with: data!, options: .allowFragments) as? [String: Any] else {
print("error in JSONSerialization")
return
}
guard let routes = jsonResult["routes"] as? [Any] else { return }
guard let route = routes[0] as? [String: Any] else { return }
guard let legs = route["legs"] as? [Any] else { return }
guard let leg = legs[0] as? [String: Any] else { return }
guard let steps = leg["steps"] as? [Any] else { return }
guard let duration = leg["duration"] as? [String: Any] else { return }
guard let distance = leg["distance"] as? [String: Any] else { return }
RouteData.append(RouteInfo(Time: String(describing: duration["text"]! as Any), Distance: String(describing: distance["text"]! as Any)))
for item in steps {
guard let step = item as? [String: Any] else { return }
guard let stepTurns = step["html_instructions"] as? String else { return }
guard let stepDistance = step["distance"] as? [String: Any] else { return }
guard let stepTime = step["duration"] as? [String: Any] else { return }
guard let polyline = step["polyline"] as? [String: Any] else { return }
guard let polyLineString = polyline["points"] as? String else { return }
guard let maneuver = step["maneuver"] as? Any else { return }
print(maneuver)
Here are the results:
nil
nil
Optional(turn-right)
Optional(turn-left)
Optional(ramp-left)
Optional(ramp-right)
Optional(straight)
Optional(turn-slight-left)
Optional(turn-slight-left)
Optional(turn-left)

Swift - Remove key and values from dictionary [String : Any]

I am trying to removed block users from a dictionary [String : Any] that I am grabbing from the database. At first I grab the list of UID's that the current user has blocked:
var updatedBlockList: Any?
func findBlockUsers() {
// find current users list of blocked users
guard let currentUserUid = Auth.auth().currentUser?.uid else { return }
let blockedUsers = Database.database().reference().child("users").child(currentUserUid)
blockedUsers.observeSingleEvent(of: .value, with: { (snapshot) in
guard let userIdsDictionary = snapshot.value as? [String: Any] else { return }
userIdsDictionary.forEach({ (key, value) in
guard let userDictionary = value as? [String: Any] else { return }
var blockedList : Any
blockedList = userDictionary.keys
print(blockedList)
self.updateBlockList(blockedList: blockedList)
})
})
}
func updateBlockList(blockedList: Any) {
updatedBlockList = blockedList
print(updatedBlockList)
}
If I print updatedBlockList I get: ["gdqzOXPWaiaTn93YMJBEv51UUUn1", "RPwVj59w8pRFLf55VZ6LGX6G2ek2", "xmigo8CPzhNLlXN4oTHMpGo7f213"]
I now want to take those UID's (which will be the key in UserIdsDictionary and remove them after I pull ALL the users:
fileprivate func fetchAllUserIds() {
let ref = Database.database().reference().child("users")
ref.observeSingleEvent(of: .value, with: { (snapshot) in
guard let userIdsDictionary = snapshot.value as? [String: Any] else { return }
userIdsDictionary.forEach({ (key, value) in
// attempting to remove the blocked users here without any luck
var updatedKey = key as String?
updatedKey?.remove(at: self.updatedBlockList as! String.Index)
print(updatedKey!)
guard let userDictionary = value as? [String: Any] else { return }
let user = User(uid: key, dictionary: userDictionary)
self.fetchPostsWithUser(user: user)
})
}) { (err) in
print("Failed to fetch following user ids:", err)
}
}
I get this error when trying to remove: Could not cast value of type 'Swift.Dictionary.Keys' (0x1de2f6b78) to 'Swift.String.Index'
I'm sure i'm going about this the wrong way, but I know i'm close. The end goal is to take the blocked users UID's and remove them from the dictionary. Any help would be very much appreciated!!
Your forEach loop on userIdsDictionary is the wrong approach here so rather than trying to fix that code I would use a different approach and loop over the updatedBlockList
for item in updatedBlockList {
if let userID = item as? String {
userIdsDictionary.removeValue(forKey: userID)
}
}
For anyone wondering, here is the final changes that were made to make it work.
var updatedBlockList = [String]()
func findBlockUsers() {
// find current users list of blocked users
guard let currentUserUid = Auth.auth().currentUser?.uid else { return }
let blockedUsers = Database.database().reference().child("users").child(currentUserUid)
blockedUsers.observeSingleEvent(of: .value, with: { (snapshot) in
guard let userIdsDictionary = snapshot.value as? [String: Any] else { return }
userIdsDictionary.forEach({ (key, value) in
guard let userDictionary = value as? [String: Any] else { return }
let blockedList = Array(userDictionary.keys)
print(blockedList)
self.updateBlockList(blockedList: blockedList)
})
})
}
func updateBlockList(blockedList: [String]) {
updatedBlockList = blockedList
print(updatedBlockList)
}
fileprivate func fetchAllUserIds() {
let ref = Database.database().reference().child("users")
ref.observeSingleEvent(of: .value, with: { [weak self] (snapshot) in
guard var userIdsDictionary = snapshot.value as? [String: Any], let self = self else { return }
for item in self.updatedBlockList {
userIdsDictionary.removeValue(forKey: item)
}
userIdsDictionary.forEach({ (key, value) in
guard let userDictionary = value as? [String: Any] else { return }
let user = User(uid: key, dictionary: userDictionary)
self.fetchPostsWithUser(user: user)
})
}) { (err) in
print("Failed to fetch following user ids:", err)
}
}

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

Problems with getting values out of nested dictionary in Swift 3 and Xcode 8

I parse JSON with this :
let dictionary = try JSONSerialization.jsonObject(with: geocodingResultsData as Data, options: .mutableContainers)
and get the following nested dictionary as a result
{ response = { GeoObjectCollection = { featureMember =
(
{ GeoObject = { Point = { pos = "40.275713 59.943413"; }; }; },
{ GeoObject = { Point = { pos = "40.273162 59.944292"; }; }; }
);
};
};
}
I'm trying to get the values of coordinates out of this dictionary and save them into new latutudeString and longitudeString variables
Until Xcode 8 GM it worked for me with this code:
if let jsonCoordinatesString: String = dictionary["response"]!!["GeoObjectCollection"]!!["featureMember"]!![0]["GeoObject"]!!["Point"]!!["pos"]!! as? String {
var latLongArray = jsonCoordinatesString.components(separatedBy: " ")
let latitudeString = latLongArray[1]
let longitudeString = latLongArray[0]
}
But since I've installed Xcode 8 GM i receive an error:
Type Any has no Subscript members
How to fix it it Swift 3 with Xcode 8 ? I've read that I can cast it but don't know exactly how to make it work with my nested dictionary in swift 3 with the latest Xcode. I've read can't resolve "Ambiguous use of subscript" but it really did not helped me in my case.
Your JSON data has this type in Swift:
[String: [String: [String: [[String: [String: [String: String]]]]]]]
I would avoid using such a too deeply nested type, but you can write something like this, if you dare use it:
enum MyError: Error {
case invalidStructure
}
do {
guard let dictionary = try JSONSerialization.jsonObject(with: geocodingResultsData as Data, options: .mutableContainers) as? [String: [String: [String: [[String: [String: [String: String]]]]]]] else {
throw MyError.invalidStructure
}
if let jsonCoordinatesString: String = dictionary["response"]?["GeoObjectCollection"]?["featureMember"]?[0]["GeoObject"]?["Point"]?["pos"] {
var latLongArray = jsonCoordinatesString.components(separatedBy: " ")
let latitudeString = latLongArray[1]
let longitudeString = latLongArray[0]
}
} catch let error {
print(error)
}
But you may be hiding some irrelevant members of the JSON data, which might break this as? conversion.
So, you can go step by step, in some cases "need to" in Swift 3, like this:
do {
guard let dictionary = try JSONSerialization.jsonObject(with: geocodingResultsData as Data, options: .mutableContainers) as? [String: AnyObject] else {
throw MyError.invalidStructure
}
if
let response = dictionary["response"] as? [String: AnyObject],
let geoObjectCollection = response["GeoObjectCollection"] as? [String: AnyObject],
let featureMember = geoObjectCollection["featureMember"] as? [[String: AnyObject]],
!featureMember.isEmpty,
let geoObject = featureMember[0]["GeoObject"] as? [String: AnyObject],
let point = geoObject["Point"] as? [String: AnyObject],
let jsonCoordinatesString = point["pos"] as? String
{
var latLongArray = jsonCoordinatesString.components(separatedBy: " ")
let latitudeString = latLongArray[1]
let longitudeString = latLongArray[0]
}
} catch let error {
print(error)
}
(lets are mandatory for each Optional-bindings in Swift 3. And you can change all AnyObjects to Anys, if you prefer.)
The problem is that you have not specify the type of dictionary object, you need to explicitly specify the type of dictionary object as Dictionary like this way.
let dictionary = try JSONSerialization.jsonObject(with: geocodingResultsData as Data, options: .mutableContainers) as! [String: Any]
if let response = dictionary["response"] as? [String: Any],
let geoObjectCollection = response["GeoObjectCollection"] as? [String: Any],
let featureMember = geoObjectCollection["featureMember"] as? [[String: Any]] {
if let geoObject = featureMember[0]["GeoObject"] as? [String: Any],
let point = geoObject["Point"] as? [String: String] {
let latLongArray = point["pos"].components(separatedBy: " ")
let latitudeString = latLongArray[1]
let longitudeString = latLongArray[0]
}
}