Programación en Swift y SwiftUI para iOS Developers

Color Picker personalizado en SwiftUI

Como iOS Developer, sabes que el ecosistema de Apple evoluciona a un ritmo vertiginoso. Cada año, Xcode y SwiftUI nos regalan nuevas herramientas que simplifican nuestro trabajo. Sin embargo, hay momentos en los que las soluciones nativas no son suficientes. Ya sea por restricciones de diseño de tu equipo de UI/UX, por la necesidad de mantener una identidad de marca estricta, o simplemente porque estás desarrollando para plataformas donde ciertos controles no existen (como watchOS), saber crear componentes desde cero es lo que separa a un desarrollador junior de un verdadero experto en la programación Swift.

En este extenso tutorial, vamos a sumergirnos en las profundidades de SwiftUI para construir un Custom Color Picker en SwiftUI (Selector de Color Personalizado). No nos limitaremos a una sola plataforma; diseñaremos un componente verdaderamente universal que funcionará de manera impecable en iOS, macOS y watchOS.


1. ¿Por qué construir un Custom Color Picker en SwiftUI?

Apple introdujo la vista ColorPicker nativa en iOS 14 y macOS 11. Es una herramienta fantástica que invoca el selector de color del sistema operativo, permitiendo al usuario elegir colores mediante una cuadrícula, un espectro continuo o introduciendo códigos hexadecimales.

Entonces, ¿por qué molestarse en hacer uno personalizado?

  1. Falta de soporte en watchOS y tvOS: El ColorPicker nativo simplemente no está disponible en estas plataformas. Si estás construyendo una aplicación multiplataforma real, necesitas una alternativa.
  2. Control de la Paleta: A menudo, no quieres que el usuario elija cualquier color. Si tienes una app de dibujo simple o una app de notas, quizás solo quieras ofrecer 10 o 12 colores específicos que encajen con el tema de tu aplicación.
  3. Experiencia de Usuario (UX) Integrada: El selector nativo a menudo se presenta como un modal o un popover. Un Custom Color Picker en SwiftUI te permite integrar la selección de color directamente en tu vista en línea, reduciendo la fricción y los clics.

2. Preparando el Proyecto en Xcode

Para comenzar nuestro viaje en la programación Swift, vamos a configurar nuestro entorno de desarrollo.

  1. Abre Xcode (asegúrate de usar la versión 14 o superior para aprovechar las últimas mejoras del framework).
  2. Selecciona Create a new Xcode project.
  3. En la parte superior, elige la pestaña Multiplatform y luego selecciona la plantilla App.
  4. Nombra tu proyecto, por ejemplo, ProCustomColorPicker.
  5. Verifica que la interfaz esté configurada en SwiftUI y el lenguaje en Swift.

Al utilizar la plantilla Multiplatform, Xcode configurará automáticamente los targets para iOS y macOS (y puedes añadir watchOS fácilmente). Esto significa que escribiremos nuestro código una sola vez y lo adaptaremos según sea necesario.


3. Fase 1: Diseñando el Modelo de Datos y la Paleta

Un buen iOS Developer sabe que la interfaz de usuario debe estar separada de los datos. Para nuestro Custom Color Picker en SwiftUI, primero definiremos los colores que queremos mostrar.

Crea un nuevo archivo de Swift llamado ColorPalette.swift y añade el siguiente código:

import SwiftUI

// Definimos una estructura estática para mantener nuestros colores organizados
struct ColorPalette {
    static let mainColors: [Color] = [
        .red, .orange, .yellow, .green, .mint,
        .teal, .cyan, .blue, .indigo, .purple,
        .pink, .brown, .gray, .black, .white
    ]
    
    // Podemos crear paletas temáticas
    static let pastelColors: [Color] = [
        Color(red: 1.0, green: 0.8, blue: 0.8),
        Color(red: 0.8, green: 1.0, blue: 0.8),
        Color(red: 0.8, green: 0.8, blue: 1.0),
        Color(red: 1.0, green: 0.9, blue: 0.8)
    ]
}

Usar un array de Color de SwiftUI es directo y eficiente. Además, al encapsularlo en una estructura, mantenemos nuestro código limpio y escalable.


4. Fase 2: Construyendo la Cuadrícula Adaptativa (Grid)

Aquí es donde entra la magia de SwiftUI. Queremos que nuestros colores se muestren en una cuadrícula. En iOS y macOS, queremos varias columnas. En watchOS, queremos menos columnas para que los objetivos táctiles (tap targets) sigan siendo lo suficientemente grandes.

Para lograr esto de manera elegante en Swift, usaremos LazyVGrid.

