I'm not going to say "always", but I am going to say, "probably always".
These IDs would always be paired with the values they identify. There would never be a necessity to access any data from the IDs, because you can get it directly from the identified values.
What's being talked about here is a similar idea to using a type for custom Hashable and Equatable conformance. It doesn't have to be private, but it's probably always the appropriate scope.
struct Value {
let string = "The same for every instance."
let bool: Bool
var int: Int
}
extension Value: Hashable {
private struct EquatablesAndHashables: Hashable {
let bool: Bool
let int: Int
init(_ value: Value) {
bool = value.bool
int = value.int
}
}
static func == (lhs: Self, rhs: Self) -> Bool {
EquatablesAndHashables(lhs) == EquatablesAndHashables(rhs)
}
func hash(into hasher: inout Hasher) {
hasher.combine(EquatablesAndHashables(self))
}
}
Cool idea, would be great if constants were not included in EQ/hash by default† ! And maybe they should be not included in the instances at all, as if they were static.
† However, it would break some existing code. Cool idea for a new language still.
struct A: Hashable {
let x = "a"
let y: Int
let z: Int
}
struct B: Hashable {
var x = "a"
let y: Int
let z: Int
}
print(A(y: 1, z: 1).hashValue == B(y: 1, z: 1).hashValue) // true
A synthesized conformance here would definitely be nice, especially since Identifiable is a requirement of many SwiftUI APIs, for example navigation. And that means using enumerations in these APIs can be a real pain.
I do think in practice, though, the simplest and most straightforward synthesis would involve combining the enum tag (case offset) and the identity of each payload, and not the hashability of each payload, and so it falls in line with other synthesized conformances:
Equatable: synthesized when every associated value is equatable
Hashable: synthesized when every associated value is hashable
Identifiable: synthesized when every associated value is identifiable
While this may not satisfy some of the examples in this thread, it does satisfy a typical example of an enum one may want to make identifiable:
@Observable
final class FeatureAModel: Identifiable { // Trivially identifiable
// ...
}
@Observable
final class FeatureBModel: Identifiable { // Trivially identifiable
// ...
}
enum Destination: Identifiable { // Ideally synthesized
case featureA(FeatureAModel)
case featureB(FeatureBModel)
}
// In a view:
.sheet(item: $destination) {
switch $0 {
case .featureA(let model):
FeatureAView(model: model)
case .featureB(let model):
FeatureBView(model: model)
}
}
Cases with identifiable value types or without associated values would not interfere with the synthesis:
@Observable
final class FeatureAModel: Identifiable { // Trivially identifiable
// ...
}
struct FeatureBValue: Identifiable {
let id: UUID
// ...
}
enum Destination: Identifiable { // Ideally synthesized
case featureA(FeatureAModel)
case featureB(FeatureBValue)
case featureC
}
// In a view:
.sheet(item: $destination) {
switch $0 {
case .featureA(let model):
FeatureAView(model: model)
case .featureB(let value):
FeatureBView(value: value)
case .featureC:
FeatureCView()
}
}
These ideas are based off heavy real world use of enums in SwiftUI, especially in navigation, and have been codified in the swift-navigation package, which uses swift-case-paths to enhance enum bindings and more.
I appreciate all of the discussion in this thread. My initial idea was to have the associated values be required to be Identifiable as well and, if that were not the case, then the synthesized conformance would not be generated (similar to how it is not generated for Equatable, Hashable, and other protocols if the requirements are not met). An auto-generated conformance does not prevent the developer from generating their own conformance if an alternative is better-suited for the situation: the synthesized conformance is just there for the most common scenarios.
I think right now the biggest "blocker" I foresee with this would be an enum with multiple associated values where multiple of those values are Identifiable: should instead the requirement for auth-synthesis be that there be only 0 or 1 associated value per case? Should it instead be that there are 0 or 1 Identifiable associated values? Should the id case just pick the first Identifiable associated value? There are definitely some edge cases here. My gut feel says that the synthesized conformance should only be generated when either all or none of the associated values are themselves Identifiable since those are the cases that can most un-ambiguously be generated (and anything else will just require the developer to write out the conformance manually).
I have edited the original post with some updated thoughts and an example.
That's a good question. I think probably synthesis should only support enums with cases that are empty and cases that have a single value that is identifiable. Extending it beyond that opens up questions related to compound identity and whether or not the equivalent struct should synthesize a conformance:
enum E: Identifiable { case a(b: Identifiable, c: Identifiable) }
struct S: Identifiable { let b: Identifiable, c: Identifiable }
Which could be interesting, but requires more thought, motivating examples, etc., and could always come later.
FWIW, I don't believe that would address most of the cases in my $day_job codebase; I checked 20 different UICollectionView Item types and only 2 of them would've been satisfied by this synthesis (both simple enums without associated values).
That said, by far the most common case not satisfied (about 16/20) was items with Hashable associated values using Self as the identity (some of which may well be wrong), for which you can already just write var id: Self { self }, so although it would be nice for synthesis to be universal, I could accept that this is a different special case.
I think things could be addressed with small data model refactors that SwiftUI already demands in some cases. You earlier brought up this example:
enum MyTableCell {
case aboutThisApp
case book(ISBN, title: String, author: String)
case bookReference(Book)
}
By bundling up the product of ISBN and the title/author strings into an identifiable struct, this would become identifiable via synthesis:
extension Book {
struct Value: Identifiable {
let id: ISBN
var title: String
var author: String
}
}
enum MyTableCell: Identifiable {
case aboutThisApp
case book(Book.Value)
case bookReference(Book)
}
This is exactly what SwiftUI would demand of you if you wanted to use .sheet(item:) with the product of (ISBN, String, String).