TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

SOLVED: Radio buttons in SwiftUI?

Forums > SwiftUI

Hey all, I have a custom button style as so:

struct SeasonButton : ButtonStyle {
    @State private var isSelected : Bool = true

    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .font(bodyFontBold)
            .frame(width: 50, height: 50)
            .background(Color(red: 28/255, green: 35/255, blue: 40/255))
            .foregroundStyle(self.isSelected ? Color.orange : Color.white)
            .clipShape(RoundedRectangle(cornerRadius: 10.0))
            .overlay(
                RoundedRectangle(cornerRadius: 10)
                    .stroke(self.isSelected ? .orange : Color(red: 28/255, green: 35/255, blue: 40/255), lineWidth: 2))
            .onChange(of: configuration.isPressed) {
              if $0 {
                isSelected.toggle()
              }
            }
    }
}

I want this to act like a radio button as the image below (i.e, one button should be orange at a time):

How can I accomplish this? Right now the buttons are in a ForEach being passed the season number:

ForEach(seasons, id: \.self) { season in
    Button(String(season)) {
        print("Yo")                  
    }
     .buttonStyle(SeasonButton())
}

Thanks!

2      

In you previous post I suggested one of the solutions. In order to make it work you need to set up your model for that. Think this way:

  • you have e.g. array of buttons to use in the view
  • each button has property to keep if it was selected or not
  • on each button press you loop over that array and check which button was pressed and change that selected property to true, and false for other buttons.
  • while looping to find which button was pressed you can use seasonNumber for example or create UUID property for each button
  • once buttons' properties will be assigned accordingly after the loop, you will have one button as orange and others as white.

2      

Well seems like I cannot do it now as @Vendetagainst deleted initial question. So let the person to find solution on his own... Part of my offered solution is used in the above code, and there was more than enough to handle this challenge ...

2      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

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

Yeah, I accidentally deleted my original post, but I'll try to figure it out on my own, thanks!

2      

In its simplest form (as I may assume using observable etc. might look way too much to refactor the whole code), below version might be more sutable. There we just can check if season id == season which was passed to the button. And if it was just mark its property as pressed true.

struct Season: Identifiable {
    let id = UUID()
    let number: Int
}

struct ContentView: View {
    @State var seasons: [Season] = [
        Season(number: 1),
        Season(number: 2),
        Season(number: 3)
    ]

    @State var seasonSelected: Season?

    var body: some View {
        HStack {
            ForEach(seasons) { season in
                    SeasonButton(season: season, seasonSelected: $seasonSelected)
            }
        }
    }
}

struct SeasonButton: View {
    let season: Season
    @Binding var seasonSelected: Season?

    let brandColor: Color = Color(red: 28/255, green: 35/255, blue: 40/255)

    var isPressed: Bool {
        if let seasonSelected {
            if seasonSelected.id == season.id { return true }
        }
        return false
    }

    var body: some View {
        Button {
            seasonSelected = season
        } label: {
            Text("\(season.number)")
        }
        .font(.title2)
        .frame(width: 50, height: 50)
        .background(RoundedRectangle(cornerRadius: 8)
            .fill(brandColor)
        )
        .foregroundStyle(isPressed ? .orange : .white)
        .overlay {
            RoundedRectangle(cornerRadius: 10)
                .stroke(isPressed ? Color.orange : Color.white, lineWidth: 3)
        }
    }
}

2      

import SwiftUI

struct RadioButtonsView: View { @State private var selectedOption: Int = 0 let options = ["Option 1", "Option 2", "Option 3"]

var body: some View {
    Picker("Select an option", selection: $selectedOption) {
        ForEach(0..<options.count) { index in
            Text(options[index]).tag(index)
        }
    }
    .pickerStyle(SegmentedPickerStyle())
    .padding()
}

}

struct ContentView: View { var body: some View { VStack { Text("Radio Buttons in SwiftUI") .font(.title) .padding()

        RadioButtonsView()

        // You can use the selectedOption value here for further processing.
        Text("Selected Option: \(RadioButtonsView().selectedOption)")
            .padding()
    }
}

}

@main struct YourApp: App { var body: some Scene { WindowGroup { ContentView() } } } Also check: gasolineracercaubicaion. com

2      

@ygeras
That worked, thanks so much!! However, now I have this:

ForEach(seasons, id: \.self) { season in
  SeasonButtonView(season: season, seasonSelected: $seasonSelected)
    .onTapGesture {
      print("Yo")
    }
}

This doesn't print anything. Why? As well, how do I make it so that the first button is selected by default?

2      

Hi! Why do you attach onTapGesture to SeasonButtonView?

