Writing swift dictionary to file - swift

There are limitations with writing NSDictionaries into files in swift. Based on what I have learned from api docs and this stackoverflow answer, key types should be NSString, and value types also should be NSx type, and Int, String, and other swift types might not work.
The question is that if I have a dictionary like: Dictionary<Int, Dictionary<Int, MyOwnType>>, how can I write/read it to/from a plist file in swift?

Anyway, when you want to store MyOwnType to file, MyOwnType must be a subclass of NSObject and conforms to NSCoding protocol. like this:
class MyOwnType: NSObject, NSCoding {
var name: String
init(name: String) {
self.name = name
}
required init(coder aDecoder: NSCoder) {
name = aDecoder.decodeObjectForKey("name") as? String ?? ""
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(name, forKey: "name")
}
}
Then, here is the Dictionary:
var dict = [Int : [Int : MyOwnType]]()
dict[1] = [
1: MyOwnType(name: "foobar"),
2: MyOwnType(name: "bazqux")
]
So, here comes your question:
Writing swift dictionary to file
You can use NSKeyedArchiver to write, and NSKeyedUnarchiver to read:
func getFileURL(fileName: String) -> NSURL {
let manager = NSFileManager.defaultManager()
let dirURL = manager.URLForDirectory(.DocumentDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: false, error: nil)
return dirURL!.URLByAppendingPathComponent(fileName)
}
let filePath = getFileURL("data.dat").path!
// write to file
NSKeyedArchiver.archiveRootObject(dict, toFile: filePath)
// read from file
let dict2 = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as [Int : [Int : MyOwnType]]
// here `dict2` is a copy of `dict`
But in the body of your question:
how can I write/read it to/from a plist file in swift?
In fact, NSKeyedArchiver format is binary plist. But if you want that dictionary as a value of plist, you can serialize Dictionary to NSData with NSKeyedArchiver:
// archive to data
let dat:NSData = NSKeyedArchiver.archivedDataWithRootObject(dict)
// unarchive from data
let dict2 = NSKeyedUnarchiver.unarchiveObjectWithData(data) as [Int : [Int : MyOwnType]]

Response for Swift 5
private func getFileURL(fileName: String) throws -> URL {
let manager = FileManager.default
let dirURL = try manager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
return dirURL.appendingPathComponent(fileName)
}
if let filePath = try? getFileURL(fileName: "data.dat").path {
NSKeyedArchiver.archiveRootObject(data, toFile: filePath)
}

Related

Can someone explain how to use NSKeyedArchiver and NSKeyedUnarchiver in Swift?

I have troubles understanding NSKeyedArchiver and NSKeyedUnarchiver in Swift.
This is some example code I have been given to get a picture of its use:
class Workout : NSObject, Coding {
var name: String!
var entries: Int = 0
required convenience init(coder aDecoder: NSCoder) {
self.init()
name = aDecoder.decodeObject(forKey: "name") as! String
entries = aDecoder.decodeInteger(forKey: "entries")
}
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encode(self.name, forKey: "name")
aCoder.encode(self.entries, forKey: "entries")
}
}
let libDir = FileManager.default.urls(for: .libraryDirectory,
in: .userDomainMask)[0]
print(libDir)
let workout = Workout()
workout.entries = 14
workout.name = "Pull-ups"
let path = libDir.appendingPathComponent("workouts")
// Serialisere til disk
if let data = try? NSKeyedArchiver.archivedData(withRootObject: workout, requiringSecureCoding: true) {
try? data.write(to: path)
}
// Deserialisere fra disk
if let archivedData = try? Data(contentsOf: path),
let savedWorkout = (try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(archivedData)) as? Workout {
print("\(savedWorkout.name), entries: \(savedWorkout.entries)")
}
I somehow get how NSKeyedArchiver works, but the Workout class in NSKeyedUnarchiver is something i find difficult to understand. What is going on in the class? And when i try to paste the class in a new swift file, I get this error: "Cannot find type 'Coding' in scope".

How to save a CapturedRoom using NSCoder

