Como iOS Developer, sabes que Apple nos proporciona un conjunto increíble de herramientas y componentes de interfaz de usuario predeterminados. El Picker estándar de SwiftUI es fantástico para la mayoría de los casos de uso básicos. Sin embargo, cuando trabajas en un proyecto con un sistema de diseño estricto, o cuando simplemente quieres que tu aplicación destaque entre la multitud con animaciones fluidas y una estética única, el componente estándar puede quedarse corto.
Es aquí donde brilla la programación Swift moderna. En este tutorial, te guiaré paso a paso sobre cómo construir un picker personalizado en SwiftUI desde cero. Lo haremos altamente reutilizable, animado, accesible y, lo más importante, compatible con múltiples plataformas: iOS, macOS y watchOS, todo desde un único proyecto en Xcode.
1. Por Qué Necesitamos un Picker Personalizado
Antes de sumergirnos en Xcode y escribir código, es crucial entender por qué y cuándo debemos alejarnos de los componentes nativos.
El Picker nativo en SwiftUI adapta su estilo dependiendo de la plataforma y del modificador .pickerStyle(). Puede ser un menú desplegable, un control segmentado (SegmentedPickerStyle), o una rueda (WheelPickerStyle). Aunque esto es genial para la consistencia del sistema operativo, presenta limitaciones:
- Falta de control sobre la altura y el padding: El control segmentado nativo tiene un tamaño fijo que es difícil de modificar.
- Limitaciones de color: Cambiar el color de fondo del elemento seleccionado o el color del texto nativo no siempre responde bien a nuestras necesidades de branding.
- Animaciones restringidas: No podemos modificar cómo transiciona el indicador de un elemento a otro.
Al crear nuestro propio picker personalizado en SwiftUI, obtenemos control total sobre cada píxel, cada milisegundo de animación y cada estado de accesibilidad.
2. Preparando el Entorno en Xcode
Para comenzar, abre Xcode y crea un nuevo proyecto multiplataforma (Multiplatform App) o simplemente una aplicación para iOS si prefieres enfocarte primero en esa plataforma.
Asegúrate de seleccionar SwiftUI como la interfaz y Swift como el lenguaje.
Requisitos previos:
- Conocimientos intermedios de programación Swift.
- Familiaridad con los modificadores de SwiftUI y el manejo de estado (
@State,@Binding). - Xcode 14 o superior (recomendado Xcode 15+ para aprovechar las últimas optimizaciones de Swift).
3. Definiendo el Modelo de Datos
Para que nuestro picker sea verdaderamente profesional y reutilizable, no debemos codificar cadenas de texto (Strings) directamente. En su lugar, utilizaremos Generics y el protocolo Hashable. Para este ejemplo, crearemos un enum simple, pero nuestro componente final aceptará cualquier tipo de dato.
Crea un nuevo archivo llamado PickerOpciones.swift:
import Foundation
// Definimos un enum que representará las opciones de nuestro picker de ejemplo
enum PeriodoTiempo: String, CaseIterable, Identifiable {
case dia = "Día"
case semana = "Semana"
case mes = "Mes"
case anio = "Año"
var id: String { self.rawValue }
}
Al conformar CaseIterable e Identifiable, facilitamos la iteración sobre estas opciones dentro de las vistas de SwiftUI.
4. Construyendo la Vista del Picker Personalizado en SwiftUI
Ahora vamos a crear el núcleo de nuestro componente. Queremos un diseño tipo “Segmented Control”, pero con esquinas más redondeadas, colores personalizados y una animación fluida cuando el usuario cambia de opción.
Crea un nuevo archivo de vista de SwiftUI llamado CustomSegmentedPicker.swift.
4.1 La Estructura Básica y Generics
Para hacer de esto una herramienta digna de un iOS Developer Senior, usaremos genéricos para que el picker pueda aceptar cualquier tipo de opción.
import SwiftUI
struct CustomSegmentedPicker<T: Hashable & Identifiable & RawRepresentable>: View where T.RawValue == String {
// El estado seleccionado que se comparte con la vista padre
@Binding var selection: T
// Las opciones disponibles
let options: [T]
// Namespace para la animación del fondo (Matched Geometry Effect)
@Namespace private var animationNamespace
// Colores personalizables
var activeBgColor: Color = .blue
var inactiveBgColor: Color = Color.gray.opacity(0.2)
var activeTextColor: Color = .white
var inactiveTextColor: Color = .primary
var body: some View {
HStack(spacing: 0) {
ForEach(options) { option in
pickerItem(for: option)
}
}
.padding(4)
.background(inactiveBgColor)
.clipShape(Capsule())
}
}
4.2 Detallando el Elemento Individual (Picker Item)
Dentro de la misma estructura CustomSegmentedPicker, vamos a añadir el método que construye cada botón individual. Aquí es donde ocurre la magia de la animación en Swift.
extension CustomSegmentedPicker {
@ViewBuilder
private func pickerItem(for option: T) -> some View {
let isSelected = selection == option
ZStack {
if isSelected {
Capsule()
.fill(activeBgColor)
// El secreto para una animación fluida estilo Apple
.matchedGeometryEffect(id: "activeBackground", in: animationNamespace)
}
Text(option.rawValue)
.font(.system(size: 14, weight: isSelected ? .bold : .medium, design: .rounded))
.foregroundColor(isSelected ? activeTextColor : inactiveTextColor)
.padding(.vertical, 10)
.padding(.horizontal, 16)
.lineLimit(1)
.minimumScaleFactor(0.8)
}
.contentShape(Capsule())
.onTapGesture {
withAnimation(.interactiveSpring(response: 0.3, dampingFraction: 0.7, blendDuration: 0.2)) {
selection = option
}
}
// Accesibilidad: Muy importante para cualquier iOS Developer
.accessibilityElement(children: .ignore)
.accessibilityLabel(Text(option.rawValue))
.accessibilityAddTraits(isSelected ? [.isSelected, .isButton] : [.isButton])
}
}
Explicación Técnica de la Implementación:
- Matched Geometry Effect: El modificador
.matchedGeometryEffectes una de las herramientas más potentes en la programación Swift para UI. Le dice a SwiftUI que rastree la geometría de laCapsule()de fondo. Cuando la variableselectioncambia, SwiftUI no destruye y recrea el fondo en una nueva posición al instante; en su lugar, interpola (anima) el tamaño y la posición desde el botón anterior al nuevo. - Animación Spring: En lugar de un simple
.easeInOut, usamos.interactiveSpring. Esto imita la física real y es lo que le da a las aplicaciones de Apple ese toque “premium”. - Accesibilidad: Un verdadero profesional no olvida el VoiceOver. Hemos añadido
accessibilityAddTraitspara que los usuarios con discapacidad visual sepan qué elemento está seleccionado y que funciona como un botón.
5. Implementación en la Vista Principal (iOS y macOS)
Ahora que tenemos nuestro picker personalizado en SwiftUI, vamos a usarlo en nuestra ContentView.
import SwiftUI
struct ContentView: View {
@State private var periodoSeleccionado: PeriodoTiempo = .semana
var body: some View {
NavigationView {
VStack(spacing: 40) {
// Nuestro Custom Picker
CustomSegmentedPicker(
selection: $periodoSeleccionado,
options: PeriodoTiempo.allCases,
activeBgColor: .indigo,
inactiveBgColor: .gray.opacity(0.15)
)
.padding(.horizontal)
// Contenido que reacciona al picker
VStack(spacing: 20) {
Image(systemName: iconFor(periodo: periodoSeleccionado))
.font(.system(size: 80))
.foregroundColor(.indigo)
// Añadimos una transición para el cambio de contenido
.transition(.scale.combined(with: .opacity))
.id(periodoSeleccionado) // Fuerza a SwiftUI a recrear la vista para animar
Text("Estás viendo los datos de la \(periodoSeleccionado.rawValue.lowercased())")
.font(.headline)
.foregroundColor(.secondary)
}
Spacer()
}
.padding(.top, 40)
.navigationTitle("Estadísticas")
// Usamos animation global para el cambio de contenido
.animation(.easeInOut, value: periodoSeleccionado)
}
}
// Función helper para cambiar el icono
private func iconFor(periodo: PeriodoTiempo) -> String {
switch periodo {
case .dia: return "sun.max.fill"
case .semana: return "calendar"
case .mes: return "chart.bar.doc.horizontal"
case .anio: return "globe.americas.fill"
}
}
}
Si ejecutas esto en tu simulador de Xcode para iOS o macOS, verás un control elegante, con una transición suave del fondo índigo y un cambio dinámico en el contenido inferior.
6. Adaptando el Picker para watchOS
Una de las grandes ventajas de SwiftUI es poder compartir código entre plataformas. Sin embargo, la pantalla de un Apple Watch es radicalmente más pequeña que la de un iPhone o un Mac. Un HStack con 4 opciones se desbordará de la pantalla en watchOS, rompiendo la interfaz.
Como iOS Developer experimentado, debes usar directivas de compilación y adaptar el diseño.
Vamos a modificar ligeramente nuestro componente principal CustomSegmentedPicker para que, cuando compile en watchOS, se disponga en formato de lista vertical (VStack) o en un ScrollView horizontal.
Regresemos a CustomSegmentedPicker.swift y modifiquemos la variable body:
var body: some View {
#if os(watchOS)
// En watchOS usamos un ScrollView horizontal si hay muchas opciones, o un VStack
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
ForEach(options) { option in
pickerItem(for: option)
}
}
.padding(4)
}
#else
// En iOS y macOS mantenemos el HStack adaptativo
HStack(spacing: 0) {
ForEach(options) { option in
pickerItem(for: option)
}
}
.padding(4)
.background(inactiveBgColor)
.clipShape(Capsule())
#endif
}
Para el pickerItem en watchOS, también podemos ajustar el tamaño del texto y el padding dinámicamente:
@ViewBuilder
private func pickerItem(for option: T) -> some View {
let isSelected = selection == option
ZStack {
if isSelected {
Capsule()
.fill(activeBgColor)
.matchedGeometryEffect(id: "activeBackground", in: animationNamespace)
} else {
#if os(watchOS)
// En watchOS le damos un fondo visible a los inactivos también
Capsule()
.fill(Color.gray.opacity(0.3))
#endif
}
Text(option.rawValue)
// Ajustamos fuente según OS
#if os(watchOS)
.font(.system(size: 12, weight: isSelected ? .bold : .regular))
.padding(.vertical, 8)
.padding(.horizontal, 12)
#else
.font(.system(size: 14, weight: isSelected ? .bold : .medium, design: .rounded))
.padding(.vertical, 10)
.padding(.horizontal, 16)
#endif
.foregroundColor(isSelected ? activeTextColor : inactiveTextColor)
.lineLimit(1)
}
.contentShape(Capsule())
// ... (resto del código de gestos y accesibilidad se mantiene igual)
Gracias a estas simples directivas #if os(watchOS), Xcode compilará el componente adecuado para el dispositivo destino. El Apple Watch mostrará un carrusel de píldoras deslizable de izquierda a derecha, mientras que iOS mostrará el control segmentado clásico unificado.
7. Temas Avanzados: Manejo de Entornos y Preferencias
Para que este componente se sienta como una API nativa de Apple, en lugar de pasar los colores en el inicializador cada vez, podemos utilizar los @Environment values de SwiftUI. Esto es un nivel superior en la programación Swift.
Podemos definir nuestras propias claves de entorno (Environment Keys) para el color de acento de nuestro picker.
// Definimos la clave de entorno
private struct PickerAccentColorKey: EnvironmentKey {
static let defaultValue: Color = .blue
}
// Extendemos EnvironmentValues
extension EnvironmentValues {
var customPickerAccentColor: Color {
get { self[PickerAccentColorKey.self] }
set { self[PickerAccentColorKey.self] = newValue }
}
}
// Creamos un modificador para facilitar su uso
extension View {
func customPickerAccentColor(_ color: Color) -> some View {
environment(\.customPickerAccentColor, color)
}
}
Ahora, en nuestro CustomSegmentedPicker, podemos reemplazar la propiedad activeBgColor por la lectura del entorno:
@Environment(\.customPickerAccentColor) var activeBgColor
Esto permite al iOS Developer establecer el color del picker desde una vista superior, aplicándose a todos los pickers dentro de esa jerarquía, igual que como funciona .accentColor() nativo.
// Ejemplo de uso con Environment
VStack {
CustomSegmentedPicker(selection: $estado, options: Opciones.allCases)
CustomSegmentedPicker(selection: $filtro, options: Filtros.allCases)
}
.customPickerAccentColor(.orange) // Aplica naranja a AMBOS pickers a la vez
8. Optimizando el Rendimiento (Performance)
Cuando desarrollas un picker personalizado en SwiftUI, es fácil introducir problemas de rendimiento si no tienes cuidado, especialmente cuando usas MatchedGeometryEffect en listas grandes o componentes complejos.
Aquí hay tres consejos vitales de rendimiento que debes implementar en Xcode:
- Evita el uso de vistas pesadas dentro del Item: Mantén el
pickerItemlo más ligero posible. Usa primitivas deTextyCapsule. No incrustes imágenes grandes ni realices cálculos complejos dentro de este método, ya que se renderiza varias veces (una por cada opción). - El modificador
.id(): Como vimos en el ejemplo deContentView, usamos.id(periodoSeleccionado)en la vista que reacciona al picker. Esto es crucial. Le dice a SwiftUI explícitamente: “Cuando el ID cambie, trata esto como una vista completamente nueva”. Esto asegura que las transiciones (.transition) se ejecuten sin esfuerzo y evita sobrecargas de estado innecesarias. - Animaciones explícitas vs implícitas: En nuestro código usamos
withAnimation { selection = option }en el eventoonTapGesture. Esto es una animación explícita. Solo anima los cambios de estado resultantes de ese toque específico. Evita usar.animation(.default)al final delHStackcompleto, ya que esto podría provocar que otros redibujados no relacionados también se animen accidentalmente (un bug visual muy común en la programación Swift).
9. Pruebas y Depuración en Xcode
Un buen flujo de trabajo de desarrollo en Swift implica pruebas constantes. Xcode ofrece Previews que son invaluables para iterar el diseño de nuestro picker.
Al final de tu archivo CustomSegmentedPicker.swift, asegúrate de configurar un Preview exhaustivo que pruebe los diferentes estados:
struct CustomSegmentedPicker_Previews: PreviewProvider {
static var previews: some View {
VStack(spacing: 30) {
// Preview Modo Claro
StatefulPreviewWrapper(.semana) { binding in
CustomSegmentedPicker(selection: binding, options: PeriodoTiempo.allCases)
}
// Preview Modo Oscuro y Color Personalizado
StatefulPreviewWrapper(.anio) { binding in
CustomSegmentedPicker(
selection: binding,
options: PeriodoTiempo.allCases,
activeBgColor: .green,
inactiveBgColor: .gray.opacity(0.3)
)
}
.preferredColorScheme(.dark)
}
.padding()
.previewLayout(.sizeThatFits)
}
}
// Un wrapper necesario en SwiftUI para hacer Previews de bindings que funcionen al hacer clic
struct StatefulPreviewWrapper<Value, Content: View>: View {
@State var value: Value
var content: (Binding<Value>) -> Content
init(_ value: Value, content: @escaping (Binding<Value>) -> Content) {
self._value = State(wrappedValue: value)
self.content = content
}
var body: some View {
content($value)
}
}
Gracias a este StatefulPreviewWrapper, puedes interactuar con el picker directamente en el Canvas de Xcode sin necesidad de compilar la aplicación entera en un simulador. Verás las animaciones del MatchedGeometryEffect en tiempo real.
10. Conclusión y Siguientes Pasos
Hemos pasado de depender de los componentes estándar restrictivos a construir un sistema de UI modular, animado y accesible.
Crear un picker personalizado en SwiftUI te enseña conceptos fundamentales y avanzados del framework: el manejo de genéricos, animaciones interactivas basadas en geometría espacial (MatchedGeometryEffect), accesibilidad manual y adaptabilidad multiplataforma (iOS, macOS, watchOS).
Como iOS Developer, tu objetivo principal no es solo hacer que el código funcione, sino crear interfaces de usuario que deleiten a los usuarios, que se sientan vivas y que refuercen la identidad del producto en el que estás trabajando. El entorno de Xcode y el poder de SwiftUI hacen que esta tarea sea más declarativa y menos propensa a errores que nunca.








