Search code examples
iosswiftdiffabledatasource

DiffableDataSource throws "Fatal: supplied identifiers are not unique." when wrapping MPMediaItem inside a struct


I used UITableviewDiffableDataSource with UITableView to display songs from the music library. This code worked fine:

let tracks: [MPMediaItem] = MPMediaQuery.songs().items ?? []
self.dataSource.apply(section: 0, items: tracks)

But when I wrapped MPMediaItem inside a custom Track struct, I got this error: Fatal: supplied identifiers are not unique.

struct Track: Equatable, Hashable {
  let item: MPMediaItem

  var title: String? { item.title }

  init(item: MPMediaItem) {
    self.item = item
  }
}
let items = MPMediaQuery.songs().items ?? []
let tracks: [Track] = items.map { Track(item: $0) }
self.dataSource.apply(section: 0, items: tracks)

MPMediaItem has already conformed Equatable and Hashable so I think it should be fine if I use it in another struct which also conforms Equatable and Hashable (Track struct).

Update 1: apply(section:items:) is an extension I added to UITableViewDiffableDataSource for convenient:

extension UITableViewDiffableDataSource {
  func apply(section: SectionIdentifierType, items: [ItemIdentifierType], animatingDifferences: Bool = false) {
    var snapshot = NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType>()
    snapshot.appendSections([section])
    snapshot.appendItems(items)
    apply(snapshot, animatingDifferences: animatingDifferences)
  }
}

Update 2: It worked after I conformed Track to Identifiable protocol:

struct Track: Equatable, Hashable, Identifiable {
  let item: MPMediaItem

  let id: MPMediaEntityPersistentID

  var title: String? { item.title }

  init(item: MPMediaItem) {
    self.item = item
    self.id = item.persistentID
  }
}

or even changed title to store property also worked without any errors:

struct Track: Equatable, Hashable {
  let item: MPMediaItem

  let title: String?

  init(item: MPMediaItem) {
    self.item = item
    self.title = item.title
  }
}

What makes it so different between these cases? And why do I got the error when using MPMediaItem as the only store property of the Track struct? Thanks in advance!


Solution

  • I'm going to guess that there's a bug in MPMediaItem's hashability. This can cause you to get different answers for the two situations you have described. In this example, I'll deliberately make a buggy NSObject:

    class Dog : NSObject {
        let name : String?
        init(name:String?) {self.name = name}
        override func isEqual(_ object: Any?) -> Bool {
            if let dog = object as? Dog {
                return self.name == dog.name
            }
            return false
        }
    }
    
    struct DogHolder : Hashable {
        let dog : Dog
        var name : String? { dog.name }
    }
    

    Here's a test:

        var set = Set<DogHolder>()
        let dh1 = DogHolder(dog:Dog(name:"rover"))
        let dh2 = DogHolder(dog:Dog(name:"rover"))
        set.insert(dh1)
        set.insert(dh2)
        print(set.count)
    
        do {
            var set = Set<Dog>()
            let dh1 = Dog(name:"rover")
            let dh2 = Dog(name:"rover")
            set.insert(dh1)
            set.insert(dh2)
            print(set.count)
        }
    

    Run the test over and over. Sometimes I get 1 and 2. Sometimes I get 2 and 1. Sometimes I get 1 and 1. Sometimes I crash.

    I don't know what the precise problem is, but evidently exposing the NSObject hashability to Swift's hashability requirements exposes the bug. I would suggest reporting this to Apple and meanwhile continue to use a workaround such as your identifier.