Learn Courses My Dashboard

Problems with Picker

I fetch data’s from a firestore database within a view by using .appear {}. The retrieved values populate a State property test: [String]

@State private var  test: [String] = []
@State private var selTest: String = ""

Within the view I have a picker

      VStack {
            
            Text("test")

                    Picker ("Filter", selection: $selTest) {
                        ForEach(test, id: \.self) {
                            Text($0)
                        }
                    }
                    //.pickerStyle(WheelPickerStyle())
        }

I’m facing a couple of problems.

1.) the values from the Firestore database don’t appear when the view is called. What’s the best practice, that these values are ready, when the view get’s rendered?
2.) the picker only shows up with .pickerStyle(WheelPickerStyle()). In the picture enclosed you see .pickerstyle uncommented.


Somehow I can click on a picker, which is a small blue rectangle Second set of pictures: picker style → wheel. values don’t show up when view get’s rendered first time. Switching to another view and switching back, the wheel picker shows up.


I’m more than confused … Can anyone explain me what happens there and what I’m doing wrong?

Thanks, Peter

You should consider making use of an ObservableObject which has a @Published property which would be your test array. The generally accepted architecture for SwiftUI projects is MVVM which is Model, View and ViewModel where the ViewModel is the ObservableObject.

Make your call, using whatever method is required, to populate that published property within the observable object.

As a rough guide your ViewModel would be something like this:

class ViewModel: ObservableObject {
    @Published var test: [String] = []

    init() {
        loadDataFromFirebase()
    }

    func loadDataFromFirebase() {

        //  Your code to retrieve the records from Firebase

        DispatchQueue.main.async {
            // Set test equal to the data retrieved from Firebase
            self.test = ["arrayItem1", "arrayItem2", "arrayItem3", "arrayItem4", "arrayItem5", "arrayItem6"]
        }

    }
}

For the purpose of the example above I have populated test with some dummy data.

Then in your View where you have your picker you would have something like this.

struct ContentView: View {
    @ObservedObject var vm = ViewModel()
    @State private var selTest: String = ""

    var body: some View {
        VStack {
            Text("test")

            Picker ("Filter", selection: $selTest) {
                ForEach(vm.test, id: \.self) {
                    Text($0)
                }
            }
            .pickerStyle(WheelPickerStyle())
        }
    }
}
1 Like

Hello Chris!

Thanks for your answer. The init() has solved one of the major issues … but I still have another one.

I had already implemented a MVVM model which looks like:

class ContentModel: ObservableObject {
    
    // list of supported countries in the app
    @Published var supportedCountries = [Country]()

init() {
        getSupportedCountries()
    }

    func getSupportedCountries() {
        
        // Get a referene to the countries collection
        let db = Firestore.firestore()
        
        db.collection("countries")
            .getDocuments { snapshot, error in
            // Check there's no errors
            if error == nil {
                
                // Declare temp country list
                var temp = [Country]()
                
                for doc in snapshot!.documents {
                    var m = Country(id: doc["id"] as? String ?? "",
                                name: doc["name"] as? String ?? "",
                                language: doc["language"] as? String ?? "")
                    
                    temp.append(m)
                }
                
                DispatchQueue.main.async {
                    self.countries = temp
                }
            }
        }   
    }
}

Country is defined as follows:

struct Country: Decodable, Hashable, Identifiable {
    var id: String = "" // ISO 3166 - ALPHA-3
    var name: String = ""
    var language: String = ""
}

In a view I use the fetched values in a picker

struct SettingsView: View {
    
   @EnvironmentObject var model: ContentModel
   
   Section {
       Picker ("Country", selection: $country) {
          ForEach(model.countries, id: \.self) { arg in
             Text(String(arg.name))
          }
       }
    }
}

The fetched countries get shown in the picker, but the selection doesn’t get stored/saved. If I store the country names in an array of strings [string], the picker works as intended. Why or what has to be changed that the picker works with the values retrieved form the Country struct?

Thanks, Peter

Picker ("Country", selection: $country) {
  ForEach(model.countries) { arg in
     Text(arg.name).tag(arg)
  }
}
  1. Since Country conforms to Identifiable, you don’t need to explicitly indicate an id in the ForEach when looping through a list of Country items.
  2. The type of the bound selection parameter and the type of the id of the items being looped through need to be the same in order for the Picker to work. You have a selection parameter of (presumably) type Country but the id is of type String. Adding an explicit .tag() modifier of type Country will make it work.
  3. Country.name is a String so you don’t need to cast it to a String when you put it in a Text element.
