SwiftUI NavigationView pop itself when a datasource is finished loading - ios

Let say you are loading some data from network when you are at screen A that it takes sometime. While you are waiting you can navigate to other screens by using NavigationLink. So, at the moment you are at the screen B then the data from network is finished loading and send value back to datasource in screen A. The NavigationView pop itself automatically so you back to screen A by unintentionally. Do you have any idea? Thanks.
Example
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
NavigationView {
List(viewModel.dataSource, id: \.self) { item in
NavigationLink(destination: Text("\(item)")) {
Text("\(item)")
.padding()
}
}
}
}
}
class ViewModel: ObservableObject {
#Published private(set) var dataSource = [1, 2, 3, 4, 5]
init() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { // simulate calling webservice
self.dataSource[0] = 99
}
}
}

This happens because you're specifying id as item itself, and when list updated there's no original item anymore, so it closes
If you just wanna modify items without adding/removing/reordering, you can make index your item id:
NavigationView {
List(viewModel.dataSource.indices, id: \.self) { i in
let item = viewModel.dataSource[i]
NavigationLink(destination: Text("\(item)")) {
Text("\(item)")
.padding()
}
}
}
But with more complex data you need to have your items Identifiable with unique ids, and you won't have such problem. Check out this example:
struct ContentView: View {
#ObservedObject var viewModel = ViewModel()
var body: some View {
NavigationView {
List(viewModel.dataSource) { item in
NavigationLink(destination: Text("\(item.value)")) {
Text("\(item.value)")
.padding()
}
}
}
}
}
class ViewModel: ObservableObject {
#Published private(set) var dataSource: [Item] = [1, 2, 3, 4, 5]
init() {
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [self] in // simulate calling webservice
// you're modifying value but id stays the same
self.dataSource[0].value = 99
}
}
}
struct Item: Identifiable, ExpressibleByIntegerLiteral {
let id = UUID()
var value: Int
init(integerLiteral value: IntegerLiteralType) {
self.value = value
}
}

Related

