0

What are the most 'modern' Swift ways to store objects like CMTime and CMTimeRange in plist? I have tried the following approaches. The dictionary object gets stored in plist.

  dictionary["timeRange"] =  NSValue(timeRange: timeRange)

and also,

  dictionary["timeRange"] = CMTimeRangeCopyAsDictionary(timeRange, allocator: kCFAllocatorDefault)

The problem with the first approach as someone pointed out is NSValue is more of Objective-C kind of stuff and it needs to be archived before it can be stored in a plist. The second approach is a dictionary based approach where timeRange is stored as CFDictionary but is compatible with plist without any need to archive. But it is CFDictionary that is even farther away from Swift!

So what's a modern way of making CMTimeRange property list serialisable?

Deepak Sharma
  • 5,577
  • 7
  • 55
  • 131

1 Answers1

1

Use PropertyListEncoder/PropertyListDecoder with a Codable model type. CMTime and CMTimeRange are not Codable conformant by default, so you need to add the conformance yourself.

extension CMTime: Codable {
    enum CodingKeys: String, CodingKey {
        case value
        case timescale
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let value = try container.decode(CMTimeValue.self, forKey: .value)
        let timescale = try container.decode(CMTimeScale.self, forKey: .timescale)
        self.init(value: value, timescale: timescale)
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(value, forKey: .value)
        try container.encode(timescale, forKey: .timescale)
    }
}

extension CMTimeRange: Codable {
    enum CodingKeys: String, CodingKey {
        case start
        case duration
    }

    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let start = try container.decode(CMTime.self, forKey: .start)
        let duration = try container.decode(CMTime.self, forKey: .duration)
        self.init(start: start, duration: duration)
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(start, forKey: .start)
        try container.encode(duration, forKey: .duration)
    }
}

struct Model: Codable {
    let time: CMTime
    let timeRange: CMTimeRange
}

let model = Model(time: CMTime.invalid, timeRange: CMTimeRange(start: CMTime(), end: CMTime()))
do {
    let encodedData = try PropertyListEncoder().encode(model)
    let decodedData = try PropertyListDecoder().decode(Model.self, from: encodedData)
    print(decodedData)
} catch {
    print(error)
}
Dávid Pásztor
  • 51,403
  • 9
  • 85
  • 116
  • Will this handle cases such as CMTime.invalid and other values? – Deepak Sharma Sep 28 '20 at 09:25
  • 1
    @DeepakSharma check my updated answer, now it handles `.invalid` and other values correctly as well - included an example with which you can test – Dávid Pásztor Sep 28 '20 at 09:42
  • It works, but I do not need this Model struct. I want to save lot of codable fields in plist and decode back an arbitrary dictionary like this: let dictionary:[String:Any] = ["point": CGPoint(x: 0.5, y: 0.5), "transform":CGAffineTransform(rotationAngle: CGFloat.pi/2), "startTime":CMTime.invalid, "timeRange": CMTimeRange(start: CMTime(seconds: 0.5, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), end: CMTime(value: 6000, timescale: 600))] – Deepak Sharma Sep 28 '20 at 10:04
  • I wonder how can I decode arbitrary plist to [String:Any] dictionary back and then read back all codable values – Deepak Sharma Sep 28 '20 at 10:05
  • @DeepakSharma don't use `[String:Any]`. Use a proper type. If you are working with plists, you should know the content of the plist, so having to use a concrete type should not be a problem (instead of the weakly typed `Any`). – Dávid Pásztor Sep 28 '20 at 10:11
  • I posted it as a separate question here, please have a look - https://stackoverflow.com/questions/64100207/plist-encoding-and-decoding-back-to-dictionary-stringdecodable – Deepak Sharma Sep 28 '20 at 10:16
  • Oh I see, after implementing this extension, I still can not use this in dictionary without Model struct. Writing to plist fails ""Property list invalid for format: 100 (property lists cannot contain objects of type 'CFType')" UserInfo={NSDebugDescription=Property list invalid for format: 100 (property lists cannot contain objects of type 'CFType')}" – Deepak Sharma Sep 28 '20 at 10:23
  • @DeepakSharma as I've said before, Swift is a strongly typed language, you __should__ always use a concrete type for your models, not `[String:Any]`. If you're looking for a modern, Swifty solution, you need to adopt to the basic principles of Swift. – Dávid Pásztor Sep 28 '20 at 10:26
  • It means I need to define structure of the data and then serialise/deserialise it. But what if the structure of data changes in future. We could have version 1.0, 1.1, 2.0, etc of the structure of plist. How to deal with it? – Deepak Sharma Sep 28 '20 at 10:31
  • @DeepakSharma then you update your model as well. You'd run into the same problem with a `Dictionary` when you actually try to parse values from it... – Dávid Pásztor Sep 28 '20 at 10:37
  • With dictionary, I can read the plist/xml version number first and then parse in datatype that corresponds to that version. I can't seem to do that in the modern approach. – Deepak Sharma Sep 28 '20 at 10:44
  • @DeepakSharma sure you can. However, this is really not related to the original question, so if you're looking for an answer on how to store generic models in plists and handle migrations of different plist versions, I'd suggest asking a new question. But in general, for this requirement, you should be using a database framework, not a plist. Database frameworks have built in functionality for handling migrations. – Dávid Pásztor Sep 28 '20 at 10:58