Property List Serialization

Hi,

I found have the following bit of code (iOS, Swift 3) that I got from an online tutorial but am having trouble making it work in swift 3. Any ideas what exactly needs changing?:


var notesArray:NSMutableArray!
    var plistPath:String!

    override func viewWillAppear(_ animated: Bool) {
    
        let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
        plistPath = appDelegate.plistPathInDocument
        /
        let data:NSData =  NSFileManager.defaultManager().contentsAtPath(plistPath)!
        do{
            notesArray = try NSPropertyListSerialization.propertyListWithData(data, options: NSPropertyListMutabilityOptions.MutableContainersAndLeaves, format: nil) as! NSMutableArray
        }catch{
            print("Error occured while reading from the plist file")
        }
        self.tableView.reloadData()
    }


The plistPathInDocument is setup in app delegate and set to the path of the plist already. This method is essentially trying to populate a table view with data from a plist. Errors come from a few lines and suggests replacements making the code look like this:


var notesArray:NSMutableArray!
    var plistPath:String!
  
    override func viewWillAppear(_ animated: Bool) {
      
        let appDelegate = UIApplication.shared.delegate as! AppDelegate
        plistPath = appDelegate.plistPathInDocument
        /
        let data:NSData =  FileManager.default.contents(atPath: plistPath)! as NSData
        do{
            notesArray = try PropertyListSerialization.propertyList(from: data as Data, options: PropertyListSerialization.MutabilityOptions.mutableContainersAndLeaves, format: nil) as! NSMutableArray
        }catch{
            print("Error occured while reading from the plist file")
        }
        self.tableView.reloadData()
    }


This gets rid of any errors prior to running it however once run in the simulator I get a "Thread 1: signal SIGABRT" error on line 11 after it crashes. Any ideas how to fix this would be appreciated.


Thanks

Owen

First of all, in line 9 you take the Data you get from FileManager and cast it to NSData then cast it right back to Data in line 11. You don't have to do that; if you rewrite line 9 like this,

let data = FileManager.default.contents(atPath: plistPath)!

you can get rid of the "as Data" in line 11. That would make your code easier to read.


Now about the crash, if you rewrite line 11 like this it might work:

notesArray = try PropertyListSerialization.propertyList(from: data, options: [.mutableContainersAndLeaves], format: nil) as! NSMutableArray

Usually, options are specified by an array-like syntax. If you were to mean "no options," then you'd use [ ].


One last note: did you notice that I left off the PropertyListSerialization.MutabilityOptions? Swift's type inference system usually lets you leave stuff like that off, making code easier to read. Oh, and is the / in line 8 actually in your source file? It shouldn't be there.

>> Usually, options are specified by an array-like syntax


It's not necessary to use [ ] in this case, because .mutableContainersAndLeaves is a static property of the class, so it's already an OptionsSet, which is what the [ ] syntax means here. In fact, it's a quirk of OptionsSet that:


     [.mutableContainersAndLeaves] == .mutableContainersAndLeaves


for any single value of the options set that is represented by a static property.


>> Thread 1: signal SIGABRT


This isn't the error, this is just your app crashing itself after an error was detected and reported. You need to locate the actual error message that led to the crash.


It could be that the "as! NSMutableArray" cast that's failing. You might need to take the statement apart (split it into multiple statements) to find out what fails.

Thank you, this has helped tidy the code up a bit 🙂 However still getting an error, found the actual error, it says


"Could not cast value of type '__NSCFDictionary' (0x1021f92c0) to 'NSMutableArray' (0x1021f8cd0)."


Not sure how to fix this.

Thanks

Owen

Have edited the few things suggested, code now looks like this:


var notesArray:NSMutableArray!
    var plistPath:String!

    override func viewWillAppear(_ animated: Bool) {
     
        let appDelegate = UIApplication.shared.delegate as! AppDelegate
        plistPath = appDelegate.plistPathInDocument

        let data = FileManager.default.contents(atPath: plistPath)!
        do{
            notesArray = try PropertyListSerialization.propertyList(from: data, options: .mutableContainersAndLeaves, format: nil) as! NSMutableArray
        }catch{
            print("Error occured while reading from the plist file")
        }
        self.tableView.reloadData()
    }


Still getting the error, the actual error is:


"Could not cast value of type '__NSCFDictionary' (0x1021f92c0) to 'NSMutableArray' (0x1021f8cd0)."


Not too sure what this means and how to fix it :/

Thanks

Owen

