Programación en Swift y SwiftUI para iOS Developers

Cómo cambiar la fuente del Picker en SwiftUI

Si estás inmerso en la programación Swift y estás construyendo interfaces modernas con SwiftUI, es muy probable que te hayas topado con un muro que todos hemos enfrentado: intentar personalizar un elemento nativo y darte cuenta de que el framework parece resistirse. Uno de los casos más notorios y frustrantes para cualquier iOS Developer es intentar cambiar la fuente del Picker en SwiftUI.

Aunque SwiftUI es una herramienta maravillosamente declarativa y rápida dentro de Xcode, a veces carece de la granularidad visual que teníamos (y seguimos teniendo) en UIKit o AppKit. Aplicar un simple modificador .font() a un Picker estándar a menudo no produce el resultado esperado, especialmente dependiendo del estilo del selector (.wheel, .segmented, .menu) y de la plataforma (iOS, macOS, o watchOS).

En este tutorial exhaustivo (ideal para tu blog tecnológico), vamos a desglosar exactamente por qué ocurre esto y te enseñaré todas las técnicas, desde los “hacks” oficiales usando Appearance Proxies hasta la creación de componentes 100% personalizados y multiplataforma en Swift.


1. ¿Por qué .font() no siempre funciona?

En el mundo ideal de la programación Swift declarativa, el código debería verse así:

Picker("Selecciona un sabor", selection: $sabor) {
    Text("Vainilla").tag(0)
    Text("Chocolate").tag(1)
}
.font(.custom("MiFuentePersonalizada", size: 18)) // Spoiler: A menudo ignorado
.pickerStyle(.wheel)

Dependiendo de la versión de iOS y del PickerStyle, SwiftUI envuelve los componentes nativos del sistema subyacente (UIPickerView en iOS, NSPopUpButton en macOS, etc.). Estos componentes heredados no siempre escuchan los modificadores de entorno de SwiftUI.

  • En estilo .menu (iOS 14+): A veces respeta el modificador de fuente si se aplica directamente al contenido (Text) dentro del closure, pero no siempre en el botón principal.
  • En estilo .wheel: Ignora el modificador .font() por completo en la mayoría de las versiones.
  • En estilo .segmented: Ignora las fuentes de SwiftUI, ya que depende estrictamente del renderizado de UISegmentedControl.

Veamos cómo resolver esto plataforma por plataforma y estilo por estilo.


2. Solución en iOS: Integrando UIKit con SwiftUI

Para un iOS Developer, la solución más rápida y robusta para los componentes estándar que se resisten a SwiftUI es volver a las raíces: UIAppearance. Este proxy nos permite cambiar el diseño global de los componentes de UIKit, los cuales SwiftUI utiliza por debajo de la mesa.

2.1 Cambiar la fuente en un WheelPickerStyle (La clásica “rueda”)

El WheelPickerStyle está respaldado por UIPickerView. Para cambiar la fuente del Picker en SwiftUI usando este estilo, necesitamos inyectar nuestra configuración en UIKit.

import SwiftUI

struct WheelPickerPersonalizado: View {
    @State private var seleccion = "Opción 1"
    let opciones = ["Opción 1", "Opción 2", "Opción 3"]
    
    // Inicializador para configurar el proxy de apariencia
    init() {
        // Obtenemos la fuente del sistema o una personalizada (ej. Avenir Next)
        let customFont = UIFont(name: "AvenirNext-Bold", size: 24) ?? UIFont.systemFont(ofSize: 24)
        
        // Configuramos los atributos de texto
        let attributes: [NSAttributedString.Key: Any] = [
            .font: customFont,
            .foregroundColor: UIColor.systemBlue // También podemos cambiar el color
        ]
        
        // Aplicamos los atributos a todos los UIPickerView de la app
        UIPickerView.appearance().setValue(UIColor.clear, forKey: "magnifyingGlass") // Opcional: quitar lupa
    }
    
