Reputation: 1705
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.
Upvotes: 12
Views: 5412
Reputation: 13679
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
Upvotes: 3
Reputation: 59496
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
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 }
}
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.
Upvotes: 0