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.")
}
}
Related
I was using the code below to set an HKQueryAnchor when making a HKAnchoredObjectQuery however 'unarchiveObject(with:)' has been deprecated and I can't figure out how to write it with the new API?
private func getAnchor() -> HKQueryAnchor? {
let encoded = UserDefaults.standard.data(forKey: AnchorKey)
if(encoded == nil){
return nil
}
let anchor = NSKeyedUnarchiver.unarchiveObject(with: encoded!) as? HKQueryAnchor
return anchor
}
private func saveAnchor(anchor : HKQueryAnchor) {
let encoded = NSKeyedArchiver.archivedData(withRootObject: anchor)
defaults.setValue(encoded, forKey: AnchorKey)
defaults.synchronize()
}
This is what I came up with based on Martin R's link, look ok?
private func getAnchor() -> HKQueryAnchor? {
let encoded = UserDefaults.standard.data(forKey: AnchorKey)
guard let unwrappedEncoded = encoded else { return nil }
guard let anchor = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(unwrappedEncoded as Data) as? HKQueryAnchor
else {
return nil
}
return anchor
}
private func saveAnchor(anchor : HKQueryAnchor) {
do {
let encoded = try NSKeyedArchiver.archivedData(withRootObject: anchor, requiringSecureCoding: false)
defaults.setValue(encoded, forKey: AnchorKey)
defaults.synchronize()
} catch {
return
}
}
Try using JSONDecoder and JSONEncoder to get the data to and from HKQueryAnchor instance, i.e.
private func getAnchor() -> HKQueryAnchor? {
guard let encoded = UserDefaults.standard.data(forKey: AnchorKey) else {
return nil
}
let anchor = try? JSONDecoder().decode(HKQueryAnchor.self, from: encoded)
return anchor
}
private func saveAnchor(anchor : HKQueryAnchor) {
if let encoded = try? JSONEncoder().encode(anchor) {
defaults.setValue(encoded, forKey: AnchorKey)
defaults.synchronize()
}
}
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
}
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
I'm trying to save my json file and show it to offline. I'm trying this code but it is not working for me ..
let myData = NSKeyedArchiver.archivedData(withRootObject: self.data)
UserDefaults.standard.set(myData, forKey: "userJson")
UserDefaults.standard.synchronize()
Can any one suggest me better way to save data and show off line ?
You should not save JSON in the UserDefault, Instead save it in file in document directory
I have created generic class which allows to do it easily
//
// OfflineManager.swift
//
//
// Created by Prashant on 01/05/18.
// Copyright © 2018 Prashant. All rights reserved.
//
import UIKit
class OfflineManager: NSObject {
static let sharedManager = OfflineManager()
let LocalServiceCacheDownloadDir = "LocalData"
enum WSCacheKeys {
case CampignList
case CampignDetail(id:String)
case ScreenShotList
var value:String {
switch self {
case .CampignList:
return "CampignList"
case .CampignDetail(let id):
return id
case .ScreenShotList :
return "ScreenShotList"
}
}
}
func getBaseForCacheLocal(with fileName:String) -> String? {
let filePath = FileManager.default.getDocumentPath(forItemName: self.LocalServiceCacheDownloadDir)
if FileManager.default.directoryExists(atPath: filePath) {
return filePath.stringByAppendingPathComponent(fileName)
} else {
if FileManager.default.createDirectory(withFolderName: self.LocalServiceCacheDownloadDir) {
return filePath.stringByAppendingPathComponent(fileName)
}
}
return nil
}
//------------------------------------------------------------
#discardableResult
func cacheDataToLocal<T>(with Object:T,to key:WSCacheKeys) -> Bool {
let success = NSKeyedArchiver.archiveRootObject(Object, toFile: getBaseForCacheLocal(with: key.value)!)
if success {
print( "Local Data Cached\(String(describing: getBaseForCacheLocal(with: key.value)))")
} else {
print("Error")
}
return success
}
//------------------------------------------------------------
func loadCachedDataFromLocal<T>(with key:WSCacheKeys ) -> T? {
return NSKeyedUnarchiver.unarchiveObject(withFile: getBaseForCacheLocal(with: key.value)!) as? T
}
//------------------------------------------------------------
func removeAllCacheDirs () {
do {
try FileManager.default.removeItem(atPath: self.getBaseForCacheLocal(with: "")!)
} catch {
print("error in remove dir \(error.localizedDescription)")
}
}
//--------------------------------------------------------------------------------
}
Here is some helper methods of extension FileManager
public var getDocumentDirectoryPath: String {
let documentDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
return documentDirectory
}
public func getDocumentPath(forItemName name: String)-> String {
return getDocumentDirectoryPath.stringByAppendingPathComponent(name)
}
public func directoryExists(atPath filePath: String)-> Bool {
var isDir = ObjCBool(true)
return FileManager.default.fileExists(atPath: filePath, isDirectory: &isDir )
}
public func createDirectory(withFolderName name: String)-> Bool {
let finalPath = getDocumentDirectoryPath.stringByAppendingPathComponent(name)
return createDirectory(atPath: finalPath)
}
Here Is String extension's method
public func stringByAppendingPathComponent(_ path: String) -> String {
let fileUrl = URL.init(fileURLWithPath: self)
let filePath = fileUrl.appendingPathComponent(path).path
return filePath
}
How to use it ?
To save
OfflineManager.sharedManager.cacheDataToLocal(with: object as! [String:Any], to: .CampignList)
To read data
DispatchQueue.global().async {
// GET OFFLINE DATA
if let object:[String:Any] = OfflineManager.sharedManager.loadCachedDataFromLocal(with: .CampignList) {
do {
let data = try JSONSerialization.data(withJSONObject: object, options: [])
let object = try CampaignListResponse.init(data: data)
self.arrCampignList = object.data ?? []
DispatchQueue.main.async {
self.tableVIew.reloadData()
}
} catch {
}
}
}
Note: You can define your own WSCacheKeys for type of your json like i am fetching some campaign list
You can use Realm or CoraData for saving data and showing it when you are offline.
Here is the official link for Realm.You can learn from here.
https://realm.io/docs/swift/latest
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")
}