    var body: some View {
        VStack {
            Text("Seleccionado: \(seleccion)")
                .font(.headline)
            
            // IMPORTANTE: El Text dentro del Picker es lo que UIPickerView renderiza.
            // En versiones recientes de iOS, a veces podemos usar .font directamente en el Text, 
            // pero usar NSAttributedString vía UIKit garantiza soporte retroactivo.
            Picker("Opciones", selection: $seleccion) {
                ForEach(opciones, id: \.self) { opcion in
                    Text(opcion)
                        // Para iOS 15+, aplicar la fuente aquí a veces funciona para Wheel!
                        .font(.custom("AvenirNext-Bold", size: 24))
                        .foregroundColor(.blue)
                }
            }
            .pickerStyle(.wheel)
        }
        .padding()
    }
}

Nota para el desarrollador: Si bien UIPickerView.appearance() es potente, ten cuidado, ya que afecta a todos los Wheel Pickers de tu aplicación. Si necesitas fuentes diferentes para distintos Pickers, tendrás que crear una vista UIViewRepresentable personalizada.

2.2 Cambiar la fuente en un SegmentedPickerStyle

El control segmentado es extremadamente rígido en SwiftUI. Está respaldado por UISegmentedControl. Aquí es imperativo usar UIAppearance.

import SwiftUI

struct SegmentedPickerPersonalizado: View {
    @State private var seleccion = 0
    
    init() {
        let font = UIFont(name: "Papyrus", size: 16) ?? UIFont.systemFont(ofSize: 16)
        
        // Atributos para el estado normal
        let normalAttributes: [NSAttributedString.Key: Any] = [
            .font: font,
            .foregroundColor: UIColor.darkGray
        ]
        
        // Atributos para el estado seleccionado
        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("Opciones", selection: $seleccion) {
            Text("Uno").tag(0)
            Text("Dos").tag(1)
            Text("Tres").tag(2)
        }
        .pickerStyle(.segmented)
        .padding()
    }
}

3. Solución para macOS: Abrazando AppKit

Si tu programación Swift te está llevando al ecosistema de Mac, notarás que macOS usa AppKit en lugar de UIKit. SwiftUI en Mac traduce el Picker a controles como NSPopUpButton o NSRadioGroup.

En macOS, SwiftUI ha mejorado enormemente la capacidad de heredar el modificador .font(). Sin embargo, si necesitas un control absoluto, a veces es necesario envolver un NSPopUpButton. Afortunadamente, para el caso de uso general en macOS moderno (macOS 12+), el menú nativo respeta en gran medida el modificador en el contenido.

#if os(macOS)
import SwiftUI

struct MacOSPickerPersonalizado: View {
    @State private var seleccion = "Swift"
    let lenguajes = ["Swift", "Objective-C", "C++"]
    
    var body: some View {
        VStack {
            Picker("Lenguaje preferido:", selection: $seleccion) {
                ForEach(lenguajes, id: \.self) { lenguaje in
                    Text(lenguaje)
                        // En macOS, la fuente aplicada directamente al Text 
                        // suele reflejarse en el menú desplegable.
                        .font(.custom("Menlo", size: 14))
                }
            }
            .pickerStyle(.menu)
            .frame(width: 250)
            
            // Para cambiar la fuente del "Label" o etiqueta del Picker:
            .font(.custom("HelveticaNeue-Bold", size: 16)) 
        }
        .padding()
    }
}
#endif

4. Solución para watchOS: Espacio Limitado, Grandes Decisiones

El Apple Watch, operando con watchOS, tiene un paradigma de interfaz completamente distinto. El selector de tipo rueda se usa a través de la Corona Digital (Digital Crown). En watchOS, los componentes nativos están muy optimizados.

Para cambiar la fuente del Picker en SwiftUI en watchOS, debes aplicar el modificador directamente al contenido.

#if os(watchOS)
import SwiftUI

struct WatchOSPickerPersonalizado: View {
    @State private var cantidad = 1
    
    var body: some View {
        VStack {
            Text("Selecciona cantidad")
                .font(.footnote)
            
            Picker("Cantidad", selection: $cantidad) {
                ForEach(1...10, id: \.self) { numero in
                    Text("\(numero)")
                        // En watchOS, esto cambia el tamaño del texto en la rueda
                        .font(.system(size: 30, weight: .black, design: .rounded))
                        .foregroundColor(.green)
                }
            }
            .pickerStyle(.wheel)
        }
    }
}
#endif

