2

I have archived an Array of my NSCoding-conforming class "Assignment" to a file in a shared (App Groups) container:

class private func updateSharedFiles() {
    let path = NSFileManager().containerURLForSecurityApplicationGroupIdentifier("group.agenda-touch")!.path! + "/widgetData"
    if let incompleteAssignmentsArray = self.incompleteAssignments {
        NSKeyedArchiver.archiveRootObject(incompleteAssignmentsArray, toFile: path)
    }
}

Now when my extension wants to read from that file I called NSFileManager.fileExistsAtPath: on the file and it returns true. Finally I call NSKeyedUnarchiver.unarchiveObjectWithFile(path) as? AssignmentArray and get an NSInvalidUnarchiveOperationException. The thing is, when I unarchive the file in my main app, I get no such exception. And to be clear, I HAVE added Assignment.swift to my compile sources for the widget. So my TodayViewController knows what Assignment is, but can't decode it for some reason. As an addendum, here is the NSCoding implementation for Assignment:

//MARK: NSCoding
    func encodeWithCoder(aCoder: NSCoder) {
        aCoder.encodeObject(self.assignmentName, forKey: "assignmentName")
        aCoder.encodeObject(self.assignmentDueDate, forKey: "assignmentDueDate")
        aCoder.encodeObject(self.assignmentSubject, forKey: "assignmentSubject")
        aCoder.encodeBool(self.completed, forKey: "assignmentCompleted")
        aCoder.encodeObject(self.creationDate, forKey: "assignmentCreationDate")
        aCoder.encodeInteger(self.assignmentType.rawValue, forKey: "assignmentType")
        aCoder.encodeBool(self.earmarkedForDeletion, forKey: "assignmentEarmarked")
        aCoder.encodeObject(self.modificationDates, forKey: "assignmentModificationDates")
    }
    required convenience init(coder aDecoder: NSCoder) {
        let assignmentName = aDecoder.decodeObjectForKey("assignmentName") as String
        let assignmentDueDate = aDecoder.decodeObjectForKey("assignmentDueDate") as NSDate
        let assignmentSubject = aDecoder.decodeObjectForKey("assignmentSubject") as String
        let completed = aDecoder.decodeBoolForKey("assignmentCompleted")
        self.init(name:assignmentName,dueDate:assignmentDueDate,subject:assignmentSubject,completed:completed)
        self.creationDate = aDecoder.decodeObjectForKey("assignmentCreationDate") as NSDate
        let assignmentTypeRaw =  aDecoder.decodeIntegerForKey("assignmentType")
        self.assignmentType = AssignmentType(rawValue: assignmentTypeRaw)!
        self.earmarkedForDeletion = aDecoder.decodeBoolForKey("assignmentEarmarked")
        self.modificationDates = aDecoder.decodeObjectForKey("assignmentModificationDates") as Dictionary<String,NSDate>
    }
PopKernel
  • 4,110
  • 5
  • 29
  • 51

2 Answers2

3

I have the exact same problem. I tried to remove and add the file to and from compile sources. Not working either. Have you found a solution yet?

Only difference with my problem: I try to unarchive an NSArray of objects xxx. It works, when archiving and unarchiving in the main app OR archiving and unarchiving in the WatchKit app, but not when I archive it in the main app and unarchive it in the WatchKit app (and otherwise).

let defaults: NSUserDefaults = NSUserDefaults(suiteName: "group.name")!

if let object: NSData = defaults.objectForKey(ArrayKey) as? NSData {
    if let array: AnyObject = NSKeyedUnarchiver.unarchiveObjectWithData(object) {
        return array as NSArray
    }
}

return NSArray()

The exchange of objects through NSUserDefaults within my app group works. I only experience this problem with the NSKeyedArchiver and NSKeyedUnarchiver of an NSArray with objects xxx.

* Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: '* -[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class xxx


EDIT: I resolved the issue in making my object NSSecureCoding compliant. I therefore replaced

propertyName = aDecoder.decodeObjectForKey("propertyName")

with

propertyName = aDecoder.decodeObjectOfClass(NSString.self, forKey: "propertyName") as NSString

and added

class func supportsSecureCoding() -> Bool {
    return true
}

I also set the className of NSKeyedUnarchiver und NSKeyedArchiver in applicationFinishLaunchingWithOptions of the main app and in the first WKInterfaceController in the WatchKitApp

    NSKeyedArchiver.setClassName("Song", forClass: Song.self)
    NSKeyedUnarchiver.setClass(Song.self, forClassName: "Song")
helkarli
  • 131
  • 1
  • 8
  • 1
    Wait, I don't think this is an answer. – PopKernel Mar 31 '15 at 15:22
  • Sorry, I couldn't comment on the original question, because of low reputation. :) I edited the post, so it may contain the answer to the original question. – helkarli Apr 01 '15 at 11:00
  • 1
    All that is necessary is to call `NSKeyedArchiver.setClassname` and `NSKeyedUnarchiver.setClass`. Adopting `NSSecureCoding` should not make any difference. The underlying issue is that the class name is stored including the module, and your extension and main app have different module names. – ianthetechie Sep 03 '15 at 06:18
0

I also had the exact same problem. To sum up from previous answers and comments, the solution lies the setClass/setClassName statements. I wrap my encoding as functions in the example code below (Swift 5):

import Foundation


public class User: NSObject, NSSecureCoding
{
    public var id: String
    public var name: String

    public init(id: String, name: String)
    {
        self.id = id
        self.name = name
    }

    // NSSecureCoding

    public static var supportsSecureCoding: Bool { return true }

    public func encode(with coder: NSCoder)
    {
        coder.encode(id  , forKey: "id")
        coder.encode(name, forKey: "name")
    }

    public required init?(coder: NSCoder)
    {
        guard
            let id   = coder.decodeObject(forKey: "id") as? String,
            let name = coder.decodeObject(forKey: "name") as? String
        else {
            return nil
        }

        self.id = id
        self.name = name
    }

    // (Un)archiving

    public static func decode(from data: Data) -> Self?
    {
        NSKeyedUnarchiver.setClass(Self.self, forClassName: String(describing: Self.self))

        return try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? Self
    }

    public func encode() -> Data?
    {
        NSKeyedArchiver.setClassName(String(describing: Self.self), for: Self.self)

        return try? NSKeyedArchiver.archivedData(withRootObject: self, requiringSecureCoding: Self.supportsSecureCoding)
    }
}
Hans Terje Bakke
  • 1,412
  • 12
  • 14