1 Like

Thanks Patrick!

Do you have an idea why the picker (see second picker) behave’s that strange?

:wink: Peter

See my point #2 above.

Can it be, that there is a difference between

struct Country: Decodable, Hashable, Identifiable {
    var id: String = "" // ISO 3166 - ALPHA-3
    var name: String = ""
    var language: String = ""
}
struct Country: Decodable, Hashable, Identifiable {
    var id: UID // ISO 3166 - ALPHA-3
    var name: String = ""
    var language: String = ""
}

id as UUID the picker doesn’t store the selected value

I really don’t understand what you’re asking. I’ve tried your Picker using the code you posted (both versions of Country) and with the changes I suggested and all three worked.

Can you post a reproducible example of what you see not working? That includes all variables needed to make the code sample run.

would there be a chance to share screens?

Hy Patrick,

i have reduced everything to minimum to reproduce my problems. With the project included you will see one of the problems:
Switch to Tab “Settings”, Select “League”. The selection doesn’t get stored, whereas country does. The difference is, that in the first case the id is a String, in the second case it is an UUID and im using an AppStorage Property Wrapper.

The second problem, the strange behavior of the Picker View at the home Tab, I just can send you the two screenshots that you see the difference.


The only difference is, that the values get populated by a Firestore database (there the Pickerview doesn’t show up), whereas when the value’s for the picker get populated hardcoded (code enclosed), it works.

It’s also interesting, that in the Homeview with a State property for the “selection” the Picker “leagues” works as designed, whereas in the Settingsview with an AppState property wrapper not.

Because I can’t upload the code files, I will insert them here:

ProblemsWithPicker.swift

import SwiftUI

@main
struct ProblemsWithPickerApp: App {
    var body: some Scene {
        WindowGroup {
            MainTabView()
                .environmentObject(ContentModel())
                .environmentObject(GameModel())
        }
    }
}

Models.swift

import SwiftUI
import Foundation


// Enumerations

enum Tab: String {
    case home
    case settings
}


// structs
struct Country: Decodable, Hashable, Identifiable {
    var id: String = ""     // ISO 3166 - ALPHA-3
    var name: String = ""
    var language: String = ""
}

struct League: Decodable, Hashable, Identifiable {
    var id: UUID
    var name: String = ""
    var since: String = ""
    var short: String = ""
    var division: String = ""
}

struct Game: Identifiable, Hashable {
    var id: Int
    var league: String = ""
    var homeTeam: String = ""
    var homeTeamLogo: String = ""
    var guestTeam: String = ""
    var guestTeamLogo: String = ""
    var scheduledDate: String = ""
    var scheduledTime: String = ""
    var ballpark: String = ""
}

//classes
class TabController: ObservableObject {
    
    @Published var activeTab = Tab.home
    
    func open(_ tab: Tab) {
        activeTab = tab
    }
}

ContenModel.swift

import Foundation

// MARK: GameModel
class GameModel: ObservableObject {
    
    @Published var schedule = [Game]()
    
    @Published var selectedScheduledGame: Int?
    
    init() {
        
        // Create some dummy games
        schedule.append(Game(id: 0, league: "U12", homeTeam: "Little Indians", homeTeamLogo: "Primary Logo Dornbirn Indians", guestTeam:"Bulls U12", guestTeamLogo: "Primary Logo Hard Bulls", scheduledDate: "Gestern", scheduledTime: "10:00", ballpark: "Sportanlage Rohrbach"))
        schedule.append(Game(id: 1, league: "U10", homeTeam: "Indians Kids", homeTeamLogo: "Primary Logo Dornbirn Indians", guestTeam: "Cardinals", guestTeamLogo: "Primary Logo Feldkirch Cardinals", scheduledDate: "Heute", scheduledTime: "13:00", ballpark: "Sportanlage Rohrbach"))//DateFormatter().date(from:"08-02-2022 20:00:00 +0100")!))
        schedule.append(Game(id: 2, league: "U12", homeTeam: "Bulls U12", homeTeamLogo: "Primary Logo Hard Bulls", guestTeam:"Cardinals", guestTeamLogo: "Primary Logo Feldkirch Cardinals", scheduledDate: "19.02.2022", scheduledTime: "11:00", ballpark: "Ballpark am See"))//DateFormatter().date(from:"10-02-2022 20:00:00 +0100")!))
        schedule.append(Game(id: 3, league: "U12", homeTeam: "Cardinals U12", homeTeamLogo: "Primary Logo Feldkirch Cardinals", guestTeam:"Indians", guestTeamLogo: "Primary Logo Dornbirn Indians", scheduledDate: "28.02.2022", scheduledTime: "11:00", ballpark: "GRAWE Ballpark"))
    }

}