I'm trying to build an app that creates a floor plan of a room. I used ARWorldMap with ARPlaneAnchors for this but I recently discovered the Beta version of the RoomPlan API, which seems to lead to far better results.
However, I used te be able to just save an ARWorldMap using the NSCoding protocol, but this throws an error when I try to encode a CapturedRoom object:
-[__SwiftValue encodeWithCoder:]: unrecognized selector sent to instance 0x141c18110
My code for encoding the class containing the CapturedRoom:
import RoomPlan
class RoomPlanScan: NSObject, NSCoding {
var capturedRoom: CapturedRoom
var title: String
var notes: String
init(capturedRoom: CapturedRoom, title: String, notes: String) {
self.capturedRoom = capturedRoom
self.title = title
self.notes = notes
}
required convenience init?(coder: NSCoder) {
guard let capturedRoom = coder.decodeObject(forKey: "capturedRoom") as? CapturedRoom,
let title = coder.decodeObject(forKey: "title") as? String,
let notes = coder.decodeObject(forKey: "notes") as? String
else { return nil }
self.init(
capturedRoom: capturedRoom,
title: title,
notes: notes
)
}
func encode(with coder: NSCoder) {
coder.encode(capturedRoom, forKey: "capturedRoom")
coder.encode(title, forKey: "title")
coder.encode(notes, forKey: "notes")
}
}
To be clear, the following code does work:
import RoomPlan
class RoomPlanScan: NSObject, NSCoding {
var worldMap: ARWorldMap
var title: String
var notes: String
init(worldMap: ARWorldMap, title: String, notes: String) {
self.worldMap = worldMap
self.title = title
self.notes = notes
}
required convenience init?(coder: NSCoder) {
guard let capturedRoom = coder.decodeObject(forKey: "worldMap") as? ARWorldMap,
let title = coder.decodeObject(forKey: "title") as? String,
let notes = coder.decodeObject(forKey: "notes") as? String
else { return nil }
self.init(
worldMap: worldMap,
title: title,
notes: notes
)
}
func encode(with coder: NSCoder) {
coder.encode(worldMap, forKey: "worldMap")
coder.encode(title, forKey: "title")
coder.encode(notes, forKey: "notes")
}
}
I'm writing the object to a local file using NSKeyedArchiver so it would be nice if I could keep the same structure using NSCoder. How can I fix this and save a CapturedRoom?
The issue is about saving CaptureRoom. According to the doc, it's not NS(Secure)Coding compliant, but it conforms to Decodable, Encodable, and Sendable
So you can use an Encoder/Decoder, to do CaptureRoom <-> Data, you could use the bridge NSData/Data, since NSData is NS(Secure)Coding compliant.
So, it could be something like the following code. I'll use JSONEncoder/JSONDecoder as partial Encoder/Decoder because they are quite common.
Encoding:
let capturedRoomData = try! JSONEncoder().encode(capturedRoom) as NSData
coder.encode(capturedRoomData, forKey: "capturedRoom")
Decoding:
let captureRoomData = coder.decodeObject(forKey: "capturedRoom") as! Data
let captureRoom = try! JSONDecoder().decode(CaptureRoom.self, data: captureRoomData)
Side note:
I used force unwrap (use of !) to simplify the code logic, but of course, you can use do/try/catch, guard let, if let, etc.)

Swift: pList with "complex" data