Crea un nuevo archivo de vista de SwiftUI llamado PaletteColorPicker.swift:

import SwiftUI

struct PaletteColorPicker: View {
    // 1. El estado compartido. El Binding permite que la vista padre lea y escriba este valor.
    @Binding var selectedColor: Color
    
    // 2. Definimos las opciones que este picker mostrará.
    let colors: [Color]
    
    // 3. Configuración del Grid Adaptativo.
    // .adaptive le dice a SwiftUI que meta tantas columnas como quepan, 
    // con un ancho mínimo de 45 puntos.
    let columns = [
        GridItem(.adaptive(minimum: 45, maximum: 60), spacing: 15)
    ]
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: columns, spacing: 15) {
                ForEach(colors, id: \.self) { color in
                    colorButton(for: color)
                }
            }
            .padding()
        }
    }
    
    // Separamos la lógica del botón individual para mantener el body limpio
    @ViewBuilder
    private func colorButton(for color: Color) -> some View {
        let isSelected = selectedColor == color
        
        Circle()
            .fill(color)
            .frame(height: 45)
            // Añadimos una sombra sutil para dar profundidad
            .shadow(color: color.opacity(0.4), radius: 3, x: 0, y: 2)
            // Si está seleccionado, mostramos un anillo exterior
            .overlay(
                Circle()
                    .stroke(Color.primary, lineWidth: isSelected ? 3 : 0)
                    .padding(-4) // Expande el borde hacia afuera
            )
            // Una pequeña animación de escala al ser seleccionado
            .scaleEffect(isSelected ? 1.1 : 1.0)
            // Hacemos que toda el área sea tocable
            .onTapGesture {
                withAnimation(.spring(response: 0.3, dampingFraction: 0.6)) {
                    selectedColor = color
                }
            }
            // Accesibilidad: Vital en la programación Swift profesional
            .accessibilityLabel(Text("Color"))
            .accessibilityValue(Text(color.description))
            .accessibilityAddTraits(isSelected ? [.isSelected, .isButton] : [.isButton])
    }
}

Explicación Técnica para el iOS Developer

  • LazyVGrid y GridItem(.adaptive(...)): Esto es crucial para la adaptabilidad multiplataforma. En lugar de codificar HStack y VStack y calcular los anchos de pantalla manualmente, SwiftUI hace las matemáticas por nosotros. Si ejecutas esto en un iPad, verás quizás 10 columnas. En un Apple Watch, verás 3.
  • Animación .spring(): En la programación Swift orientada a UI, los detalles importan. Usar un resorte (spring) en lugar de una animación lineal hace que la interfaz se sienta orgánica y premium.
  • Accesibilidad: He incluido accessibilityAddTraits. Nunca subestimes la importancia de VoiceOver. Un Custom Color Picker en SwiftUI es inútil si parte de tu base de usuarios no puede interactuar con él.

5. Fase 3: El Desafío de los Sliders (RGB Picker)

Un selector de paleta está bien, pero ¿qué pasa si queremos un Custom Color Picker en SwiftUI que permita seleccionar cualquier color continuo?

Vamos a añadir otra vista a nuestro proyecto. Crearemos un selector basado en controles deslizantes (Sliders) para Rojo, Verde y Azul (RGB). Esta es una excelente práctica de programación Swift para entender cómo derivar estados.

Crea un archivo llamado RGBColorPicker.swift:

import SwiftUI

struct RGBColorPicker: View {
    @Binding var selectedColor: Color
    
    // Estados internos para manejar los valores de los sliders (0.0 a 1.0)
    @State private var red: Double = 0.5
    @State private var green: Double = 0.5
    @State private var blue: Double = 0.5
    
    var body: some View {
        VStack(spacing: 25) {
            
            // Previsualización del color actual
            RoundedRectangle(cornerRadius: 15)
                .fill(Color(red: red, green: green, blue: blue))
                .frame(height: 100)
                .shadow(radius: 5)
                .overlay(
                    RoundedRectangle(cornerRadius: 15)
                        .stroke(Color.secondary.opacity(0.3), lineWidth: 1)
                )
            
            VStack(spacing: 15) {
                colorSlider(value: $red, color: .red, label: "Rojo")
                colorSlider(value: $green, color: .green, label: "Verde")
                colorSlider(value: $blue, color: .blue, label: "Azul")
            }
        }
        .padding()
        // Cuando los sliders cambian, actualizamos el color principal
        .onChange(of: red) { _ in updateMainColor() }
        .onChange(of: green) { _ in updateMainColor() }
        .onChange(of: blue) { _ in updateMainColor() }
        // Cuando la vista aparece, inicializamos los sliders con el color seleccionado
        .onAppear {
            extractRGB(from: selectedColor)
        }
    }
    
