Since upgrading to Swift 4.2 I've found that many of the NSKeyedUnarchiver and NSKeyedArchiver methods have been deprecated and we must now use the type method static func unarchivedObject<DecodedObjectType>(ofClass: DecodedObjectType.Type, from: Data) -> DecodedObjectType? to unarchive data.
I have managed to successfully archive an Array of my bespoke class WidgetData, which is an NSObject subclass:
private static func archiveWidgetDataArray(widgetDataArray : [WidgetData]) -> NSData {
guard let data = try? NSKeyedArchiver.archivedData(withRootObject: widgetDataArray as Array, requiringSecureCoding: false) as NSData
else { fatalError("Can't encode data") }
return data
}
The problem comes when I try to unarchive this data:
static func loadWidgetDataArray() -> [WidgetData]? {
if isKeyPresentInUserDefaults(key: USER_DEFAULTS_KEY_WIDGET_DATA) {
if let unarchivedObject = UserDefaults.standard.object(forKey: USER_DEFAULTS_KEY_WIDGET_DATA) as? Data {
//THIS FUNCTION HAS NOW BEEN DEPRECATED:
//return NSKeyedUnarchiver.unarchiveObject(with: unarchivedObject as Data) as? [WidgetData]
guard let nsArray = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSArray.self, from: unarchivedObject as Data) else {
fatalError("loadWidgetDataArray - Can't encode data")
}
guard let array = nsArray as? Array<WidgetData> else {
fatalError("loadWidgetDataArray - Can't get Array")
}
return array
}
}
return nil
}
But this fails, as using Array.self instead of NSArray.self is disallowed. What am I doing wrong and how can I fix this to unarchive my Array?
You can use unarchiveTopLevelObjectWithData(_:) to unarchive the data archived by archivedData(withRootObject:requiringSecureCoding:). (I believe this is not deprecated yet.)
But before showing some code, you should better:
Avoid using NSData, use Data instead
Avoid using try? which disposes error info useful for debugging
Remove all unneeded casts
Try this:
private static func archiveWidgetDataArray(widgetDataArray : [WidgetData]) -> Data {
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: widgetDataArray, requiringSecureCoding: false)
return data
} catch {
fatalError("Can't encode data: \(error)")
}
}
static func loadWidgetDataArray() -> [WidgetData]? {
guard
isKeyPresentInUserDefaults(key: USER_DEFAULTS_KEY_WIDGET_DATA), //<- Do you really need this line?
let unarchivedObject = UserDefaults.standard.data(forKey: USER_DEFAULTS_KEY_WIDGET_DATA)
else {
return nil
}
do {
guard let array = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(unarchivedObject) as? [WidgetData] else {
fatalError("loadWidgetDataArray - Can't get Array")
}
return array
} catch {
fatalError("loadWidgetDataArray - Can't encode data: \(error)")
}
}
But if you are making a new app, you should better consider using Codable.
unarchiveTopLevelObjectWithData(_:)
is deprecated as well. So to unarchive data without secure coding you need to:
Create NSKeyedUnarchiver with init(forReadingFrom: Data)
Set requiresSecureCoding of created unarchiver to false.
Call decodeObject(of: [AnyClass]?, forKey: String) -> Any? to get your object, just use proper class and NSKeyedArchiveRootObjectKeyas key.
As unarchiveTopLevelObjectWithData is also deprecated after iOS 14.3 only the Hopreeeenjust's answer is correct now.
But if you don't need NSSecureCoding you also can use answer of Maciej S
It is very easy to use it, by adding extension to NSCoding protocol:
extension NSCoding where Self: NSObject {
static func unsecureUnarchived(from data: Data) -> Self? {
do {
let unarchiver = try NSKeyedUnarchiver(forReadingFrom: data)
unarchiver.requiresSecureCoding = false
let obj = unarchiver.decodeObject(of: self, forKey: NSKeyedArchiveRootObjectKey)
if let error = unarchiver.error {
print("Error:\(error)")
}
return obj
} catch {
print("Error:\(error)")
}
return nil
}
}
With this extension to unarchive e.g. NSArray you only need:
let myArray = NSArray.unsecureUnarchived(from: data)
For Objective C use NSObject category:
+ (instancetype)unsecureUnarchivedFromData:(NSData *)data {
NSError * err = nil;
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData: data error: &err];
unarchiver.requiresSecureCoding = NO;
id res = [unarchiver decodeObjectOfClass:self forKey:NSKeyedArchiveRootObjectKey];
err = err ?: unarchiver.error;
if (err != nil) {
NSLog(#"NSKeyedUnarchiver unarchivedObject error: %#", err);
}
return res;
}
Note that if the requiresSecureCoding is false, class of unarchived object is not actually checked and objective c code returns valid result even if it is called from wrong class.
And swift code when called from wrong class returns nil (because of optional casting), but without error.
Swift 5- IOS 13
guard let mainData = UserDefaults.standard.object(forKey: "eventDetail") as? NSData
else {
print(" data not found in UserDefaults")
return
}
do {
guard let finalArray =
try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(mainData as Data) as? [EventDetail]
else {
return
}
self.eventDetail = finalArray
}
You are likely looking for this:
if let widgetsData = UserDefaults.standard.data(forKey: USER_DEFAULTS_KEY_WIDGET_DATA) {
if let widgets = (try? NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, WidgetData.self], from: widgetsData)) as? [WidgetData] {
// your code
}
}
if #available(iOS 12.0, *) {
guard let unarchivedFavorites = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(favoritesData!)
else {
return
}
self.channelFavorites = unarchivedFavorites as! [ChannelFavorite]
} else {
if let unarchivedFavorites = NSKeyedUnarchiver.unarchiveObject(with: favoritesData!) as? [ChannelFavorite] {
self.channelFavorites = unarchivedFavorites
}
// Achieving data
if #available(iOS 12.0, *) {
// use iOS 12-only feature
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: channelFavorites, requiringSecureCoding: false)
UserDefaults.standard.set(data, forKey: "channelFavorites")
} catch {
return
}
} else {
// handle older versions
let data = NSKeyedArchiver.archivedData(withRootObject: channelFavorites)
UserDefaults.standard.set(data, forKey: "channelFavorites")
}
This is the way I have updated my code and its working for me
Related
I would like to share data between my Main Project and my Share Extension. This is what I did:
1. enable App Groups in both Project & Share Extension
2. save data in Project inside viewDidLoad (works fine, I tested it):
DataHandler.getWishlists { (success, dataArray, dropOptionsArray) in
if success && dataArray != nil {
self.shouldAnimateCells = true
self.dataSourceArray = dataArray as! [Wishlist]
self.theCollectionView.isHidden = false
self.theCollectionView.reloadData()
self.dropOptions = dropOptionsArray as! [DropDownOption]
self.addButton.isEnabled = true
self.activityIndicator.stopAnimating()
// save dataSourceArray in UserDefaults
if let defaults = UserDefaults(suiteName: UserDefaults.Keys.groupKey) {
defaults.setDataSourceArray(data: dataArray as! [Wishlist])
defaults.synchronize()
} else {
print("error Main")
}
}
}
3. retrive data in Share Extension (error 2 fires!)
if let defaults = UserDefaults(suiteName: UserDefaults.Keys.groupKey) {
if let data = defaults.getDataSourceArray() {
dataSourceArray = data
defaults.synchronize()
}else {
print("error 2")
}
} else {
print("error 1")
}
UserDefaults + Helpers
extension UserDefaults {
public struct Keys {
public static let groupKey = "group.wishlists-app.wishlists"
public static let dataSourceKey = "dataSourceKey"
}
func setDataSourceArray(data: [Wishlist]){
set(try? PropertyListEncoder().encode(data), forKey: Keys.dataSourceKey)
synchronize()
}
func getDataSourceArray() -> [Wishlist]? {
if let data = UserDefaults.standard.value(forKey: Keys.dataSourceKey) as? Data {
if let dataSourceArray = try? PropertyListDecoder().decode(Array<Wishlist>.self, from: data) as [Wishlist] {
return dataSourceArray
}
}
return nil
}
}
I can not retrieve the data inside my Share Extension but I have no idea why. Could anyone help me out here?
Your helper function getDataSourceArray() tries to access UserDefaults.standard which is not shared between your host app and the extension app. You need to use the shared container.
UserDefaults.standard -> not shared between host and extension
UserDefaults(suiteName:) -> shared between host and extension
Try to change your function to this:
func getDataSourceArray() - > [Wishlist] ? {
if let data = UserDefaults(suiteName: UserDefaults.Keys.groupKey).value(forKey: Keys.dataSourceKey) as ? Data {
if let dataSourceArray =
try ? PropertyListDecoder().decode(Array < Wishlist > .self, from: data) as[Wishlist] {
return dataSourceArray
}
}
return nil
}
I'm trying to decode an array of URL objects using NSKeyedUnarchiver. Here is the code:
let urlArray: [URL] = [URL(string: "https://apple.com")!,
URL(string: "https://google.com")!]
do {
let archivedUrls = try NSKeyedArchiver.archivedData(withRootObject: urlArray, requiringSecureCoding: false)
let _ = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSArray.self, from: archivedUrls)
} catch {
print(error)
}
I get the following error:
Error Domain=NSCocoaErrorDomain Code=4864 "value for key 'NS.objects' was of unexpected class 'NSURL'. Allowed classes are '{(
NSArray
)}'." UserInfo={NSDebugDescription=value for key 'NS.objects' was of unexpected class 'NSURL'. Allowed classes are '{(
NSArray
)}'.}
If I replace let _ = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSArray.self, from: archivedUrls) by let _ = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, NSURL.self], from: archivedUrls), then it works. But that means it can decode either an NSArray or NSURL object, not an NSArray containing NSURL objects.
If I change the array to be an array of String instead, everything works fine:
let stringArray: [String] = ["string", "string2"]
do {
let archivedStrings = try NSKeyedArchiver.archivedData(withRootObject: stringArray, requiringSecureCoding: false)
let _ = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSArray.self, from: archivedStrings)
} catch {
print(error)
}
Does anyone have an explanation for that behaviour?
If you do not require Secure Coding (requiringSecureCoding: false), you can use unarchiveTopLevelObjectWithData(_:).
do {
let archivedUrls = try NSKeyedArchiver.archivedData(withRootObject: urlArray, requiringSecureCoding: false)
if let urls = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(archivedUrls) as? [URL] {
print(urls)
} else {
print("not URLs")
}
} catch {
print(error)
}
Or you can specify the types included in the archive using unarchivedObject(ofClasses:from:).
do {
let archivedUrls = try NSKeyedArchiver.archivedData(withRootObject: urlArray, requiringSecureCoding: true)
if let urls = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSArray.self, NSURL.self], from: archivedUrls) as? [URL] {
print(urls)
} else {
print("not URLs")
}
} catch {
print(error)
}
NSString seems to be an exception for this rule.
I'm still new to Swift and i'm trying archive and unarchive an array of UIColours to NSUserDefaults. I'm aware that in ios 12 i need to use unarchivedObject(ofClass:from:) - but i'm not sure how to use that.
I've tried to follow this question: Unarchive Array with NSKeyedUnarchiver unarchivedObject(ofClass:from:)
but i think i'm doing something wrong.
Here is the code i am trying:
let faveColoursArray = [colour1, colour2]
private func archiveColours() -> Data {
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: faveColoursArray, requiringSecureCoding: false)
return data
} catch {
fatalError("can't encode data.")
}
}
func loadColours() -> [UIColor]? {
guard let unarchivedObject = UserDefaults.standard.data(forKey: "faveColours") else {
return nil
}
do {
guard let array = try NSKeyedUnarchiver.unarchivedObject(ofClass: UIColor.self, from: unarchivedObject) else {
fatalError("Can't load colours.")
}
return array
} catch {
fatalError("Can't load colours.")
}
}
Thankyou
You can use unarchiveTopLevelObjectWithData(_:):
func loadColours() -> [UIColor]? {
guard let unarchivedObject = UserDefaults.standard.data(forKey: "faveColours") else {
return nil
}
do {
guard let array = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(unarchivedObject) as? [UIColor] else {
fatalError("Can't load colours.")
}
return array
} catch {
fatalError("Can't load colours.")
}
}
in my iOS project I need to save an entire JSON as user data and then reload it on next app launch.
Squashing it into many values and then recreate the JSON is not an option, I just need some serializable way of saving the entire raw JSON.
I tried to convert it to String by doing json.rawString() and recreate it by passing the obtained string to JSON(string), but it doesn't work.
I'm both astonished by the difficulty of making such a simple thing and by the lack of informations about a thing like this online, so I can not wait to discover how to do that :)
Example:
public func saveJSON(j: JSON) {
let defaults = NSUserDefaults.standardUserDefaults()
defaults.setValue(j.rawString()!, forKey: "json")
// here I save my JSON as a string
}
public func loadJSON() -> JSON {
let defaults = NSUserDefaults.standardUserDefaults()
return JSON(defaults.valueForKey("json") as! String))
// here the returned value doesn't contain a valid JSON
}
Thank you for your answers but they didn't solve my problem. I finally found the solution, which was really simple in facts:
public func loadJSON() -> JSON {
let defaults = NSUserDefaults.standardUserDefaults()
return JSON.parse(defaults.valueForKey("json") as! String))
// JSON from string must be initialized using .parse()
}
Really simple but not documented well.
Swift 5+
func saveJSON(json: JSON, key:String){
if let jsonString = json.rawString() {
UserDefaults.standard.setValue(jsonString, forKey: key)
}
}
func getJSON(_ key: String)-> JSON? {
var p = ""
if let result = UserDefaults.standard.string(forKey: key) {
p = result
}
if p != "" {
if let json = p.data(using: String.Encoding.utf8, allowLossyConversion: false) {
do {
return try JSON(data: json)
} catch {
return nil
}
} else {
return nil
}
} else {
return nil
}
}
Use this if you using SwiftyJSON.
I used the following code and it works like a charm!
NSString *json = #"{\"person\":{\"first_name\":\"Jim\", \"last_name\":\"Bell\"}} ";
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
if([defaults objectForKey:#"json"]== nil){
[defaults setObject:json forKey:#"json"];
//[defaults synchronize];
}
else{
NSLog(#"JSON %#", [defaults objectForKey:#"json"]);
}
First try to see whether you can save a hard-coded string to the NSUserDefaults first.
Also try to call a [defaults synchronize]; call when you want to save the data. Although that is NOT required, it might be needed in extreme conditions such as if the app is about to terminate.
to retrieve from UserDefaults
func get(_ key: String)-> JSON? {
if let standard = UserDefaults.standard.data(forKey: key), let data = try? standard.toData() {
return JSON(data)
} else {
return nil
}
}
You should parse everything to Data, in order to save model (Better from JSON / JSONSerialization) to UserDefaults
Coded In Swift 5.x
Swift 4+
A cleaner version to the one provided by Alfi up above, for any else that might need this.
func addUserJSONDataToUserDefaults(userData: JSON) {
guard let jsonString = userData.rawString() else { return }
userDefaults.set(jsonString, forKey: "user")
}
func getCachedUserJSONData() -> JSON? {
let jsonString = userDefaults.string(forKey: "user") ?? ""
guard let jsonData = jsonString.data(using: .utf8, allowLossyConversion: false) else { return nil }
return try? JSON(data: jsonData)
}
Here's a swift example that works
import SwiftyJSON
class Users{
init(){
let yourJSON = {"name":"Deeznuts"}
let response = JSON(yourJSON)
// Store your
let httpMessage = response["name"].stringValue
}
}
I extended Userdefaults and added a new var for easy usage and consistency of my keys.
Here is my code:
extension UserDefaults {
var downloadedMarkersInfo : JSON? {
get {
if let jsonString = defaults.value(forKey: "markers") as? String {
if let json = jsonString.data(using: String.Encoding.utf8, allowLossyConversion: false) {
return try! JSON(data: json)
}
}
return nil
}
set {
if let json = newValue {
let jsonString = json.rawString()!
defaults.setValue(jsonString, forKey: "markers")
}
}
}
}
The usage in my View Controller:
if let jsonData = defaults.downloadedMarkersInfo {
// Your code here.
}
using SwiftyJSON - SWIFT 5
var data = JSON()
if(CustomDefaults().checkObject(key: "list2")){
data = JSON.init(parseJSON: CustomDefaults().getObject(key: "list2") as? String ?? "")
}
else{
var bomb = [JSON]()
bomb.append(["name":"Happy","url":"google.com"])
let finalData = JSON(bomb).rawString() ?? "" //data.rawString() ?? ""
CustomDefaults().setObject(value: finalData, key: "list2")
}
The following code throws a message which says "Initializer for conditional binding must have Optional type, not 'AnyObject'"
func parseData2(){
var data:NSData?
if let data2 = data {
do {
let details = try NSJSONSerialization.JSONObjectWithData(data2, options: .AllowFragments)
if let actualDetails = details where actualDetails.isKindOfClass(NSDictionary) {
print("Parse Data")
}
}catch {
print("Error \(error)")
}
}
}
To resolve the above error I used the following code.
func parseData2(){
var data:NSData?
if let data2 = data {
do {
let details:AnyObject = try NSJSONSerialization.JSONObjectWithData(data2, options: .AllowFragments)
if let actualDetails:AnyObject = details where actualDetails.isKindOfClass(NSDictionary) {
print("Parse Data")
}
}catch {
print("Error \(error)")
}
}
}
Is there any better approach then the above or my code might crash?
There is one more code which I want to add considering nil check,type check and then type cast check. The reason behind that Swift offers great flexibility but litle bit difficult to fix issues. Let's say I have a dictionary, cityDetails and I am trying to get data for self.cityZipCode and self.cityIdentifier, which are optional, defined as var cityZipCode:Int? and var cityIdentifier:Int?
if let cityBasic = cityDetails["basicDetails"] where
cityBasic!.isKindOfClass(NSDictionary) {
self.cityZipCode = (cityBasic as! NSDictionary)["zip"].integerValue ?? 0
self.cityIdentifier = (cityBasic as! NSDictionary)["cityId"].integerValue ?? 0
}
No need to unwrap the result from try. It is not an optional. You do need to cast the result from try to an NSDictionary. Use as? to downcast it.
Best practice: full access to returned error for good error handling
func parseData2(){
var data:NSData?
if let data2 = data {
do {
let details = try NSJSONSerialization.JSONObjectWithData(data2, options: .AllowFragments)
if let detailsDict = details as? NSDictionary {
print("Parse Data")
} else if let detailsArray = details as? NSArray {
print("array")
}
} catch {
print("Error \(error)")
}
}
}
Quick and dirty: error handling is not for me!
func parseData2(){
var data:NSData?
if let data2 = data {
let details = try? NSJSONSerialization.JSONObjectWithData(data2, options: .AllowFragments)
if let detailsDict = details as? NSDictionary {
print("Parse Data")
} else {
print("details might be nil, or not an NSDictionary")
}
}
}
Bad Ass Mode: crashes are features
func parseData2(){
var data:NSData?
if let data2 = data {
let details = try! NSJSONSerialization.JSONObjectWithData(data2, options: .AllowFragments) as! NSDictionary
}
}
Some extra info on multiple unwraps :
Drop the code below in a playground.
struct SomeStruct {
var anOptional : Int?
init() {
}
}
func unwrapWithIfLet() {
if let unWrappedStruct = myStruct, let unWrappedSomething = unWrappedStruct.anOptional {
print("multiple optional bindings succeeded")
// both unWrappedStruct and unWrappedSomething are available here
} else {
print("something is nil")
}
}
func unwrapWithGuard() {
guard let unWrappedStruct = myStruct, let unWrappedSomething = unWrappedStruct.anOptional else {
print("something is nil")
return
}
print("multiple optional bindings succeeded")
// both unWrappedStruct and unWrappedSomething are available here
}
var myStruct : SomeStruct?
//unwrapWithGuard()
//unwrapWithIfLet()
myStruct = SomeStruct()
myStruct!.anOptional = 1
unwrapWithGuard()
unwrapWithIfLet()
You are looking for as?, which attempts to convert the thing on the left to the type on the right, and returns nil if the conversion is not possible:
let details = try NSJSONSerialization.JSONObjectWithData(data2, options: .AllowFragments)
if let actualDetails = details as? NSDictionary {
print("Parse Data")
}
You rarely need to use isKindOfClass in Swift. If you find yourself using it, ask why, and consider whether as or as? will work instead.