// MARK: ContentModel
class ContentModel: ObservableObject {

    
    // List of countries
    @Published var countries = [Country]()
    
    // List of leagues in a country
    @Published var leagues = [League]()
    
    // List of games
    @Published var games = [Game]()
    
    init()  {
        // load
        getSupportedCountries()
        getLeagesInCountry(country: "AUT", division: "")
    }
    
    // MARK: - Authentication methods
    func getSupportedCountries() {
        
        // original code below, now just get values polulated
                countries.append(Country(id: "AUT", name: "Austria", language: "German"))
                countries.append(Country(id: "SUI", name: "Switzerland", language: "German"))
                countries.append(Country(id: "ITA", name: "Italy", language: "Italiano"))
                               
//        // Get a referene to the countries collection
//        let db = Firestore.firestore()
//        //let collection = db.collection("countries")
//
//        db.collection("countries")
//            .getDocuments { snapshot, error in
//            // Check there's no errors
//            if error == nil {
//
//                // Declare temp country list
//                var temp = [Country]()
//
//                for doc in snapshot!.documents {
//                    var m = Country(id: doc["id"] as? String ?? "",
//                                name: doc["name"] as? String ?? "",
//                                language: doc["language"] as? String ?? "")
//
//                    temp.append(m)
//                }
//
//                DispatchQueue.main.async {
//                    self.countries = temp
//                }
//            }
//        }
//
                                 
    }
    
    func getLeagesInCountry(country: String, division: String) {
        
// original code below, now just get values polulated
        leagues.append(League(id: UUID(), name: "Test 1", since: "1990", short: "TES", division: "west"))
        leagues.append(League(id: UUID(), name: "Test 2", since: "2005", short: "TAS", division: "west"))
        leagues.append(League(id: UUID(), name: "Test 3", since: "2020", short: "TOS", division: "east"))
        
        
//        // Get a referene to the countries collection
//        let db = Firestore.firestore()
//
//        //var collection = db.collection("countries").document("\(country)").collection("leagues")
//
//        if division == "" || division == "none" {
//            db.collection("countries").document("\(country)").collection("leagues")
//                .getDocuments() { (querySnapshot, error) in
//                        if let error = error {
//                            print("Error getting documents: \(error)")
//                        } else {
//                            // Declare temp league list
//                            var leagues = [League]()
//
//                            for doc in querySnapshot!.documents {
//                                let m = League(id: doc["id"] as? UUID ?? UUID(),
//                                    name: doc["name"] as? String ?? "",
//                                    since: doc["since"] as? String ?? "",
//                                    short: doc["short"] as? String ?? "",
//                                    division: doc["division"] as? String ?? "")
//                                leagues.append(m)
//
//                            }
//                            self.leagues = leagues
//                            DispatchQueue.main.async {
//                                self.leagues = leagues
//                            }
//                        }
//                }
//        } else {
//            db.collection("countries").document("\(country)").collection("leagues").whereField("division", isEqualTo: "\(division)")
//                .getDocuments() { (querySnapshot, error) in
//                        if let error = error {
//                            print("Error getting documents: \(error)")
//                        } else {
//                            // Declare temp league list
//                            var leagues = [League]()
//
//                            for doc in querySnapshot!.documents {
//                                //print("\(doc.documentID) => \(doc.data())")
//                                let m = League(id: doc["id"] as? UUID ?? UUID(),
//                                    name: doc["name"] as? String ?? "",
//                                    since: doc["since"] as? String ?? "",
//                                    short: doc["short"] as? String ?? "",
//                                    division: doc["division"] as? String ?? "")
//                                leagues.append(m)
//
//                            }
//                            self.leagues = leagues
//                            DispatchQueue.main.async {
//                            }
//                        }
//                }
//        }
        
    }
}

SettingsModel.swift

import Foundation
import SwiftUI

class AppSettings: ObservableObject {
    
    // MARK: properties
    @Published var country: String
    @Published var language: String

    
    init(country: String = "Austrian", language: String = "English") {
        self.country = country
        self.language = language
    }
}

MainTabView

import SwiftUI

struct MainTabView: View {
    
