SwiftUI is it ok to use hashValue in Identifiable - ios

For structs, Swift auto synthesizes the hashValue for us. So I am tempting to just use it to conform to the identifiable.
I understand that hashValue is many to one, but ID must be one to one. However, hash collision is very rare and I have faith in the math that it's most likely never going to happen to my app before I die.
I am wondering is there any other problems besides collision?
This is my code:
public protocol HashIdentifiable: Hashable & Identifiable {}
public extension HashIdentifiable {
var id: Int {
return hashValue
}
}

using hashValue for id is a bad idea !
for example you have 2 structs
struct HashID: HashIdentifiable {
var number: Int
}
struct NormalID: Identifiable {
var id = UUID()
var number: Int
}
when number is changed:
HashID's id will be changed as well makes SwiftUI thinks that this is completely new item and old one is gone
NormalID's id stays the same, so SwiftUI knows that item only modified its property
It's very important to let SwiftUI knows what's going on, because it will affect animations, performance, ... That's why using hashValue for id makes your code looks bad and you should stay away from it.

I am wondering is there any other problems besides collision?
Yes, many.
E.g., with this…
struct ContentView: View {
#State private var toggleData = (1...5).map(ToggleDatum.init)
var body: some View {
List($toggleData) { $datum in
Toggle(datum.display, isOn: $datum.value)
}
}
}
Collisions cause complete visual chaos.
struct ToggleDatum: Identifiable & Hashable {
var value: Bool = false
var id: Int { hashValue }
var display: String { "\(id)" }
init(id: Int) {
// Disregard for now.
}
}
No collisions, with unstable identifiers, breaks your contract of "identity" and kills animation. Performance will suffer too, but nobody will care about that when it's so ugly even before they notice any slowness or battery drain.
struct ToggleDatum: Identifiable & Hashable {
var value: Bool = false
var id: Int { hashValue }
let display: String
init(id: Int) {
self.display = "\(id)"
}
}
However, while it is not acceptable to use the hash value as an identifier, it is fine to do the opposite: use the identifier for hashing, as long as you know the IDs to be unique for the usage set.
/// An `Identifiable` instance that uses its `id` for hashability.
public protocol HashableViaID: Hashable, Identifiable { }
// MARK: - Hashable
public extension HashableViaID {
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
struct ToggleDatum: HashableViaID {
var value: Bool = false
let id: Int
var display: String { "\(id)" }
init(id: Int) {
self.id = id
}
}
This protocol works perfectly for Equatable classes, as classes already have a default ID ready for use.
extension Identifiable where Self: AnyObject {
public var id: ObjectIdentifier {
return ObjectIdentifier(self)
}
}
Not that I at all recommend using a reference type for this example, but it would look like this:
public extension Equatable where Self: AnyObject {
static func == (class0: Self, class1: Self) -> Bool {
class0 === class1
}
}
class ToggleDatum: HashableViaID {

Related

VStack shows error "Instance method 'sheet(item:onDismiss:content:)' requires that 'Int' conform to 'Identifiable'"

The line VStack shows error "Instance method 'sheet(item:onDismiss:content:)' requires that 'Int' conform to 'Identifiable'" but i think i dont make any mistake there? did i?
It feel like error come from my sheet complaining my Int should conform to 'Identifiable' however is not Int is an 'Identifiable' since it's primitive? how would i solve this problem?
struct SheetView: View {
#State var bindingItemDiscounts: [Double] =
[ 0.99, 0.25, 0.31, 11.99, 19.99, 0.1, 0.5]
#State var discountMoreThanTen: Int?
var body: some View {
VStack(alignment: .leading) { <- this line
Button {
discountMoreThanTen = bindingItemDiscounts.count
} label: { Text("Check Discounts") }
}.sheet(item: $discountMoreThanTen) { newValue in
if discountMoreThanTen ?? 0 <= 10 {
List(bindingItemDiscounts, id: \.self) { discount in
Text("Discount : \(discount)")
}
}
}
}
}.
There is no such concept as Primitive in Swift. The "Primitives" you refer to are normal structs.
Int does not conform to Identifiable.
To fix your problem just extend Int to conform to Identifiable like so:
extension Int: Identifiable {
public var id: Int { return self }
}
You are using the Binding<Item?> sheet. The Item is a type, so it must conform to Identifiable in order to be identified uniquely.
Also, Int is not a primitive in Swift. Int is basically a Type just like your other custom data types, so you need to make it Identifiable manually.
Add this extension in your file will fix this problem:
extension Int: Identifiable {
public var id: Self { self }
}
If UUID() is required, then use this instead:
extension Int: Identifiable {
public var id: String {
return UUID().uuidString
}
}

Why can't one iterate through enum data within a SwiftUI view?

Say I have some enum, Channel, filled with sample property data:
enum Channel: CaseIterable {
case abc
case efg
case hij
var name: String {
switch self {
case .abc: return "ABC"
case .efg: return "EFG"
case .hij: return "HIJ"
}
}
}
extension Channel: Identifiable {
var id: Self { self }
}
And some view implementation to fill said content with:
struct ChannelHome: View {
var body: some View {
VStack {
ForEach(Channel.allCases) { channel in
NavigationLink(destination: ChannelDetail(channel)) {
Text(channel.name)
}
}
}
}
} // big closure yikes
Why doesn't this work? Is this due to the nature of enums not having static properties and SwiftUI being declarative? Are there ways around this? Such as:
struct ChannelData {
static let channels: [Channel] = [.abc, .efg, .hij]
// or
static func getChannels() -> [Channel] {
var channelArray: [Channel]
for channel in Channel.allCases {
channelArray.append(channel)
}
return channelArray
}
}
Or, more preferably, are there better practices for presenting small data sets like these in a SwiftUI view? I like being able to implement a visual UI of my properties for when I'm debugging, as a whole, at runtime (closer to the production stage).
I apologies in advance for the question, I am just getting into it with SwiftUI.
Here are 2 simple ways for you, but not sure where can be helpful for you because with enum you cannot update ForEach elements, but it has a use case for showing data!
First way without any Identifiable id:
struct ContentView: View {
var body: some View {
ForEach(TestEnum.allCases, id: \.rawValue) { item in
Text(item.rawValue)
}
}
}
enum TestEnum: String, CaseIterable { case a, b, c }
with Identifiable id that conforms to Identifiable protocol, you can cut , id: \.id even:
struct ContentView: View {
var body: some View {
ForEach(TestEnum.allCases, id: \.id) { item in
Text(item.rawValue)
}
}
}
enum TestEnum: String, CaseIterable, Identifiable { case a, b, c
var id: String { return self.rawValue }
}
Either Xcode was getting hung up on cache errors or I am a fool (probably the latter). Regardless, the issue seems to have been resolved with the following Identifiable extension to your enum, as well as a good ol' derived data purge and clean:
extension Channel: Identifiable {
var id: Self { self }
}
I seem to have confused this issue with another long-time issue that I've constantly run into; mostly regarding enums and their distaste for storing static data (as is simply just the nature of their fast and lite structure).

SwiftUI String conform Identifiable seems not correct

I am using an array in a List, in the List there's a ForEach, like:
struct AView: View {
#State var foo: [String] = ["a", "b", "c"]
var body: some View {
ZStack {
Color.white
List {
ForEach(foo.indices) { index in
Text(foo[index])
}
}
}
}
}
This works well, then I want to add a button to insert new items:
List {
ForEach(foo.indices) { index in
Text(foo[index])
}
}
Button("Add") {
foo.append("foo")
}
}
Then I got the error, which is obviously:
ForEach<Range<Int>, Int, Text> count (4) != its initial count (3). `ForEach(_:content:)` should only be used for *constant* data. Instead conform data to `Identifiable` or use `ForEach(_:id:content:)` and provide an explicit `id`!
Here mentioned
Identifiable or use ForEach(_:id:content:)
I can use ForEach(foo.indices, id:\.self) which solved the issue.
I also want to try Identifiable, and don't use id:\.self in the ForEach, ForEach(foo.indices).
I added extension to String like:
extension String: Identifiable {
public var id: String { self }
}
But still got the same issue. Any thing I miss-understood? thanks!
EDIT
According to the comment #New Dev, as I literal indices, so I added extension to Int:
extension Int: Identifiable {
public var id: Int { self }
}
still doesn't work.
ForEach has multiple init overloads.
init(Range<Int>, content: (Int) -> Content) only works with a constant range - hence the error.
init(Data, content: (Data.Element) -> Content) requires Data to conform to RandomAccessCollection of Identifiable elements. That's the one you want to use.
The problem is that your RandomAccessCollection (to which Range<Int> conforms) is a collection of Int elements.
Though you can conform Int to Identifiable, this would still not work. It would still use the first ForEach.init with a Range<Int> parameter, because of method overload preference - i.e. Range<Int> matches the more specific init with Range<Int> parameter, rather than the less specific init with RandomAccessCollection.
So, your choices are:
Use the third init(Data, id: KeyPath<Data.Element, ID>, content: (Data.Element) -> Content) by explicitly specifying the id.
ForEach(foo.indices, id:\.self) { index in
}
Convert to Array<Int> and conform Int: Identifiable:
extension Int: Identifiable { var id: Self { self } }
ForEach(Array(foo.indices)) { index in
}

Crash Supplied identifiers are not unique

I have a searchController, when I try to delete the text in searchController it crash the message is.
supplied identifiers are not unique
I try to fix it with hasher function like this and make my model Hashable, and Equatable. but still not fix the problem, please help. this is my code, if there is something not clear in my code. feel free to mention me I will update it :)
struct PhotosResults: Decodable {
var total, totalPages: Int
var results: [PhotoItem]
}
// MARK: - Result
struct PhotoItem: Decodable {
var id: String
var createdAt: String
var width, height: Int
var color: String
var likes: Int
var likedByUser: Bool
var description: String?
var user: User
var urls: Urls
}
extension PhotoItem: Hashable, Equatable {
static func == (lhs: PhotoItem, rhs: PhotoItem) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
// MARK: - Urls
struct Urls: Decodable {
var raw, full, regular, small: String
var thumb: String
}
// MARK: - User
struct User: Decodable {
var id, username, name, firstName: String
var lastName, instagramUsername, twitterUsername: String?
var portfolioUrl: String?
var profileImage: ProfileImage
}
extension User: Hashable, Equatable {
static func == (lhs: User, rhs: User) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
}
// MARK: - ProfileImage
struct ProfileImage: Decodable {
var small, medium, large: String
}
Base on Apple Documentation:
Hash values are not guaranteed to be equal across different executions
of your program. Do not save hash values to use during a future
execution.
Although you have conformed to hashable somehow it is possible that two objects have the same id and in this situation, you will get an error so in order to make sure all objects are unique you must add a UUID to your desired struct For Example the PhotoItem should look like this:
struct PhotoItem: Decodable {
let uuid = UUID()
var id: String
var createdAt: String
var width, height: Int
var color: String
var likes: Int
var likedByUser: Bool
var description: String?
var user: User
var urls: Urls
}
extension PhotoItem: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(uuid)
}
static func ==(lhs: PhotoItem, rhs: PhotoItem) -> Bool {
return lhs.uuid == rhs.uuid
}
}
you can use this solution for other structs. Hashable conforms to Equatable by default so you don't need to conform to it again.
Note: I was accidentally loading my data twice. I only got this when I scrolled back over the cell containing the id's, not first collectionview snapshot apply. So if yours only is doing it sometimes, make sure you are only loading your cells on the initial load.