As I read and tried out :-) I can only save some simple data types on pList files. Nevertheless I like to use structs, classes etc to represent my data. This should be saved as easily as possible to a pList file and gets reloaded.
I see, that NSData is a valid type for pLists. And also, that this is a general data type. So it is a good idea to move/convert/force a struct or class variable into a NSData object to be saved and reloaded? How would that be done?
Till now I'm using something like this for saving:
let dict: NSMutableDictionary = ["XYZ": "XYZ"]
// saving values
dict.setObject(myBasicArray, forKey: "BasicArray")
dict.writeToFile(path, atomically: false)
Updated:
I used the offered code and extended it to handle a struct:
import Cocoa
struct SteeringItem {
var ext = String() // extension including dot e.g. ".JPG"
var bitmap = Int() // flag for grouping file types, e.g. photos
init?(ext: String, bitmap: Int) {
// Initialize stored properties.
self.ext = ext
self.bitmap = bitmap
}
}
class Foo {
let one: Int
let two: SteeringItem
init(one:Int, two: SteeringItem) {
self.one = one
self.two = two
}
init?(dict:[String: AnyObject]) {
guard let
one = dict["one"] as? Int,
two = dict["two"] as? SteeringItem else { return nil }
self.one = one
self.two = two
}
func toDictionary() -> [String: AnyObject] {
var retval = [String: AnyObject]()
if let
one = self.one as? AnyObject,
two = self.two as? AnyObject {
retval["one"] = one
retval["two"] = two
}
return retval
}
}
// create struct
let writeStruct = Foo(one: 1, two: SteeringItem(ext: "one",bitmap: 1)!)
print(writeStruct, "\n")
// write to plist
let writeDict = writeStruct.toDictionary() as NSDictionary
let path = ("~/test.plist" as NSString).stringByExpandingTildeInPath
writeDict.writeToFile(path, atomically: true)
// print contents of file
print(try NSString(contentsOfFile: path, encoding: NSUTF8StringEncoding))
// read plist and recreate struct
if let
readDict = NSDictionary(contentsOfFile: path) as? [String:AnyObject],
readStruct = Foo(dict: readDict) {
print(readStruct)
}
but this does not write anymore. With String it worked, with "struct SteeringItem" it doesn't!
Update 2: class instead of struct
class SteeringItem : NSObject, NSCoding {
var ext = String() // extension including dot e.g. ".JPG"
var bitmap = Int() // flag for grouping file types, e.g. photos
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(ext, forKey: "ext")
aCoder.encodeObject(bitmap, forKey: "bitmap")
}
required convenience init?(coder aDecoder: NSCoder) {
let ext = aDecoder.decodeObjectForKey("ext") as! String
let bitmap = aDecoder.decodeObjectForKey("bitmap") as! Int
self.init(ext: ext, bitmap: bitmap)
}
init?(ext: String, bitmap: Int) {
// Initialize stored properties.
self.ext = ext
self.bitmap = bitmap
super.init()
}
}
There's several ways to do this, you can adhere to the NSCoding protocol or you can write methods to convert your class/struct to a Dictionary and serialize from there.
Here's a good intro to using the NSCoding protocol.
As for converting to and from a Dictionary the usual way is to provide a failable init method that takes a Dictionary<String, AnyObject> which validates and copies the key:value pairs to the member variables. You also provide a method that returns a Dictionary<String, AnyObject> with the same key:value pairs as the init takes. Then you can serialize by calling the create method and serializing the resulting Dictionary, you deserialize by reading into a Dictionary and passing that in to the init method.
Here's an example of the conversion:
/// Provides conversion to and from [String: AnyObject] for use in serialization
protocol Serializable {
init?(dict:[String: AnyObject])
func toDictionary() -> [String: AnyObject]
}
struct SteeringItem {
// Changed var to let, it's a good practice with a simple struct
let ext : String // extension including dot e.g. ".JPG"
let bitmap : Int // flag for grouping file types, e.g. photos
}
struct Foo {
let one: Int
let two: SteeringItem
}
// Add serialization to structs
extension SteeringItem: Serializable {
init?(dict:[String: AnyObject]) {
guard let
ext = dict["ext"] as? String,
bitmap = dict["bitmap"] as? Int else { return nil }
self.ext = ext
self.bitmap = bitmap
}
func toDictionary() -> [String: AnyObject] {
var retval = [String: AnyObject]()
if let
ext = self.ext as? AnyObject,
bitmap = self.bitmap as? AnyObject {
retval["ext"] = ext
retval["bitmap"] = bitmap
}
return retval
}
}
extension Foo: Serializable {
init?(dict:[String: AnyObject]) {
guard let
one = dict["one"] as? Int,
twoDict = dict["two"] as? [String: AnyObject],
two = SteeringItem(dict: twoDict) else { return nil }
self.one = one
self.two = two
}
func toDictionary() -> [String: AnyObject] {
var retval = [String: AnyObject]()
let twoDict = self.two.toDictionary()
if let
one = self.one as? AnyObject,
two = twoDict as? AnyObject {
retval["one"] = one
retval["two"] = two
}
return retval
}
}
Here's how to test it (in a playground):
import Foundation
// create struct
let writeStruct = Foo(one: 1, two: SteeringItem(ext: "jpg", bitmap: 1))
print(writeStruct, "\n")
// write to plist
let writeDict = writeStruct.toDictionary() as NSDictionary
let path = ("~/test.plist" as NSString).stringByExpandingTildeInPath
writeDict.writeToFile(path, atomically: true)
// print contents of file
print(try NSString(contentsOfFile: path, encoding: NSUTF8StringEncoding))
// read plist and recreate struct
if let
readDict = NSDictionary(contentsOfFile: path) as? [String:AnyObject],
readStruct = Foo(dict: readDict) {
print(readStruct)
}
Results:
Foo(one: 1, two: SteeringItem(ext: "jpg", bitmap: 1))
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>one</key>
<integer>1</integer>
<key>two</key>
<dict>
<key>bitmap</key>
<integer>1</integer>
<key>ext</key>
<string>jpg</string>
</dict>
</dict>
</plist>
Foo(one: 1, two: SteeringItem(ext: "jpg", bitmap: 1))

