Si eres un iOS Developer que busca llevar las interfaces de sus aplicaciones al siguiente nivel, sabes que los detalles importan. Las interfaces de usuario genéricas cumplen su función, pero los diseños personalizados son los que realmente cautivan a los usuarios y destacan en la App Store. Uno de los elementos más atractivos y modernos que puedes implementar hoy en día es un Tab Bar curvo en SwiftUI.
En este extenso tutorial de programación, vamos a sumergirnos profundamente en la programación Swift para construir desde cero un Tab Bar personalizado con una elegante curva central. Lo mejor de todo es que no nos limitaremos a una sola plataforma; aprovecharemos el poder de SwiftUI y Xcode para adaptar este componente a iOS, macOS y watchOS.
1. Por qué un iOS Developer debería dominar las interfaces personalizadas
El ecosistema de Apple es conocido por su extrema atención al diseño. Como iOS Developer, dominar la programación Swift te permite ir más allá de los componentes estándar proporcionados por UIKit o las implementaciones por defecto de SwiftUI.
Un Tab Bar curvo en SwiftUI no es solo un capricho estético; ofrece ventajas clave en la Experiencia de Usuario (UX):
- Foco de atención: La curva central, a menudo acompañada de un botón flotante (Floating Action Button o FAB), dirige la mirada del usuario hacia la acción principal de la app (por ejemplo, crear un nuevo post, escanear un código, o añadir una transacción).
- Identidad de marca: Te permite salir del molde estándar y darle a tu aplicación una personalidad visual única.
- Ergonomía: En pantallas grandes (como los modelos Pro Max), un botón central grande y elevado es más fácil de alcanzar con el pulgar.
2. Preparando el Entorno en Xcode
Antes de escribir nuestra primera línea de código, necesitamos configurar nuestro entorno de trabajo.
- Abre Xcode (asegúrate de tener la versión más reciente para aprovechar las últimas características de SwiftUI).
- Selecciona Create a new Xcode project.
- En la pestaña Multiplatform, elige la plantilla App. Esto nos dará un punto de partida para iOS y macOS. Más adelante, añadiremos el target de watchOS si no se incluyó automáticamente.
- Nombra tu proyecto (por ejemplo,
CurvedTabBarApp). - Asegúrate de que la interfaz seleccionada sea SwiftUI y el lenguaje sea Swift.
Estructura del Proyecto
Para mantener las buenas prácticas de la programación Swift, organizaremos nuestro código en carpetas (Groups):
Model: Para los datos de nuestras pestañas.Views: Para nuestras vistas principales.Components: Aquí vivirá la magia de nuestro Tab Bar curvo en SwiftUI.Shapes: Para la lógica matemática de la curva.
3. La Matemática del Diseño: Trazando la Curva
El corazón de este tutorial es la creación de la forma curva. En SwiftUI, podemos dibujar cualquier forma imaginada utilizando el protocolo Shape y su método path(in rect: CGRect).
Para lograr la muesca o curva donde se asentará nuestro botón central, necesitamos entender las curvas de Bézier. Utilizaremos el método addCurve(to:control1:control2:) para trazar una transición suave desde el borde plano superior hacia un “valle” central.
Crea un nuevo archivo de Swift llamado CurvedShape.swift dentro de la carpeta Shapes y añade el siguiente código:
import SwiftUI
struct CurvedShape: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
// Empezamos en la esquina superior izquierda
path.move(to: CGPoint(x: 0, y: 0))
// Dibujamos una línea recta hasta antes del centro (donde empieza la curva)
let mid = rect.width / 2
let curveWidth: CGFloat = 80 // Ancho total de la curva
let curveDepth: CGFloat = 40 // Profundidad de la curva
path.addLine(to: CGPoint(x: mid - (curveWidth / 2), y: 0))
// Primera mitad de la curva (bajando)
path.addCurve(
to: CGPoint(x: mid, y: curveDepth),
control1: CGPoint(x: mid - 20, y: 0),
control2: CGPoint(x: mid - 20, y: curveDepth)
)
// Segunda mitad de la curva (subiendo)
path.addCurve(
to: CGPoint(x: mid + (curveWidth / 2), y: 0),
control1: CGPoint(x: mid + 20, y: curveDepth),
control2: CGPoint(x: mid + 20, y: 0)
)
// Continuamos la línea recta hasta la esquina superior derecha
path.addLine(to: CGPoint(x: rect.width, y: 0))
// Bajamos hasta la esquina inferior derecha
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
// Vamos hasta la esquina inferior izquierda
path.addLine(to: CGPoint(x: 0, y: rect.height))
// Cerramos el trazado
path.closeSubpath()
return path
}
}
Análisis del Código (Para el iOS Developer curioso)
curveWidthycurveDepth: Estas variables controlan el ancho y la profundidad del “valle”. Ajustando estos valores, puedes hacer la curva más pronunciada o más sutil.- Puntos de control (
control1,control2): Estos puntos actúan como “imanes” que tiran de la línea para formar la curva suave de Bézier en lugar de una línea recta dentada en forma de “V”.
4. Definiendo el Modelo de Datos
Para que nuestro Tab Bar sea dinámico y reutilizable, vamos a definir qué información representa una pestaña. Crea un archivo TabItem.swift:
import SwiftUI
enum TabItem: String, CaseIterable {
case home = "house.fill"
case search = "magnifyingglass"
case favorites = "heart.fill"
case profile = "person.fill"
var title: String {
switch self {
case .home: return "Inicio"
case .search: return "Buscar"
case .favorites: return "Favoritos"
case .profile: return "Perfil"
}
}
}
Usar un enum es una excelente práctica de programación Swift porque garantiza seguridad de tipos y facilita la iteración a través de todos los casos posibles usando CaseIterable.
5. Construyendo el Tab Bar Curvo en SwiftUI
Ahora que tenemos la forma matemática y nuestros datos, vamos a ensamblar la pieza visual principal. Crea un archivo llamado CustomTabBar.swift.
Aquí debemos tener en cuenta algo crucial: Xcode y el framework SwiftUI manejan la disposición espacial muy bien con HStack, pero como queremos añadir un botón central que se eleve por encima de la barra, usaremos un ZStack junto con un HStack.
import SwiftUI
struct CustomTabBar: View {
@Binding var selectedTab: TabItem
var action: () -> Void // Acción para el botón central
var body: some View {
ZStack {
// 1. El fondo con la forma curva
CurvedShape()
.fill(Color(UIColor.systemBackground))
.shadow(color: Color.black.opacity(0.1), radius: 10, x: 0, y: -5)
.frame(height: 80)
// 2. Los iconos del Tab Bar
HStack {
ForEach(TabItem.allCases.prefix(2), id: \.self) { tab in
TabBarButton(tab: tab, selectedTab: $selectedTab)
}
// Espaciador para dejar hueco al botón central
Spacer()
.frame(width: 80)
ForEach(TabItem.allCases.suffix(2), id: \.self) { tab in
TabBarButton(tab: tab, selectedTab: $selectedTab)
}
}
.padding(.horizontal, 25)
// 3. El Botón Flotante Central (FAB)
Button(action: action) {
Image(systemName: "plus")
.font(.system(size: 24, weight: .bold))
.foregroundColor(.white)
.frame(width: 60, height: 60)
.background(
Circle()
.fill(LinearGradient(gradient: Gradient(colors: [Color.blue, Color.purple]), startPoint: .topLeading, endPoint: .bottomTrailing))
)
.shadow(color: .purple.opacity(0.4), radius: 10, x: 0, y: 5)
}
.offset(y: -25) // Lo subimos para que encaje perfectamente en la curva
}
}
}
// Subcomponente para cada botón individual
struct TabBarButton: View {
var tab: TabItem
@Binding var selectedTab: TabItem
var body: some View {
GeometryReader { proxy in
Button(action: {
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
selectedTab = tab
}
}) {
VStack(spacing: 4) {
Image(systemName: tab.rawValue)
.font(.system(size: 22))
.foregroundColor(selectedTab == tab ? .blue : .gray)
if selectedTab == tab {
Circle()
.fill(Color.blue)
.frame(width: 5, height: 5)
.matchedGeometryEffect(id: "TabIndicator", in: animationNamespace)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
.frame(height: 50)
}
@Namespace private var animationNamespace
}
Animaciones Fluidas
Observa el uso de matchedGeometryEffect y withAnimation(.spring()). Un verdadero iOS Developer sabe que las animaciones estáticas son aburridas. Al usar matchedGeometryEffect, cuando el usuario toca otra pestaña, el pequeño punto azul debajo del icono “volará” suavemente hacia el nuevo icono, proporcionando un deleite visual que eleva la calidad del Tab Bar curvo en SwiftUI.
6. Integración Multiplataforma: iOS, macOS y watchOS
Como prometimos, este tutorial cubre el desarrollo multiplataforma en Xcode. La programación Swift moderna con SwiftUI nos permite reutilizar lógica, pero la UX varía dramáticamente entre un iPhone, un Mac y un Apple Watch.
Aquí usaremos compilación condicional (#if os()) para adaptar nuestra vista principal (ContentView.swift).
Implementación en ContentView
import SwiftUI
struct ContentView: View {
@State private var currentTab: TabItem = .home
var body: some View {
#if os(iOS)
iOSLayout()
#elseif os(macOS)
macOSLayout()
#elseif os(watchOS)
watchOSLayout()
#endif
}
// MARK: - Diseño iOS
@ViewBuilder
func iOSLayout() -> some View {
ZStack(alignment: .bottom) {
// Contenido Principal
TabView(selection: $currentTab) {
Text("Vista de Inicio").tag(TabItem.home)
Text("Vista de Búsqueda").tag(TabItem.search)
Text("Vista de Favoritos").tag(TabItem.favorites)
Text("Vista de Perfil").tag(TabItem.profile)
}
// Ocultamos el TabBar nativo
.toolbar(.hidden, for: .tabBar)
// Nuestro Custom Tab Bar Curvo
CustomTabBar(selectedTab: $currentTab) {
print("Botón central presionado en iOS")
}
.padding(.bottom, UIApplication.shared.windows.first?.safeAreaInsets.bottom == 0 ? 15 : 0)
}
.ignoresSafeArea(.keyboard, edges: .bottom)
}
// MARK: - Diseño macOS
@ViewBuilder
func macOSLayout() -> some View {
NavigationSplitView {
// En macOS, un SideBar suele ser mejor que un Tab Bar inferior
List(selection: $currentTab) {
ForEach(TabItem.allCases, id: \.self) { tab in
Label(tab.title, systemImage: tab.rawValue)
.tag(tab)
}
}
.listStyle(SidebarListStyle())
Button("Acción Principal") {
print("Botón de acción en Mac presionado")
}
.buttonStyle(.borderedProminent)
.padding()
} detail: {
// Contenido
Text("Estás en la sección: \(currentTab.title)")
.font(.largeTitle)
}
}
// MARK: - Diseño watchOS
@ViewBuilder
func watchOSLayout() -> some View {
// En la pequeña pantalla del reloj, un TabBar curvo inferior no tiene sentido espacial.
// Usamos el TabView paginado nativo de watchOS para una UX correcta.
TabView(selection: $currentTab) {
VStack {
Image(systemName: TabItem.home.rawValue).font(.title)
Text("Inicio")
}
.tag(TabItem.home)
VStack {
Image(systemName: TabItem.search.rawValue).font(.title)
Text("Buscar")
}
.tag(TabItem.search)
// Reemplazamos el botón flotante con una vista principal de acción rápida
Button(action: {
print("Acción en watchOS")
}) {
Image(systemName: "plus.circle.fill")
.font(.system(size: 60))
.foregroundColor(.blue)
}
.buttonStyle(PlainButtonStyle())
.tag(TabItem.favorites) // Usamos un tag temporal para el ejemplo
}
.tabViewStyle(PageTabViewStyle())
}
}
Entendiendo la Arquitectura Multiplataforma
Este es el verdadero poder de Swift y SwiftUI.
- iOS: Renderizamos nuestro Tab Bar curvo en SwiftUI anclado a la parte inferior de la pantalla usando un
ZStack. Tenemos cuidado con lossafeAreaInsetspara asegurarnos de que la barra no se superponga con el indicador de inicio del iPhone. - macOS: Forzar un Tab Bar inferior tipo móvil en un Mac rompe las guías de interfaz humana (HIG) de Apple. Como un buen iOS Developer (y Mac Developer), transformamos la navegación en un
NavigationSplitViewlateral, lo cual es idiomático para escritorio, manteniendo el mismo enumTabItemcomo fuente de la verdad. - watchOS: La pantalla es extremadamente limitada. Renderizar curvas de Bézier aquí ocuparía espacio valioso de contenido. Adaptamos nuestra estructura de datos a un
PageTabViewStyle(), que es la forma natural en la que los usuarios navegan en su reloj.
7. Optimización y Buenas Prácticas Avanzadas en Xcode
Si vas a publicar un tutorial o llevar esta implementación a una aplicación en producción comercial en la App Store, necesitas asegurarte de que tu código es robusto.
A. Accesibilidad (Accessibility)
Nunca olvides la accesibilidad. En la vista TabBarButton, añade modificadores de accesibilidad para que VoiceOver pueda leerlos correctamente:
Button(action: { /* ... */ }) { /* ... */ }
.accessibilityLabel(Text(tab.title))
.accessibilityAddTraits(selectedTab == tab ? .isSelected : [])
B. Rendimiento y GeometryReader
Muchos desarrolladores abusan de GeometryReader, lo cual puede causar cálculos excesivos de layout en SwiftUI resultando en caídas de frames o “jankiness” durante el desplazamiento. En nuestro código de CurvedShape, no usamos GeometryReader en absoluto; pasamos directamente el CGRect que provee el protocolo Shape. Esto garantiza un rendimiento gráfico ultrarrápido procesado directamente por el motor gráfico de Apple.
C. Modo Oscuro (Dark Mode)
El uso de colores de sistema como UIColor.systemBackground en iOS (o NSColor.windowBackgroundColor en macOS si estuvieras dibujándolo allí) asegura que cuando el usuario cambie su dispositivo al Modo Oscuro, tu Tab Bar curvo en SwiftUI invierta su fondo automáticamente sin requerir código condicional adicional de tu parte. Las sombras también deben ajustarse (usando colores semitransparentes en lugar de gris opaco) para que se vean bien en ambos esquemas.
Conclusión
Acabas de construir y comprender a fondo un componente de interfaz de usuario de primer nivel. Como iOS Developer, aprender la programación Swift a este nivel —manipulando Path, dominando transiciones con matchedGeometryEffect, y orquestando la lógica multiplataforma en Xcode— es lo que diferencia a los programadores junior de los arquitectos de aplicaciones senior.
El Tab Bar curvo en SwiftUI es solo el principio. Puedes experimentar añadiendo un gradiente en lugar de un color sólido, o tal vez hacer que la curva se anime de forma responsiva dependiendo de qué lado de la pantalla toca el usuario. El límite lo dicta tu creatividad y la potente API declarativa de SwiftUI.
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