The body of view to be destroyed gets called (but it shouldn't)

While verifying how binding invalidates a view (indirectly), I find an unexpected behavior.
If the view hierarchy is
list view -> detail view
it works fine (as expected) to press a button in the detail view to delete the item.
However, if the view hierarchy is
list view -> detail view -> another detail view (containing the same item)
it crashes when I press a button in the top-most detail view to delete the item. The crash occurs in the first detail view (the underlying one), because its body gets called.
To put it in another way, the behavior is:
If the detail view is the top-most view in the navigation stack, its body doesn't get called.
Otherwise, its body gets called.
I can't think out any reason for this behavior. My debugging showed below are what happened before the crash:
I pressed a button in top-most detail view to delete the item.
The ListView's body got called (as a result of ContentView body got called). It created only the detail view for the left item.
Then the first DetailView's body get called. This is what caused the crash. I can't think out why this occurred, because it certainly didn't occur for the top-most detail view.
Below is the code. Note the ListView and DetailView contains only binding and regular properties (they don't contain observable object or environment object, which I'm aware complicate the view invalidation behavior).
import SwiftUI
struct Foo: Identifiable {
var id: Int
var value: Int
}
// Note that I use forced unwrapping in data model's APIs. This is intentional. The rationale: the caller of data model API should make sure it passes a valid id.
extension Array where Element == Foo {
func get(_ id: Int) -> Foo {
return first(where: { $0.id == id })!
}
mutating func remove(_ id: Int) {
let index = firstIndex(where: { $0.id == id })!
remove(at: index)
}
}
class DataModel: ObservableObject {
#Published var foos: [Foo] = [Foo(id: 1, value: 1), Foo(id: 2, value: 2)]
}
struct ListView: View {
#Binding var foos: [Foo]
var body: some View {
NavigationView {
List {
ForEach(foos) { foo in
NavigationLink {
DetailView(foos: $foos, fooID: foo.id, label: "First detail view")
} label: {
Text("\(foo.value)")
}
}
}
}
}
}
struct DetailView: View {
#Binding var foos: [Foo]
var fooID: Int
var label: String
var body: some View {
// The two print() calls are for debugging only.
print(Self._printChanges())
print(label)
print(fooID)
return VStack {
Text(label)
Divider()
Text("Value: \(foos.get(fooID).value)")
NavigationLink {
DetailView(foos: $foos, fooID: fooID, label: "Another detail view")
} label: {
Text("Create another detail view")
}
Button("Delete It") {
foos.remove(fooID)
}
}
}
}
struct ContentView: View {
#StateObject var dataModel = DataModel()
var body: some View {
ListView(foos: $dataModel.foos)
}
}
Test 1: Start the app, click on an item in the list view to go to the detail view, then click on "Delete It" button. This works fine.
The view hierarchy: list view -> detail view
Test 2: Start the app, click on an item in the list view to go to the detail view, then click on "Create another detail view" to go to another detail view. Then click on "Delete It" button. The crashes the first detail view.
The view hierarchy: list view -> detail view -> another detail view
Could it be just another bug of #Binding? Is there any robust way to work around the issue?
You need to use your data model rather than performing procedural code in your views. Also, don't pass items by id; Just pass the item.
Because you use the id of the Foo instead of the Foo itself, and you have a force unwrap in your get function, you get a crash.
If you refactor to use your model and not use ids it works as you want.
You don't really need your array extension. Specialised code as an extension to a generic object doesn't look right to me.
The delete code is so simple you can just handle it in your model, and do so safely with conditional unwrapping.
class DataModel: ObservableObject {
#Published var foos: [Foo] = [Foo(id: 1, value: 1), Foo(id: 2, value: 2)]
func delete(foo: Foo) {
if let index = firstIndex(where: { $0.id == id }) {
self.foos.remove(at: index)
}
}
}
struct ListView: View {
#ObservedObject var model: DataModel
var body: some View {
NavigationView {
List {
ForEach(model.foos) { foo in
NavigationLink {
DetailView(model: model, foo: foo, label: "First detail view")
} label: {
Text("\(foo.value)")
}
}
}
}
}
}
struct DetailView: View {
#ObservedObject var model: DataModel
var foo: Foo
var label: String
var body: some View {
// The two print() calls are for debugging only.
print(Self._printChanges())
print(label)
print(foo.id)
return VStack {
Text(label)
Divider()
Text("Value: \(foo.value)")
NavigationLink {
DetailView(model: model, foo: foo, label: "Another detail view")
} label: {
Text("Create another detail view")
}
Button("Delete It") {
model.delete(foo:foo)
}
}
}
}
I think this is very much like Paul's approach. I just kept the Array extension with the force unwrap as in OP.
struct Foo: Identifiable {
var id: Int
var value: Int
}
// Note that I use forced unwrapping in data model's APIs. This is intentional. The rationale: the caller of data model API should make sure it passes a valid id.
extension Array where Element == Foo {
func get(_ id: Int) -> Foo {
return first(where: { $0.id == id })!
}
mutating func remove(_ id: Int) {
let index = firstIndex(where: { $0.id == id })!
remove(at: index)
}
}
class DataModel: ObservableObject {
#Published var foos: [Foo] = [Foo(id: 1, value: 1), Foo(id: 2, value: 2), Foo(id: 3, value: 3)]
}
struct ListView: View {
#EnvironmentObject var dataModel: DataModel
var body: some View {
NavigationView {
List {
ForEach(dataModel.foos) { foo in
NavigationLink {
DetailView(foo: foo, label: "First detail view")
} label: {
Text("\(foo.value)")
}
}
}
}
}
}
struct DetailView: View {
#EnvironmentObject var dataModel: DataModel
var foo: Foo
var label: String
var body: some View {
// The two print() calls are for debugging only.
print(Self._printChanges())
print(label)
print(foo.id)
return VStack {
Text(label)
Divider()
Text("Value: \(foo.value)")
NavigationLink {
DetailView(foo: foo, label: "Yet Another detail view")
} label: {
Text("Create another detail view")
}
Button("Delete It") {
dataModel.foos.remove(foo.id)
}
}
}
}
struct ContentView: View {
#StateObject var dataModel = DataModel()
var body: some View {
ListView()
.environmentObject(dataModel)
}
}
Here is a working version. It's best to pass the model around so you can use array subscripting to mutate.
I also changed your id to UUID because that's what I'm used to and changed some vars that should be lets.
import SwiftUI
struct Foo: Identifiable {
//var id: Int
let id = UUID()
var value: Int
}
// Note that I use forced unwrapping in data model's APIs. This is intentional. The rationale: the caller of data model API should make sure it passes a valid id.
//extension Array where Element == Foo {
// func get(_ id: Int) -> Foo {
// return first(where: { $0.id == id })!
// }
//
// mutating func remove(_ id: Int) {
// let index = firstIndex(where: { $0.id == id })!
// remove(at: index)
// }
//}
class DataModel: ObservableObject {
//#Published var foos: [Foo] = [Foo(id: 1, value: 1), Foo(id: 2, value: 2)]
#Published var foos: [Foo] = [Foo(value: 1), Foo(value: 2)]
func foo(id: UUID) -> Foo? {
foos.first(where: { $0.id == id })
}
}
struct ListView: View {
//#Binding var foos: [Foo]
#StateObject var dataModel = DataModel()
var body: some View {
NavigationView {
List {
//ForEach(foos) { foo in
ForEach(dataModel.foos) { foo in
NavigationLink {
//DetailView(foos: $foos, fooID: foo.id, label: "First detail view")
DetailView(dataModel: dataModel, foo: foo, label: "First detail view")
} label: {
Text("\(foo.value)")
}
}
}
}
}
}
struct DetailView: View {
//#Binding var foos: [Foo]
#ObservedObject var dataModel: DataModel
//var fooID: Int
let foo: Foo
let label: String
var body: some View {
// The two print() calls are for debugging only.
print(Self._printChanges())
print(label)
//print(fooID)
print(foo.id)
return VStack {
Text(label)
Divider()
//Text("Value: \(foos.get(fooID).value)")
if let foo = dataModel.foo(id:foo.id) {
Text("Value: \(foo.value) ")
}
NavigationLink {
DetailView(dataModel: dataModel, foo: foo, label: "Another detail view")
} label: {
Text("Create another detail view")
}
Button("Delete It") {
//foos.remove(fooID)
if let index = dataModel.foos.firstIndex(where: { $0.id == foo.id } ) {
dataModel.foos.remove(at: index)
}
}
}
}
}
struct ContentView: View {
// no need for # here because body doesn't need to update when model changes
//#StateObject var dataModel = DataModel()
var body: some View {
//ListView(foos: $dataModel.foos)
ListView()
}
}
This is a version that uses Paul's approach but still uses binding. Note both versions don't really "solve" the issue (the behavior I described in my original question still exists) but instead "avoid" the crash by not accessing data model when rendering the view hierarchy in the body. I think this is a key point to use a framework successfully - don't fight it.
Regarding the use of binding in the code example, I'm aware most people use ObservableObject or EnvironmentObject. I used to do that too. I noticed the use of binding in Apple's demo app. But I may consider to switch back to the view model approach.
import SwiftUI
struct Foo: Identifiable {
var id: Int
var value: Int
}
// Note that I use forced unwrapping in data model's APIs. This is intentional. The rationale: the caller of data model API should make sure it passes a valid id.
extension Array where Element == Foo {
func get(_ id: Int) -> Foo {
return first(where: { $0.id == id })!
}
mutating func remove(_ id: Int) {
let index = firstIndex(where: { $0.id == id })!
remove(at: index)
}
}
class DataModel: ObservableObject {
#Published var foos: [Foo] = [Foo(id: 1, value: 1), Foo(id: 2, value: 2)]
}
struct ListView: View {
#Binding var foos: [Foo]
var body: some View {
NavigationView {
List {
ForEach(foos) { foo in
NavigationLink {
DetailView(foos: $foos, foo: foo, label: "First detail view")
} label: {
Text("\(foo.value)")
}
}
}
}
}
}
struct DetailView: View {
#Binding var foos: [Foo]
var foo: Foo
var label: String
var body: some View {
// The two print() calls are for debugging only.
print(Self._printChanges())
print(label)
print(foo)
return VStack {
Text(label)
Divider()
Text("Value: \(foo.value)")
NavigationLink {
DetailView(foos: $foos, foo: foo, label: "Another detail view")
} label: {
Text("Create another detail view")
}
Button("Delete It") {
foos.remove(foo.id)
}
}
}
}
struct ContentView: View {
#StateObject var dataModel = DataModel()
var body: some View {
ListView(foos: $dataModel.foos)
}
}

Can't Update Text on Previous View using #EnvironmentObject SwiftUI

i'm new in SwiftUI. So, I am doing my practice and create some kind of cashier app and here I been stuck for a while. In ProductPageView I can increase and decrease the number of item. Then, when I go to DetailOrderView (like a cart view), I can also increase and decrease the quantity number. The print shows the quantity number correctly.
But if I go back to ProductPageView, the label doesn't update it self.
Take a look at this screenshots:
First, I add 2 items.
Then I go to DetailOrderView, the quantity number is the same.:
Then I add 2 items in DetailOrderView (so it's now 4 items) and going back to ProductPageView, notice how the total price has increased but the quantity doesn't change. I feel like I want to "refresh" or "reload data" on this page.
How can I fix this and update the Text when I press the back button?
Here's my code:
Main
import SwiftUI
#main
struct CashierApp: App {
#StateObject var productOder = ProductPresenter()
var body: some Scene {
WindowGroup {
MainPageView()
.environmentObject(productOder)
}
}
}
Product Page View
import SwiftUI
struct ProductPageView: View {
#EnvironmentObject var productOrder: ProductPresenter
var body: some View {
VStack {
ScrollView {
VStack {
ForEach(productOrder.cartItems, id: \.item.idItem) { element in
ProductRow(cartItems: CartItems(item: element.item, quantity: element.quantity))
}
}
}
TotalPaymentRow()
}
}
}
Detail Order View or Cart View
import SwiftUI
struct DetailOrderView: View {
#EnvironmentObject var productOrder: ProductPresenter
var arrayOrdered: [CartItems] = []
var body: some View {
ZStack {
ScrollView {
VStack {
ForEach(arrayOrdered, id: \.item.idItem) { element in
ProductRow(cartItems: CartItems(item: element.item, quantity: element.quantity))
}
}
.padding(.top, 10)
}
CustomModalGrandTotal()
}
.navigationTitle("Cart")
.navigationBarTitleDisplayMode(.inline)
}
}
The Presenter
import SwiftUI
import Combine
class ProductPresenter: ObservableObject {
#Published var cartItems: [CartItems] = []
var totalQuantity = 0
// Increment product
func increment(cartItem: inout CartItems) {
let index: Int = cartItems.firstIndex(where: {$0.item == cartItem.item}) ?? -1
cartItems[index].quantity += 1
totalQuantity += 1
}
// Decrement product
func decrement(cartItem: inout CartItems) {
let index: Int = cartItems.firstIndex(where: {$0.item == cartItem.item}) ?? -1
if cartItems[index].quantity > 0 {
cartItems[index].quantity -= 1
totalQuantity -= 1
}
}
}
Product Row
import SwiftUI
struct ProductRow: View {
#State var cartItems: CartItems
#EnvironmentObject var productOrder: ProductPresenter
var body: some View {
HStack(alignment: .center) {
Image(cartItems.item.image ?? "")
VStack(alignment: .leading, spacing: 8) {
Text(cartItems.item.name ?? "")
Text(getPrice(value: cartItems.item.price ?? 0))
}
Spacer()
VStack {
Spacer()
HStack{
Button(action: {
if cartItems.quantity > 0 {
cartItems.quantity -= 1
productOrder.decrement(cartItem: &cartItems)
productOrder.calculateTotalPrice()
}
}, label: {
imageMinusPlus(name: "minus")
})
Text("\(cartItems.quantity)") // This Text should be updated.
Button(action: {
cartItems.quantity += 1
productOrder.increment(cartItem: &cartItems)
productOrder.calculateTotalPrice()
}, label: {
imageMinusPlus(name: "plus")
})
}
}
}
}
}
PS: I deleted the styling to make the code shorter.
Thankyou in advance
Besides passing "ProductPresenter" instance around as "environment" object (which you are handling correctly) you need to declare your properties as #Published, as in this case for "totalQuantity", which now updates the total overall quantity.
class ProductPresenter: ObservableObject {
#Published var cartItems: [CartItems] = [
CartItems(item: CartItems.Item(image: "system.car", name: "My item", price: 10, idItem: UUID()), quantity: 3)
]
#Published var totalQuantity = 0
}
But this doesnt solve this line:
Text("\(cartItems.quantity)") // This Text should be updated.
for following reasons:
your model (CartItems) has to be a class type and conform to
ObservableObject protocol
your property has to be set using #Published.
I think productOrder need to be defined as ObservableObject with #Published properties and int this case it need to be class not a structure, for example:
import SwiftUI
import Combine
final class ProductOder: ObservableObject {
#Published var cartItems: [CartItem] = []
}

SwiftUI is it possible to pass range of binding in ForEach?

I'd like to pass a range of an array in a model inside ForEach.
I recreated an example:
import SwiftUI
class TheModel: ObservableObject {
#Published var list: [Int] = [1,2,3,4,5,6,7,8,9,10]
}
struct MainView: View {
#StateObject var model = TheModel()
var body: some View {
VStack {
ForEach (0...1, id:\.self) { item in
SubView(subList: $model.list[0..<5]) <-- error if I put a range
}
}
}
}
struct SubView: View {
#Binding var subList: [Int]
var body: some View {
HStack {
ForEach (subList, id:\.self) { item in
Text("\(item)")
}
}
}
}
struct MainView_Previews: PreviewProvider {
static var previews: some View {
MainView()
}
}
The work around
I found is to pass all the list and perform the range inside the subView. But I'd like don't do this because the array is very big:
struct MainView: View {
#StateObject var model = TheModel()
var body: some View {
VStack {
ForEach (0...1, id:\.self) { i in
SubView(subList: $model.list, number: i, dimension: 5)
}
}
}
}
struct SubView: View {
#Binding var subList: [Int]
var number: Int
var dimension: Int
var body: some View {
HStack {
ForEach (subList[number*dimension..<dimension*(number+1)].indices, id:\.self) { idx in
Button(action: {
subList[idx] += 1
print(subList)
}, label: {
Text("num: \(subList[idx])")
})
}
}
}
}
I would pass the model to the subview since it is a class and will be passed by reference and then pass the range as a separate parameter.
Here is my new implementation of SubView
struct SubView: View {
var model: TheModel
var range: Range<Int>
var body: some View {
HStack {
ForEach (model.list[range].indices, id:\.self) { idx in
HStack {
Button(action: {
model.list[idx] += 1
print(model.list)
}, label: {
Text("num: \(model.list[idx])")
})
}
}
}
}
}
Note that I added indices to the ForEach header to make sure we access the array using an index and not with a value from the array.
The calling view would then look like
var body: some View {
VStack {
SubView(model: model, range: (0..<5))
Text("\(model.list.map(String.init).joined(separator: "-"))")
}
The extra Text is just there for testing purposes

How to update ParentView after updating SubView #ObservedObject SwiftUI

This is a simple example for my case.
I have a #ObservedObject viewModel (Object1), pass a property (Object2) to another view (View2) . Change value in View 2, and when i go back to View 1, i wish the value is updated too. What is the best solution?
In this Example, when i press the blue number, i wish black number is also updated.
Actually I don't know why do the black number is updated after pressing button "Show".
I would really appreciate if you could help me. Thanks.
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var object1: Object1 = Object1(ob: Object2(n: 0))
#State var isShow = false
var body: some View {
NavigationView {
VStack {
Text("\(object1.object2.n)")
//NavigationLink(destination: View2(object2: object1.object2)) {
// Text("Go to view 2")
//}
View2(object2: object1.object2)
Button {
isShow = true
} label: {
Text("Show")
}.alert(isPresented: $isShow, content: {
Alert(title: Text("\(object1.object2.n)"))
})
}
}
}
}
struct View2: View {
#ObservedObject var object2: Object2
var body: some View {
Button {
object2.n += 1
} label: {
Text("\(object2.n)")
}
}
}
class Object1: ObservableObject {
#Published var object2: Object2
init(ob: Object2) {
self.object2 = ob
}
}
class Object2: ObservableObject {
#Published var n: Int = 0
init(n: Int) {
self.n = n
}
}
Here is possible solution:
var body: some View {
NavigationView {
VStack {
Text("\(object1.object2.n)")
.onChange(of: object1.object2.n) { _ in
object1.objectWillChange.send()
}
// .. other code
Alternate is to move every object2 dependent part into separated subview observed object2 explicitly.

SwiftUI View not updating based on #ObservedObject

In the following code, an observed object is updated but the View that observes it is not. Any idea why?
The code presents on the screen 10 numbers (0..<10) and a button. Whenever the button is pressed, it randomly picks one of the 10 numbers and flips its visibility (visible→hidden or vice versa).
The print statement shows that the button is updating the numbers, but the View does not update accordingly. I know that updating a value in an array does not change the array value itself, so I use a manual objectWillChange.send() call. I would have thought that should trigger the update, but the screen never changes.
Any idea? I'd be interested in a solution using NumberLine as a class, or as a struct, or using no NumberLine type at all and instead rather just using an array variable within the ContentView struct.
Here's the code:
import SwiftUI
struct ContentView: View {
#ObservedObject var numberLine = NumberLine()
var body: some View {
VStack {
HStack {
ForEach(0 ..< numberLine.visible.count) { number in
if self.numberLine.visible[number] {
Text(String(number)).font(.title).padding(5)
}
}
}.padding()
Button(action: {
let index = Int.random(in: 0 ..< self.numberLine.visible.count)
self.numberLine.objectWillChange.send()
self.numberLine.visible[index].toggle()
print("\(index) now \(self.numberLine.visible[index] ? "shown" : "hidden")")
}) {
Text("Change")
}.padding()
}
}
}
class NumberLine: ObservableObject {
var visible: [Bool] = Array(repeatElement(true, count: 10))
}
With #ObservedObject everything's fine... let's analyse...
Iteration 1:
Take your code without changes and add just the following line (shows as text current state of visible array)
VStack { // << right below this
Text("\(numberLine.visible.reduce(into: "") { $0 += $1 ? "Y" : "N"} )")
and run, and you see that Text is updated so observable object works
Iteration 2:
Remove self.numberLine.objectWillChange.send() and use instead default #Published pattern in view model
class NumberLinex: ObservableObject {
#Published var visible: [Bool] = Array(repeatElement(true, count: 10))
}
run and you see that update works the same as on 1st demo above.
*But... main numbers in ForEach still not updated... yes, because problem in ForEach - you used constructor with Range that generates constant view's group by-design (that documented!).
!! That is the reason - you need dynamic ForEach, but for that model needs to be changed.
Iteration 3 - Final:
Dynamic ForEach constructor requires that iterating data elements be identifiable, so we need struct as model and updated view model.
Here is final solution & demo (tested with Xcode 11.4 / iOS 13.4)
struct ContentView: View {
#ObservedObject var numberLine = NumberLine()
var body: some View {
VStack {
HStack {
ForEach(numberLine.visible, id: \.id) { number in
Group {
if number.visible {
Text(String(number.id)).font(.title).padding(5)
}
}
}
}.padding()
Button("Change") {
let index = Int.random(in: 0 ..< self.numberLine.visible.count)
self.numberLine.visible[index].visible.toggle()
}.padding()
}
}
}
class NumberLine: ObservableObject {
#Published var visible: [NumberItem] = (0..<10).map { NumberItem(id: $0) }
}
struct NumberItem {
let id: Int
var visible = true
}
I faced the same issue.
For me, replacing #ObservedObject with #StateObject worked.
Using your insight, #Asperi, that the problem is with the ForEach and not with the #ObservableObject functionality, here's a small modification to the original that does the trick:
import SwiftUI
struct ContentView: View {
#ObservedObject var numberLine = NumberLine()
var body: some View {
VStack {
HStack {
ForEach(Array(0..<10).filter {numberLine.visible[$0]}, id: \.self) { number in
Text(String(number)).font(.title).padding(5)
}
}.padding()
Button(action: {
let index = Int.random(in: 0 ..< self.numberLine.visible.count)
self.numberLine.visible[index].toggle()
}) {
Text("Change")
}.padding()
}
}
}
class NumberLine: ObservableObject {
#Published var visible: [Bool] = Array(repeatElement(true, count: 10))
}
There is nothing Wrong with observed object, you should use #Published in use of observed object, but my code works without it as well. And also I updated your logic in your code.
import SwiftUI
struct ContentView: View {
#ObservedObject var model = NumberLineModel()
#State private var lastIndex: Int?
var body: some View {
VStack(spacing: 30.0) {
HStack {
ForEach(0..<model.array.count) { number in
if model.array[number] {
Text(String(number)).padding(5)
}
}
}
.font(.title).statusBar(hidden: true)
Group {
if let unwrappedValue: Int = lastIndex { Text("Now the number " + unwrappedValue.description + " is hidden!") }
else { Text("All numbers are visible!") }
}
.foregroundColor(Color.red)
.font(Font.headline)
Button(action: {
if let unwrappedIndex: Int = lastIndex { model.array[unwrappedIndex] = true }
let newIndex: Int = Int.random(in: 0...9)
model.array[newIndex] = false
lastIndex = newIndex
}) { Text("shuffle") }
}
}
}
class NumberLineModel: ObservableObject {
var array: [Bool] = Array(repeatElement(true, count: 10))
}
The problem is with the function, do not forget to add id: \.self in your ForEach function, and make your Model Hashable, Identifiable.

Resources