SeasonButtonView is the BUTTON itself. You have to put your print statement inside that view like so:

struct SeasonButton: View {
    let season: Season
    @Binding var seasonSelected: Season?

    let brandColor: Color = Color(red: 28/255, green: 35/255, blue: 40/255)

    var isPressed: Bool {
        if let seasonSelected {
            if seasonSelected.id == season.id { return true }
        }
        return false
    }

    var body: some View {
        Button {
            seasonSelected = season

            print("Yo") // <--- YOUR ACTION TRIGGERED HERE

        } label: {
            Text("\(season.number)")
        }
        .font(.title2)
        .frame(width: 50, height: 50)
        .background(RoundedRectangle(cornerRadius: 8)
            .fill(brandColor)
        )
        .foregroundStyle(isPressed ? .orange : .white)
        .overlay {
            RoundedRectangle(cornerRadius: 10)
                .stroke(isPressed ? Color.orange : Color.white, lineWidth: 3)
        }
    }
}

2      

The "beauty" of this approach is that you have @State var seasonSelected: Season? which you can use to display the selection etc...

struct ContentView: View {
    @State var seasons: [Season] = [
        Season(number: 1),
        Season(number: 2),
        Season(number: 3)
    ]

    // Once you have season selected
    @State var seasonSelected: Season?

    var body: some View {
        VStack {
            // You can display that selection
            Text(seasonSelected != nil ? "Your selected SEASON is \(seasonSelected!.number)" : "SEASON is not yet selected!")

            HStack {
                ForEach(seasons) { season in
                    SeasonButton(season: season, seasonSelected: $seasonSelected)
                }
            }
        }
    }
}

2      

The reason I put onTapGesture there is because the button filters a viewModel's list of episodes, and I don't want to inject the viewModel into the buttonsView.

2      

Then pass in closure to the view and use it like so. Will that help?

struct ContentView: View {
    @State var seasons: [Season] = [
        Season(number: 1),
        Season(number: 2),
        Season(number: 3)
    ]

    // Once you have season selected
    @State var seasonSelected: Season?

    var body: some View {
        VStack {
            // You can display that selection
            Text(seasonSelected != nil ? "Your selected SEASON is \(seasonSelected!.number)" : "SEASON is not yet selected!")

            HStack {
                ForEach(seasons) { season in
                    SeasonButton(season: season, seasonSelected: $seasonSelected) {
                        print("USE YOUR MODEL HERE to find \(seasonSelected!.number)") // <-- YOU can use closure instead of tap gesture, it will be triggered upon button press
                    }
                }
            }
        }
    }
}

struct SeasonButton: View {
    let season: Season
    @Binding var seasonSelected: Season?
    let brandColor: Color = Color(red: 28/255, green: 35/255, blue: 40/255)

    var action: () -> Void // <-- add closure

    var isPressed: Bool {
        if let seasonSelected {
            if seasonSelected.id == season.id { return true }
        }
        return false
    }

    var body: some View {
        Button {
            seasonSelected = season
            action() // <-- will be triggered upon button press
        } label: {
            Text("\(season.number)")
        }
        .font(.title2)
        .frame(width: 50, height: 50)
        .background(RoundedRectangle(cornerRadius: 8)
            .fill(brandColor)
        )
        .foregroundStyle(isPressed ? .orange : .white)
        .overlay {
            RoundedRectangle(cornerRadius: 10)
                .stroke(isPressed ? Color.orange : Color.white, lineWidth: 3)
        }
    }
}

2      

Didn't notice that question before.

As well, how do I make it so that the first button is selected by default?

So that can be handled this way

// Once you have season selected
    @State var seasonSelected: Season?

    // You will need to add this custom init for view where you have @State var seasonSelected: Season?
    // What it does? So it accesses wrappedValue of @State var seasonSelected
    // And provides inital value for @State (it has to be in this format: State(initialValue: YOUR VALUE))
    init() {
        _seasonSelected = State(initialValue: seasons[0]) // <-- Here you pass what you want to be selecteb by default
    }

2      

Hmm, when I try this:

    init(shortName: String) {
        self.shortName = shortName
        _viewModel = StateObject(wrappedValue: EpisodeViewModel(shortName: shortName))
        _seasonSelected = State(initialValue: viewModel.seasons[0])

    }

I get an index out of range error. I guess it's a race condition? The _viewModel was there before to pass the shortName from the view to the view model.

2      

I cannot say what is the right way to handle that particular case, as I do not see the full picture of the project and your view model set up and how you pass that object to other views.

This part of the code:

_seasonSelected = State(initialValue: viewModel.seasons[0])

