Enum Identifiable in SwiftUI [closed] - swiftui

Closed. This question is opinion-based. It is not currently accepting answers.
Want to improve this question? Update the question so it can be answered with facts and citations by editing this post.
Closed 7 months ago.
Improve this question
I am trying to conform my enum Sheets to Identifiable. This means, I must implement the id property and return it.
struct Product: Hashable {
let name: String
let price: Double
}
enum Sheets: Identifiable {
case add
case edit(Product)
var id: Int {
hashValue
}
}
The above implementation does not work and an error says that hashValue is undefined. The other implementation I used is as follows:
enum Sheets: Identifiable {
case add
case edit(Product)
var id: Int {
switch self {
case .add:
return 1
case .edit(_):
return 2
}
}
}
This does return a stable identity. Is this the recommended way to solve the problem of creating a stable identity for my enum cases.

It depends on Sheets purpose and usage, so yes, if there would be only two variants, e.g. menu item > view type, then your variant would be enough.
If you would want complete uniqueness including sheet/product combination, then I recommend something like below
struct Product: Hashable, Identifiable {
let id = UUID().uuidString // << here !!
let name: String
let price: Double
}
enum Sheets: Identifiable {
case add
case edit(Product)
var id: String {
switch self {
case .add:
return "1"
case .edit(let product):
return "2.\(product.id)" // << here !!
}
}
}
*Note: hash is not enough for identity (hash guaranties difference if two items gives different hashes, but two different items could produce same hash, even with low probability)

Related

In SwiftUI, how to create a non View object only once inside a child View

I want to link to a view that contains a non-view object - created once per user tap of the "Start" link - that is dependent on data selected by the user. The code below is as close as I've gotten. QuestionView.init is called as soon as HomeView appears, and again every time you select a new value for Highest Number, thus creating the Question object repeatedly, which is what I'm trying to avoid. I want to only create the Question object one time - when the user taps on the "Start" link.
I've tried many different approaches. It feels like I am stuck problem solving from an imperative UI oriented approach, instead of the new-for-me declarative approach of SwiftUI. Perhaps there's a bridge I'm missing from the state-driven approach of Views to the more familiar-to-me non-view objects, like my Question object I want to create only once.
struct Question {
let value1: Int
let value2: Int
let answer: Int
init(_ highestNumber: Int) {
print("Question.init \(highestNumber)")
value1 = Int.random(in: 1...highestNumber)
value2 = Int.random(in: 1...highestNumber)
answer = value1 * value2
}
var prompt: String {
"\(value1) x \(value2) = ?"
}
}
struct HomeView: View {
#State var highestNumber: Int = 12
var body: some View {
NavigationStack {
Picker("Highest Number", selection: $highestNumber) {
ForEach(4...12, id: \.self) { Text(String($0)) }
}
.pickerStyle(.wheel)
NavigationLink(destination: QuestionView(highestNumber: $highestNumber)) {
Text("Start")
}
}
}
}
struct QuestionView: View {
#Binding var highestNumber: Int
#State var question: Question
init(highestNumber: Binding<Int>) {
print("QuestionView.init")
self._highestNumber = highestNumber
question = Question(highestNumber.wrappedValue)
}
var body: some View {
Text(question.prompt)
Button("I got it") {
question = Question(highestNumber)
}
}
}

SwiftUI List: inserting item + changing section order = app crash

