I have a TabView where I have a ForEach loop for various items inside it and I want to change the selection of the TabView item using a timer which automatically changes the selection index. I am able to achieve it using ForEach loop having either array.indices or 0..<array.count but since it is unsafe to use the earlier mentioned methods so I am thinking to pass simply the array itself into the ForEach loop but while simply using the array in foreach loop doesn't changes the selected index using the timer or other programmatic ways. The user need to do it manually scroll to change the selection. Below is my line of code that i tried, can anyone please point out what I an doing wrong.
struct ContentView: View {
@State var currentIndex = 0
@State var itemNames = [ItemNames]()
@State var timer: Timer.TimerPublisher = Timer.publish (every: 6, on: .main, in: .common)
var body: some View {
TabView(selection: $currentIndex) {
ForEach(itemNames, id: \.id) { item in
Text(item.name).tag(Int(item.id))
}
}.tabViewStyle(.page(indexDisplayMode: .never))
.onReceive(timer, perform: {
_ in withAnimation {
currentIndex = currentIndex < itemNames.count ? currentIndex 1 : 0
}
}).onDisappear {
self.cancelTimer()
}.onAppear {
setDemoNames()
self.instantiateTimer()
_ = self.timer.connect()
}
}
func instantiateTimer() {
self.timer = Timer.publish (every: 6, on: .main, in: .common)
return
}
func cancelTimer() {
self.timer.connect().cancel()
return
}
func setDemoNames(){
let item1 = ItemNames(id: "1", name: "John")
let item2 = ItemNames(id: "2", name: "Mark")
let item3 = ItemNames(id: "3", name: "Steve")
let item4 = ItemNames(id: "4", name: "Peter")
itemNames = [item1, item2, item3, item4]
}
}
struct ItemNames : Identifiable, Hashable {
var id:String, name:String;
init(id: String, name: String) {
self.id = id
self.name = name
}
}
CodePudding user response:
You can safely get the index in a ForEach using zip:
ForEach(Array(zip(itemNames, itemNames.indices)), id: \.0) { item, index in
Since itemNames is identifiable, you don't need to use id: \.id, so you can normally do this:
ForEach(itemNames) { item in
and SwiftUI will use itemNames.id as the ForEach id:. With zip, which returns sequence pairs, and not an array, you then turn the zip sequence back into an array of tuples. All you need to do for the ForEach id: is pass it an Identifiable, which in this case is the first of the tuple, or .0. This gives you a properly identifiable ForEach for things such as .onDelete and provides you with the index of the current item as well.
CodePudding user response:
The types have to match exactly
Change your tag to
.tag(Int(item.id) as! Int)
The above line works because it remove the optional/forces.
You have to make sure that the item.id will ALWAYS be a valid Int
Int(item.id) produces a value of type Int? because it might fail
You can also do something like below to not force but you might have duplicate tags if for some reason you have 2 ids that fail.
.tag((Int(item.id) ?? itemNames.count 1) as Int)
This is very prone to bugs, it only works in a very specific set of circumstances and there is little fallback.
I would switch the selection variable to String and keep the tag with the original id then create a function to pull the next id to set with the timer.
No forcing or optionals.
You can also achieve this by making the the id and Int instead of a String
