So, this still seems impossible, where it really shouldn't. The other option is to rely on generics, but that adds quite a bit of boiler plate.
Any one suggest a way to get this working? I feel this should work...
protocol SimpleModeSelectable: Identifiable, Hashable {
var selectableModes: [Self] { get }
var title: String { get }
var id: ID { get }
}
struct SimpleModeSelectorView: View {
var title: String
@Binding var selected: any SimpleModeSelectable
var body: some View {
VStack {
SubtitleText(title)
Picker("Modes", selection: $selected) {
ForEach(selected.selectableModes) { mode in
Text(mode.title)
}
}
}
}
}
Result is the well known Type 'any XXX' cannot conform to 'Hashable' ... why? The implementation should enforce hashable, so the reason it doesn't compile makes no sense, no matter what technical jargon you throw at it. By definition it should work, because you cannot have a non "hashable" in place.
Can you use a generic version instead, if not why?
protocol SimpleModeSelectable: Identifiable, Hashable {
var selectableModes: [Self] { get }
var title: String { get }
var id: ID { get }
}
struct SimpleModeSelectorView<T: SimpleModeSelectable> : View {
var title: String
@Binding var selected: T
var body: some View {
VStack {
SubtitleText(title)
Picker("Modes", selection: $selected) {
ForEach(selected.selectableModes) { mode in
Text(mode.title)
}
}
}
}
}
Hashable refines Equatable. Two values of type any SimpleModeSelectable may have different underlying types; there’s no implementation of == which takes two values of different types to be compared for equality (nor does Swift provide a way for you to write one yourself), which is to say, any SimpleModeSelectable does not and cannot conform to Equatable, and therefore it does not and cannot conform to Hashable.
I wouldn't be so pessimistic: there is a mostly reasonable implementation (attempt to cast the types to one another, compare equality if they match, and return false otherwise), and the standard library of course already provides this via AnyHashable. So I wouldn't say it's the case that any SimpleModeSelectablecannot conform to Equatable in any sort of theoretical sense.
Of course, today, the compiler doesn't automatically synthesize this conformance for protocols which inherit from Equatable (which is why any Hashable doesn't give the same ability as AnyHashable). And even if you wanted to write this conformance yourself, it's not the implementation of == that presents the sticking point, it's the ability to conform existential types to protocols.
AnyHashable does something significantly more complex than that so as to support values of different numeric types comparing equal, and even to this day there are standing bugs where the implementation violates certain invariants. Unless things have changed recently, it is also to be noted that the implementation of this type is in C++, not Swift. An end user cannot actually replicate the behavior of AnyHashable even if the declaration of a custom Hashable conformance for an existential type were utterable.
Yeah, you're right that I was glossing over a lot of complexity underlying AnyHashable but as far as I understood much of that complexity is due to Objective-C bridging behavior rather than a desire for arbitrary numeric types to compare equal, no? Not that such behavior can be totally discounted, but in 'pure' Swift I think AnyHashable would be significantly simpler and not require custom runtime hooks.
The intended behavior of AnyHashable hasn’t been hashed out (). However, two values of arbitrarily mismatched integer types can compare equal in concrete code; it wouldn’t be very nice if they compare not-equal when boxed in AnyHashable…
I didn't realize we can compare integers of different sizes (but not floats).
Interestingly:
var x: Int16 = 1
var y: Int = 1
print(x == y, x.hashValue == y.hashValue) // true false, swift 4.2 and higher
(was "true true" in earlier versions of swift 4)
It looks like there's a set of special cased "==", "<>", "<", etc function implementations which were taught to compare integers of different sizes; perhaps the same could theoretically be done within AnyHashable to treat those comparisons the same way.
I'm not sure this totally justifies things—after all, AnyHashable does not attempt to recover this behavior for arbitrary BinaryInteger types:
struct S: BinaryInteger {
var wrapped: Int
// details omitted...
}
0 as Int == 0 as S // true
AnyHashable(0 as Int) == AnyHashable(0 as S) // false
Of course, we can restore this by conforming the 'secret' _HasCustomAnyHashableRepresentation protocol:
extension S: _HasCustomAnyHashableRepresentation {
public func _toCustomAnyHashable() -> AnyHashable? {
return wrapped._toCustomAnyHashable()
}
}
AnyHashable(0 as Int) == AnyHashable(0 as S) // true
But based on the implementation of _toCustomAnyHashable() for integer types it seems clear to me that the goal there is to match NSNumber semantics, not to attempt to provide a canonical representation based on BinaryInteger equality.
This is the exact solution I ended up going with. It feels like this is a mishmash of generics and protocols, I always felt it could/should be done in pure protocols.
In case it hasn’t been clear, I think “the ability to confirm existentials to protocols” is the feature you’re waiting for, as are many of us, to varying degrees
For reference, the final result is actually quite beautiful, even though not what I had initially desired.
See code below :
protocol SimpleModeSelectable: Identifiable, Hashable {
static var selectableModes: [Self] { get }
var title: String { get }
var id: ID { get }
}
struct SimpleModeSelectorView<T: SimpleModeSelectable>: View {
var title: String?
@Binding var modeSelectable: T
var body: some View {
VStack {
if let title {
SubtitleText(title)
}
Picker("Modes", selection: $modeSelectable) {
ForEach(T.selectableModes) { mode in
Text(mode.title)
.tag(mode)
}
}
}
}
}
Followed by something like this to implement the list of modes from an existing enum
extension DrivingLightDevice.Mode: SimpleModeSelectable {
var title: String {
switch self {
case .custom:
return R.string.localizable.main_device_driving_light_mode_custom()
case .fourbyfour:
return R.string.localizable.main_device_driving_light_mode_fourbyfour()
case .dirt:
return R.string.localizable.main_device_driving_light_mode_dirt()
case .highway:
return R.string.localizable.main_device_driving_light_mode_highway()
}
}
var id: UInt8 {
self.rawValue
}
static var selectableModes: [DrivingLightDevice.Mode] {
return [
.custom,
.dirt,
.fourbyfour,
.highway
]
}
}