Toggle es uno de los controles más sencillos de SwiftUI, pero su apariencia por defecto no siempre encaja con el diseño de una aplicación. Aunque podemos utilizar estilos integrados como .switch o .button, SwiftUI también permite sustituir por completo su representación visual mediante el protocolo ToggleStyle.
La ventaja de utilizar un estilo en lugar de construir un control independiente es que seguimos trabajando con Toggle, con su Binding, su etiqueta y su integración dentro de la jerarquía de SwiftUI. El componente conserva su intención semántica mientras la presentación queda desacoplada del lugar donde se utiliza.
Cómo funciona ToggleStyle
Crear un estilo personalizado requiere definir un tipo que adopte ToggleStyle e implementar un único método:
struct CustomToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
// Representación del Toggle
}
}
El parámetro configuration contiene la información necesaria para representar el control.
Por un lado tenemos configuration.label, que es la vista utilizada como etiqueta del Toggle. Por otro, configuration.isOn, que proporciona acceso al estado mediante un Binding<Bool>.
Esto significa que podemos consultar su valor para decidir qué mostrar y también modificarlo cuando el usuario interactúa con nuestro control.
struct SimpleSymbolToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
Button {
configuration.isOn.toggle()
} label: {
HStack {
configuration.label
Spacer()
Image(
systemName: configuration.isOn
? "checkmark.circle.fill"
: "circle"
)
}
}
.buttonStyle(.plain)
}
}
El Toggle continúa siendo el propietario del estado. El estilo únicamente decide cómo representarlo y qué interacción provoca el cambio.
Un SymbolToggleStyle reutilizable
En lugar de limitar el estilo a un único diseño podemos permitir que los símbolos utilizados para cada estado sean configurables.
struct SymbolToggleStyle: ToggleStyle {
let onSymbol: String
let offSymbol: String
let mixedSymbol: String
init(
onSymbol: String = "checkmark.circle.fill",
offSymbol: String = "circle",
mixedSymbol: String = "minus.circle.fill"
) {
self.onSymbol = onSymbol
self.offSymbol = offSymbol
self.mixedSymbol = mixedSymbol
}
func makeBody(configuration: Configuration) -> some View {
Button {
configuration.isOn.toggle()
} label: {
HStack(spacing: 12) {
configuration.label
Spacer()
Image(systemName: symbol(for: configuration))
.imageScale(.large)
.foregroundStyle(
configuration.isOn || configuration.isMixed
? Color.accentColor
: Color.secondary
)
.contentTransition(.symbolEffect(.replace))
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
private func symbol(for configuration: Configuration) -> String {
if configuration.isMixed {
mixedSymbol
} else if configuration.isOn {
onSymbol
} else {
offSymbol
}
}
}
Además de cambiar el símbolo según el estado, utilizamos .contentTransition(.symbolEffect(.replace)) para que SF Symbols realice una transición entre ambas imágenes cuando el valor cambia. El estilo sigue sin conocer nada sobre el modelo de datos concreto que lo utilizará.
Podemos aplicarlo directamente sobre cualquier Toggle:
struct NetworkSettingsView: View {
@State private var syncUsingCellular = false
var body: some View {
Toggle(
"Sincronizar usando datos móviles",
isOn: $syncUsingCellular
)
.toggleStyle(
SymbolToggleStyle(
onSymbol: "antenna.radiowaves.left.and.right.circle.fill",
offSymbol: "antenna.radiowaves.left.and.right.circle"
)
)
.padding()
}
}
La pantalla únicamente declara qué representa el control y cuál es su estado. Toda la lógica visual queda encapsulada dentro del estilo.
Sintaxis más parecida a los estilos de SwiftUI
Cuando un estilo se utiliza de forma habitual resulta conveniente exponerlo mediante una extensión de ToggleStyle, siguiendo el mismo patrón que SwiftUI utiliza con .switch, .button o .automatic.
extension ToggleStyle where Self == SymbolToggleStyle {
static var symbol: SymbolToggleStyle {
.init()
}
}
Ahora el código de la vista puede reducirse a:
Toggle(
"Sincronizar usando datos móviles",
isOn: $syncUsingCellular
)
.toggleStyle(.symbol)
Este pequeño detalle mejora bastante la ergonomía cuando el estilo forma parte del sistema de diseño de una aplicación.
El tercer estado: isMixed
Aunque normalmente pensamos en Toggle como un control binario, ToggleStyle.Configuration también dispone de la propiedad isMixed.
Este estado aparece cuando un único Toggle representa una colección cuyos elementos no comparten el mismo valor. Si todos están activos, el control está activado; si ninguno lo está, aparece desactivado; y cuando algunos están activos y otros no, SwiftUI puede representar el estado como mixto.
Supongamos que una aplicación permite decidir qué tipos de contenido deben estar disponibles sin conexión:
struct OfflineContent: Identifiable {
let id = UUID()
let name: String
var isEnabled: Bool
}
Podemos mantener una colección dentro de la vista:
struct OfflineSettingsView: View {
@State private var content = [
OfflineContent(name: "Artículos", isEnabled: true),
OfflineContent(name: "Vídeos", isEnabled: false),
OfflineContent(name: "Podcasts", isEnabled: true)
]
var body: some View {
Form {
Section {
Toggle(
"Todo el contenido",
sources: $content,
isOn: \.isEnabled
)
.toggleStyle(.symbol)
ForEach($content) { $item in
Toggle(item.name, isOn: $item.isEnabled)
}
}
}
}
}
En este ejemplo SwiftUI calcula el estado del Toggle superior a partir de la colección.
Como hay elementos activos e inactivos al mismo tiempo, configuration.isMixed será true y nuestro SymbolToggleStyle mostrará minus.circle.fill. Si todos pasan a compartir el mismo valor, el estilo utilizará automáticamente el símbolo correspondiente a activado o desactivado.
Lo interesante es que el estilo no necesita saber que detrás existe una colección. Solo interpreta la configuración que recibe.
No siempre necesitamos reconstruir el control
Existe otra posibilidad interesante dentro de ToggleStyle. Si únicamente queremos modificar la presentación del control pero conservar el estilo y la interacción actuales, podemos crear un nuevo Toggle directamente a partir de su configuración.
struct CardToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
Toggle(configuration)
.padding()
.background(
.thinMaterial,
in: RoundedRectangle(cornerRadius: 16)
)
}
}
Aquí no modificamos manualmente configuration.isOn ni reproducimos el comportamiento del control. SwiftUI crea el Toggle utilizando la misma configuración y nosotros nos limitamos a añadir una presentación alrededor.
Esta diferencia es importante. Si nuestro diseño necesita sustituir por completo el mecanismo visual del control, tiene sentido construirlo dentro de makeBody(configuration:). Si únicamente queremos envolver o decorar el Toggle existente, Toggle(configuration) reduce el código y mantiene el comportamiento proporcionado por el sistema.
Los estilos se propagan por la jerarquía
toggleStyle(_:) no tiene por qué aplicarse individualmente. Al igual que ocurre con muchos otros estilos de SwiftUI, puede añadirse a un contenedor y afectar a los Toggle que aparecen dentro de él.
VStack {
Toggle("Descargas automáticas", isOn: $automaticDownloads)
Toggle("Sincronización en segundo plano", isOn: $backgroundSync)
Toggle("Contenido recomendado", isOn: $recommendations)
}
.toggleStyle(.symbol)
Esto hace que los estilos personalizados sean especialmente útiles como parte de un sistema de diseño. Las vistas describen su contenido y estado, mientras que un nivel superior de la jerarquía decide cómo deben representarse los controles.
También permite sustituir el estilo de una sección completa sin tocar cada una de sus vistas.
Separar estado, comportamiento y apariencia
La principal utilidad de ToggleStyle no consiste únicamente en dibujar interruptores diferentes. Su verdadero valor está en separar responsabilidades.
El modelo mantiene el estado. Toggle establece la relación entre ese estado y la interfaz. El estilo determina su representación visual.
Gracias a esa separación podemos reutilizar el mismo Toggle con apariencias diferentes según el contexto, cambiar el diseño de toda una sección desde un único modificador y representar estados más complejos como isMixed sin trasladar esa lógica visual a cada pantalla.
Además, utilizar SF Symbols dentro del estilo permite aprovechar su escalado, sus modos de representación y sus transiciones manteniendo una implementación muy compacta.
Conclusión
ToggleStyle es una de las APIs de personalización más interesantes de SwiftUI porque permite modificar un control sin mezclar su presentación con el estado de la aplicación.
configuration.label, configuration.isOn y configuration.isMixed proporcionan prácticamente todo lo necesario para crear controles personalizados y reutilizables. Si además encapsulamos el resultado en un estilo accesible mediante .toggleStyle(.symbol), obtenemos una API que se integra de forma natural con el resto del framework.
Y cuando no necesitamos sustituir completamente el control, Toggle(configuration) permite conservar su comportamiento existente y limitar el estilo a la presentación. Esa flexibilidad convierte a ToggleStyle en una pieza especialmente útil para construir sistemas de diseño consistentes en SwiftUI.