Please see the code below. Pressing the button once (or twice at most) is almost certain to crash the app. The app shows a list containing two sections, each of which have four items. When button is pressed, it inserts a new item into each section and also changes the section order.
I have just submitted FB9952691 to Apple. But I wonder if anyone on SO happens to know 1) Does UIKit has the same issue? I'm just curious (the last time I used UIkit was two years ago). 2) Is it possible to work around the issue in SwiftUI? Thanks.
import SwiftUI
let groupNames = (1...2).map { "\($0)" }
let groupNumber = groupNames.count
let itemValues = (1...4)
let itemNumber = itemValues.count
struct Item: Identifiable {
var value: Int
var id = UUID()
}
struct Group: Identifiable {
var name: String
var items: [Item]
var id = UUID()
// insert a random item to the group
mutating func insertItem() {
let index = (0...itemNumber).randomElement()!
items.insert(Item(value: 100), at: index)
}
}
struct Data {
var groups: [Group]
// initial data: 2 sections, each having 4 items.
init() {
groups = groupNames.map { name in
let items = itemValues.map{ Item(value: $0) }
return Group(name: name, items: items)
}
}
// multiple changes: 1) reverse group order 2) insert a random item to each group
mutating func change() {
groups.reverse()
for index in groups.indices {
groups[index].insertItem()
}
}
}
struct ContentView: View {
#State var data = Data()
var body: some View {
VStack {
List {
ForEach(data.groups) { group in
Section {
ForEach(group.items) { item in
Text("\(group.name): \(item.value)")
}
}
header: {
Text("Section \(group.name)")
}
}
}
Button("Press to crash the app!") {
withAnimation {
data.change()
}
}
.padding()
}
}
}
More information:
The error message:
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'UITableView internal inconsistency: encountered out of bounds global row index while preparing batch updates (oldRow=8, oldGlobalRowCount=8)'
The issue isn't caused by animation. Removing withAnimation still has the same issue. I believe the issue is caused by the section order change (though it works fine occasionally).
Update: Thank #Yrb for pointing out an out-of-index bug in insertItem(). That function is a setup utility in the example code and is irrelevant to the issue with change(). So please ignore it.
The problem is here:
// multiple changes: 1) reverse group order 2) insert a random item to each group
mutating func change() {
groups.reverse()
for index in groups.indices {
groups[index].insertItem()
}
}
You are attempting to do too much to the array at once, so in the middle of reversing the order, the array counts are suddenly off, and the List (and it's underlying UITableView) can't handle it. So, you can either reverse the rows, or add an item to the rows, but not both at the same time.
As a bonus, this will be your next crash:
// insert a random item to the group
mutating func insertItem() {
let index = (0...itemNumber).randomElement()!
items.insert(Item(value: 100), at: index)
}
though it is not causing the above as I fixed this first. You have set a fixed Int for itemNumber which is the count of the items in the first place. Arrays are 0 indexed, which means the initial array indices will be (0...3). This line let index = (0...itemNumber).randomElement()! gives you an index that is in the range of (0...4), so you have a 20% chance of crashing your app each time this runs. In this sort of situation, always use an index of (0..<Array.count) and make sure the array is not empty.
I got Apple's reply regarding FB9952691. The issue has been fixed in iOS16 (I verified it).

ForEach - Index out of range?

Why running this code shows "Fatal error: Index out of range"?
import SwiftUI
struct MyData {
var numbers = [Int](repeating: 0, count: 5)
}
#main
struct TrySwiftApp: App {
#State var myData = MyData()
var body: some Scene {
WindowGroup {
ChildView(myData: myData)
.frame(width: 100, height: 100)
.onAppear {
myData.numbers.removeFirst() // change myData
}
}
}
}
struct ChildView: View {
let myData: MyData // a constant
var body: some View {
ForEach(myData.numbers.indices) {
Text("\(myData.numbers[$0])") // Thread 1: Fatal error: Index out of range
}
}
}
After checking other questions,
I know I can fix it by following ways
// fix 1: add id
ForEach(myData.numbers.indices, id: \.self) {
//...
}
or
// Edited:
//
// This is not a fix, see George's reply
//
// fix 2: make ChildView conforms to Equatable
struct ChildView: View, Equatable {
static func == (lhs: ChildView, rhs: ChildView) -> Bool {
rhs.myData.numbers == rhs.myData.numbers
}
...
My Questions:
How a constant value (defined by let) got out of sync?
What ForEach really did?
Let me give you a simple example to show you what happened:
struct ContentView: View {
#State private var lowerBound: Int = 0
var body: some View {
ForEach(lowerBound..<11) { index in
Text(String(describing: index))
}
Button("update") { lowerBound = 5 }.padding()
}
}
if you look at the upper code you would see that I am initializing a ForEach JUST with a Range like this: lowerBound..<11 which it means this 0..<11, when you do this you are telling SwiftUI, hey this is my range and it will not change! It is a constant Range! and SwiftUI says ok! if you are not going update upper or lower bound you can use ForEach without showing or given id! But if you see my code again! I am updating lowerBound of ForEach and with this action I am breaking my agreement about constant Range! So SwiftUI comes and tell us if you are going update my ForEach range in count or any thing then you have to use an id then you can update the given range! And the reason is because if we have 2 same item with same value, SwiftUI would have issue to know which one you say! with using an id we are solving the identification issue for SwiftUI! About id you can use it like this: id:\.self or like this id:\.customID if your struct conform to Hash-able protocol, or in last case you can stop using id if you confrom your struct to identifiable protocol! then ForEach would magically sink itself with that.
Now see the edited code, it will build and run because we solved the issue of identification:
struct ContentView: View {
#State private var lowerBound: Int = 0
var body: some View {
ForEach(lowerBound..<11, id:\.self) { index in
Text(String(describing: index))
}
Button("update") { lowerBound = 5 }.padding()
}
}
Things go wrong when you do myData.numbers.removeFirst(), because now myData.numbers.indices has changed and so the range in the ForEach showing Text causes problems.
You should see the following warning (at least I do in Xcode 13b5) hinting this could cause issues:
Non-constant range: not an integer range
The reason it is not constant is because MyData's numbers property is a var, not let, meaning it can change / not constant - and you do change this. However the warning only shows because you aren't directly using a range literal in the ForEach initializer, so it assumes it's not constant because it doesn't know.
As you say, you have some fixes. Solution 1 where you provide id: \.self works because now it uses a different initializer. Definition for the initializer you are using:
#available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ForEach where Data == Range<Int>, ID == Int, Content : View {
/// Creates an instance that computes views on demand over a given constant
/// range.
///
/// The instance only reads the initial value of the provided `data` and
/// doesn't need to identify views across updates. To compute views on
/// demand over a dynamic range, use ``ForEach/init(_:id:content:)``.
///
/// - Parameters:
/// - data: A constant range.
/// - content: The view builder that creates views dynamically.
public init(_ data: Range<Int>, #ViewBuilder content: #escaping (Int) -> Content)
}
Stating:
The instance only reads the initial value of the provided data and doesn't need to identify views across updates. To compute views on demand over a dynamic range, use ForEach/init(_:id:content:).
So that's why your solution 1 worked. You switched to the initializer which didn't assume the data was constant and would never change.
Your solution 2 isn't really a "solution". It just doesn't update the view at all, because myData.numbers changes so early that it is always equal, so the view never updates. You can see the view still has 5 lines of Text, rather than 4.
If you still have issues with accessing the elements in this ForEach and get out-of-bounds errors, this answer may help.

A Model, a ListView, and a DetailView [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 1 year ago.
Improve this question
I'm trying to understand SwiftUI and have a simple app in mind to help me learn. My app has one model and two views, shown below.
The first question is specific: how do I get "update tournament" to work? I can't figure out which, if any, variable I should bind to or if that's even the right approach.
struct Tournament { // eventually will have more properties, such as Bool, Date, arrays, etc.
var name: String
var location: String = "Franchises"
#if DEBUG
var tournamentData = [
Tournament(name: "Season Opener"),
Tournament(name: "May Day Tournament"),
Tournament(name: "Memorial Day Tournament"),
Tournament(name: "School's Out Tournament")
]
#endif
}
struct TournamentListView: View {
// should I use a different property wrapper if this is going to be something I refer to throughout the app?
#State var tournaments = tournamentData
var body: some View {
NavigationView {
VStack {
List(tournaments, id: \.name) { tournament in
NavigationLink(
destination: TournamentDetailView(tournaments: $tournaments, tournament: tournament),
label: { Text (tournament.name)}
)
}
Spacer()
NavigationLink(
destination: TournamentDetailView(tournaments: $tournaments, addingNewTournament: true),
label: { Text ("Add a new tournament") }
)
}.navigationTitle("Tournaments")
}
}
}
struct TournamentDetailView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var tournaments: [Tournament]
#State var tournament = Tournament(name: "")
var addingNewTournament: Bool = false
var body: some View {
Form {
Section(header: Text("Tournament Info")) {
TextField("Name", text: $tournament.name)
TextField("Location", text: $tournament.location)
}
Section {
Button(action: {
if addingNewTournament { tournaments.append(tournament) }
presentationMode.wrappedValue.dismiss()
}) { Text(addingNewTournament ? "Create Tournament" : "Update Tournament") }
}
}
}
}
(P. S. I removed the second question about if my code is more or less good or if I'm not doing things "The SwiftUI Way" because my question was closed ("this will help others answer the question" my left foot!). Another case of SO getting in its own way, being an obstacle to people learning rather than a vehicle for good. I asked a very specific question, but I also took the opportunity to ask if my approach was reasonable or not. This should NOT be something that admins police, especially since, the one reply I did get actually answered a question I DID NOT ASK, but had thought about, namely about Previews. So, let me get this straight: I can only ask a single question or I'm sanctioned, but people can answer more than was asked; they just need to be clairvoyant. That's nuts. I think the reply I got exemplifies the good parts of SO - people helping people and even going the extra mile. My second question of an obviously much more general nature was not going to confuse anyone who read my question.)
First thing is, you should keep in mind that you have just one source of truth to drive your entire application. This source of truth will be passed around as an EnvironmentObject. So all your tournamentData will be inside this class. For example:
final class DataStore : ObservableObject{
#if DEBUG
#Published var tournamentData = [
Tournament(name: "Season Opener"),
Tournament(name: "May Day Tournament"),
Tournament(name: "Memorial Day Tournament"),
Tournament(name: "School's Out Tournament")
]
#else
#Published var tournamentData = [Tournament]()
#endif
}
This should be passed around from your top level view.
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(DataStore())
}
}
And just access this from each of your child views.
#EnvironmentObject var store : DataStore
Then you should just update this single source of truth and the tournamentData inside of it. Everything should work as expected.
I recommend seeing apple's WWDC SwiftUI videos. They explain it pretty well. Or if you want a more hands on approach first, go through this tutorial. It is amazing.
Edit: For completion sake, I think I should add in the proper method of handling DEBUG only code as well. The above will work but what you should really do is assign models for debugging inside the PreviewProvider. Apple removes it from production code anyways.
So inside your View:
struct Listing_Previews: PreviewProvider {
static var previews: some View {
let store = DataStore()
store.tournamentData = [
Tournament(name: "Season Opener"),
Tournament(name: "May Day Tournament"),
Tournament(name: "Memorial Day Tournament"),
Tournament(name: "School's Out Tournament")
]
return Listing().environmentObject(store)
}
}
and remove the #if DEBUG from your DataStore

