0

I'm trying to use NSUserDefaultsController to implement a Preferences window in my Swift macOS app.

One of the settings that I need to persist is an array of presets, as defined by the following class:

class Preset: NSObject {
    var name = "abc"
    var value = 123

    override init() {
        super.init()
    }
}

Therefore, I need to persist a variable of type [Presets]. The visual representation in my Preferences window is an NSTableView, bound to an NSArrayController.

I followed this tutorial to set up my bindings. When running the app and trying to add a new preset (by clicking the + button), I get the following errors:

[User Defaults] Attempt to set a non-property-list object (...) as an NSUserDefaults/CFPreferences value for key presets
[General] Attempt to insert non-property list object (...) for key presets

I've tried implementing the Codable and NSCoding protocols, but the error persists.

From searching similar questions in Stack Overflow (such as this or this), it appears the solution would involve NSKeyedArchiver and NSKeyedUnarchiver. That appears simple enough to do if you're manually triggering loads and saves. Unfortunately I don't see how to use these classes along with NSUserDefaultsController.

swineone
  • 2,296
  • 1
  • 18
  • 32
  • If you want to use `NSUserDefaultsController` and Cocoa Bindings you have to implement a `ValueTransformer` to serialize the custom objects. I recommend to use `Codable`, then a simple struct is sufficient. – vadian Jan 26 '19 at 08:21
  • @vadian I am a Cocoa/Swift newbie. Could you elaborate a bit? Do you mean I need to implement a class that inherits from `ValueTransformer` and implements the `Codable` protocol? Perhaps you could make this into an answer -- it looks like this will solve my problem, and I can accept it. – swineone Jan 26 '19 at 11:13
  • No, the data model is a normal struct or class adopting `Codable`. Assuming there is a array `@objc dynamic var presets = [Preset]()` you have to implement a subclass of `ValueTransformer` to decode and encode the array from and to `Data`. Then you can bind `presets` to the `NSUserDefaultsController` applying the value transformer. – vadian Jan 26 '19 at 11:19
  • OK, I know I'm supposed to show a little effort here, but I'm a complete newbie and I'm lost. So I need to make a class with a single property, `@objc dynamic var presets = [Preset]()`? If you could make a step-by-step answer I'd be very grateful. – swineone Jan 26 '19 at 12:14

1 Answers1

0

I solved the problem by subclassing NSArrayController as follows (see comment by Hamish to my other question, which was the last missing piece of the puzzle to make this generic):

extension Encodable {
    fileprivate func encode(to container: inout SingleValueEncodingContainer) throws {
        try container.encode(self)
    }
}

struct AnyEncodable: Encodable {
    var value: Encodable
    init(_ value: Encodable) {
        self.value = value
    }
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try value.encode(to: &container)
    }
}

class NSEncodableArrayController: NSArrayController {
    override func addObject(_ object: Any) {
        let data = try! PropertyListEncoder().encode(AnyEncodable(object as! Encodable))
        let any = try! PropertyListSerialization.propertyList(from: data, options: [], format: nil)

        super.addObject(any)
    }
}
swineone
  • 2,296
  • 1
  • 18
  • 32