swiftuser-interfaceswiftuiuipickerview

How can I make a smoothed custom Picker on SwiftUI?


I would like to replicate this picker in swiftUI. In particular, I have a button on the bottom left of the screen and when I click it I would like to show different icons (similar to the image below, but vertically). As soon as I click on one of the choices the button should shrink back to the initial form (circle) with the chosen icon.

When closed:

Closed button

When open:

Open button

I am new to this language and to app in general, I tried with a Pop Up menu, but it is not the desired result, for now I have an horizontal segmented Picker.


Solution

  • You can't do this with the built-in Picker, because it doesn't offer a style like that and PickerStyle doesn't let you create custom styles (as of the 2022 releases).

    You can create your own implementation out of other SwiftUI views instead. Here's what my brief attempt looks like:

    An expanding picker. Initially it appears as a black circle containing a red icon of a speaker with a slash through it. When I tap it, it expands horizontally to also show a white speaker with an exclamation mark next to it, and a white speaker with two sound lines coming out of it. When I tap the middle icon of the speaker-with-exclamation-mark, the other two icons disappear and the picker collapses down to a black circle showing just the speaker-with-exclamation-mark icon. I tap it again and it re-expands to show all three icons. I tap the rightmost icon of the speaker-with-sound-lines and the picker collapses to the black circle, now showing just the speaker-with-sound-lines icon. I tap to expand the picker and select the red speaker-with-slash icon. The picker collapses to a black circle showing just the red icon. The entire animation then repeats indefinitely.

    Here's the code:

    enum SoundOption {
        case none
        case alertsOnly
        case all
    }
    
    struct SoundOptionPicker: View {
        @Binding var option: SoundOption
        @State private var isExpanded = false
    
        var body: some View {
            HStack(spacing: 0) {
                button(for: .none, label: "volume.slash")
                    .foregroundColor(.red)
                button(for: .alertsOnly, label: "speaker.badge.exclamationmark")
                    .foregroundColor(.white)
                button(for: .all, label: "volume.2")
                    .foregroundColor(.white)
            }
            .buttonStyle(.plain)
            .background {
                Capsule(style: .continuous).foregroundColor(.black)
            }
        }
    
        @ViewBuilder
        private func button(for option: SoundOption, label: String) -> some View {
            Button {
                withAnimation(.easeOut) {
                    if isExpanded {
                        self.option = option
                        isExpanded = false
                    } else {
                        isExpanded = true
                    }
                }
            } label: {
                Image(systemName: label)
                    .fontWeight(.bold)
                    .padding(10)
            }
            .frame(width: shouldShow(option) ? buttonSize : 0, height: buttonSize)
            .opacity(shouldShow(option) ? 1 : 0)
            .clipped()
        }
    
        private var buttonSize: CGFloat { 44 }
    
        private func shouldShow(_ option: SoundOption) -> Bool {
            return isExpanded || option == self.option
        }
    }
    
    struct ContentView: View {
        @State var option = SoundOption.none
    
        var body: some View {
            ZStack {
                Color(hue: 0.6, saturation: 1, brightness: 0.2)
                SoundOptionPicker(option: $option)
                    .shadow(color: .gray, radius: 3)
                    .frame(width: 200, alignment: .trailing)
            }
        }
    }