Search code examples
iosswiftxcodeswiftuipicker

How to create a dynamic Picker based on imported data in SwiftUI


I am importing data from an API, which means I cannot use an enum or a list for my picker. This took me a bit to sort out and I did not see any good answers for it, so here is my solution.

The .task{} makes a server call that gets us an array of Seasons. As soon as the user chooses a Season from the picker, we use an .onChange() to set our local id to the id of that Season, so that when the user chooses a team, that id will be passed through. The second onChange() was a slight hack. I wanted to set seasons in the .task{}, but it would set seasons before leagueDataModel.league.seasons! was set. This meant seasons was always empty until I left and came back to the page. Nothing else worked for me so if you have a suggestion on the correct way to do this please comment.

import SwiftUI

struct TeamSelectView: View {
    @StateObject private var leagueDataModel = LeagueDataModel()
    @State private var teamSeason = Season()
    @State private var seasons : [Season] = []
    @State private var seasonNumber = "1"
    var url = "fakeurl.com"
    var body: some View {
        VStack{
            Picker("Season", selection:  $teamSeason){
                ForEach(seasons, id: \.self) {
                    Text($0.name!).tag($0.id)
                }
            }
            ForEach(leagueDataModel.league.teamsNoAll!) { each in
                TeamCard(team: each, season: seasonNumber)
            }
        }
        
        .task{
            await self.leagueDataModel.getLeagueData(url: url)
        }
        
        .onChange(of: self.teamSeason) {
            self.seasonNumber = teamSeason.id ?? "1"
        }
         
        .onChange(of: self.leagueDataModel.league.seasons) {
            seasons = leagueDataModel.league.seasons!
        }
    }

}
import Foundation
import SwiftUI

@MainActor
class LeagueDataModel : ObservableObject {
 
    @Published var league = League()
    
    func getLeagueData(url: String) async {
        Service().getDataFromServer(stringurl: url) { (data : League) in
                    DispatchQueue.main.async { [self] in
                        self.league = data
                }
        }
    }

}
// MARK: - League
struct League: Codable {
    var seasons: [Season]? = []
}

// MARK: - Season
class Season: Codable, Hashable {
    //the seasons all have ids (1,2,3, etc) and names (playoffs 2024, preseason 2024, etc)
    let id, name: String?


    enum CodingKeys: String, CodingKey {
        case id, name
    }
    
//everything must be optional so we can set an empty Season() to start with. 
//I also could have set specific value for everything but preferred this route.
    init(id: String? = nil, name: String? = nil) {
        self.id = id
        self.name = name
    
    }
    //we need Season to be Hashable in order to use it for the Picker
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)

    }
    static func == (lhs: Season, rhs: Season) -> Bool {
        return lhs.id == rhs.id
    }
}

Solution

  • Each TeamCard shows a different team (Bruins, Rangers, Blackhawks, Etc). When that team is selected, it opens a roster view, which is based on what season it is, which is why I pass the seasonId variable when I call it. The data loads async after the page, which is why I set our lest of seasons as an empty list of type Season to start. It wont break when you open the page but fills in the data very quickly.

    The Picker needs an id (id: .self) and a tag (.tag($0.id)) in order to determine which Season you are looking for. It also requires an initial selection, so I have an empty Season(). Once seasons populates, teamSeason does as well to the first choice, so the user does not see a blank option there.

    Hope this helps! A dynamic Picker doesn't need to be super complicated. Let me know in the comments if I need to explain anything more.