iosswiftswiftui

How to make SwiftUI Picker's segments be actually as wide as its content and not as wide as the widest segment?


In SwiftUI, I have the following:

func segments() -> [String] {
    var toReturn = Calendar.current.veryShortWeekdaySymbols
    toReturn.append("T")
    return toReturn
}

Picker("Day of week?", selection: $selectedDay) {
    ForEach(segments(), id: \.self) {
        Text($0)
            .scaledToFit()
    }
}
.pickerStyle(.segmented)
.fixedSize()

This shows as:

enter image description here

However, if I change the toReturn.append("T") to be a longer string such as toReturn.append("Tasks"), then the segments become as wide as the widest segment and not actually fit its content:

enter image description here

How to make SwiftUI Picker's segments be actually as wide as its content and not as wide as the widest segment?


Solution

  • SwiftUI doesn't support this at the moment, so you'd have to drop down to UIKit, and set UISegmentedControl.apportionsSegmentWidthsByContent to true.

    If you don't want any segmented picker to have equal widths, just use the UIAppearance APIs,

    // put this in onAppear wherever you need it
    UISegmentedControl.appearance().apportionsSegmentWidthsByContent = true
    

    Otherwise, you can write your own UIViewRepresentable. Here is an example:

    struct FitWidthSegmenetedPicker<Selection: Hashable>: UIViewRepresentable {
        let options: [Selection]
        @Binding var selection: Selection
        let titleForOption: (Selection) -> String
        
        init(
            _ options: [Selection],
            selection: Binding<Selection>,
            titleForOption: @escaping (Selection) -> String = { String(describing: $0) }
        ) {
            self.options = options
            self._selection = selection
            self.titleForOption = titleForOption
        }
        
        func makeUIView(context: Context) -> UISegmentedControl {
            let segmentedControl = UISegmentedControl()
            segmentedControl.apportionsSegmentWidthsByContent = true
            segmentedControl.addTarget(context.coordinator, action: #selector(Coordinator.selectionChanged(_:)), for: .valueChanged)
            return segmentedControl
        }
        
        func updateUIView(_ uiView: UISegmentedControl, context: Context) {
            uiView.removeAllSegments()
            for option in options.reversed() {
                uiView.insertSegment(withTitle: titleForOption(option), at: 0, animated: false)
            }
            uiView.selectedSegmentIndex = options.firstIndex(of: selection) ?? 0
            context.coordinator.onSelectionChanged = { index in
                selection = options[index]
            }
        }
        
        func makeCoordinator() -> Coordinator {
            .init()
        }
        
        @MainActor
        class Coordinator: NSObject {
            var onSelectionChanged: ((Int) -> Void)?
            
            @objc func selectionChanged(_ sender: UISegmentedControl) {
                onSelectionChanged?(sender.selectedSegmentIndex)
            }
        }
    }
    

    Usage:

    struct ContentView: View {
        let options = ["x", "xx", "xxx", "xxxx", "xxxxxxxxxxxx"]
        @State private var selection = "x"
        
        var body: some View {
            FitWidthSegmenetedPicker(options, selection: $selection)
                .fixedSize()
            Text(selection)
        }
    }
    

    enter image description here