12

I'm trying to create a simple protocol that says whether or not an object is in an "on" state or an "off" state. The interpretation of what that is depends on the implementing object. For a UISwitch, it's whether the switch is on or off (duh). For a UIButton, it could be whether the button is in the selected state or not. For a Car, it could be whether the car's engine is on or not, or even if it is moving or not. So I set out to create this simple protocol:

protocol OnOffRepresentable {
    func isInOnState() -> Bool
    func isInOffState() -> Bool
}

Now I can extent the aforementioned UI controls like so:

extension UISwitch: OnOffRepresentable {
    func isInOnState() -> Bool { return on }
    func isInOffState() -> Bool { return !on }
}

extension UIButton: OnOffRepresentable {
    func isInOnState() -> Bool { return selected }
    func isInOffState() -> Bool { return !selected }
}

Now I can make an array of these kinds of objects and loop over it checking whether they are on or off:

let booleanControls: [OnOffRepresentable] = [UISwitch(), UIButton()]
booleanControls.forEach { print($0.isInOnState()) }

Great! Now I want to make a dictionary that maps these controls to a UILabel so I can change the text of the label associated with the control when the control changes state. So I go to declare my dictionary:

var toggleToLabelMapper: [OnOffRepresentable : UILabel] = [:]
// error: type 'OnOffRepresentable' does not conform to protocol 'Hashable'

Oh! Right! Silly me. Ok, so let me just update the protocol using protocol composition (after all, the controls I want to use here are all Hashable: UISwitch, UIButton, etc):

protocol OnOffRepresentable: Hashable {
    func isInOnState() -> Bool
    func isInOffState() -> Bool
}

But now I get a new set of errors:

error: protocol 'OnOffRepresentable' can only be used as a generic constraint because it has Self or associated type requirements
error: using 'OnOffRepresentable' as a concrete type conforming to protocol 'Hashable' is not supported

Ok... So I do some stack overflow digging and searching. I find many articles that seem promising, like Set and protocols in Swift, Using some protocol as a concrete type conforming to another protocol is not supported, and I see that there are some great articles out there on type erasure that seem to be exactly what I need: http://krakendev.io/blog/generic-protocols-and-their-shortcomings, http://robnapier.net/erasure, and https://realm.io/news/type-erased-wrappers-in-swift/ just to name a few.

This is where I get stuck though. I've tried reading through all these, and I've tried to create a class that will be Hashable and also conform to my OnOffRepresentable protocol, but I can't figure out how to make it all connect.

1
  • I first attempted to mimic the version from krakendev, but I got hung up on the fact that the MythicalType protocol was the only protocol he was trying to conform to. So when I try substituting MythicalType directly for my OnOffRepresentable it's not enough. I can't figure out how to jam the Hashable part into it. The same applied for Rob Napier's blog, ... maybe. His Animal protocol is generic on a Food associatedtype, but that's not quite what I'm doing either. The realm.io examples seemed like they'd be best, but SequenceType is also very different from my specific needs. Commented Aug 19, 2016 at 23:46

2 Answers 2

3

I don't know if I'd necessarily make the OnOffRepresentable protocol inherit from Hashable. It doesn't seem like something that you'd want to be represented as on or off must also be hashable. So in my implementation below, I add the Hashable conformance to the type erasing wrapper only. That way, you can reference OnOffRepresentable items directly whenever possible (without the "can only be used in a generic constraint" warning), and only wrap them inside the HashableOnOffRepresentable type eraser when you need to place them in sets or use them as dictionary keys.

protocol OnOffRepresentable {
    func isInOnState() -> Bool
    func isInOffState() -> Bool
}

extension UISwitch: OnOffRepresentable {
    func isInOnState() -> Bool { return on }
    func isInOffState() -> Bool { return !on }
}

extension UIButton: OnOffRepresentable {
    func isInOnState() -> Bool { return selected }
    func isInOffState() -> Bool { return !selected }
}

struct HashableOnOffRepresentable : OnOffRepresentable, Hashable {

    private let wrapped:OnOffRepresentable
    private let hashClosure:()->Int
    private let equalClosure:Any->Bool

    var hashValue: Int {
        return hashClosure()
    }

    func isInOnState() -> Bool {
        return wrapped.isInOnState()
    }

    func isInOffState() -> Bool {
        return wrapped.isInOffState()
    }

    init<T where T:OnOffRepresentable, T:Hashable>(with:T) {
        wrapped = with
        hashClosure = { return with.hashValue }
        equalClosure = { if let other = $0 as? T { return with == other } else { return false } }
    }
}

func == (left:HashableOnOffRepresentable, right:HashableOnOffRepresentable) -> Bool {
    return left.equalClosure(right.wrapped)
}

func == (left:HashableOnOffRepresentable, right:OnOffRepresentable) -> Bool {
    return left.equalClosure(right)
}

var toggleToLabelMapper: [HashableOnOffRepresentable : UILabel] = [:]

let anySwitch = HashableOnOffRepresentable(with:UISwitch())
let anyButton = HashableOnOffRepresentable(with:UIButton())

var switchLabel:UILabel!
var buttonLabel:UILabel!

toggleToLabelMapper[anySwitch] = switchLabel
toggleToLabelMapper[anyButton] = buttonLabel
Sign up to request clarification or add additional context in comments.

3 Comments

This is really close. I think there's something wrong with the equalClosure though. If you instantiate actual UILabels and store them in the dictionary and then try to extract the label you just stored, it's always nil. Which makes me think the == operator isn't working as intended.
@TimFuqua. Good catch! I fixed the == function and added an additional overload so you can compare directly with an unwrapped instance of the protocol. I think that should fix the issue, but I'm typing this on my phone and haven't verified in a playground yet
There we go! Yeah, I figured it was a simple fix, but I wasn't sure exactly what. Very nice answer. And it's a good, straightforward approach and example to reference again in the future.
0

Creating a protocol with an associatedType (or conforming it to another protocol that has an associatedType like Hashable) will make that protocol very unfriend with generics.

I'll suggest you a very simple workaround

OnOffRepresentable

First of all we don't need 2 functions that say exactly the opposite right? ;)

So this

protocol OnOffRepresentable {
    func isInOnState() -> Bool
    func isInOffState() -> Bool
}

becomes this

protocol OnOffRepresentable {
    var on: Bool { get }
}

and of course

extension UISwitch: OnOffRepresentable { }

extension UIButton: OnOffRepresentable {
    var on: Bool { return selected }
}

Pairing a OnOffRepresentable to a UILabel

Now we can't use OnOffRepresentable as Key of a Dictionary because our protocol must be Hashable. Then let's use another data structure!

let elms: [(OnOffRepresentable, UILabel)] = [
    (UISwitch(), UILabel()),
    (UIButton(), UILabel()),
]

That's it.

1 Comment

From the beginning, I knew I could already do an array of OnOffRepresentables. That's not the point though. I needed them to be Hashable for easy lookup in a dictionary. Linearly searching through an array of pairs to find your exact OnOffRepresentable control is not optimal and doesn't scale well. That is why a dictionary was mentioned in the example. And I'm indifferent about changing the 2 functions to 2 computed properties (yes, 2, because I'm the kind of dev that prefers using off over !on, so I'd implement an off property as well.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.