1

I have 2 simple structs in my app that are conforming to a protocol, to enable some generic operations I need to apply on both types:

import UIKit

protocol Track {
    var url: URL { get set }
}

struct TrackFromMediaFile: Track {
    var url: URL
}

struct TrackFromAsset: Track {
    var url: URL
}

Now I have a list of these structs:

var tracks = [Track]()
tracks.append(TrackFromMediaFile(url: URL(string: "http://www.remotefile")!))
tracks.append(TrackFromAsset(url: URL(string: "local://localfile")!))

I need to find the index of a specific track in this list of tracks:

// throws: 
// Value of protocol type 'Track' cannot conform to 'Equatable'; only struct/enum/class types can conform to protocols
var track = tracks[0]
tracks.firstIndex(of: track)

I tried to follow approaches from https://stackoverflow.com/a/46719045/319905 and https://developer.apple.com/videos/play/wwdc2015/408/ to no avail, I couldn't adapt these to my example.

How can I implement handling arrays of tracks in a generic way?

Wolkenarchitekt
  • 20,170
  • 29
  • 111
  • 174
  • Out of curiosity, are you expecting `TrackFromMediaFile` and `TrackFromAsset` to be equatable to one another? (I'm assuming they have properties you haven't listed here, yeah?) If so, can do you describe how you would want to compare them for equality? – Itai Ferber Oct 07 '20 at 20:33
  • I would create a common struct with an enumeration to distinguish them. You are going the wrong path. You shouldn'`t have a collection of a protocol. – Leo Dabus Oct 07 '20 at 20:38
  • Btw if you really want to follow this path (I don't recommend) `let index = tracks.firstIndex(where: {track.url == $0.url})` – Leo Dabus Oct 07 '20 at 20:47
  • Looks like my question really doesn't make so much sense (equatable doesn't belong to protocols) - I just need to use the where operator on array. – Wolkenarchitekt Oct 09 '20 at 10:19

1 Answers1

2

It's not quite clear what kind of equality you're looking for. If you want to check for both type equality and value equality, you'd do that this way:

tracks.firstIndex(where: {
    type(of: $0) == type(of: track) && $0.url == track.url
})

If you just want to validate that url is equal, you'd do it this way:

tracks.firstIndex(where: { $0.url == track.url })

In Swift, Equatable means that a value can be compared to other values of the same type. It does not apply to values of different types. A protocol is not a type, it describes types, so Track itself cannot be Equatable.

If you're going to do this a lot, it is often convenient to express "able to check for equality with other members of the protocol." That is not Equatable, but it is certainly something you can express. It looks like this:

protocol Track {
    var url: URL { get set }
    func isEqual(to: Track) -> Bool
}

For Track-conforming types that are already Equatable, you can provide a default implementation:

extension Track where Self: Equatable {
    func isEqual(to other: Track) -> Bool {
        guard let other = other as? Self else { return false }
        return self == other
    }
}

And mark your conforming types as Equatable (swift will automatically create conformances):

struct TrackFromMediaFile: Track, Equatable { ... }
struct TrackFromAsset: Track, Equatable { ... }

With that, the code is a little nicer:

tracks.firstIndex(where: { $0.isEqual(to: track) })

And if you do that a lot, you can of course make it a little nicer still:

extension Collection where Element == Track {
    func firstIndex(of track: Track) {
        firstIndex(where: { $0.isEqual(to: track) })
    }
}

And with that, you can have the syntax you started with:

tracks.firstIndex(of: track)
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Thank you! Seems I had a wrong understanding about Protocols (I just started with Swift), so my question doesn't make so much sense. Your answer pointed me into the right direction - I just need to use some array helper methods like where. – Wolkenarchitekt Oct 09 '20 at 10:17