swiftnscoding

Swift NSKeyedUnarchiver Failing After Converting from String to Data


I have a class that conforms to NSCoding. I want to convert that class into data, then into a string that can be stored on backend.

The issue is that once it's a converted from a String back to Data it fails to unarchive properly with NSKeyedUnarchiver, so converting to a string must be corrupting the data.

Here's the code:

  do {
        let data = try NSKeyedArchiver.archivedData(withRootObject: drawItems, requiringSecureCoding: false)
        let stringData = String(decoding: data, as: UTF8.self)
        
        print("HERE successfully converted to a string: ", stringData)
        
        let stringBackToData: Data? = stringData.data(using: .utf8)
        if let stringBackToData = stringBackToData, let allItems = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(stringBackToData) as? [DrawItem] {
            print("HERE items: ", allItems)
        } else {
            print("HERE FAILED to unarchive")
        }
        
    catch {
            print("Failed: ", error)
    }

What is wrong with this string conversion?


Solution

  • The default outputFormat for NSKeyedArchiver is .binary, which is not convertible to or from UTF-8. In your example, String(decoding: data, as: UTF8.self) "repairs" invalid UTF-8 bytes by replacing them with the UTF-8 replacement character (0xFFFD).

    This corrupts the archiver data, and prevents it from decoding properly.

    If you really can't transmit binary data to your backend as-is, or store it that way, and must convert the data into a string, you have two options:

    1. NSKeyedArchiver has an alternate .outputFormat of .xml, which produces UTF-8 XML data. You can produce this data by creating an archiver and configuring it:

      let archiver = NSKeyedArchiver(requiringSecureCoding: false)
      archiver.outputFormat = .xml
      archiver.encode(drawItems, forKey: NSKeyedArchiveRootObjectKey)
      
      if let error = archiver.error {
          // Handle the error. This is the same error that would be produced
          // by `NSKeyedArchiver.archivedData(withRootObject:requiringSecureCoding:)`
      } else {
          let data = archiver.encodedData
          // `data` is UTF-8 XML data ready to be transmitted/stored
      }
      
    2. Alternatively, you can continue using the convenience method you are now, but Base64 encode it:

       let data = try NSKeyedArchiver.archivedData(withRootObject: drawItems, requiringSecureCoding: false)
       let base64EncodedData = data.base64EncodedString()
      

    Which you use is up to you: on its own, the base64-encoded string is shorter than the raw XML data (because the starting binary data is significantly smaller than its equivalent XML), but the XML data does compress significantly better than the base64 data does, and may very well end up smaller. You can test both approaches to see what fits your use-case better.