3

I have an issue with a protocol I've defined below. I've got two requirements:

  1. I'd like to be able to use the protocol Peer as a type in other classes while keeping the concrete class private.
  2. I'd like to store the protocol in an array and be able to determine the index of an instance.

In order to satisfy the second point, I need to make the protocol conform to the Equatable protocol. But when I do that, I can no longer use Peer as a type, since it needs to be treated as a generic. This means I cannot have the concrete implementation private anymore, and requirement 1 is broken.

Wondering if anyone else has encountered this problem and gotten around it somehow. Maybe I'm misinterpreting the error I'm getting at indexOf...

Group.swift

import Foundation

class Group {
    var peers = [Peer]()

    init() {
        peers.append(PeerFactory.buildPeer("Buddy"))
    }

    func findPeer(peer: Peer) -> Bool {
        if let index = peers.indexOf(peer) {
            return true
        }
        return false
    }
}

Peer.swift

import Foundation

protocol Peer {
    var name: String { get }
}

class PeerFactory {
    static func buildPeer(name: String) -> Peer {
        return SimplePeer(name: name)
    }
}

private class SimplePeer: Peer {
    let name: String

    init(name: String) {
        self.name = name
    }
}

Error at indexOf if Peer is not Equatable:

cannot convert value of type 'Peer' to expected argument type '@noescape (Peer) throws -> Bool'
Mark
  • 7,167
  • 4
  • 44
  • 68
  • Array-of-protocol can be problematic. Did you watch the WWDC 2015 video on protocol-based Swift programming? – matt Oct 23 '15 at 02:58
  • I watched it a few months ago. I'll rematch it and see if it gives me any clues. – Mark Oct 23 '15 at 02:59
  • The problem of what it can mean for an array of protocol-adopters to be equatable is at the very heart of the video. But be warned: array-of-protocol is problematic. :) – matt Oct 23 '15 at 03:01
  • See my (unanswered) question here: http://stackoverflow.com/questions/33112559/protocol-doesnt-conform-to-itself – matt Oct 23 '15 at 03:01

2 Answers2

5

So I found a solution to get around the Equatable requirement by extending CollectionType to define a new indexOf for elements are of Peer type, which takes advantage of the other closure-based indexOf. This is essentially a convenience function which saves me from using the closure indexOf directly. Code below:

extension CollectionType where Generator.Element == Peer {
    func indexOf(element: Generator.Element) -> Index? {
        return indexOf({ $0.name == element.name })
    }
}

This of course assumes everything I need to test equality can be obtained from the Peer protocol (which is true for my specific use case).

EDIT: Update for Swift 3:

extension Collection where Iterator.Element == Peer {
    func indexOf(element: Iterator.Element) -> Index? {
        return index(where: { $0.name == element.name })
    }
}
Mark
  • 7,167
  • 4
  • 44
  • 68
  • In the general case, you'd need to write your own extension to handle your protocol for anything that requires the type to implement Equatable, and then manually perform an equality check. – Mark Oct 24 '15 at 01:16
  • This answer is basically the solution given in the WWDC 2015 video that I advised you to watch, is it not? – matt Oct 24 '15 at 21:37
  • No not exactly. They solve the `Equatable Drawable` issue by defining an `isEqual` method in the protocol, and then they wrote a protocol extension for all classes that are both `Drawable` and `Equatable` to handle the comparison. What I'm doing here is avoiding `Equatable` completely for my specific need of the `indexOf` method. This is the video I'm referencing, with the solution starting at 38:30: https://developer.apple.com/videos/play/wwdc2015-408/ – Mark Oct 24 '15 at 21:43
  • Okay, well, for people who don't see that that's the same idea, they don't see that that's the same idea. :) – matt Oct 24 '15 at 22:08
  • Swift 3 does not seems happy with this solution. Is there an updated one ? – Xvolks May 03 '17 at 16:38
  • Let me take a look at updating it tonight. – Mark May 03 '17 at 16:39
1

I would suggest you use public super class, so the class can conform to Equatable

class Peer: Equatable {
    // Read-only computed property so you can override.
    // If no need to override, you can simply declare a stored property 
    var name: String {
        get {
            fatalError("Should not call Base")
        }
    }

    // should only be called from subclass
    private init() {}
}

private class SimplePeer: Peer {
    override var name: String {
        get {
            return _name
        }
    }

    let _name: String

    init(name: String) {
        _name = name
        super.init()
    }
}

func == (lhs: Peer, rhs: Peer) -> Bool {
    return lhs.name == rhs.name
}

class PeerFactory {
    static func buildPeer(name: String) -> Peer {
        return SimplePeer(name: name)
    }
}
Cosyn
  • 4,404
  • 1
  • 33
  • 26
  • That works as well, although the necessary fatalError is a bit of a design smell. This might be a more viable option if I find that there's a lot of functions with Equatable requirements that I need to rewrite for my protocol. – Mark Oct 25 '15 at 13:25