Swift objects array to plist file

I am trying to save my object's array to array.plist but I get the following error:
Thread 1: signal SIGABRT error
My object class looks like this:
class Note {
// MARK: Properties
var title: String
var photo: UIImage?
var text: String
// MARK: Initialization
init?(title: String, photo: UIImage?, text: String) {
// Initialize stored properties.
self.title = title
self.photo = photo
self.text = text
// Initialization should fail if there is no name or if the rating is negative.
if title.isEmpty{
return nil
}
}
func encodeWithCoder(aCoder: NSCoder!) {
aCoder.encodeObject(title, forKey:"title")
aCoder.encodeObject(text, forKey:"text")
aCoder.encodeObject(photo, forKey:"photo")
}
init (coder aDecoder: NSCoder!) {
self.title = aDecoder.decodeObjectForKey("title") as! String
self.text = aDecoder.decodeObjectForKey("text") as! String
self.photo = aDecoder.decodeObjectForKey("photo") as! UIImage
}
}
In the controller, I try to save the array with the Notes object like this:
notes = [Notes]()
notes.append(note)
let paths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory,NSSearchPathDomainMask.AllDomainsMask, true)
let path: AnyObject = paths[0]
let arrPath = path.stringByAppendingString("/array.plist")
NSKeyedArchiver.archiveRootObject(notes, toFile: arrPath)
Not all the properties in your class are not optional, yet when you retrieve them from the plist, you are unwrapping all of them. This might cause your code to crash.
For example, if the photo is nil and you saved the object, when you are retrieving it, you are unwrapping it self.photo = aDecoder.decodeObjectForKey("photo") as! UIImage, which will crash if you did not save anything there.
Try removing the unwrapping and check again for your crash. Even if this was not the cause of your crash, it will cause a crash at some point.
If this does not fix your problem, please paste the complete error log so it is a bit more clear what is happening.
For swift 5. You can save an array of custom classes to a .plist file that inherits from NSObject and NSSecureCoding.
If we create a custom class called Person:
import Foundation
class Person: NSObject, NSSecureCoding {
//Must conform to NSSecureCoding protocol
public class var supportsSecureCoding: Bool { return true } //set to 'true'
//just some generic things to describe a person
private var name:String!
private var gender:String!
private var height:Double!
//used to create a new instance of the class 'Person'
init(name:String, gender:String, height:Double) {
super.init()
self.name = name
self.gender = gender
self.height = height
}
//used for NSSecureCoding:
func encode(with coder: NSCoder) {
coder.encode(name, forKey: "name") //encodes the name to a key of 'name'
coder.encode(gender, forKey: "gender")
coder.encode(height, forKey: "height")
}
//used for NSSecureCoding:
required init?(coder: NSCoder) {
super.init()
self.name = (coder.decodeObject(forKey: "name") as! String)
self.gender = (coder.decodeObject(forKey: "gender") as! String)
self.height = (coder.decodeObject(forKey: "height") as! Double)
}
//created just to print the data from the class
public override var description: String { return String(format: "name=%#,gender=%#,height%f", name, gender, height) }
}
Now we can create functions to save and load from a .plist file in the ViewController class:
We need to gather data from the directory system of the device:
func documentsDirectory()->String {
let paths = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)
let documentsDirectory = paths.first!
return documentsDirectory
}
func dataFilePath ()->String{
return self.documentsDirectory().appendingFormat("/your_file_name_here.plist")
}
function to save the array:
func saveData(_ people:[Person]) {
let archiver = NSKeyedArchiver(requiringSecureCoding: true)
archiver.encode(people, forKey: "your_file_name_here")
let data = archiver.encodedData
try! data.write(to: URL(fileURLWithPath: dataFilePath()))
}
function to load the array:
func loadData() -> [Person] {
let path = self.dataFilePath()
let defaultManager = FileManager()
var arr = [Person]()
if defaultManager.fileExists(atPath: path) {
let url = URL(fileURLWithPath: path)
let data = try! Data(contentsOf: url)
let unarchiver = try! NSKeyedUnarchiver(forReadingFrom: data)
//Ensure the unarchiver is required to use secure coding
unarchiver.requiresSecureCoding = true
//This is where it is important to specify classes that can be decoded:
unarchiver.setClass(Person.classForCoder(), forClassName: "parentModule.Person")
let allowedClasses =[NSArray.classForCoder(),Person.classForCoder()]
//Finally decode the object as an array of your custom class
arr = unarchiver.decodeObject(of: allowedClasses, forKey: "your_file_name_here") as! [Person]
unarchiver.finishDecoding()
}
return arr
}
In the ViewController class:
override func viewDidLoad() {
super.viewDidLoad()
let testPerson = Person(name: "Bill", gender: "Male", height: 65.5)
let people:[Person] = [testPerson]
//Save the array
saveData(people)
//Load and print the first index in the array
print(loadData()[0].description)
}
Output:
[name=Bill,gender=Male,height=65.5000000]

