How can I make CMTime conform to Hashable on all iOS versions?

292 views Asked by At

On Xcode 14, Apple added Hashable conformance to CMTime and CMTimeRange to iOS16 only. I'm trying to make it Hashable for all iOS versions, because we have many Hashable structs that contain CMTime, and they depend on CMTime to be Hashable as well.

Until now, we had an extension on CMTime that make it conform to Hashable, but on Xcode 14 this extension causes compilation error with this description:

Protocol 'Hashable' requires 'hashValue' to be available in iOS 14.0.0 and newer

If I implement hashValue like this:

  public var hashValue: Int {
    var hasher = Hasher()
    hash(into: &hasher)
    return hasher.finalize()
  }

It compile and works, but I'm not sure if it's safe because hashValue is deprecated so I'm not sure I understand why it's needed (only hash(into:) should be implemented for Hashable conformance these days).

Can anyone shed some light about whether this solution is safe, or have any other solution?

Another idea that I tried:

I added this extension on CMTime:

extension CMTime {
  struct Hashed: Hashable {
    private let time: CMTime

    init(time: CMTime) {
      self.time = time
    }

    public func hash(into hasher: inout Hasher) {
      // pre iOS16 hash computation goes here.
    }
  }

  func asHashable() -> Hashed {
    return Hashed(time: self)
  }
}

Then I changed all Hashable structs that contain CMTime from this:

struct Foo: Hashable {
  let time: CMTime

  let string: String
}

to this:

struct Foo: Hashable {
  let time: CMTime

  let string: String

  func hash(into hasher: inout Hasher) {
    if #available(iOS 16, *) {
      hasher.combine(self.time)
    } else {
      hasher.combine(self.time.asHashable())
    }
    hasher.combine(self.string)
  }
}

I'm not a fan of this since it will make a LOT of changes across the code

1

There are 1 answers

7
thedp On BEST ANSWER

EDIT 2:

The code below works with iOS 16 simulator, but crashes with iOS 15 and lower. Odd, since it compiles.

My suggestion is to implement an extension and make it available only for iOS 15 and lower:

@available(iOS, obsoleted: 16)
extension CMTime: Hashable {
    public var hashValue: Int {
        var hasher = Hasher()
        hash(into: &hasher)
        return hasher.finalize()
    }

    public func hash(into hasher: inout Hasher) {
        hasher.combine(value)
        hasher.combine(timescale)
        // more if needed
    }
}

As for why hashValue is still needed, I agree it's not suppose to be there. Might be an XCode false alarm, the error is really confusing. Notice Apple doesn't say when it was deprecated, and also it was deprecated as a requirement, it's still there behind the scenes, I'm guessing: https://developer.apple.com/documentation/swift/hashable/hashvalue

EDIT 1:

CMTime already conforms to Hashable. Tested with iOS 13, 14, 15 and 16. I don't think you need your extension.

import Foundation
import AVFoundation

struct Foo: Hashable {
    let aaa: String
    let bbb: Int
    let time: CMTime
}

func testMyFoo() {
    let foo1 = Foo(
        aaa: "yo",
        bbb: 123,
        time: CMTime(
            value: 100,
            timescale: 1
        )
    )

    let foo2 = Foo(
        aaa: "yo",
        bbb: 123,
        time: CMTime(
            value: 100,
            timescale: 1
        )
    )

    var myFoos = Set<Foo>()
    myFoos.insert(foo1)
    myFoos.insert(foo2)

    print(myFoos)
    // you will only have 1 foo in the Set, because they are the same.
    // if you change the CMTime values in foo1, you will have 2 items in the Set.
}