is necessary only for one reason. When your view loads, @State var seasonSelected: Season? this property is nil. But you want it to be something initially, so your either display that nothing is there (as for example: "Season not selected"), or you provide initial value, what was done in the line above.

BUT if you pass view model object to your view, (I have no idea again about your data setup and flow, so only guessing), why do you need to assign initial value? You just pass reference to that object and use it in that view... using @Bindable for example or via @Environment...

Sorry, maybe it sounds a bit complicated, but you probably can imagine how it is "difficult" to offer solutions whey you see onlyt tip of the iceberg 😅

2      

I will post the whole code... well I hope it is more clear what I meant in the above lines. All comments are there so should be clear what is what. You can just copy in test project and have a look if it is similar to what you're trying to do.

import SwiftUI

@main
struct ButtonProjectApp: App {
    var body: some Scene {
        WindowGroup {
            // This view is now entry point
            MainView()
        }
    }
}
import SwiftUI
import Observation

struct Movie: Identifiable {
    let id = UUID()
    let number: Int
}

struct Season: Identifiable {
    let id = UUID()
    let number: Int
    var movies: [Movie]
}

@Observable
class Seasons {
    // Pay attention that the order of movies is random with number
    // This is just to use as a sample data
    var allSeason: [Season] = [
        Season(number: 1, movies: [Movie(number: 3), Movie(number: 1), Movie(number: 2)]),
        Season(number: 2, movies: [Movie(number: 5), Movie(number: 6), Movie(number: 4)]),
        Season(number: 3, movies: [Movie(number: 8), Movie(number: 7), Movie(number: 9)])
    ]

    // fetch
    // delete
    // etc...
}

struct MainView: View {
    // We initalized view model in this view
    // just to show how you can pass further
    @State var vm = Seasons()

    var body: some View {
        ContentView(viewModel: vm)
    }
}

struct ContentView: View {
    // As we just need one way connection in this exapmle
    // we don't need to use @Bindable
    // but should you need two way communitcation you will need to add @Bindalbe
    let viewModel: Seasons

    // Once you have season selected
    @State var seasonSelected: Season?

    // You will need to add this custom init for view where you have @State var seasonSelected: Season?
    // What it does? So it accesses wrappedValue of @State var seasonSelected
    // And provides inital value for @State (it have to be in this format: State(initialValue: YOUR VALUE))
    init(viewModel: Seasons) {
        self.viewModel = viewModel
        _seasonSelected = State(initialValue: viewModel.allSeason[0])
    }

    var body: some View {
        NavigationStack {
            ZStack(alignment: .bottom) {
                // As we use seasonSelectd as Optinal, let's make sure it exists so that we dont' have to unwrap it later
                if let seasonSelected {
                    List(seasonSelected.movies) { movie in
                        Text("This is movie \(movie.number) for Season \(seasonSelected.number)")
                    }
                }

                VStack {
                    // You can display that selection
                    Text(seasonSelected != nil ? "Your selected SEASON is \(seasonSelected!.number)" : "SEASON is not yet selected!")

                    HStack {
                        ForEach(viewModel.allSeason) { season in
                            SeasonButton(season: season, seasonSelected: $seasonSelected) {
                                // Now you can sort it here
                                sortMovies()
                            }
                        }
                    }
                }
            }
        }
        // should you need to sort on launch
        .onAppear {
            sortMovies()
        }
        .navigationTitle("Movies")
    }

    private func sortMovies() {
        seasonSelected?.movies.sort { $0.number > $1.number }
    }
}

struct SeasonButton: View {
    let season: Season
    @Binding var seasonSelected: Season?
    let brandColor: Color = Color(red: 28/255, green: 35/255, blue: 40/255)

    var action: () -> Void // <-- add closure

    var isPressed: Bool {
        if let seasonSelected {
            if seasonSelected.id == season.id { return true }
        }
        return false
    }

    var body: some View {
        Button {
            seasonSelected = season
            action() // <-- will be triggered upon button press
        } label: {
            Text("\(season.number)")
        }
        .font(.title2)
        .frame(width: 50, height: 50)
        .background(RoundedRectangle(cornerRadius: 8)
            .fill(brandColor)
        )
        .foregroundStyle(isPressed ? .orange : .white)
        .overlay {
            RoundedRectangle(cornerRadius: 10)
                .stroke(isPressed ? Color.orange : Color.white, lineWidth: 3)
        }
    }
}

#Preview {
    NavigationStack {
        ContentView(viewModel: Seasons())
    }
}

2      

Ah, okay, I think I get what you're trying to say. Unfortunately I cannot post the whole code so I will have to try to figure it out with what you gave me. Thanks for all the help!

2      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

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.