Swift and SwiftUI tutorials for Swift Developers

Change Picker Font in SwiftUI

Although SwiftUI is a wonderfully declarative and fast tool within Xcode, it sometimes lacks the visual granularity we had (and still have) in UIKit or AppKit. Applying a simple .font() modifier to a standard Picker often doesn’t produce the expected result, especially depending on the picker style (.wheel, .segmented, .menu) and the platform (iOS, macOS, or watchOS).

In this comprehensive tutorial about how to change the Picker font in SwiftUI, we are going to break down exactly why this happens and I will teach you all the techniques, from the official “hacks” using Appearance Proxies to creating 100% custom, cross-platform components in Swift.


1. Why doesn’t .font() always work?

In the ideal world of declarative Swift programming, the code should look like this:

Picker("Select a flavor", selection: $flavor) {
    Text("Vanilla").tag(0)
    Text("Chocolate").tag(1)
}
.font(.custom("MyCustomFont", size: 18)) // Spoiler: Often ignored
.pickerStyle(.wheel)

Depending on the iOS version and the PickerStyle, SwiftUI wraps the underlying native system components (UIPickerView on iOS, NSPopUpButton on macOS, etc.). These legacy components do not always listen to SwiftUI’s environment modifiers.

  • In .menu style (iOS 14+): It sometimes respects the font modifier if applied directly to the content (Text) inside the closure, but not always on the main button.
  • In .wheel style: It ignores the .font() modifier entirely in most versions.
  • In .segmented style: It ignores SwiftUI fonts, as it relies strictly on UISegmentedControl rendering.

Let’s see how to solve this platform by platform and style by style.


2. iOS Solution: Integrating UIKit with SwiftUI

For an iOS Developer, the fastest and most robust solution for standard components that resist SwiftUI is to go back to the roots: UIAppearance. This proxy allows us to change the global design of UIKit components, which SwiftUI uses under the hood.

2.1 Changing the font in a WheelPickerStyle (The classic “wheel”)

The WheelPickerStyle is backed by UIPickerView. To change the Picker font in SwiftUI using this style, we need to inject our configuration into UIKit.

import SwiftUI

struct CustomWheelPicker: View {
    @State private var selection = "Option 1"
    let options = ["Option 1", "Option 2", "Option 3"]
    
    // Initializer to set up the appearance proxy
    init() {
        // Get the system font or a custom one (e.g., Avenir Next)
        let customFont = UIFont(name: "AvenirNext-Bold", size: 24) ?? UIFont.systemFont(ofSize: 24)
        
        // Configure text attributes
        let attributes: [NSAttributedString.Key: Any] = [
            .font: customFont,
            .foregroundColor: UIColor.systemBlue // We can also change the color
        ]
        
        // Apply attributes to all UIPickerViews in the app
        UIPickerView.appearance().setValue(UIColor.clear, forKey: "magnifyingGlass") // Optional: remove magnifying glass
    }
    
    var body: some View {
        VStack {
            Text("Selected: \(selection)")
                .font(.headline)
            
            // IMPORTANT: The Text inside the Picker is what UIPickerView renders.
            // In recent iOS versions, applying .font directly to Text sometimes works 
            // for the Wheel, but using NSAttributedString via UIKit guarantees backward compatibility.
            Picker("Options", selection: $selection) {
                ForEach(options, id: \.self) { option in
                    Text(option)
                        // For iOS 15+, applying the font here sometimes works for Wheel!
                        .font(.custom("AvenirNext-Bold", size: 24))
                        .foregroundColor(.blue)
                }
            }
            .pickerStyle(.wheel)
        }
        .padding()
    }
}

Developer Note: While UIPickerView.appearance() is powerful, be careful, as it affects all Wheel Pickers in your application. If you need different fonts for different Pickers, you will have to create a custom UIViewRepresentable view.

2.2 Changing the font in a SegmentedPickerStyle

The segmented control is extremely rigid in SwiftUI. It is backed by UISegmentedControl. Here it is imperative to use UIAppearance.

import SwiftUI

struct CustomSegmentedPicker: View {
    @State private var selection = 0
    
    init() {
        let font = UIFont(name: "Papyrus", size: 16) ?? UIFont.systemFont(ofSize: 16)
        
        // Attributes for the normal state
        let normalAttributes: [NSAttributedString.Key: Any] = [
            .font: font,
            .foregroundColor: UIColor.darkGray
        ]
        
        // Attributes for the selected state
        let selectedAttributes: [NSAttributedString.Key: Any] = [
            .font: font,
            .foregroundColor: UIColor.white
        ]
        
        UISegmentedControl.appearance().setTitleTextAttributes(normalAttributes, for: .normal)
        UISegmentedControl.appearance().setTitleTextAttributes(selectedAttributes, for: .selected)
        UISegmentedControl.appearance().selectedSegmentTintColor = .systemIndigo
    }
    
