1

I'm making a custom keyboard, and I want to build an asset (specifically, a trie dictionary) in the container app and then access it when the keyboard extension launches via a shared container. To work up to that, I'm starting with this fairly basic class:

class Person: NSObject, NSCoding {
    let name: String
    let age: Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    required init(coder decoder: NSCoder) {
        self.name = decoder.decodeObject(forKey: "name") as? String ?? ""
        self.age = decoder.decodeInteger(forKey: "age")
    }

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

Great. I've got my shared container set up, and in viewDidLoad function of the ViewController for the container app I load up the shared container as such (a few test variables and the custom object):

    let shared = UserDefaults(suiteName: "group.test_keyboard")
    shared!.set("test", forKey:"key_string")
    shared!.set(false, forKey:"key_boolean")
    shared!.set(34, forKey:"key_int")

    let newPerson = Person(name: "Joe", age: 10)
    let encodedData = NSKeyedArchiver.archivedData(withRootObject: newPerson)
    shared!.set(encodedData, forKey: "people")

To test it out, I added the following code to viewWillAppear:

    let shared = UserDefaults(suiteName: "group.test_keyboard")
    let test_string = shared?.string(forKey: "key_string")

    if(test_string != nil){
        print("string-\(test_string!)")
    }
    else{
        print("nil")
    }

    // retrieving a value for a key
    if let data = shared?.data(forKey: "people"){
        let test_person = NSKeyedUnarchiver.unarchiveObject(with: data) as? Person
        print("-\(test_person!.name)")  // Joe

        let data_size = String(data.count)
        print("-\(data_size)")

    } else {
        print("There is an issue")
    }

Everything checks out fine. But when I go into the KeyboardViewController and try to access things, retrieving the simple string works fine:

    let shared = UserDefaults(suiteName: "group.test_keyboard")
    shared?.synchronize()
    let test_string = shared?.string(forKey: "key_string")

    if(test_string != nil){
        (textDocumentProxy as UIKeyInput).insertText(test_string!)
    }
    else{
        (textDocumentProxy as UIKeyInput).insertText("nil")
    }

But then if I try to access the custom object Person, as such:

    if let data = shared?.data(forKey: "people") {
        (textDocumentProxy as UIKeyInput).insertText("found")

        (textDocumentProxy as UIKeyInput).insertText(String(data.count))

        let myPeople = NSKeyedUnarchiver.unarchiveObject(with: data) as? Person

    }

I get a crash with the following error response:

2017-05-10 14:34:46.938253-0700 test_keyboard[3196:857207] viewServiceDidTerminateWithError:: Error Domain=_UIViewServiceInterfaceErrorDomain Code=3 "(null)" UserInfo={Message=Service Connection Interrupted}

The data is there, if I do a simple data.count check on both sides it's the same size (269). If I comment out the NSKeyedUnarchiver line, there's no problem, so I'm certain that's where the issue is. What on earth is happening that causes it to crash inside the Keyboard Extension when it works just fine in the container app?

rmaddy
  • 314,917
  • 42
  • 532
  • 579
asetniop
  • 523
  • 1
  • 5
  • 12

1 Answers1

2

Most likely it's a question of running in different modules. Your app is a different module than the extension, and the fully-qualified class name includes the module name. In the app it's something like AppName.Person but in the extension it's something like KeyboardExt.Person. NSCoding can't match those up so it barfs.

There are a couple of workarounds. One is to create a framework that's used by both app and extension, and put the shared class there. Then the name with module would always be something like 'FrameworkName.Person` and everything would work.

The other is to specifically tell the (un)archive process what name to use for the class. Then it'll write the name you specify and match that to the appropriate class. Before writing any data, do something like

NSKeyedArchiver.setClassName("Person", for: Person.self)

Then before reading any data, do the opposite:

NSKeyedUnarchiver.setClass(Person.self, forClassName: "Person")
Tom Harrington
  • 69,312
  • 10
  • 146
  • 170
  • This looks very promising - I will give this a shot! – asetniop May 11 '17 at 18:59
  • @asetniop You contacted me via my web site and via Twitter to ask if I'd answer this. If it helped, please mark it as accepted. If not, please describe what problems you had with it. – Tom Harrington May 15 '17 at 19:55
  • It did! (Sorry to be slow - my wife needed out macbook for deposition) Thank you so much! – asetniop May 15 '17 at 21:34