En el competitivo mundo del desarrollo de aplicaciones, la experiencia de usuario (UX) marca la diferencia entre una app promedio y una excelente. Como iOS Developer, constantemente te enfrentas al reto de crear interfaces que sean no solo funcionales, sino también intuitivas y visualmente atractivas. Una de las interacciones más comunes es la selección de valores dentro de un rango.
Si bien Apple nos proporciona componentes nativos excelentes, a veces se quedan cortos en la retroalimentación visual. El Slider estándar es fantástico, pero cuando necesitas que el usuario seleccione valores en incrementos específicos (por ejemplo, 10, 20, 30…), la falta de indicadores visuales puede resultar confusa.
En este tutorial de programación Swift, vamos a profundizar en cómo crear un slider con marcas en SwiftUI. Este componente personalizado no solo mejorará la usabilidad de tus aplicaciones, sino que también será completamente compatible con iOS, macOS y watchOS, aprovechando al máximo el poder del ecosistema de Apple utilizando Xcode.
1. El Problema con el Slider Estándar
En SwiftUI, implementar un control deslizante es tan sencillo como declarar un Slider(value: $miValor, in: 0...100). Incluso podemos añadirle un parámetro step para que el valor “salte” en incrementos específicos:
Slider(value: $volume, in: 0...100, step: 10)
El problema radica en que, aunque el “pulgar” (thumb) del slider se ajusta magnéticamente a estos incrementos (0, 10, 20, etc.), el usuario no ve ninguna marca en la pista que le indique dónde están esas paradas. Para solucionar esto y llevar nuestra programación Swift al siguiente nivel, construiremos un componente personalizado y reutilizable.
2. Requisitos Previos
Para seguir este tutorial, necesitarás:
- Un Mac con Xcode 14 o superior instalado.
- Conocimientos básicos e intermedios de Swift y SwiftUI.
- Ganas de crear interfaces increíbles.
3. Diseñando nuestro Componente: MarkedSlider
Nuestro objetivo es crear una vista en SwiftUI que combine el comportamiento nativo del Slider con una capa visual de marcas (ticks) distribuidas uniformemente a lo largo de la pista.
Paso 3.1: Definición de la Estructura
Abre Xcode, crea un nuevo proyecto de tipo SwiftUI App (asegúrate de que sea multiplataforma si deseas probarlo en Mac y Watch) y crea un nuevo archivo llamado MarkedSlider.swift.
Comenzaremos definiendo las propiedades que nuestro componente necesita para ser flexible:
import SwiftUI
struct MarkedSlider: View {
@Binding var value: Double
let bounds: ClosedRange<Double>
let step: Double
let activeColor: Color
let inactiveColor: Color
// Inicializador para dar valores por defecto
init(value: Binding<Double>,
in bounds: ClosedRange<Double>,
step: Double = 1.0,
activeColor: Color = .blue,
inactiveColor: Color = .gray.opacity(0.3)) {
self._value = value
self.bounds = bounds
self.step = step
self.activeColor = activeColor
self.inactiveColor = inactiveColor
}
var body: some View {
// Aquí irá nuestra vista
Text("Slider en construcción")
}
}
Paso 3.2: Calculando el Número de Marcas
Para dibujar las marcas, necesitamos saber cuántas hay en función del rango (bounds) y del incremento (step). Añadiremos una propiedad calculada a nuestra estructura:
private var tickCount: Int {
let range = bounds.upperBound - bounds.lowerBound
return Int((range / step).rounded(.down)) + 1
}
Paso 3.3: Construyendo la Interfaz con GeometryReader
El secreto para crear un slider con marcas en SwiftUI de manera precisa es alinear nuestras marcas visuales exactamente con la pista del Slider nativo. Para lograr esto, utilizaremos GeometryReader, una herramienta fundamental en SwiftUI que nos permite conocer el tamaño exacto del contenedor.
Vamos a actualizar la propiedad body:
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
// 1. Capa base: Las marcas visuales (Ticks)
drawTicks(in: geometry)
// 2. Capa interactiva: El Slider nativo
Slider(value: $value, in: bounds, step: step)
.tint(.clear) // Ocultamos la pista nativa opcionalmente, o la dejamos
}
}
// Damos una altura fija para evitar que GeometryReader colapse
.frame(height: 44)
}
Nota para el iOS Developer: El Slider nativo tiene márgenes internos para el pulgar (thumb). Si intentamos dibujar marcas exactamente de borde a borde del GeometryReader, las marcas de los extremos no se alinearán con el centro del pulgar cuando este llegue al final. Abordaremos esto en el siguiente paso ajustando el dibujo.
Paso 3.4: Dibujando las Marcas (Ticks)
Ahora, vamos a crear la función drawTicks(in:) que iterará sobre nuestro tickCount y dibujará pequeñas líneas verticales.
@ViewBuilder
private func drawTicks(in geometry: GeometryProxy) -> some View {
// El thumb del slider tiene un ancho aproximado.
// Dejamos un margen horizontal (padding) para alinear las marcas con el centro del thumb.
let horizontalPadding: CGFloat = 14
let width = geometry.size.width - (horizontalPadding * 2)
let tickSpacing = width / CGFloat(tickCount - 1)
ZStack(alignment: .leading) {
// Dibujamos la pista de fondo (Track)
Capsule()
.fill(inactiveColor)
.frame(height: 4)
.padding(.horizontal, horizontalPadding)
// Dibujamos la pista activa (Progreso)
Capsule()
.fill(activeColor)
.frame(width: activeTrackWidth(totalWidth: width) + horizontalPadding * 2, height: 4)
.padding(.horizontal, horizontalPadding)
// Dibujamos las marcas
ForEach(0..<tickCount, id: \.self) { index in
let isPast = isTickPast(index: index)
Capsule()
.fill(isPast ? activeColor : Color.gray)
.frame(width: 2, height: 12)
.offset(x: horizontalPadding + (CGFloat(index) * tickSpacing) - 1)
}
}
}
Paso 3.5: Lógica de Progreso y Color de las Marcas
Como puedes ver en el código anterior, llamamos a dos funciones auxiliares: activeTrackWidth y isTickPast. Estas funciones permiten que el progreso se rellene del color activo y que las marcas cambien de color una vez que el pulgar las sobrepasa.
Añade estas funciones a tu estructura MarkedSlider:
// Calcula el ancho de la barra de progreso
private func activeTrackWidth(totalWidth: CGFloat) -> CGFloat {
let range = bounds.upperBound - bounds.lowerBound
let currentValue = value - bounds.lowerBound
let percentage = currentValue / range
return totalWidth * CGFloat(percentage)
}
// Determina si una marca específica ya fue superada por el valor actual
private func isTickPast(index: Int) -> Bool {
let tickValue = bounds.lowerBound + (Double(index) * step)
return value >= tickValue
}
Paso 3.6: Ensamblando la Vista Final y Ocultando el Slider Nativo
Para que nuestro diseño personalizado brille, debemos superponer el Slider nativo haciéndolo invisible, de modo que solo actúe como el controlador de la lógica táctil, mientras que los gráficos personalizados hacen el trabajo visual.
Modifiquemos el body para lograr el efecto perfecto:
var body: some View {
GeometryReader { geometry in
ZStack(alignment: .center) {
// Nuestra UI Personalizada
drawTicks(in: geometry)
// Slider invisible encima para capturar gestos
Slider(value: $value, in: bounds, step: step)
.opacity(0.01) // Casi invisible, pero interactivo
}
}
.frame(height: 44)
}
Truco de experto en programación Swift: Usamos .opacity(0.01) en lugar de .opacity(0) o .hidden(). Si pones opacidad cero o lo ocultas, SwiftUI deshabilita la interacción táctil en ese elemento. Con un 1%, es invisible al ojo humano, pero el sistema iOS sigue registrando los toques del usuario.
4. Implementación Multiplataforma: iOS, macOS y watchOS
Una de las maravillas de Swift y SwiftUI es su filosofía de “Aprende una vez, aplica en cualquier lugar”. El código que acabamos de escribir es intrínsecamente multiplataforma.
Consideraciones para watchOS
En el Apple Watch, el espacio es extremadamente limitado. Al crear un slider con marcas en SwiftUI para watchOS, la interacción táctil fina es difícil. Afortunadamente, al usar el Slider nativo subyacente de manera invisible, la Corona Digital (Digital Crown) controlará automáticamente nuestro MarkedSlider de manera fluida, respetando el step que configuramos. ¡Magia pura en Xcode!
Consideraciones para macOS
En Mac, el usuario interactuará con el ratón o el trackpad. El comportamiento del clic en la pista del slider saltará directamente a la marca más cercana, ofreciendo una experiencia nativa y predecible para los usuarios de escritorio.
5. Accesibilidad (VoiceOver)
Cualquier iOS Developer senior sabe que una app no está terminada hasta que es accesible. Al ocultar visualmente el slider nativo y dibujar nuestra propia interfaz, podríamos estar perjudicando la accesibilidad si no tenemos cuidado.
Por defecto, al dejar el Slider con .opacity(0.01), VoiceOver aún puede leerlo. Sin embargo, para asegurarnos de ofrecer la mejor experiencia, podemos aplicar modificadores de accesibilidad explícitos a nuestro contenedor principal:
var body: some View {
GeometryReader { geometry in
// ... (ZStack code)
}
.frame(height: 44)
.accessibilityElement(children: .ignore)
.accessibilityLabel("Control de nivel")
.accessibilityValue("\(Int(value))")
.accessibilityAdjustableAction { direction in
switch direction {
case .increment:
if value < bounds.upperBound { value += step }
case .decrement:
if value > bounds.lowerBound { value -= step }
@unknown default:
break
}
}
}
Este bloque asegura que los usuarios con discapacidades visuales puedan interactuar con nuestro slider utilizando los gestos estándar de incremento/decremento de VoiceOver.
6. Poniendo a prueba el MarkedSlider
Ahora que tenemos nuestro componente listo, vamos a usarlo en nuestra vista principal ContentView.
struct ContentView: View {
@State private var brillo: Double = 50
@State private var volumen: Double = 0
var body: some View {
VStack(spacing: 40) {
Text("Ajustes de Sistema")
.font(.largeTitle)
.bold()
VStack(alignment: .leading, spacing: 10) {
Text("Brillo: \(Int(brillo))%")
.font(.headline)
// Usando nuestro componente
MarkedSlider(value: $brillo, in: 0...100, step: 25)
HStack {
Text("0%")
Spacer()
Text("100%")
}
.font(.caption)
.foregroundColor(.secondary)
}
.padding()
VStack(alignment: .leading, spacing: 10) {
Text("Volumen por fases")
.font(.headline)
// Otro ejemplo con diferentes colores y rangos
MarkedSlider(
value: $volumen,
in: 0...10,
step: 1,
activeColor: .green,
inactiveColor: .green.opacity(0.2)
)
}
.padding()
Spacer()
}
.padding()
}
}
Analizando el Resultado
Cuando compiles esto en Xcode, verás dos sliders hermosos. El primero (Brillo) tendrá 5 marcas (0, 25, 50, 75, 100). El segundo (Volumen) tendrá 11 marcas (del 0 al 10). Al deslizar el dedo (o el ratón en macOS), notarás cómo el valor salta de marca en marca, y el color de progreso se rellena dinámicamente, cambiando el color de los ticks a medida que los sobrepasa.
7. Consejos de Rendimiento y Arquitectura
Para un iOS Developer avanzado, es crucial entender el impacto de lo que programamos:
- Evitar el sobre-cálculo en GeometryReader:
GeometryReaderpuede causar que la vista se recalcule si cambian los tamaños del contenedor. Como le hemos dado un.frame(height: 44)estricto, mitigamos problemas de layout que a menudo plagan las interfaces complejas en SwiftUI. - Uso de @ViewBuilder: Al extraer la lógica de dibujo a la función
drawTicks, usamos@ViewBuilder. Esto mantiene elbodyde la vista limpio, legible y permite que el compilador de Swift optimice mejor el árbol de vistas. - Estado vs. Binding: Observa cómo usamos
@Bindingpara elvalue. Esto asegura que elMarkedSliderno sea dueño de sus propios datos, sino que simplemente actúa como un controlador del estado que vive en su vista principal (ContentView). Este es el núcleo de la arquitectura declarativa.
Conclusión
Saber crear un slider con marcas en SwiftUI demuestra una comprensión profunda de cómo manipular vistas, aprovechar GeometryReader de manera segura y mejorar la experiencia del usuario (UX) más allá de los componentes estándar que proporciona el SDK.








