2

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

thedp
  • 8,350
  • 16
  • 53
  • 95
Guy Niv
  • 23
  • 2

1 Answers1

1

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.
}
thedp
  • 8,350
  • 16
  • 53
  • 95
  • I tried that, but even when I add the `available` marker, compilation fails with `Protocol 'Hashable' requires 'hashValue' to be available in iOS 14.0.0 and newer`, but requiring to implementing `hashValue` is surprising because it's deprecated. – Guy Niv Nov 03 '22 at 14:31
  • @GuyNiv I've rewritten my answer. CMTime already conforms to Hashable. Am I missing something? – thedp Nov 03 '22 at 14:47
  • 1
    I appreciate your help :). I ran your code, and while it compiles, it crashes on the line `myFoos.insert(foo1)` with `EXC_BAD_ACCESS`. Tested on iPhone 11 pro, iOS 15.0. – Guy Niv Nov 03 '22 at 15:09
  • @GuyNiv NP. You're right about the crash. It works fine with iOS 16 simulator though, weird. I've updated my answer *EDIT 2*, I think you will need to implement the extension and obsolete it for iOS 16 (see code in EDIT 2) – thedp Nov 03 '22 at 16:02
  • Thanks everyone for this topic. I'm running into the same issue and getting a crash in release mode. I filed a bug [here](https://github.com/apple/swift/issues/65063), feel free to provide any further details in there. – Ian Bytchek Apr 11 '23 at 12:19
  • P.S. @GuyNiv did you manage to get around the crash? I figured a way with using `hashValue` directly, e.g., `[CMTime.zero.hashValue: "foo bar"]` – but this looks messed up… – Ian Bytchek Apr 11 '23 at 12:21
  • 1
    @IanBytchek Unfortunately @thedp answer didn't solve my problem, the implemented `hashValue` was never getting called, even for older iOS versions - so it made the app compile, but I had crashes in production because of this issue so this wasn't good for me. In the end I used the `Hashed` struct that is described in the original post. Your idea sounds similar to mine - messy but at least it works :) – Guy Niv Apr 12 '23 at 13:29