Well, the error message is that you're trying to cast a dictionary (which is apparently what propertyListFromData is returning) to an array. Perhaps that plist is structured differently than you expect? You might want to take another look at it to verify that its root is in fact an array, not a dictionary. Additionally, is there a specific reason you're using NSMutableArray? You might find Swift's Array syntax to be a little easier to use. You can write the type of array just as [MyClass] or [Any] and you'll get mutable behavior simply by virtue of the fact that notesArray is a var, not a let.


Here's an idea that could also work: you could use an optional cast instead of a forced cast and then throw if the cast fails. Like this:

var notesArray: [Any]!

enum ReadError: Error { case CastFailed }
do {
     let propertyList = try PropertyListSerialization.propertyList(from: data, options: .mutableContainersAndLeaves, format: nil)
     guard let convertedPropertyList = propertyList as? [Any] else { throw ReadError.castFailed }
     notesArray = convertedPropertyList
} catch{ 
     print("Error occured while reading from the plist file") 
}


One more thing I need to mention: I would caution you about attempting to take the container objects returned by this method and use them directly as your model. You're potentially setting yourself up for some complex race-condition and ownership problems. At the very least, you should make a deep copy (if you don't know what this means I can explain) so that the model objects are entirely yours.

>> You're potentially setting yourself up for some complex race-condition and ownership problems.


Did you have something specific in mind? It's not clear why there should be any "ownership" problems, since ownership really means holding a strong reference (that is, it's not exclusive ownership), and ARC takes care of that for you. There shouldn't be any race conditions in a non-threaded situation like this. IAC, each invocation of PropertyListSerialization.propertyList is going to give you a new (or effectively new) hierarchy of property list objects.


Perhaps you're concerned that mutable objects may be shared elsewhere via PropertyListSerialization, but this shouldn't be the case.


>> At the very least, you should make a deep copy


Now that the OP has started using native Swift types (e.g. [Any]), they're now value types. That means the concept of deep copy doesn't apply — any copy of a value-type value is automatically a deep copy. It's true that many of the value types that may be present in the property list hierarchy wrap reference types, but that's an implementation detail that's taken care of by the compiler/standard library.

Well, yeah, but trying to use NSMutableArray in Swift re-introduces some of that old confusion, so that's why I brought it up.

Hi,

This seems to have worked, thanks very much.

However consequently I am getting a new error in the following function to do with notesArray being of type any:


func tableView(tableView: UITableView,
                            cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
       
        let cell:UITableViewCell! = tableView.dequeueReusableCell(withIdentifier: "cellIdentifier")
        cell.textLabel!.text = notesArray.objectAtIndex(indexPath.row) as? String
        return cell
    }


The error is on line 5: "Value of type '[Any]' has no member 'objectAtIndex'

Any ideas?


Thanks very much again for your help, sorry I'm a bit new to this.

Thanks

Owen

Remember how you changed notesArray from a NSMutableArray to an [Any] ? The two work slightly differently, and Swift arrays don't have objectAtIndex(). Just use the subscript, like this:

cell.textLabel!.text = notesArray[indexPath.row] as? String

Thanks, this seems to have solved any sort of errors, however once the table loads in the simulator it is no populated with the data from the plist file and the the do method on line 16 throws an error and line 21 is printed to the console. Not too sure why this is happening, any idea? Sorry this is dragging on so much.

Thanks

Owen

(Here is my viewController class)

class ViewController: UITableViewController {
  
    var notesArray: [Any]!
    var plistPath:String!
  
    override func viewWillAppear(_ animated: Bool) {
      
        let appDelegate = UIApplication.shared.delegate as! AppDelegate
        plistPath = appDelegate.plistPathInDocument
        print(plistPath)
        /
        let data = FileManager.default.contents(atPath: plistPath)!
      
      
        enum ReadError: Error { case CastFailed }
        do {
            let propertyList = try PropertyListSerialization.propertyList(from: data, options: .mutableContainersAndLeaves, format: nil)
            guard let convertedPropertyList = propertyList as? [Any] else { throw ReadError.CastFailed }
            notesArray = convertedPropertyList
        } catch{
            print("Error occured while reading from the plist file")
        }
        self.tableView.reloadData()
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        /
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        /
    }
  
    func tableView(tableView: UITableView,
                            cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
      
        let cell:UITableViewCell! = tableView.dequeueReusableCell(withIdentifier: "cellIdentifier")
        cell.textLabel!.text = notesArray[indexPath.row] as? String
        return cell
    }
  
   func tableView(tableView: UITableView,
                            numberOfRowsInSection section: Int) -> Int{
        return notesArray.count
    }
  
}

