When I try to display selected data of the picker in a Text(), there is an error called "Index out of range" occurs.
However, it works fine when I commented the Text() that display the selected data. Below is the codes for picker in form.
struct VMPickerView: View {
@State var vmIndex = 0
@ObservedObject var stockViewModel = StockViewModel()
var body: some View {
let allVM = self.stockViewModel.arrKey
return VStack {
Form {
Section {
Picker(selection: $vmIndex, label: Text("Location")) {
ForEach(0..<allVM.count, id: \.self) {
Text(allVM[$0]).tag($0)
}
}
//Text(allVM[vmIndex])
}
}
}
}
}
Below is the image of my application when I commented the "Text(allVM[vmIndex])"
Below is the codes that I used to retrieve data from firebase and store into the array.
class StockViewModel: ObservableObject {
@Published var itemList = [ItemList]()
@Published var arrKey = [String]()
init() {
retrieveAllVM()
}
func retrieveAllVM() {
var arrKey = [String]()
let ref = Database.database().reference().child("VM")
ref.observeSingleEvent(of: .value, with: { snapshot in
for items in snapshot.children {
let itemSnap = items as! DataSnapshot
let allKey = itemSnap.key
arrKey.append(allKey)
}
self.arrKey = arrKey
print(self.arrKey)
})
}
}
*My codes after changes made:
class StockViewModel: ObservableObject {
@Published var itemList = [ItemList]()
@Published var arrKey = [String]()
func retrieveAllVM() {
var arrKey = [String]()
let ref = Database.database().reference().child("VM")
ref.observeSingleEvent(of: .value, with: { snapshot in
for items in snapshot.children {
let itemSnap = items as! DataSnapshot
let allKey = itemSnap.key
arrKey.append(allKey)
}
DispatchQueue.main.async {
self.arrKey = arrKey
print(self.arrKey)
}
//self.arrKey = arrKey
})
}
}
struct VMPickerView: View {
@State var vmIndex = 0
@ObservedObject var stockViewModel: StockViewModel
var body: some View {
let allVM = self.stockViewModel.arrKey
return VStack {
Form {
Section {
Picker(selection: $vmIndex, label: Text("Location")) {
ForEach(0..<allVM.count, id: \.self) {
Text(allVM[$0]).tag($0)
}
}
//Text(allVM[vmIndex])
}
}
}.onAppear {
self.stockViewModel.retrieveAllVM()
}
}
}
The observeSingleEvent
method looks to be asynchronous. Make sure you update your @Published
properties on the main thread.
Replace:
self.arrKey = arrKey
with:
DispatchQueue.main.async {
self.arrKey = arrKey
}
Your code in init will run every time the ViewModel is created.
class StockViewModel: ObservableObject {
...
init() {
retrieveAllVM()
}
You can move calling retrieveAllVM
to the .onAppear
:
struct VMPickerView: View {
@State var vmIndex = 0
@ObservedObject var stockViewModel = StockViewModel()
var body: some View {
let allVM = self.stockViewModel.arrKey
return VStack {
...
}.onAppear {
self.stockViewModel.retrieveAllVM()
}
}
}
Alternatively don't create a ViewModel directly in the VMPickerView
. Create the ViewModel int the parent view and pass it to the VMPickerView
:
struct VMPickerView: View {
@State var vmIndex = 0
@ObservedObject var stockViewModel: StockViewModel // pass only
...
}
Or, if you're using SwiftUI 2.0, you can use a @StateObject
:
struct VMPickerView: View {
@State var vmIndex = 0
@StateObject var stockViewModel = StockViewModel()
...
}
Dealing with indices is risky. If for any other reason your code fails, try to use if
or guard
statements to make sure you never access the invalid index.
Instead of:
Form {
Section {
...
Text(allVM[vmIndex])
}
}
you can add in the picker view a computed property returning the current key view:
@ViewBuilder
var currentKeyText: some View {
if vmIndex < stockViewModel.arrKey.count {
Text(stockViewModel.arrKey[vmIndex])
}
}
and access it like this:
Form {
Section {
...
currentKeyText
}
}