Home > OS >  SwiftUI Country Picker - Show Country Name but store Country ID
SwiftUI Country Picker - Show Country Name but store Country ID

Time:12-03

I am trying to create a picker that displays a country name, and depending on what country is selected, stores the corresponding country id. This way the user sees the name of the country but I can pass only the country id into my database.

I have created two arrays (one for the country name, and one for the country id), and combined them into a single array using zip and a struct. This worked in a playground without the views to output a dump of Countries.init, but in my swift.view file it throws all kinds of errors.

I am using SwiftUI with the latest Swift 5 and XCode 13.1.

Here's what I've got so far:

import SwiftUI

    struct Country: View {
     
        @State private var selectedCountry = ""
        @State private var selectedCountryId = ""
     
        var countryId = Locale.isoRegionCodes
        var countryName = Locale.isoRegionCodes.compactMap { Locale.current.localizedString(forRegionCode: $0) }
     
        struct Countries { let countryId: String; let countryName: String }
     
        countryId.insert("US", at:0)
        countryName.insert("United States", at:0)
        zip(countryId, countryName).map(Countries.init)
    
        var body: some view {
     
        Picker("Country", selection: $selectedCountry) {
            ForEach(Self.Countries(Countries.init), id: \.countryName) {
                        Text($0)
                     }
             }
    }

I'm sure it's completely the wrong way to do this, so don't worry so much about correcting my code. I'd really just love to know how to do this, because I'll also need to implement the same thing when it comes to selecting a US State (i.e. show the full name of the State but store the abbreviation).

Also, there is much more to the body view, but I've stripped down the code here just to show this specific issue.

Thanks in advance!

CodePudding user response:

This my friend is where dictionaries come in handy. A dictionary has two parts Key and Value or in swift terms ["Key":"Value"] There are three things to note about a dictionary.

  • #1, all key-value-pairs MUST be the same type, for example, [32:"String", 33: "String"] Which is important to remember.
  • #2, it does NOT guarantee order.
  • #3, It can only contain unique keys.

How does this apply to your problem? Well, it has to do with the type of data that you have. Currently you have 2 separate arrays, one with the Country, and one with the Codes. You can combine them into the dictionary and ensure that they are always together, referencing the value from the key, or searching for the value, to get a key or multiple keys. That previous sentence is important to pay attention to, though not for your case, you're guaranteed to only get one value for one key, but you can get multiple keys from one value. In your case you have a unique country, and unique ID.

var countries = ["USA": 9999,
                 "UK": 9998,
                 "Canada": 9997] // Etc..

Getting a value from a dictionary is even easier, but works similar to an array. You sub-script it. For example:

var canadaID= countries["Canada"]

Now it gets trickier getting a key from a value because you have to iterate over the whole dictionary to grab it. It's also possible that there are duplicate values, meaning you could technically get back an array of "Keys". In this example, I grabbed only the first found value. Again, remember that the order is not guaranteed and if you have multiple of the same value you may get the incorrect key.

var countryID = 9998

if let key = countries.first(where: { $0.value == someValue })?.key {
    print(key)
}

From here it becomes trivial to store it.

func storeCountryIDFromKey(country: String) {
     let countryId = countries[country]
     // Store your ID.
}

What if my order is important??!??

This could be important for your case as you might want to display the countries in alphabetical order. To do that simply map the keys to an array and sort, as is tradition.

let keys: [String] = countries.map{String($0.key) }

Additional Reading

There is a concept in programming called Big-O notation typically expressed as O(n) or pronounced O-of-N. Which is the way that we describe time and space complexities. It's a great skill to learn if you want to become a great developer as it has to do with Data Structures and Algorithms. To make more sense of this, as it applies to your question, having two separate arrays to loop over vs one dictionary effectively takes 2x as long to accomplish with the double arrays. Furthermore it doubles the space complexity. Combining both into one Dictionary reduces your performance overhead by 1/2 which is a huge performance gain. With a small data-set such as countries, which there are a finite amount, it doesn't really matter; However, if you start working with massive datasets then suddenly 1/2 faster is a substantial performance boost.

Without digging too much into it, and to simply get your wheels spinning, every time you make a variable, or the compiler does that for you, that increases space complexity. Every time you run a line of code, or loop over a line of code, that increases the time complexity. Always, and I mean always, try your best to reduce that overhead. It'll force you to think outside the box and in turn, you'll learn better practices.

CodePudding user response:

The Picker documentation says to use the tag modifier on each Text to control what the Picker stores in its selection.

There's no reason to store an array of country names if you just want to store the selected country code. And you should use SwiftUI's Environment to get the current Locale, so that your view will be redrawn if the user changes her locale.

import SwiftUI
import PlaygroundSupport

struct CountryPicker: View {
    @Binding var countryId: String
    @Environment(\.locale) var locale
    
    var body: some View {
        Picker("", selection: $countryId) {
            ForEach(Locale.isoRegionCodes, id: \.self) { iso in
                Text(locale.localizedString(forRegionCode: iso)!)
                    .tag(iso)
            }
        }
    }
}

struct Test: View {
    @State var countryId: String = ""
    
    var body: some View {
        VStack {
            CountryPicker(countryId: $countryId)
            Text("You picked \(countryId).")
        }
        .padding()
    }
}

PlaygroundPage.current.setLiveView(Test())

CodePudding user response:

I would just do something simple like this:

struct Country: Identifiable, Hashable {
    let id: String
    let name: String
}

struct CountryView: View {
    let countries = Locale.isoRegionCodes.compactMap{
        Country(id: $0, name: Locale.current.localizedString(forRegionCode: $0)!) }
    
    @State var selectedCountry: Country?
    
    var body: some View {
        Picker("Country", selection: $selectedCountry) {
            ForEach(countries) {
                Text($0.name).tag(Optional($0))
            }
        }.pickerStyle(.wheel)
            .onChange(of: selectedCountry) { selected in
                if let cntry = selected {
                    print("--> store country id: \(cntry.id)")
                }
            }
    }
}
  • Related