Switching to Swift 5 and IOS 12, I need to conform to NSSecureCoding for Archiver.
I tried using Codable, but I need to store an array of [String: Any] dictionaries and could not get it work.
Maybe JSON encoder could be a good option here ?
The class is pretty simple.
let theKey = "theKey"
class MyFavorites : NSObject, NSCoding {
var myData: [[String: Any]]?
override init() {
super.init()
myData = []
}
required init(coder decoder: NSCoder) {
mesVEs = decoder.decodeObject(forKey: favorisVEKey) as? [[String: Any]]
}
func encode(with coder: NSCoder) {
if let savedData = myData {
coder.encode(savedData, forKey: theKey)
}
}Archiving and unArchiving are done in a utlity class:
class Util {
class func dataFilePath() -> String {
let paths = NSSearchPathForDirectoriesInDomains(
FileManager.SearchPathDirectory.documentDirectory,
FileManager.SearchPathDomainMask.userDomainMask, true)
let documentsDirectory = paths[0] as NSString
return documentsDirectory.appendingPathComponent("data.archive") as String
}
class func loadData() -> MyFavorites {
let filePath = Util.dataFilePath()
if (FileManager.default.fileExists(atPath: filePath)) {
let data = NSMutableData(contentsOfFile: filePath)!
let unarchiver = NSKeyedUnarchiver(forReadingWith: data as Data)
let allFavoris = unarchiver.decodeObject(forKey: rootKey) as! MyFavorites // whole dictionary
unarchiver.finishDecoding()
return allFavoris
} else {
return MyVEFavoris()
}
}
class func saveData() {
let filePath = dataFilePath()
let favorisToSave = MyFavorites()
favorisToSave.myData = // content read from a stored array of dictionaries
let data = NSMutableData()
let archiver = NSKeyedArchiver(forWritingWith: data)
archiver.encode(favorisToSave, forKey: rootKey)
archiver.finishEncoding()
data.write(toFile: filePath, atomically: true)
}To make it conform to SecureCoding, I tried to minimize the changes (may be not the best option), so I changed the class as follows:
class MyFavorites : NSObject, NSSecureCoding {
var myData: [[String: Any]]? // No change
static var supportsSecureCoding: Bool {
return true
}
override init() {
super.init() // No change here
self.myData = []
}
required init(coder decoder: NSCoder) {
if let topObject = decoder.decodeObject(of: MyFavorites.self, forKey: theKey) {
myData = topObject.myData
} else {
self.myData = []
}
}
func encode(with coder: NSCoder) {
coder.encode(self, forKey: theKey)
}Now, I am struggling to adapt the calls to Archiver (loadData and SaveData)
class func loadData() -> MyFavorites {
let filePath = Util.dataFilePath()
if (FileManager.default.fileExists(atPath: filePath)) {
guard let dataObject = NSData(contentsOfFile: filePath) else { return MyFavorites() }
guard let unarchived = try? NSKeyedUnarchiver.unarchivedObject(ofClass: MyFavorites.self, from: dataObject as! Data)
else { return MyFavorites() }
guard let unarchivedArray = unarchived as? [[String: Any]]
else { return MyFavorites() }
let returnFavorites = MyFavorites()
returnFavorites.myData = unarchivedArray
return returnFavorites
} else {
return MyFavorites()
}And cannot find a way to adapt NSKeyedArchiver to secureCoding
I do not think your revised `MyFavorite` is properly conforming to `NSSecureCoding`.
You may need to encode and decode your `myData` securely.
class MyFavorites : NSObject, NSSecureCoding {
var myData: [[String: Any]]? // No change
static var supportsSecureCoding: Bool {
return true
}
override init() {
super.init() // No change here
self.myData = []
}
required init(coder decoder: NSCoder) {
myData = decoder.decodeObject(of: [
NSArray.self, NSDictionary.self, MyItem.self, NSDate.self, //...
], forKey: theKey) as? [[String: Any]] ?? []
}
func encode(with coder: NSCoder) {
if let savedData = myData {
coder.encode(savedData, forKey: theKey)
}
}
}You need to specify all possible classes stored in `myData` as Objective-C bridged type.
(As far as I tried, you have no need to specify NSString, NSNumber or NSData.)
With the above implementation, you can write `loadData()` and `saveData()` easily:
class Util {
static func dataFileURL() -> URL {
let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectoryURL = urls[0]
return documentsDirectoryURL.appendingPathComponent("data.archive")
}
static func loadData() -> MyFavorites {
let fileURL = Util.dataFileURL()
if FileManager.default.fileExists(atPath: fileURL.path) {
do {
let data = try Data(contentsOf: fileURL)
guard let unarchived = try NSKeyedUnarchiver.unarchivedObject(ofClass: MyFavorites.self, from: data) else {
return MyFavorites()
}
return unarchived
} catch {
print(error)
return MyFavorites()
}
} else {
return MyFavorites()
}
}
static func saveData() {
let fileURL = dataFileURL()
let favorisToSave = MyFavorites()
favorisToSave.myData = readContentForMyData() // content read from a stored array of dictionaries
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: favorisToSave, requiringSecureCoding: true)
try data.write(to: fileURL)
} catch {
print(error)
//You may need some better error handling...
}
}
}My preferred way to work with file paths recently is using `URL`, so I changed `dataFilePath()` to `dataFileURL()`. And I changed `class func` to `static func`. If you think each method requires to be overridable, please revert it to `class func`.