Does not conform to protocol Decodable

I have the following code which represents a Hockey Stick and some information about it. I have an issue where the stick isn't conforming to Decodable. I understand that every type used in the struct needs to also be codeable, and they are. For some reason however the "var conditions" line causes the error that I am unsure how to fix. Thank you!
enum StickLocation: Int, Codable, Hashable, CaseIterable {
case handle, mid, bottom
}
enum StickCondition: Int, Codable, Hashable, CaseIterable {
case pristine, scuffed, damaged, broken
}
struct HockeyStick: Identifiable, Codable {
var barcode: Int
var brand: String
var conditions: [StickLocation:(condition:StickCondition, note:String?)] // Offending line
var checkouts: [CheckoutInfo]
var dateAdded: Date
var dateRemoved: Date?
// Conform to Identifiable.
var id: Int {
return self.barcode
}
// If the stick was never removed then its in service.
var inService: Bool {
return self.dateRemoved == nil
}
}
The value type of your conditions dictionary is (StickCondition, String?), which is a tuple. Tuples are not Decodable/Encodable, and you cannot make them conform to protocols, so to fix this I recommend you make a new struct to replace the tuple like this:
enum StickLocation: Int, Codable, Hashable, CaseIterable {
case handle, mid, bottom
}
enum StickCondition: Int, Codable, Hashable, CaseIterable {
case pristine, scuffed, damaged, broken
}
struct StickConditionWithNote: Codable, Hashable {
var condition: StickCondition
var note: String?
}
struct HockeyStick: Identifiable, Codable {
var barcode: Int
var brand: String
var conditions: [StickLocation: StickConditionWithNote]
var checkouts: [CheckoutInfo]
var dateAdded: Date
var dateRemoved: Date?
// Conform to Identifiable.
var id: Int {
return self.barcode
}
// If the stick was never removed then its in service.
var inService: Bool {
return self.dateRemoved == nil
}
}

Resources