Search code examples
firebase-realtime-databaseswiftuipickerswiftui-form

SwiftUI Picker in Form - Index out of Range


When I try to display selected data of the picker in a Text(), there is an error called "Index out of range" occurs.

Image of error message

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])"

Screen 1

Screen 2

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

Solution

  • 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()
        ...
    }
    

    EDIT

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