    @EnvironmentObject var model: ContentModel
    @EnvironmentObject var games: GameModel
    @StateObject private var tabController = TabController()

    
    var body: some View {
            TabView (selection: $tabController.activeTab){

                HomeView()
                    .tabItem {
                        Label("Home", systemImage: "house")
                    }
                    .tag(Tab.home)

                 SettingsView()
                     .tabItem {
                         Label("Settings", systemImage: "gear")
                     }
                     .tag(Tab.settings)
            }
            .environmentObject(tabController)
    }
}

struct MainTabView_Previews: PreviewProvider {
    static var previews: some View {
        MainTabView()
            .environmentObject(ContentModel())
            .environmentObject(GameModel())
    }
}

HomeView.swift

import SwiftUI
import Foundation

struct HomeView: View {
    
    @EnvironmentObject var model: ContentModel
    @EnvironmentObject var games: GameModel
    
    // selection filters
    @State private var filterLeague: String = ""
    @State private var filterCountry: String = ""
    @State private var selectedGame: Game?
    @State private var scheduleCollapsed: Bool = false
    @State private var scoreCollapsed: Bool = false
    @State private var tabIndex: Int = 0
    
    init() {
        if #available(iOS 15.0, *) {
            let appearance = UITabBarAppearance()
            UITabBar.appearance().scrollEdgeAppearance = appearance
        }

        UINavigationBar.appearance().titleTextAttributes = [.font:UIFont.preferredFont(forTextStyle:.subheadline)]
    }
    
    var body: some View {
        
        NavigationView{
            VStack {
                //MARK - Schedule
                HStack {
                    Text("Schedule")
                        .padding(.leading)
                        .font(.largeTitle)

                    Spacer()

                    Button(
                       action: { self.scheduleCollapsed.toggle()
                       },
                       label: {
                           Image(systemName: self.scheduleCollapsed ? "chevron.down" : "chevron.up")
                               .resizable()
                               .scaledToFit()
                       }
                    )
                    .frame(width: 32)
                    .padding(.trailing)
                }

                    HStack {

                        Picker ("League", selection: $filterLeague) {
                            ForEach(model.leagues) { arg in
                                Text(arg.name).tag(arg)
                            }
                        }
                        
                        Picker ("Country", selection: $filterCountry) {
                            ForEach(model.countries) { arg in
                                Text(arg.name).tag(arg)
                            }
                        }.pickerStyle(WheelPickerStyle())
                    }
                    


                ScrollView{
                    LazyVStack {
                        VStack(spacing: 10) {
                            ForEach(games.schedule) {game in
                                NavigationLink(
                                    tag: game.id,
                                    selection: $games.selectedScheduledGame,
                                    destination: {
                                        //print(value)

                                    }, label: {
                                        ScheduleCardView(scheduledGame: game)
                                    }
                                )
                            }
                        }
                    }

                }
                .background(Color(UIColor.systemBackground))
                .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: scheduleCollapsed ? 0 : .none)
                .padding(.horizontal, 10)

            // Divider()

            }
            
            .navigationBarItems(leading: HStack {
                Button(action: {
                    //SettingsView()
                }, label: {NavigationLink(destination: SettingsView()) {
                    Image(systemName: "person")
                     }})
            }, trailing: HStack {
                Button(action: {
                }, label: {Image(systemName: "xmark")})
            })
            .navigationBarTitle(Text("Hello"),displayMode: .inline)
        }
    }
}


struct HomeView_Previews: PreviewProvider {
    static var previews: some View {
        // Create a dummy GameModel and pass it to the detail view so that we can see a preview
        let game = GameModel()

        Group {
            HomeView()
                .environmentObject(ContentModel())
                .environmentObject(GameModel())
        }
    }
}

struct Collapsible<Content: View>: View {
    @State var label: () -> Text
    @State var content: () -> Content
    
    @State private var collapsed: Bool = true
    
    var body: some View {
        VStack {
            Button(
                action: { self.collapsed.toggle() },
                label: {
                    HStack {
                        self.label()
                        Spacer()
                        Image(systemName: self.collapsed ? "chevron.down" : "chevron.up")
                    }
                    .padding(.bottom, 1)
                    .background(Color.white.opacity(0.01))
                }
            )
            .buttonStyle(PlainButtonStyle())
            
            VStack {
                self.content()
            }
            .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: collapsed ? 0 : .none)
            .clipped()
            .animation(.easeOut)
            .transition(.slide)
        }
    }
}


struct ScheduleCardView: View {
    
    var scheduledGame: Game
    