5. El Enfoque Definitivo: Construir un Picker 100% Personalizado en SwiftUI

Si estás cansado de pelear con las limitaciones de los Appearance Proxies, de que UIKit contamine tus vistas declarativas en Xcode, y quieres un control milimétrico multiplataforma, la mejor decisión que puede tomar un iOS Developer es… no usar el Picker nativo de SwiftUI.

Al crear un componente personalizado, garantizamos que el diseño funcione exactamente igual en todas partes, utilizando pura programación Swift.

Vamos a crear un DropdownPicker (estilo Menú) desde cero utilizando DisclosureGroup y ScrollView.

Paso 1: Definir la Vista del Picker Personalizado

import SwiftUI

struct CustomFontPicker<T: Hashable>: View {
    let titulo: String
    @Binding var seleccion: T
    let opciones: [T]
    let nombreOpcion: (T) -> String // Closure para convertir genérico a String
    
    // Personalización de fuente
    var fuenteTitulo: Font = .headline
    var fuenteOpciones: Font = .body
    var colorAcento: Color = .blue
    
    @State private var estaExpandido = false
    
    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            
            // Botón principal / Cabecera
            Button(action: {
                withAnimation(.spring()) {
                    estaExpandido.toggle()
                }
            }) {
                HStack {
                    Text(titulo + ": " + nombreOpcion(seleccion))
                        .font(fuenteTitulo)
                        .foregroundColor(.primary)
                    
                    Spacer()
                    
                    Image(systemName: "chevron.down")
                        .rotationEffect(.degrees(estaExpandido ? 180 : 0))
                        .foregroundColor(colorAcento)
                        .font(fuenteTitulo)
                }
                .padding()
                .background(Color(.systemGray6))
                .cornerRadius(estaExpandido ? 0 : 10)
                // Esquinas redondeadas condicionales
                .clipShape(
                    RoundedRectangle(cornerRadius: 10)
                )
            }
            
            // Lista desplegable
            if estaExpandido {
                ScrollView {
                    VStack(alignment: .leading, spacing: 0) {
                        ForEach(opciones, id: \.self) { opcion in
                            Button(action: {
                                withAnimation(.spring()) {
                                    seleccion = opcion
                                    estaExpandido = false
                                }
                            }) {
                                HStack {
                                    Text(nombreOpcion(opcion))
                                        .font(fuenteOpciones) // AQUÍ ESTÁ NUESTRO CONTROL TOTAL
                                        .foregroundColor(seleccion == opcion ? colorAcento : .primary)
                                    
                                    Spacer()
                                    
                                    if seleccion == opcion {
                                        Image(systemName: "checkmark")
                                            .foregroundColor(colorAcento)
                                    }
                                }
                                .padding()
                                .background(Color(.systemBackground))
                            }
                            
                            Divider()
                        }
                    }
                }
                .frame(maxHeight: 200) // Limitar altura del scroll
                .background(Color(.systemBackground))
                .cornerRadius(10)
                .shadow(color: Color.black.opacity(0.1), radius: 5, x: 0, y: 5)
            }
        }
        .padding()
    }
}

Paso 2: Implementación en tu App

Ahora, usarlo en tu Xcode project es un paseo, y las fuentes responderán sin chistar.

struct ContentView: View {
    @State private var lenguajeSeleccionado = "SwiftUI"
    let frameworks = ["SwiftUI", "UIKit", "AppKit", "WatchKit"]
    
    var body: some View {
        ZStack {
            Color(.systemGroupedBackground).edgesIgnoringSafeArea(.all)
            
            VStack {
                Text("Panel de Preferencias")
                    .font(.largeTitle)
                    .fontWeight(.heavy)
                    .padding(.bottom, 20)
                
                // Usando nuestro Picker 100% puro SwiftUI
                CustomFontPicker(
                    titulo: "Framework",
                    seleccion: $lenguajeSeleccionado,
                    opciones: frameworks,
                    nombreOpcion: { $0 }, // La opción ya es un String
                    fuenteTitulo: .custom("Courier", size: 18).bold(),
                    fuenteOpciones: .custom("Courier", size: 16),
                    colorAcento: .purple
                )
                
                Spacer()
            }
        }
    }
}

