NEW: Learn to build amazing SwiftUI apps for macOS with my new book! >>

SOLVED: Day 19 completed - finally! after many days

Forums > 100 Days of SwiftUI

I really like this challenge. Although, I was surprised that it took me so long to complete. I had to leave it and come back to it for many days because I was getting too frustrated with it not working for me. 2 things challenged me:

  1. I think there is a bug or the Apple documentation needs to be updated. For the pick list of temperature units I decided to use an enumeration. Apple's documentation describes this:

    enum Flavor: String, CaseIterable, Identifiable {
        case chocolate
        case vanilla
        case strawberry
    
        var id: String { self.rawValue }
    }
    @State private var selectedFlavor = Flavor.chocolate
    Picker("Flavor", selection: $selectedFlavor) {
        ForEach(Flavor.allCases) { flavor in
            Text(flavor.rawValue.capitalized)
        }
    }

    which is quite similar to what Paul covered and also pretty neat, IMHO, with the clever use of the enum. The names of each enum value are made available as String constants associated with the enum value (accessed using .rawValue) and the enum values can be iterated over (using .allCases) in the order they are declared.

Unfortunately, try as I might I could not get this solution to work. The @State variable just wouldn't change it's value. I finally worked out that this functions properly (excerpt from my project code):

Picker("Convert From Units", selection: $fromUnits) {
    ForEach(TemperatureUnits.allCases) { Text($0.rawValue).tag($0) }
}

Explicitly associating the tag with the Picker entry seems to be the only way to get this to work. I am using the most recent macOS on Intel with all updates applied. Maybe something is broken with my XCode installation...

  1. I don't like that the custom keyboard button appears at the bottom of the screen when I am using the keyboard for input to a TextField. It just doesn't look clean to me. So I spent ages trying to find a way to only add the toolbar to the form if no keyboard was detected. Sadly I couldn't find a solution.

1      

Apple's docs, at the end of the Overview section on the same page you link to, say to use a tag to associate a particular case of the enum with each item in the Picker. Here's the example they give:

@State private var selectedFlavor = Flavor.chocolate

Picker("Flavor", selection: $selectedFlavor) {
    Text("Chocolate").tag(Flavor.chocolate)
    Text("Vanilla").tag(Flavor.vanilla)
    Text("Strawberry").tag(Flavor.strawberry)
}
Text("Selected flavor: \(selectedFlavor.rawValue)")

You append a tag to each text view so that the type of each selection matches the type of the bound state variable.

That matching to the type of the state variable is the important thing to remember with Picker and others similar elements.

1      

Thanks @roosterboy; but immediately after the overview section; in the Iterating Over a Picker's Options section it describes using ForEach and says this:

To provide selection values for the Picker without explicitly listing each option, you can create the picker with a ForEach construct, like this:

Picker("Flavor", selection: $selectedFlavor) {
    ForEach(Flavor.allCases) { flavor in
        Text(flavor.rawValue.capitalized)
    }
}

In this case, ForEach automatically assigns a tag to the selection views, using each option’s id, which it can do because Flavor conforms to the Identifiable protocol. On the other hand, if the selection type doesn’t match the input to the ForEach, you need to provide an explicit tag. The following example shows a picker that’s bound to a Topping type, even though the options are all Flavor instances. Each option uses tag(_:) to associate a topping with the flavor it displays.

I think my selection type is the same as the input to the ForEach; but if I don't use the tag() it just doesn't work. If you could spare another moment could you please let me know where I have this wrong - perhaps TemperatureUnits.allCases doesn't produce a TemperatureUnits enum type...

enum TemperatureUnits: String, CaseIterable, Identifiable {
    case Celsius
    case Fahrenheit
    case Kelvin

    var id: String { self.rawValue }
}
@State private var fromUnits: TemperatureUnits = TemperatureUnits.Celsius
Picker("Convert From Units", selection: $fromUnits) {
    ForEach(TemperatureUnits.allCases) { Text($0.rawValue).tag($0) }
}

This is where I think the documentation/my understanding/installation, is broken; because the example looks almost identical to what I put in my code.

1      

To follow up - In a new project I did a straight copy of the documentation's code and it just doesn't work (selectedFlavor doesn't change it's value) on my system - strange.

import SwiftUI

enum Flavor: String, CaseIterable, Identifiable {
    case chocolate
    case vanilla
    case strawberry

    var id: String { self.rawValue }
}

struct ContentView: View {
    @State private var selectedFlavor = Flavor.chocolate

    var body: some View {
        NavigationView {
            Picker("Flavor", selection: $selectedFlavor) {
                ForEach(Flavor.allCases) { flavor in
                    Text(flavor.rawValue.capitalized)
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

I think there's some kind of problem with using String for the Identifiable protocol. If I set the id to the rawValue explicitly it still doesn't work:

ForEach(Flavor.allCases, id:\.self.rawValue) { flavor in

but if I set the id to the enum itself it does work:

ForEach(Flavor.allCases, id:\.self) { flavor in

1      

The key is the bolded part:

ForEach automatically assigns a tag to the selection views, using each option’s id

The type of the id of your Flavor enum is String, not Flavor:

var id: String { self.rawValue }

So to get it to work without explicitly specifying a tag, you would need your selectedFlavor variable to be the same type:

@State private var selectedFlavor = Flavor.chocolate.rawValue
//the rawValue of a Flavor is a String because of: enum Flavor: String

With this change, selectedFlavor matches the type of Flavor's id and the Picker will work again.

Another option to avoid having to explicitly set a tag would be to do this:

enum Flavor: String, CaseIterable {
    //note: no Identifiable conformance
    case chocolate
    case vanilla
    case strawberry
}

struct FlavorView: View {
    @State private var selectedFlavor = Flavor.chocolate

    var body: some View {
        Picker("Flavor", selection: $selectedFlavor) {
            //we identify each item using \.self
            ForEach(Flavor.allCases, id: \.self) { flavor in
                Text(flavor.rawValue.capitalized)
            }
        }
    }
}

We can do without Identifiable conformance because, since Flavor has a RawValue type that is Hashable (which String is), we can use id: \.self in the ForEach and the implicit tags assigned by the ForEach will be of type Flavor and will match the type of selectedFlavor. And so the Picker will work.

Again, the key is that the bound variable for the selection has to be the same type as the items' id.

Edit: I do think you are right that there is an error in Apple's documentation. selectedFlavor has to be a String for their implicit tag example to work, based on their own documentation.

Or, since Flavor has a RawValue that is Hashable, changing id to this will also work: var id: Flavor { self }

1      

Thank you so much @roosterboy. I really appreciate the time you spent on this helping me. Now I understand what's happening and why.

I sent Apple a bug report for the documentation page.

1      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Spend less time managing in-app purchase infrastructure so you can focus on building your app. RevenueCat gives everything you need to easily implement, manage, and analyze in-app purchases and subscriptions without managing servers or writing backend code.

Get Started

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.