    var body: some View {
        ZStack {
            Rectangle()
                .foregroundColor(.white)
                .cornerRadius(10)
                .shadow(radius: 3)
                .aspectRatio(3, contentMode: .fit)
            
            VStack(alignment: .leading) {
                HStack{
                    Text("\(scheduledGame.league)")
                        .font(.callout).foregroundColor(.gray)
                        .padding(.leading)
                    Spacer()
                    Text("\(scheduledGame.ballpark)")
                        .font(.callout).foregroundColor(.gray)
                        .padding(.trailing)
                }
                
                Group{
                    HStack {
                        VStack {
                            HStack {
                                Image("\(scheduledGame.guestTeamLogo)")
                                    .resizable()
                                    .scaledToFit()
                                    .frame(width: 30, height: 30)
                                    .padding(.horizontal)
                                
                                Text("\(scheduledGame.guestTeam)")
                                
                                Spacer()
                            }
                            
                            HStack {
                                Image("\(scheduledGame.homeTeamLogo)")
                                    .resizable()
                                    .scaledToFit()
                                    .frame(width: 30, height: 30)
                                    .padding(.horizontal)
                                
                                Text("\(scheduledGame.homeTeam)")
                                
                                Spacer()
                            }
                        }
                        
                        Divider()
                        
                        VStack {
                            Text("\(scheduledGame.scheduledDate)")
                            Text("\(scheduledGame.scheduledTime)")
                        }
                        .font(.subheadline)
                        .frame(width: 80)
                        .padding(.trailing)
                    }
                }
                .foregroundColor(.black)
            }
            .padding(.vertical, 8)
        }
        .padding(5)
        
    }
}

SettingsView

import SwiftUI
import Foundation

struct SettingsView: View {
    
    // Store Settings by using class UserdDefaults
    @AppStorage("country") var country: String = ""
    @AppStorage("league") var league: String = ""
    
    @EnvironmentObject var settings: AppSettings
    @EnvironmentObject var model: ContentModel
    @EnvironmentObject var tabController: TabController
    
    @State private var navigateBackHome: Bool = false
    
    var body: some View {
        NavigationView {
                Form {
                    Section {
                        Picker ("Country", selection: $country) {
                            ForEach(model.countries) { arg in
                                Text(arg.name).tag(arg)
                                }
                            }
                    }
                    
                    Section {
                        Picker ("League", selection: $league) {
                            ForEach(model.leagues) { arg in
                                Text(arg.name).tag(arg)
                            }
                        }
                    }
                }
                .navigationBarItems(leading: HStack {
                    Button(action: {
                        tabController.open(.home)
                        //appState.rootViewID = UUID()
                    }, label: {Image(systemName: "arrow.backward")})
                }, trailing: HStack {
                    Button(action: {}, label: {Text("Save")})
                })
                .navigationBarTitle(Text("Settings"), displayMode: .inline)
            //}
            
        }
    }
}

struct SettingsView_Previews: PreviewProvider {
    static var previews: some View {
        SettingsView()
            .environmentObject(AppSettings())
    }
}

Thank you so much for your help and efforts!

Peter

@peter_luger

Hi Peter,

There is alternative way to share your project and that is to compress it at the root folder (the folder that contains the .xcodeproj file and the related folder) and then post that to DropBox. Create a share link in Dropbox and copy that link and post that in a reply.

Probably a lot easier than having to manually copy and create each file.

Hallo Chris,

enclosed the link:

[Dropbox - ProblemsWithPicker - Simplify your life]

Just having a quick look before I go to sleep. I’ll have more of an in depth look tomorrow.

some Update from my side (after finding out parts of the problem).

as roostersboy explained: the picker selection parameter and the id of the items looped through must be type consistent. I declared the League.id as UUID and wanted to store it in UserDefaults "league using the @AppStorage property wrapper. The class UserDefault only allows a few number of types to be stored (Int, Double, String, Bool, URL and Data) - UUID is not included. So the selection parameter was from type String, whereas the item.id was an UUID.

i also found out, that when you retrieve an document from the Firestore database the automated generated Document ID get stored as an String. So there was no need to declare the document id as an UUID. After changing the struct declaration of League from id: UUID to id: String, the picker works in the SettingsView and the selected value get permanently stored in the UserDefaults too.

The main problem, that in the Homeview the picker doesn’t get shown or just as the small blue rectangle, is still alive. The only thing I know is, that - when the data’s are fetched from a database, the picker doesn’t show up. Whereas when I put the HStack{} in a Form, the Picker gets shown. Strange. Very Strange.