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
UISegmentedControlrendering.
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 customUIViewRepresentableview.
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.