    var body: some View {
        Picker("Options", selection: $selection) {
            Text("One").tag(0)
            Text("Two").tag(1)
            Text("Three").tag(2)
        }
        .pickerStyle(.segmented)
        .padding()
    }
}

3. macOS Solution: Embracing AppKit

If your Swift programming is taking you into the Mac ecosystem, you’ll notice that macOS uses AppKit instead of UIKit. SwiftUI on the Mac translates the Picker to controls like NSPopUpButton or NSRadioGroup.

On macOS, SwiftUI has vastly improved its ability to inherit the .font() modifier. However, if you need absolute control, sometimes you need to wrap an NSPopUpButton. Fortunately, for the general use case in modern macOS (macOS 12+), the native menu does largely respect the modifier on the content.

#if os(macOS)
import SwiftUI

struct CustomMacOSPicker: View {
    @State private var selection = "Swift"
    let languages = ["Swift", "Objective-C", "C++"]
    
    var body: some View {
        VStack {
            Picker("Preferred language:", selection: $selection) {
                ForEach(languages, id: \.self) { language in
                    Text(language)
                        // On macOS, the font applied directly to the Text 
                        // is usually reflected in the dropdown menu.
                        .font(.custom("Menlo", size: 14))
                }
            }
            .pickerStyle(.menu)
            .frame(width: 250)
            
            // To change the font of the Picker's "Label":
            .font(.custom("HelveticaNeue-Bold", size: 16)) 
        }
        .padding()
    }
}
#endif

4. watchOS Solution: Limited Space, Big Decisions

The Apple Watch, running watchOS, has a completely different interface paradigm. The wheel-style picker is operated via the Digital Crown. In watchOS, native components are highly optimized.

To change the Picker font in SwiftUI on watchOS, you must apply the modifier directly to the content.

#if os(watchOS)
import SwiftUI

struct CustomWatchOSPicker: View {
    @State private var amount = 1
    
    var body: some View {
        VStack {
            Text("Select amount")
                .font(.footnote)
            
            Picker("Amount", selection: $amount) {
                ForEach(1...10, id: \.self) { number in
                    Text("\(number)")
                        // In watchOS, this changes the text size in the wheel
                        .font(.system(size: 30, weight: .black, design: .rounded))
                        .foregroundColor(.green)
                }
            }
            .pickerStyle(.wheel)
        }
    }
}
#endif

5. The Ultimate Approach: Building a 100% Custom Picker in SwiftUI

If you’re tired of fighting the limitations of Appearance Proxies, having UIKit pollute your declarative views in Xcode, and you want pinpoint cross-platform control, the best decision an iOS Developer can make is… not to use SwiftUI’s native Picker.

By creating a custom component, we guarantee that the design will work exactly the same everywhere, using pure Swift programming.

Let’s create a DropdownPicker (Menu style) from scratch using DisclosureGroup and ScrollView.

Step 1: Define the Custom Picker View

import SwiftUI

struct CustomFontPicker<T: Hashable>: View {
    let title: String
    @Binding var selection: T
    let options: [T]
    let optionName: (T) -> String // Closure to convert generic to String
    
    // Font customization
    var titleFont: Font = .headline
    var optionsFont: Font = .body
    var accentColor: Color = .blue
    
    @State private var isExpanded = false
    
    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            
            // Main Button / Header
            Button(action: {
                withAnimation(.spring()) {
                    isExpanded.toggle()
                }
            }) {
                HStack {
                    Text(title + ": " + optionName(selection))
                        .font(titleFont)
                        .foregroundColor(.primary)
                    
                    Spacer()
                    
                    Image(systemName: "chevron.down")
                        .rotationEffect(.degrees(isExpanded ? 180 : 0))
                        .foregroundColor(accentColor)
                        .font(titleFont)
                }
                .padding()
                .background(Color(.systemGray6))
                .cornerRadius(isExpanded ? 0 : 10)
                // Conditional rounded corners
                .clipShape(
                    RoundedRectangle(cornerRadius: 10)
                )
            }
            