    // Constructor de Sliders reutilizable
    @ViewBuilder
    private func colorSlider(value: Binding<Double>, color: Color, label: String) -> some View {
        HStack {
            Text(label.prefix(1)) // Primera letra (R, V, A)
                .font(.headline)
                .foregroundColor(color)
                .frame(width: 20)
            
            Slider(value: value, in: 0...1)
                .accentColor(color)
            
            // Mostramos el valor de 0 a 255
            Text("\(Int(value.wrappedValue * 255))")
                .font(.subheadline)
                .monospacedDigit()
                .frame(width: 40, alignment: .trailing)
        }
    }
    
    private func updateMainColor() {
        selectedColor = Color(red: red, green: green, blue: blue)
    }
    
    // Función avanzada de programación Swift para extraer RGB
    private func extractRGB(from color: Color) {
        #if canImport(UIKit)
        // Para iOS, tvOS y watchOS
        var r: CGFloat = 0
        var g: CGFloat = 0
        var b: CGFloat = 0
        var a: CGFloat = 0
        
        // Convertimos a UIColor para extraer los componentes
        if UIColor(color).getRed(&r, green: &g, blue: &b, alpha: &a) {
            red = Double(r)
            green = Double(g)
            blue = Double(b)
        }
        #elseif canImport(AppKit)
        // Para macOS
        var r: CGFloat = 0
        var g: CGFloat = 0
        var b: CGFloat = 0
        var a: CGFloat = 0
        
        // Convertimos a NSColor para extraer los componentes
        if let nsColor = NSColor(color).usingColorSpace(.sRGB) {
            nsColor.getRed(&r, green: &g, blue: &b, alpha: &a)
            red = Double(r)
            green = Double(g)
            blue = Double(b)
        }
        #endif
    }
}

Detalles de Nivel Senior en Swift

Extraer los componentes RGB del tipo Color de SwiftUI ha sido históricamente complicado porque Color es una vista, no solo un modelo de datos. En nuestra función extractRGB, usamos las directivas del compilador #if canImport(UIKit) y #elseif canImport(AppKit) de Xcode.

Esto es esencial para un iOS Developer que crea código multiplataforma. En iOS y watchOS usamos UIColor bajo el capó, mientras que en macOS usamos NSColor. Este nivel de detalle asegura que nuestro Custom Color Picker en SwiftUI nunca falle al compilar, independientemente del destino.


6. Fase 4: Creando el Componente Unificado

Ahora tenemos dos selectores potentes: uno de paleta y uno de RGB. Vamos a unirlos en un solo control maestro, imitando el comportamiento de pestañas (tabs) que Apple utiliza en sus propias aplicaciones.

Crea UniversalColorPicker.swift:

import SwiftUI

struct UniversalColorPicker: View {
    @Binding var selection: Color
    @State private var pickerMode: PickerMode = .palette
    
    enum PickerMode {
        case palette
        case rgb
    }
    
    var body: some View {
        VStack(spacing: 0) {
            // Control Segmentado para cambiar de modo
            Picker("Modo de Selector", selection: $pickerMode) {
                Text("Paleta").tag(PickerMode.palette)
                Text("Deslizadores").tag(PickerMode.rgb)
            }
            .pickerStyle(SegmentedPickerStyle())
            .padding()
            
            // Mostramos la vista correspondiente según el modo
            Group {
                if pickerMode == .palette {
                    PaletteColorPicker(selectedColor: $selection, colors: ColorPalette.mainColors)
                        // Transición suave al cambiar de pestaña
                        .transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing)))
                } else {
                    RGBColorPicker(selectedColor: $selection)
                        .transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)))
                }
            }
            .animation(.easeInOut, value: pickerMode)
            
            Spacer(minLength: 0)
        }
        .background(Color(UIColor.systemBackground)) // Color de fondo adaptable
        .cornerRadius(20)
    }
}

Nota: Si compilas estrictamente para macOS, UIColor.systemBackground dará error, puedes usar Color(NSColor.windowBackgroundColor) condicionalmente, o simplemente Color.white / Color.black o dejar el fondo transparente.


7. Fase 5: Adaptación Específica para watchOS

Como se mencionó al principio, el mayor desafío de un Custom Color Picker en SwiftUI es watchOS, debido a su reducida pantalla. El Picker segmentado y los Slider funcionan de manera diferente en un Apple Watch.

Siendo un iOS Developer meticuloso, optimizaremos nuestra vista unificada para el reloj usando las directivas del compilador que Xcode nos provee.