Write to plist file in Swift

I have a sample plist-file, favCities.plist. This is my sample code:
override func viewDidLoad() {
super.viewDidLoad()
let path = NSBundle.mainBundle().pathForResource("favCities", ofType: "plist")
var plistArray = NSArray(contentsOfFile: path) as [Dictionary<String, String>]
var addDic: Dictionary = ["ZMV": "TEST", "Name": "TEST", "Country": "TEST"]
plistArray += addDic
(plistArray as NSArray).writeToFile(path, atomically: true)
var plistArray2 = NSArray(contentsOfFile: path)
for tempDict1 in plistArray2 {
var tempDict2: NSDictionary = tempDict1 as NSDictionary
var cityName: String = tempDict2.valueForKey("Name") as String
var cityZMV: String = tempDict2.valueForKey("ZMV") as String
var cityCountry: String = tempDict2.valueForKey("Country") as String
println("City: \(cityName), ZMV: \(cityZMV), Country: \(cityCountry)")
}
At first glance, everything works well. The output looks like this:
City: Moscow, ZMV: 00000.1.27612, Country: RU
City: New York, ZMV: 10001.5.99999, Country: US
City: TEST, ZMV: TEST, Country: TEST
But when I interrupt the app, I see that my file favCities.plist has not changed. There are still two values. These values ​​- City: TEST, ZMV: TEST, Country: TEST - were not added. If I restarted the application, then again I see 3 lines of output, although there should be 4.
What is wrong?
UPDATED:
I was changed code to this:
override func viewDidLoad() {
super.viewDidLoad()
let fileManager = (NSFileManager.defaultManager())
let directorys : [String]? = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory,NSSearchPathDomainMask.AllDomainsMask, true) as? [String]
if (directorys! != nil){
let directories:[String] = directorys!;
let dictionary = directories[0];
let plistfile = "favCities.plist"
let plistpath = dictionary.stringByAppendingPathComponent(plistfile);
println("\(plistpath)")
var plistArray = NSArray(contentsOfFile: plistpath) as [Dictionary<String, String>]
var addDic: Dictionary = ["ZMV": "TEST", "Name": "TEST", "Country": "TEST"]
plistArray += addDic
(plistArray as NSArray).writeToFile(plistpath, atomically: false)
var plistArray2 = NSArray(contentsOfFile: plistpath)
for tempDict1 in plistArray2 {
var tempDict2: NSDictionary = tempDict1 as NSDictionary
var cityName: String = tempDict2.valueForKey("Name") as String
var cityZMV: String = tempDict2.valueForKey("ZMV") as String
var cityCountry: String = tempDict2.valueForKey("Country") as String
println("City: \(cityName), ZMV: \(cityZMV), Country: \(cityCountry)")
}
}
else {
println("ERROR!")
}
}
Now when you run the application the number of rows in the output increases:
City: Moscow, ZMV: 00000.1.27612, Country: RU
City: New York, ZMV: 10001.5.99999, Country: US
City: TEST, ZMV: TEST, Country: TEST
City: TEST, ZMV: TEST, Country: TEST
........
BUT! If view the file favCities.plist, which is located in the project folder (Project Navigator in Xcode), it still remains unchanged - there are two lines!
If walk along the path, which is stored in the variable plistpath - /Users/admin/Library/Developer/CoreSimulator/Devices/55FD9B7F-78F6-47E2-9874-AF30A21CD4A6/data/Containers/Data/Application/DEE6C3C8-6A44-4255-9A87-2CEF6082A63A/Documents/
Then there is one more file favCities.plist. It contains all the changes that make the application. What am I doing wrong? How can I see all the changes in a file that is located in the project folder (Project Navigator)?
Mostly, people want to store a list of something, so here is my share on how to do this, also, here I don't copy the plist file, I just create it. The actual saving/loading is quite similar to the answer from Rebeloper
xcode 7 beta, Swift 2.0
saving
func SaveItemFavorites(items : Array<ItemFavorite>) -> Bool
{
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true) as NSArray
let docuDir = paths.firstObject as! String
let path = docuDir.stringByAppendingPathComponent(ItemFavoritesFilePath)
let filemanager = NSFileManager.defaultManager()
let array = NSMutableArray()
for var i = 0 ; i < items.count ; i++
{
let dict = NSMutableDictionary()
let ItemCode = items[i].ItemCode as NSString
dict.setObject(ItemCode, forKey: "ItemCode")
//add any aditional..
array[i] = dict
}
let favoritesDictionary = NSDictionary(object: array, forKey: "favorites")
//check if file exists
if(!filemanager.fileExistsAtPath(path))
{
let created = filemanager.createFileAtPath(path, contents: nil, attributes: nil)
if(created)
{
let succeeded = favoritesDictionary.writeToFile(path, atomically: true)
return succeeded
}
return false
}
else
{
let succeeded = notificationDictionary.writeToFile(path, atomically: true)
return succeeded
}
}
Little note from the docs:
NSDictionary.writeToFile(path:atomically:)
This method recursively validates that all the contained objects are property list objects (instances of NSData, NSDate, NSNumber, NSString, NSArray, or NSDictionary) before writing out the file, and returns NO if all the objects are not property list objects, since the resultant file would not be a valid property list.
So whatever you set at dict.SetObject() should be one of the above mentioned types.
loading
private let ItemFavoritesFilePath = "ItemFavorites.plist"
func LoadItemFavorites() -> Array<ItemFavorite>
{
let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true) as NSArray
let docuDir = paths.firstObject as! String
let path = docuDir.stringByAppendingPathComponent(ItemFavoritesFilePath)
let dict = NSDictionary(contentsOfFile: path)
let dictitems : AnyObject? = dict?.objectForKey("favorites")
var favoriteItemsList = Array<ItemFavorite>()
if let arrayitems = dictitems as? NSArray
{
for var i = 0;i<arrayitems.count;i++
{
if let itemDict = arrayitems[i] as? NSDictionary
{
let ItemCode = itemDict.objectForKey("ItemCode") as? String
//get any additional
let ItemFavorite = ItemFavorite(item: ItemCode)
favoriteItemsList.append(ItemFavorite)
}
}
}
return favoriteItemsList
}
Apart from the fact that the application bundle is read-only (for obvious reasons), since Swift 4 there is PropertyListDecoder/Encoder to read and write property lists without the bridged Objective-C APIs.
First create a struct for the model conforming to Codable
struct FavCity : Codable {
let city, zmv, country: String
}
Then specify two URLs, the url of the default file in the bundle and one URL in the documents directory to be able to modify the file
let fileManager = FileManager.default
let applicationBundleFileURL = Bundle.main.url(forResource: "favCities",
withExtension: "plist")!
let documentsFileURL = try! fileManager.url(for: .documentDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: false)
.appendingPathComponent("favCities.plist")
the try! doesn't matter because the system makes sure that the folder Documents exists.
Create a new favorite city
let newCity = FavCity(city: "TEST", zmv: "TEST", country: "TEST")
Now read the file in the documents directory. If it doesn't exist read the file in the bundle. Finally append the new city and write the property list data back to the documents directory
let data : Data
do {
data = try Data(contentsOf: documentsFileURL)
} catch {
data = try! Data(contentsOf: applicationBundleFileURL)
}
do {
var favCities = try PropertyListDecoder().decode([FavCity].self, from: data)
favCities.append(newCity)
let newData = try PropertyListEncoder().encode(favCities)
try newData.write(to: documentsFileURL)
} catch {
print(error)
}