SwiftUI picker driven by an enum: value not updated

According to Apple's documentation regarding Picker in SwiftUI using an Enum, if the enum conforms to the Identifiable protocol in addition to CaseIterable, a picker iterating over all cases should update the bound variable natively.
I tested it, and it does not work as expected.
enum Flavor: String, CaseIterable, Identifiable {
case chocolate
case vanilla
case strawberry
var id: String { self.rawValue }
}
struct EnumView: View {
#State private var selectedFlavor = Flavor.chocolate
var body: some View {
VStack {
Picker("Flavor", selection: $selectedFlavor) {
ForEach(Flavor.allCases) { flavor in
Text(flavor.rawValue.capitalized)//.tag(flavor)
}
}
Text("Selected flavor: \(selectedFlavor.rawValue)")
}
}
}
However, if I pass a tag for each view, it works.
What's happening here? Is the Apple documentation wrong? The selectedFlavor variable expects a value of type Flavor, but the id used in the picker is actually a String.
Thanks.
For a Picker to work properly, its elements need to be identified.
Note that the selectedFlavor variable is of type Flavor. Which means the options in the Picker should be identified as Flavors (not Strings).
However, in your code your id is of type String:
var id: String { self.rawValue }
You can either:
provide a tag (of type Flavor):
Text(flavor.rawValue.capitalized)
.tag(flavor)
conform Flavor to Identifiable by providing a custom id of type Flavor:
var id: Flavor { self }
specify the id parameter (of type Flavor) explicitly in the ForEach:
ForEach(Flavor.allCases, id: \.self) { ... }
change the selectedFlavor to be a String:
#State private var selectedFlavor = Flavor.chocolate.rawValue
This is an addition to #pawello2222 already awesome answer.
Just to clarify that the solution to conform the enum to Identifiable using the below code works to correctly tag the UI picker without explicitly declaring it within the picker code:
var id: Flavor { self }
The official Apple documentation for the swiftUI picker says to id the enum using var id: String { self.rawValue } and does not work as expected using the rest of the example code.