swiftuiforeachuipickerviewswiftui-foreach

Using ForEach inside a Picker


I'm having issues pulling data from an Array into a picker using SwiftUI. I can correctly make a list of the data I'm interested in, but can't seem to make the same logic work to pull the data into a picker. I've coded it a few different ways but the current way I have gives this error:


Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'Text' conform to 'TableRowContent'

The code is below:

import SwiftUI

struct BumpSelector: View {
    @ObservedObject var model = ViewModel()
    @State var selectedStyle = 0
        
    init(){
        model.getData2()}
    
    
    var body: some View {
        VStack{
            List (model.list) { item in
                Text(item.style)}
           
            Picker("Style", selection: $selectedStyle, content: {
                ForEach(0..<model.list.count, content: { index in
                    Text(index.style)
                           })
                       })
            }
}

The model is here:

import Foundation

struct Bumps: Identifiable{
    var id: String
    var style: String
}

and the ViewModel is here:

import Foundation
import Firebase
import FirebaseFirestore

class ViewModel: ObservableObject {
    @Published var list = [Bumps]()
    @Published var styleArray = [String]()
   
    func getData2() {
           
        let db = Firestore.firestore()
        db.collection("bumpStop").getDocuments { bumpSnapshot, error in

            //Check for errors first:
            if error == nil {

                //Below ensures bumpSnapshot isn't nil
                if let bumpSnapshot = bumpSnapshot {

                    DispatchQueue.main.async {

                        self.list = bumpSnapshot.documents.map{ bump in

                            return Bumps(id: bump.documentID,
                                         style: bump["style"] as? String ?? "")
                        }
                    }
                }
            }

            else {
                //Take care of the error
            }
        }

        }
}

Solution

  • index in your ForEach is just an Int, there is no style associated with an Int. You could try this approach to make the Picker work with its ForEach:

    struct BumpSelector: View {
        @ObservedObject var model = ViewModel()
        @State var selectedStyle = 0
        
        init(){
            model.getData2()
        }
        
        var body: some View {
            VStack{
                List (model.list) { item in
                    Text(item.style)}
                
                Picker("Style", selection: $selectedStyle) {
                    ForEach(model.list.indices, id: \.self) { index in
                        Text(model.list[index].style).tag(index)
                    }
                }
                
            }
        }
    }
    

    EDIT-1:

    Text(model.list[selectedStyle].style) will give you the required style of the selectedStyle. However, as always when using index, you need to ensure it is valid at the time of use. That is, use if selectedStyle < model.list.count { Text(model.list[selectedStyle].style) }.

    You could also use this alternative approach that does not use index:

     struct Bumps: Identifiable, Hashable {   // <-- here
         var id: String
         var style: String
     }
    
     struct BumpSelector: View {
         @ObservedObject var model = ViewModel()
         @State var selectedBumps = Bumps(id: "", style: "") // <-- here
         
         init(){
             model.getData2()
         }
         
         var body: some View {
             VStack{
                 List (model.list) { item in
                     Text(item.style)
                 }
                 Picker("Style", selection: $selectedBumps) {
                     ForEach(model.list) { bumps in
                         Text(bumps.style).tag(bumps)  // <-- here
                     }
                 }
             }
             .onAppear {
                 if let first = model.list.first {
                     selectedBumps = first
                 }
             }
         }
     }
     
    

    Then use selectedBumps, just like any Bumps, such as selectedBumps.style