            // Dropdown List
            if isExpanded {
                ScrollView {
                    VStack(alignment: .leading, spacing: 0) {
                        ForEach(options, id: \.self) { option in
                            Button(action: {
                                withAnimation(.spring()) {
                                    selection = option
                                    isExpanded = false
                                }
                            }) {
                                HStack {
                                    Text(optionName(option))
                                        .font(optionsFont) // HERE IS OUR TOTAL CONTROL
                                        .foregroundColor(selection == option ? accentColor : .primary)
                                    
                                    Spacer()
                                    
                                    if selection == option {
                                        Image(systemName: "checkmark")
                                            .foregroundColor(accentColor)
                                    }
                                }
                                .padding()
                                .background(Color(.systemBackground))
                            }
                            
                            Divider()
                        }
                    }
                }
                .frame(maxHeight: 200) // Limit scroll height
                .background(Color(.systemBackground))
                .cornerRadius(10)
                .shadow(color: Color.black.opacity(0.1), radius: 5, x: 0, y: 5)
            }
        }
        .padding()
    }
}

Step 2: Implementation in your App

Now, using it in your Xcode project is a breeze, and the fonts will respond without complaint.

struct ContentView: View {
    @State private var selectedLanguage = "SwiftUI"
    let frameworks = ["SwiftUI", "UIKit", "AppKit", "WatchKit"]
    
    var body: some View {
        ZStack {
            Color(.systemGroupedBackground).edgesIgnoringSafeArea(.all)
            
            VStack {
                Text("Preferences Panel")
                    .font(.largeTitle)
                    .fontWeight(.heavy)
                    .padding(.bottom, 20)
                
                // Using our 100% pure SwiftUI Picker
                CustomFontPicker(
                    title: "Framework",
                    selection: $selectedLanguage,
                    options: frameworks,
                    optionName: { $0 }, // The option is already a String
                    titleFont: .custom("Courier", size: 18).bold(),
                    optionsFont: .custom("Courier", size: 16),
                    accentColor: .purple
                )
                
                Spacer()
            }
        }
    }
}

The advantages of this approach?

  • No UIKit: You don’t accidentally alter global appearances.
  • Fully Animatable: You can change the appearance transitions.
  • Unrestricted Fonts: Use .custom, change the weight, the tracking, whatever you need.

6. Important Considerations on Dynamic Type and Accessibility

As an iOS Developer, your job is not just to make the app look pretty with a cool font; you must also ensure that users with visual impairments can read it.

When you change the Picker font in SwiftUI to a custom font (.custom("FontName", size: X)), you must ensure that the font scales with iOS accessibility settings (Dynamic Type).

Instead of setting a fixed size, use relative scaling in SwiftUI:

// Bad practice: fixed size, breaks accessibility
Text("Option")
    .font(.custom("Avenir", size: 18)) 

// Good practice: scales relative to the Body text style
Text("Option")
    .font(.custom("Avenir", size: 18, relativeTo: .body))

If you decide to use the UIAppearance approach (Step 2), you must be prepared to listen for system notifications when the preferred font size changes (UIContentSizeCategoryDidChangeNotification) to recalculate your NSAttributedString, which can be tedious. This is another massive reason why the approach in Step 5 (100% Custom Picker in SwiftUI) is vastly superior for production-quality apps.


7. Strategy Comparison Table

To summarize which path you should choose when coding in Xcode:

Method Compatible Style Pros Cons
Direct .font() .menu (iOS 15+), .wheel (watchOS) Fast, clean code. Often ignored on iOS/macOS for core components and .segmented.
UIAppearance .wheel, .segmented (iOS) Safely fixes legacy UIKit components. Globally affects the entire App; harder to integrate with Dynamic Type.
UIViewRepresentable All UIKit styles Total instance control. Requires a lot of boilerplate code (Delegate, DataSource, Coordinator). Loses declarative magic.
100% SwiftUI View N/A (You build it) Absolute visual and font control. 100% Declarative. True cross-platform. You have to program the selection, expansion, and accessibility logic yourself.

Conclusion

The SwiftUI ecosystem has matured enormously, but when it comes to components that act as “bridges” to older APIs, we still find bumps in the road. Knowing how to change the Picker font in SwiftUI distinguishes you from being a simple coder to being a true expert iOS Developer in Swift programming.

Whether you decide to apply “band-aids” via UIAppearance for a quick project, or invest time in designing your own dropdown component that perfectly respects your design team’s guidelines, you now have the tools in your arsenal.

Keep experimenting, keep reading our blog, and above all, have fun coding in Xcode. The beauty of Swift is that there are always multiple paths to an elegant solution.

If you have any questions about this article, please contact me and I will be happy to help you ðŸ™‚. You can contact me on my X profile or on my Instagram profile.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

SwiftUI Grid Tutorial

Next Article

How to Localize Text in SwiftUI

Related Posts