¿Las ventajas de este enfoque?

  • Sin UIKit: No alteras la apariencia global accidentalmente.
  • Totalmente Animable: Puedes cambiar las transiciones de aparición.
  • Fuentes sin restricciones: Usa .custom, cambia el weight, el tracking, lo que necesites.

6. Consideraciones Importantes sobre Dynamic Type y Accesibilidad

Como iOS Developer, tu trabajo no solo consiste en hacer que la app se vea bonita con una fuente genial; también debes asegurar que los usuarios con problemas de visión puedan leerla.

Al cambiar la fuente del Picker en SwiftUI por una fuente personalizada (.custom("NombreFuente", size: X)), debes asegurarte de que la fuente escale con las configuraciones de accesibilidad de iOS (Dynamic Type).

En lugar de poner un tamaño fijo, usa el escalado relativo en SwiftUI:

// Mala práctica: tamaño fijo, rompe la accesibilidad
Text("Opción")
    .font(.custom("Avenir", size: 18)) 

// Buena práctica: escala en relación con el estilo del cuerpo (Body)
Text("Opción")
    .font(.custom("Avenir", size: 18, relativeTo: .body))

Si decides utilizar el enfoque de UIAppearance (Paso 2), debes estar preparado para escuchar notificaciones del sistema cuando cambie el tamaño de letra preferido (UIContentSizeCategoryDidChangeNotification) para recalcular tus NSAttributedString, lo cual puede ser tedioso. Esta es otra razón gigantesca por la cual el enfoque del Paso 5 (Picker 100% Personalizado en SwiftUI) es ampliamente superior para aplicaciones de calidad de producción.


7. Tabla Comparativa de Estrategias

Para resumir qué camino debes elegir cuando estés codificando en Xcode:

MétodoEstilo CompatibleVentajasDesventajas
.font() Directo.menu (iOS 15+), .wheel (watchOS)Rápido, código limpio.A menudo ignorado en iOS/macOS para componentes principales y .segmented.
UIAppearance.wheel, .segmented (iOS)Soluciona componentes heredados de UIKit de forma segura.Afecta globalmente a toda la App; más difícil de integrar con Dynamic Type.
UIViewRepresentableTodos los de UIKitControl total de instancia.Requiere mucho código extra (Delegate, DataSource, Coordinator). Pierde la magia declarativa.
Vista 100% SwiftUIN/A (Construyes el tuyo)Control visual y de fuente absoluto. 100% Declarativo. Multiplataforma real.Debes programar la lógica de selección, expansión y accesibilidad por tu cuenta.

Conclusión

El ecosistema de SwiftUI ha madurado enormemente, pero cuando se trata de componentes que actúan como “puentes” hacia APIs antiguas, aún encontramos baches en el camino. Saber cómo cambiar la fuente del Picker en SwiftUI te distingue de ser un simple codificador a ser un verdadero experto iOS Developer en programación Swift.

Ya sea que decidas aplicar “tiritas” mediante UIAppearance para un proyecto rápido, o que inviertas tiempo en diseñar tu propio componente desplegable que respete perfectamente las directrices de tu equipo de diseño, ahora tienes las herramientas en tu arsenal.

Sigue experimentando, sigue leyendo nuestro blog y, sobre todo, diviértete programando en Xcode. La belleza de Swift es que siempre hay múltiples caminos para llegar a una solución elegante.

Si tienes cualquier duda sobre este artículo, contacta conmigo y estaré encantado de ayudarte 🙂. Puedes contactar conmigo en mi perfil de X o en mi perfil de Instagram

Leave a Reply

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

Previous Article

Tutorial de Grid en SwiftUI

Next Article

Cómo localizar texto en SwiftUI

Related Posts