Si eres un iOS Developer, es muy probable que te hayas topado con este escenario: tienes un diseño increíble en Figma, estás construyendo una lista personalizada con tarjetas hermosas, pero al momento de implementar la navegación, Apple decide insertar automáticamente ese pequeño icono de chevron (la famosa flechita gris) a la derecha de tu celda. A veces, la interfaz predeterminada del sistema simplemente no encaja con la visión creativa de tu aplicación.
En este extenso tutorial de programación Swift, vamos a sumergirnos en las profundidades del framework declarativo de Apple. Aprenderemos paso a paso como esconder la flecha en un NavigationLink con SwiftUI, analizando diferentes técnicas, desde los trucos clásicos (legacy) hasta los enfoques más modernos introducidos en las últimas versiones de Swift. Todo esto utilizando Xcode y asegurándonos de que nuestro código sea perfectamente escalable y funcional tanto en iOS, como en macOS y watchOS.
1. El Problema del “Disclosure Indicator”
Antes de solucionar el problema, es fundamental entender por qué ocurre. En el ecosistema de Apple, la consistencia de la Interfaz de Usuario (UI) y la Experiencia de Usuario (UX) son sagradas. Según las Human Interface Guidelines (HIG), cuando un elemento dentro de una lista lleva a una nueva pantalla de detalles, debe indicarse visualmente para que el usuario sepa que ese elemento es interactivo.
Esa indicación visual es el Disclosure Indicator (la flecha).
Cuando usas el componente List en SwiftUI y colocas un NavigationLink dentro de él, el framework asume automáticamente que estás construyendo una tabla de navegación estándar y añade la flecha por defecto.
// Ejemplo del comportamiento por defecto
List {
NavigationLink(destination: DetailView()) {
Text("Mi Celda Personalizada")
}
}
El código anterior generará una celda con el texto alineado a la izquierda y una flecha gris a la derecha. Pero, ¿qué pasa si tu celda es una tarjeta (Card) compleja, con imágenes, botones internos y un diseño de borde a borde? La flecha rompe completamente la estética. Veamos cómo eliminarla.
2. Solución Clásica: El Truco del ZStack y Opacity (Compatible con iOS 13+)
Si estás manteniendo una aplicación antigua o necesitas dar soporte a versiones previas a iOS 16, la técnica más antigua y fiable en la programación Swift para solucionar esto es engañar al sistema de renderizado de SwiftUI.
El concepto es simple: separamos el componente visual (lo que el usuario ve) del componente interactivo (el NavigationLink), los apilamos uno encima del otro usando un ZStack, y volvemos invisible el NavigationLink usando .opacity(0).
Implementación en Xcode
Abre Xcode, crea una nueva vista y prueba el siguiente código:
import SwiftUI
struct LegacyHiddenArrowView: View {
var body: some View {
NavigationView {
List {
ForEach(0..<5) { item in
ZStack(alignment: .leading) {
// 1. Tu diseño visual personalizado
CustomCardView(title: "Elemento \(item)")
// 2. El NavigationLink invisible
NavigationLink(destination: Text("Detalle del elemento \(item)")) {
EmptyView()
}
.opacity(0) // Oculta la flecha y el link
}
// Opcional: Eliminar los separadores de la lista
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets())
}
}
.navigationTitle("ZStack Hack")
}
}
}
struct CustomCardView: View {
let title: String
var body: some View {
HStack {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
Text(title)
.font(.headline)
Spacer()
}
.padding()
.background(Color.blue.opacity(0.1))
.cornerRadius(10)
.padding(.horizontal)
}
}
¿Por qué funciona esto?
Al envolver el NavigationLink en un ZStack y pasarle un EmptyView() como etiqueta (label), reducimos su tamaño visual a la mínima expresión. Al añadir .opacity(0), garantizamos que la flecha predeterminada que el sistema intenta dibujar sea completamente transparente. La magia radica en que, aunque sea invisible, el NavigationLink sigue absorbiendo los toques (taps) del usuario sobre toda el área del ZStack.
Nota para el iOS Developer: Aunque este truco es muy popular, puede generar problemas sutiles de accesibilidad (VoiceOver podría leer elementos duplicados si no se configura correctamente) y a veces intercepta gestos de manera impredecible si tu tarjeta tiene botones internos.
3. Solución Intermedia: Modificadores de Estilo con .buttonStyle
Otra forma muy elegante y con menos “hackeo” de vistas es aprovechar cómo SwiftUI interpreta los enlaces bajo el capó. Un NavigationLink dentro de una lista actúa, a efectos prácticos, como un botón.
Si aplicamos un estilo de botón plano o sin formato, en algunas versiones y contextos de la plataforma, podemos eliminar los adornos por defecto de la lista.
import SwiftUI
struct ButtonStyleView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: Text("Detalle")) {
CustomCardView(title: "Celda con ButtonStyle")
}
.buttonStyle(PlainButtonStyle()) // Elimina el comportamiento de resaltado predeterminado
// Nota: En las últimas versiones de iOS, PlainButtonStyle no siempre elimina la flecha en una List,
// pero es crucial para evitar que toda la celda parpadee en azul al tocarla.
}
.navigationTitle("Button Style")
}
}
}
Es importante destacar que el comportamiento de .buttonStyle(PlainButtonStyle()) puede variar enormemente entre iOS, macOS y watchOS. Por ello, muchos desarrolladores prefieren abandonar el componente List por completo cuando se requiere una personalización extrema.
4. La Solución Definitiva para Diseños a Medida: ScrollView + LazyVStack
Como iOS Developer experimentado, te darás cuenta de que pelear contra el comportamiento predeterminado del sistema suele ser una mala idea. El componente List en SwiftUI está diseñado específicamente para verse y comportarse como las tablas nativas de iOS (el clásico UITableView de UIKit). Si tu diseño no se parece a una tabla nativa, no deberías usar un List.
Para construir interfaces completamente personalizadas sin rastros de flechas, separadores ni márgenes extraños, la combinación ganadora en la programación Swift es usar un ScrollView junto con un LazyVStack.
Ejemplo Práctico en Swift
import SwiftUI
struct CustomScrollViewNavigation: View {
var body: some View {
NavigationView {
ScrollView {
LazyVStack(spacing: 16) {
ForEach(0..<10) { index in
NavigationLink(destination: Text("Detalle \(index)")) {
// Tu celda es ahora el Label del NavigationLink
CustomCardView(title: "Tarjeta personalizada \(index)")
}
.buttonStyle(PlainButtonStyle()) // Evita el resaltado azul por defecto
}
}
.padding()
}
.navigationTitle("ScrollView Custom")
}
}
}
Ventajas de este enfoque:
- Control Absoluto: Adiós a como esconder la flecha en un NavigationLink con SwiftUI. Al no estar dentro de un contexto de
List, la flecha (disclosure indicator) nunca se renderiza. - Rendimiento:
LazyVStacksolo carga en memoria las vistas que están visibles en la pantalla, ofreciendo un rendimiento similar al deList. - Sin hacks: El código es limpio, semántico y fácil de mantener. No hay vistas ocultas ni opacidades en cero.
5. El Estándar Moderno (iOS 16+): NavigationStack y Navegación Programática
En la WWDC 2022, Apple revolucionó la navegación en SwiftUI deprecando NavigationView e introduciendo NavigationStack. Este cambio trajo consigo un paradigma de navegación basado en datos (value-based navigation).
Esta es, sin duda, la forma más profesional y moderna de resolver nuestro problema en Xcode en la actualidad. En lugar de usar NavigationLink envolviendo nuestra vista visual, usamos botones normales que actualizan el estado de la navegación, y definimos el destino en un nivel superior.
Código Paso a Paso con NavigationStack
import SwiftUI
// 1. Definimos un modelo de datos para nuestra navegación
struct ItemData: Hashable {
let id = UUID()
let name: String
}
struct ModernNavigationView: View {
// 2. Opcional: Podemos controlar el path de navegación si lo deseamos
@State private var items = [
ItemData(name: "MacBook Pro"),
ItemData(name: "iPhone 15"),
ItemData(name: "Apple Watch Ultra")
]
var body: some View {
// 3. Usamos el nuevo NavigationStack
NavigationStack {
List(items, id: \.id) { item in
// 4. En lugar de un NavigationLink, usamos un Button normal o un .onTapGesture
Button(action: {
// Aquí es donde normalmente harías algo antes de navegar
// Pero la navegación basada en valores se maneja con NavigationLink(value:label:)
}) {
CustomCardView(title: item.name)
}
.listRowSeparator(.hidden)
// 5. El truco maestro: un NavigationLink con valor, sin etiqueta visual
.overlay(
NavigationLink(value: item) {
EmptyView()
}
.opacity(0)
)
}
.listStyle(.plain)
.navigationTitle("iOS 16+ Navigation")
// 6. El destino se define fuera de la celda
.navigationDestination(for: ItemData.self) { selectedItem in
Text("Bienvenido al detalle de: \(selectedItem.name)")
.font(.largeTitle)
}
}
}
}
Otra alternativa aún más limpia con NavigationStack si decides no usar List (y usar el enfoque de ScrollView mencionado antes), es usar la navegación puramente por estado:
import SwiftUI
struct PureStateNavigationView: View {
@State private var navigationPath = NavigationPath()
var body: some View {
NavigationStack(path: $navigationPath) {
ScrollView {
LazyVStack {
ForEach(1...5, id: \.self) { index in
// Un simple botón. Cero flechas. Cero estilos por defecto.
Button(action: {
// Añadimos el valor al path para disparar la navegación
navigationPath.append(index)
}) {
CustomCardView(title: "Elemento interactivo \(index)")
}
.buttonStyle(.plain)
}
}
}
.navigationTitle("Navegación por Estado")
.navigationDestination(for: Int.self) { value in
Text("Pantalla de destino \(value)")
}
}
}
}
Este es el santo grial para cualquier iOS Developer. Hemos desacoplado completamente la interfaz de usuario de la lógica de navegación. El componente visual es solo un botón que empuja un valor a un arreglo (NavigationPath), y el NavigationStack reacciona a ese cambio mostrando la nueva pantalla.
6. Consideraciones Multiplataforma: macOS y watchOS
La belleza de SwiftUI es su promesa de aprender una vez, aplicar en cualquier lugar. Sin embargo, como sabrás al usar Xcode, cada plataforma tiene sus peculiaridades.
macOS
En macOS, las aplicaciones suelen utilizar una arquitectura de NavigationSplitView (barras laterales o sidebars). Si intentas usar el truco del ZStack y la opacidad en macOS, podrías encontrarte con que el área de selección con el ratón se comporta de forma extraña.
Para macOS, el enfoque recomendado es siempre usar la navegación basada en datos (NavigationStack o NavigationSplitView en macOS 13+) o el enfoque de ScrollView. El uso de List en macOS está muy ligado al comportamiento de selección de filas del sistema (el resaltado azul o gris que cubre toda la fila), y alterar ese comportamiento rompe la experiencia nativa del usuario de Mac.
watchOS
En la pantalla pequeña del Apple Watch, el espacio es crítico. En watchOS, los elementos de una List casi siempre tienen un fondo en forma de cápsula. Aquí, el disclosure indicator a veces ni siquiera se muestra por defecto dependiendo del estilo de la lista.
Si desarrollas para watchOS usando programación Swift, utiliza botones nativos (Button) que naveguen programáticamente a través de NavigationStack. Evita el uso de ZStack con opacity(0) en watchOS, ya que el motor de renderizado de este dispositivo está muy optimizado y añadir vistas invisibles puede afectar ligeramente al rendimiento y la respuesta háptica de la Corona Digital.
7. Accesibilidad y Human Interface Guidelines (HIG)
Antes de concluir, pongámonos la gorra de diseñadores de producto. Has aprendido como esconder la flecha en un NavigationLink con SwiftUI, pero la pregunta real es: ¿Deberías hacerlo?
Apple incluye el chevron por una razón muy específica: la descubribilidad (Discoverability). Los usuarios están entrenados neurológicamente desde 2007 (con el primer iPhone) para saber que esa flecha significa “toca aquí para ver más detalles”.
Si decides eliminar la flecha, es tu responsabilidad como iOS Developer proporcionar pistas visuales alternativas que indiquen que el elemento es interactivo. Algunas formas de hacerlo son:
- Efectos de pulsación (Tap Feedback): Asegúrate de que tu tarjeta cambia de color, se escala ligeramente hacia abajo o reduce su opacidad cuando el usuario presiona sobre ella.
- Sombras y Profundidad: Usa
shadow(radius:)para hacer que la tarjeta parezca que está flotando sobre el fondo, lo que en el lenguaje de diseño moderno (Material Design y el propio de Apple) sugiere interactividad. - Llamadas a la acción (CTA) explícitas: Puedes incluir un botón que diga “Ver más” o “Detalles” dentro de la propia tarjeta.
- Soporte para VoiceOver: Si usas el truco del
ZStackcon unEmptyView(), prueba tu app con VoiceOver activo. Asegúrate de agrupar los elementos de accesibilidad (.accessibilityElement(children: .combine)) para que el lector de pantalla no se confunda leyendo elementos superpuestos y frustre a los usuarios con discapacidades visuales.
8. Conclusión
Dominar la navegación en SwiftUI es un rito de iniciación para cualquier iOS Developer. A lo largo de este extenso tutorial, hemos explorado cómo el framework de Apple maneja la creación de interfaces y por qué insiste en colocar elementos de UI nativos como el disclosure indicator.
Hemos visto que no existe una única respuesta correcta para esconder la flecha, sino varias herramientas en nuestro arsenal de programación Swift:
- El truco del ZStack y Opacity(0) para proyectos legacy que usan
List. - El uso de ScrollView y LazyVStack para escapar completamente de los estilos restrictivos del sistema.
- La potente navegación basada en estados con
NavigationStackintroducida en iOS 16, ideal para arquitecturas limpias y modernas en Xcode.
Como consejo final, mantén tu código lo más simple y declarativo posible. A medida que SwiftUI evoluciona año tras año, las soluciones “hacky” tienden a romperse con las nuevas actualizaciones de iOS. Adoptar las nuevas APIs como NavigationStack y NavigationPath no solo solucionará tus problemas estéticos actuales, sino que preparará tu aplicación para el futuro en todos los dispositivos de Apple.