Modifiquemos la vista principal de la aplicación (ContentView.swift) para demostrar cómo usaríamos esto condicionalmente:

import SwiftUI

struct ContentView: View {
    @State private var myAppColor: Color = .mint
    @State private var isPickerPresented: Bool = false
    
    var body: some View {
        NavigationView {
            VStack(spacing: 40) {
                // Nuestro elemento de diseño principal que reacciona al color
                Image(systemName: "paintpalette.fill")
                    .resizable()
                    .scaledToFit()
                    .frame(width: 150, height: 150)
                    .foregroundColor(myAppColor)
                    .shadow(color: myAppColor.opacity(0.6), radius: 20, x: 0, y: 10)
                
                Text("Personaliza tu Tema")
                    .font(.title2)
                    .fontWeight(.semibold)
                
                #if os(watchOS)
                // En watchOS, la pantalla es tan pequeña que es mejor usar un NavigationLink
                // en lugar de un modal o un control en línea enorme.
                NavigationLink(destination: PaletteColorPicker(selectedColor: $myAppColor, colors: ColorPalette.mainColors)) {
                    Text("Cambiar Color")
                }
                #else
                // En iOS y macOS, podemos mostrar un botón que abre un Sheet
                Button(action: {
                    isPickerPresented.toggle()
                }) {
                    Text("Abrir Selector de Color")
                        .font(.headline)
                        .foregroundColor(.white)
                        .padding()
                        .frame(maxWidth: .infinity)
                        .background(myAppColor)
                        .cornerRadius(15)
                }
                .padding(.horizontal, 40)
                #endif
                
                Spacer()
            }
            .padding(.top, 50)
            .navigationTitle("Ajustes")
            #if os(iOS) || os(macOS)
            // Presentamos nuestro UniversalColorPicker como un modal interactivo
            .sheet(isPresented: $isPickerPresented) {
                UniversalColorPicker(selection: $myAppColor)
                    // Hacemos que el modal sea de tamaño medio en iOS 16+
                    .presentationDetents([.medium, .large])
                    .presentationDragIndicator(.visible)
            }
            #endif
        }
    }
}

Analizando la Integración

En esta parte final, hemos logrado el santo grial de la programación Swift moderna:

  1. En iOS/macOS: El usuario ve un botón elegante que levanta un Bottom Sheet interactivo (gracias a .presentationDetents). Dentro de ese modal, tienen la opción de cambiar entre la paleta o usar los deslizadores RGB de manera fluida.
  2. En watchOS: Ignoramos los deslizadores RGB (que son muy engorrosos en la pantalla del reloj) y el modal pesado. En su lugar, usamos el componente nativo NavigationLink que empuja nuestra vista PaletteColorPicker a la pila de navegación. Gracias a la naturaleza de nuestro código, el LazyVGrid se adaptará automáticamente para mostrar columnas más pequeñas perfectas para el dedo del usuario en la muñeca.

8. Optimizaciones y Consideraciones Finales

Rendimiento (Performance)

Al trabajar con un Custom Color Picker en SwiftUI, especialmente si decides ampliar tu paleta a cientos de colores, el uso de LazyVGrid garantiza que tu aplicación no sufrirá caídas de frames. Las vistas perezosas solo instancian en memoria los círculos de color que el usuario está viendo actualmente en la pantalla.

Persistencia de Datos

En una aplicación real, querrás guardar el color elegido por el usuario para que, al cerrar y abrir la app, el color se mantenga. Puesto que el tipo Color no conforma a Codable directamente, puedes guardar los valores RGB que extrajimos en el paso 5 dentro de UserDefaults o CoreData, y reconstruir el color al inicio de la app.

Pruebas en Xcode

Asegúrate de aprovechar los Previews de Xcode. Puedes configurar diferentes PreviewProviders para simular el Modo Oscuro, diferentes tamaños de texto dinámico y diferentes dispositivos (iPhone SE, iPad Pro, Apple Watch Series 8) simultáneamente sin necesidad de arrancar múltiples simuladores completos.


Conclusión

Como iOS Developer, aprender a crear componentes personalizados te otorga la libertad de construir exactamente lo que tu producto necesita. Durante este tutorial hemos repasado conceptos avanzados de la programación Swift: desde el manejo de flujos de diseño adaptativos con LazyVGrid, pasando por la manipulación de estados con @Binding, hasta el uso estratégico de macros de compilación multiplataforma y la extracción de componentes subyacentes de la UI (UIKit/AppKit).

Leave a Reply

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

Previous Article

NavigationTransition en SwiftUI

Related Posts