Conforming an array to Identifiable in SwiftUI - ios

I have a little question about conforming to Identifiable in SwiftUI.
There are situations where we are required to have a given type MyType to conform to Identifiable.
But I am facing a case where I am required to have [MyType] (an array of MyType) to conform to Identifiable.
I have MyType already conforming to Identifiable. What should I do to also make [MyType] to conform to Identifiable?

I suggest embedding [MyType] in a struct, then having the struct conform to Identifiable. Something like this:
struct MyType: Identifiable {
let id = UUID()
}
struct Container: Identifiable {
let id = UUID()
var myTypes = [MyType]()
}
Usage:
struct ContentView: View {
let containers = [
Container(myTypes: [
MyType(),
MyType()
]),
Container(myTypes: [
MyType(),
MyType(),
MyType()
])
]
var body: some View {
/// no need for `id: \.self`
ForEach(containers) { container in
...
}
}
}

You can write an extension to conform an Array to Identifiable.
Since extensions can't contain stored properties, and also because it makes sense that two arrays that are the "same" to also have the same id, you'd need to compute the id based on the contents of an array.
The simplest approach here is if you can conform your type to Hashable:
extension MyType: Hashable {}
This also makes [MyType] conform to Hashable, and since id could be any Hashable, you could use the array itself as its own id:
extension Array: Identifiable where Element: Hashable {
public var id: Self { self }
}
Or, if you want, the id could be an Int:
extension Array: Identifiable where Element: Hashable {
public var id: Int { self.hashValue }
}
Of course, you can do this just for your own type where Element == MyType, but that type would need to be public.

Related

ForEach requires that '[String : String]' conform to 'Identifiable'

I don't know how to write ForEach in Array in struct by SwiftUI.
struct Group: Identifiable,Decodable{
#DocumentID var id: String?
var name: String
var members: [[String:String]]
}
I want to display member's array by using ForEach.
#ObservedObject var groupVM: GroupViewModel
var body: some View {
NavigationView {
List{
ScrollView{
LazyVStack(alignment: .leading){
ForEach(groupVM.group.members){ member in
groupMemberCell()
}
}
}
}
}
}
I got this error Referencing initializer 'init(_:content:)' on 'ForEach' requires that '[String : String]' conform to 'Identifiable'.
Please tell me how to write the right way.
This is because the value for members (of type [[String: String]]) does not automatically conform to Identifiable.
It depends on your model as to how each individual member (with data [String: String]) needs to be identified, as it's not clear from this dictionary how you would do this (a dictionary is a bunch of keys and values for each member, so how do we know which member is which based off this data?).
I'd suggest modelling each member as its own object and include a field that allows you to uniquely identify each member (such as having an id for each member, which could be their user id in your application for example).
struct Member: Identifiable, Decodable {
var id: String
var data: [String: String]
}
Your group would then look like:
struct Group: Identifiable, Decodable {
#DocumentID var id: String?
var name: String
var members: [Member]
}

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
}
}

SwiftUI is it ok to use hashValue in Identifiable

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 {

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
}

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