Don't be sorry—I went through stuff like this too when I was learning 🙂


So the do block can throw in one of two places: (1) reading the property list (line 17) and (2) casting to [Any]. Maybe drop a breakpoint on line 17 (just click the line number in the source editor) and step through the code to see what part is throwing.

Hi,

It seems to have stopping throwing an error now and the project executes without any errors and seems to work however the table view shows with no data in it. It doesnt seem to be populating the table with data so it is just empty. The plist file in the bundle is set as an array and has two string items in it so it should show these im guessing?

Here is the ViewController Code:


import UIKit
class ViewController: UITableViewController {

    var notesArray: [Any]!
    var plistPath:String!

    override func viewWillAppear(_ animated: Bool) {
   
        let appDelegate = UIApplication.shared.delegate as! AppDelegate
        plistPath = appDelegate.plistPathInDocument
        /
        let data = FileManager.default.contents(atPath: plistPath)!
   
   
        enum ReadError: Error { case CastFailed }
        do {
            let propertyList = try PropertyListSerialization.propertyList(from: data, options: .mutableContainersAndLeaves, format: nil)
            guard let convertedPropertyList = propertyList as? [Any] else { throw ReadError.CastFailed }
            notesArray = convertedPropertyList
        } catch{
            print("Error occured while reading from the plist file")
        }
        self.tableView.reloadData()
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        /
    }
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        /
    }

    func tableView(tableView: UITableView,
                            cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell{
   
        let cell:UITableViewCell! = tableView.dequeueReusableCell(withIdentifier: "cellIdentifier")
        cell.textLabel!.text = notesArray[indexPath.row] as? String
        return cell
    }

   func tableView(tableView: UITableView,
                            numberOfRowsInSection section: Int) -> Int{
        return notesArray.count
    }

}


Here is the AppDelegate Code:


import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var plistPathInDocument:String = String()
    /

    func preparePlistForUse(){
        let rootPath = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.documentDirectory, .userDomainMask, true)[0]
     
        plistPathInDocument = rootPath.appending("/table1.plist")
        if !FileManager.default.fileExists(atPath: plistPathInDocument){
            let plistPathInBundle = Bundle.main.path(forResource: "table1", ofType: "plist") as String!
         
            do {
                try FileManager.default.copyItem(atPath: plistPathInBundle!, toPath: plistPathInDocument)
            }catch{
                print("Error occurred while copying file to document \(error)")
            }
        }
    }
    var window: UIWindow?
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        self.preparePlistForUse()
        /
        return true
    }
    func applicationWillResignActive(_ application: UIApplication) {
        /
        /
    }
    func applicationDidEnterBackground(_ application: UIApplication) {
        /
        /
    }
    func applicationWillEnterForeground(_ application: UIApplication) {
        /
    }
    func applicationDidBecomeActive(_ application: UIApplication) {
        self.preparePlistForUse()
        /
    }
    func applicationWillTerminate(_ application: UIApplication) {
        /
    }
}


Here is the text from the Plist:


<?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">
<array>
  <string>Item 0</string>
  <string>Item 1 2</string>
</array>
</plist>


Hopefully something in here stands out to you as being the reason for it not showing any data. Really appreciate your help on this.

Thanks
Owen

Hmm… yeah, nothing in there really stands out to me as being obviously wrong. Here is where the debugging gets tricky. Probably the easiest thing to do is to insert the following line of code at the end of viewWillAppear(_:):

Swift.print("The unpacked notes array is ", notesArray ?? "(failed to load)")

Then run your app. What prints out in the debugger console?

Hi,

The debugger perfectly prints the 3 item's values that are in my plist array, so the issue seems to be with populating the table with the unpacked array. Is potentially something to do with the fact the tableView methods at the bottom don't get run? The original tutorial had them down as override func but it wont let me do this so i took it out. Thank you so much for your help with this.

Thanks

Owen

Your mentioning of the "override func" thing was an important clue! Yes, those totally should be override func. Put those override keywords back in and add an underscore and a space right after the openining parenthesis in those two data source methods. It should look like this:

override func tableView(_ tableView: UITableView...


This is a quirk of the way Swift methods are named and called. Because you didn't use the underscore, it changes the signature of the method, so it's technically you didn't implement the proper data source method. That's why you were prompted to remove the override keyword. Once you put the underscore in, THEN you will have implemented the correct methods and it should all work.


Oh, and you can get rid of the log lines now if you haven't already. 🙂

Property List Serialization
 
 
Q