TipKit
Agrega tips de descubrimiento de funciones, sugerencias contextuales y marcadores de enseñanza a aplicaciones iOS 17+ usando el framework TipKit de Apple. TipKit gestiona la frecuencia de visualización, las reglas de elegibilidad y la persistencia para que los tips aparezcan en el momento adecuado y desaparezcan una vez que el usuario ha aprendido la funcionalidad.
Contenidos
- Configuración
- Definición de Tips
- Visualización de Tips
- Reglas de Tip
- Acciones de Tip
- Grupos de Tip
- Control Programático
- Errores Comunes
- Lista de Verificación para Revisión
- Referencias
Configuración
Llama a Tips.configure() una vez en App.init, antes de que se renderice cualquier vista. Esto inicializa el almacén de datos de tips y comienza la evaluación de reglas. Llamarlo más tarde corre el riesgo de una condición de carrera donde las vistas de tip intentan mostrarse antes de que el almacén esté listo.
import SwiftUI
import TipKit
@main
struct MyApp: App {
init() {
try? Tips.configure([
.datastoreLocation(.applicationDefault)
])
}
var body: some Scene {
WindowGroup { ContentView() }
}
}
Opciones de Ubicación del Almacén de Datos
| Opción | Caso de Uso |
|---|---|
.applicationDefault | Ubicación predeterminada, zona de pruebas de la aplicación (la mayoría de las apps) |
.groupContainer(identifier:) | Compartir estado de tips entre la app y sus extensiones |
.url(_:) | URL de archivo personalizada para control total sobre la ubicación de almacenamiento |
Sincronización con CloudKit
Sincroniza el estado de los tips entre los dispositivos de un usuario para que no vea el mismo tip en todos ellos. Añade la opción del contenedor CloudKit junto con la ubicación del almacén de datos.
try? Tips.configure([
.datastoreLocation(.applicationDefault),
.cloudKitContainer(.named("iCloud.com.example.app"))
])
Definición de Tips
Conforma una estructura al protocolo Tip. Proporciona un title como mínimo. Añade message para detalles complementarios e image para un icono principal. Mantén los títulos cortos y orientados a la acción porque el tip aparece como un callout compacto.
import TipKit
struct FavoriteTip: Tip {
var title: Text { Text("Fija tus Favoritos") }
var message: Text? { Text("Toca el icono de corazón para guardar elementos y acceder rápidamente a ellos.") }
var image: Image? { Image(systemName: "heart") }
}
Propiedades: title (obligatorio), message (detalle opcional), image (icono principal opcional), actions (botones opcionales), rules (condiciones de elegibilidad opcionales), options (frecuencia de visualización, recuento máximo).
Ciclo de vida: Pendiente (reglas no cumplidas) -> Elegible (todas las reglas pasan) -> Invalidado (descartado, se ha actuado sobre él o eliminado programáticamente). Una vez invalidado, un tip no reaparece a menos que se reinicie el almacén de datos.
Visualización de Tips
Tips en Línea con TipView
Incrusta un TipView directamente en tu diseño. Se renderiza como una tarjeta redondeada que aparece y desaparece con animación. Úsalo para tips dentro de contenido desplazable.
let favoriteTip = FavoriteTip()
var body: some View {
VStack {
TipView(favoriteTip)
ItemListView()
}
}
Tips Popover con .popoverTip()
Adjunta un tip como un popover anclado a cualquier vista. El framework dibuja una flecha desde el popover hasta el ancla. Úsalo para tips que señalan un control específico.
Button { toggleFavorite() } label: { Image(systemName: "heart") }
.popoverTip(favoriteTip)
// Controla la dirección de la flecha (omite para que el sistema elija)
.popoverTip(favoriteTip, arrowEdge: .bottom)
Estilo de TipView Personalizado
Crea un estilo personalizado para controlar la apariencia de los tips en toda la aplicación. Conforma a TipViewStyle e implementa makeBody(configuration:).
struct CustomTipStyle: TipViewStyle {
func makeBody(configuration: Configuration) -> some View {
HStack(spacing: 12) {
configuration.image?
.font(.title2)
.foregroundStyle(.tint)
VStack(alignment: .leading, spacing: 4) {
configuration.title
.font(.headline)
configuration.message?
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
.padding()
}
}
// Aplica globalmente o por vista
TipView(favoriteTip)
.tipViewStyle(CustomTipStyle())
Reglas de Tip
Las reglas controlan cuándo un tip se vuelve elegible. Todas las reglas en el array rules deben cumplirse para que el tip se muestre. TipKit admite dos tipos de reglas: basadas en parámetros y basadas en eventos.
Reglas Basadas en Parámetros
Usa @Parameter para rastrear el estado de la aplicación. El tip se vuelve elegible cuando el valor del parámetro satisface la condición de la regla.
struct FavoriteTip: Tip {
@Parameter
static var hasSeenList: Bool = false
var title: Text { Text("Fija tus Favoritos") }
var rules: [Rule] {
#Rule(Self.$hasSeenList) { $0 == true }
}
}
// Establece el parámetro cuando el usuario llega a la lista
FavoriteTip.hasSeenList = true
Reglas Basadas en Eventos
Usa Tips.Event para rastrear acciones del usuario. Dona al evento cada vez que ocurre la acción. La regla se activa cuando se cumple la condición de recuento o tiempo de donación. Esto es ideal para tips que deberían aparecer después de que el usuario haya realizado una acción varias veces sin descubrir una función relacionada.
struct ShortcutTip: Tip {
static let appOpenedEvent = Tips.Event(id: "appOpened")
var title: Text { Text("Prueba la Acción Rápida") }
var rules: [Rule] {
#Rule(Self.appOpenedEvent) { $0.donations.count >= 3 }
}
}
// Dona cada vez que se abre la aplicación
ShortcutTip.appOpenedEvent.donate()
Combinación de Múltiples Reglas
Coloca múltiples reglas en el array. Todas deben cumplirse (Y lógico).
struct AdvancedTip: Tip {
@Parameter
static var isLoggedIn: Bool = false
static let featureUsedEvent = Tips.Event(id: "featureUsed")
var title: Text { Text("Desbloquea el Modo Avanzado") }
var rules: [Rule] {
#Rule(Self.$isLoggedIn) { $0 == true }
#Rule(Self.featureUsedEvent) { $0.donations.count >= 5 }
}
}
Opciones de Frecuencia de Visualización
Controla la frecuencia con la que aparecen los tips usando la propiedad options.
struct DailyTip: Tip {
var title: Text { Text("Recordatorio Diario") }
var options: [TipOption] {
MaxDisplayCount(3) // Muestra como máximo 3 veces en total
IgnoresDisplayFrequency(true) // Omite el límite de frecuencia global
}
}
La frecuencia de visualización global se configura al inicializar:
try? Tips.configure([
.displayFrequency(.daily) // .immediate, .hourly, .daily, .weekly, .monthly
])
Con .daily, el sistema muestra como máximo un tip al día en toda la aplicación, a menos que un tip específico establezca IgnoresDisplayFrequency(true).
Acciones de Tip
Añade botones de acción a un tip para interacción directa. Cada acción tiene un id y una etiqueta. Maneja la acción en el controlador de acciones de la vista de tip.
struct FeatureTip: Tip {
var title: Text { Text("Prueba el Nuevo Editor") }
var message: Text? { Text("Hemos añadido un potente modo de edición.") }
var actions: [Action] {
Action(id: "open-editor", title: "Abrir Editor")
Action(id: "learn-more", title: "Más Información")
}
}
Maneja las acciones en la vista:
TipView(featureTip) { action in
switch action.id {
case "open-editor":
navigateToEditor()
featureTip.invalidate(reason: .actionPerformed)
case "learn-more":
showHelpSheet = true
default:
break
}
}
Grupos de Tip
Usa TipGroup para coordinar múltiples tips dentro de una sola vista. TipGroup asegura que solo un tip del grupo se muestre a la vez, evitando la sobrecarga de tips. Los tips se muestran en orden de prioridad.
struct OnboardingView: View {
let tipGroup = TipGroup(.ordered) {
WelcomeTip()
NavigationTip()
ProfileTip()
}
var body: some View {
VStack {
if let currentTip = tipGroup.currentTip {
TipView(currentTip)
}
Button("Siguiente") {
tipGroup.currentTip?.invalidate(reason: .actionPerformed)
}
}
}
}
Opciones de Prioridad
| Inicializador | Comportamiento |
|---|---|
.ordered | Los tips se muestran en el orden en que se enumeran |
Cuando el tip actual se invalida, el siguiente tip elegible en el grupo se convierte en currentTip.
Control Programático
Invalidación de Tips
Llama a invalidate(reason:) cuando el usuario realiza la acción descubierta o cuando el tip ya no es relevante.
let tip = FavoriteTip()
tip.invalidate(reason: .actionPerformed)
| Razón | Cuándo Usarla |
|---|---|
.actionPerformed | El usuario realizó la acción que describe el tip |
.displayCountExceeded | El tip alcanzó su recuento máximo de visualizaciones |
.tipClosed | El usuario descartó explícitamente el tip |
Utilidades de Prueba
TipKit proporciona métodos estáticos para controlar la visibilidad de los tips durante el desarrollo y las pruebas. Protéjelos con #if DEBUG o comprobaciones de ProcessInfo para que nunca se ejecuten en las versiones de producción.
#if DEBUG
// Muestra todos los tips independientemente de las reglas (útil durante el desarrollo)
Tips.showAllTipsForTesting()
// Muestra solo tips específicos
Tips.showTipsForTesting([FavoriteTip.self, ShortcutTip.self])
// Oculta todos los tips (útil para pruebas de UI que no involucran tips)
Tips.hideAllTipsForTesting()
// Reinicia el almacén de datos (borra todo el estado de tips, invalidaciones y eventos)
try? Tips.resetDatastore()
#endif
Uso de ProcessInfo para Esquemas de Prueba
if ProcessInfo.processInfo.arguments.contains("--show-all-tips") {
Tips.showAllTipsForTesting()
}
Pasa --show-all-tips como argumento de lanzamiento en el esquema de Xcode para las versiones de desarrollo.
Errores Comunes
NO: Llamar a Tips.configure() en cualquier lugar que no sea App.init
Llamar a Tips.configure() en un modificador onAppear o task de una vista crea una condición de carrera donde las vistas de tip intentan renderizarse antes de que el almacén de datos esté listo, causando tips faltantes o parpadeantes.
// INCORRECTO
struct ContentView: View {
var body: some View {
Text("Hola")
.task { try? Tips.configure() } // Demasiado tarde, las vistas ya se renderizaron
}
}
// CORRECTO
@main struct MyApp: App {
init() { try? Tips.configure() }
var body: some Scene { WindowGroup { ContentView() } }
}
NO: Mostrar demasiados tips a la vez
Mostrar múltiples tips simultáneamente abruma a los usuarios y diluye el impacto de cada tip. Los usuarios aprenden a ignorarlos.
// INCORRECTO: Tres tips visibles al mismo tiempo
VStack {
TipView(tipA)
TipView(tipB)
TipView(tipC)
}
// CORRECTO: Usa TipGroup para secuenciarlos
let group = TipGroup(.ordered) { TipA(); TipB(); TipC() }
if let currentTip = group.currentTip {
TipView(currentTip)
}
NO: Olvidar invalidar los tips después de que el usuario realice la acción
Si un tip dice "Toca la estrella para marcar como favorito" y el usuario toca la estrella pero el tip permanece, se erosiona la confianza en la interfaz de usuario.
// INCORRECTO: El tip permanece visible después de que el usuario actúa
Button("Favorito") { toggleFavorite() }
.popoverTip(favoriteTip)
// CORRECTO: Invalidar al actuar
Button("Favorito") {
toggleFavorite()
favoriteTip.invalidate(reason: .actionPerformed)
}
.popoverTip(favoriteTip)
NO: Dejar los tips de prueba habilitados en producción
Tips.showAllTipsForTesting() omite todas las reglas y límites de frecuencia. Enviar esto a producción significa que todos los usuarios verán cada tip inmediatamente.
// INCORRECTO: Siempre activo
Tips.showAllTipsForTesting()
// CORRECTO: Protegido detrás de DEBUG
#if DEBUG
Tips.showAllTipsForTesting()
#endif
NO: Hacer los títulos de los tips demasiado largos
Los títulos largos se truncan o se ajustan de manera incómoda en el callout compacto del tip. Pon la acción clave en el título y el contexto complementario en el mensaje.
// INCORRECTO
var title: Text { Text("Puedes tocar el botón de corazón para guardar este elemento en tu lista de favoritos") }
// CORRECTO
var title: Text { Text("Guardar en Favoritos") }
var message: Text? { Text("Toca el icono de corazón para mantener los elementos accesibles rápidamente.") }
NO: Usar tips para información crítica
Los usuarios pueden descartar los tips en cualquier momento y no reaparecen. Nunca pongas instrucciones esenciales o información de seguridad en un tip.
// INCORRECTO: Información crítica en un tip descartable
struct DataLossTip: Tip {
var title: Text { Text("Los cambios no guardados se perderán") }
}
// CORRECTO: Usa una alerta o advertencia en línea para información crítica
// Reserva los tips para descubrimiento de funciones y revelación progresiva
Lista de Verificación para Revisión
-
Tips.configure()llamado enApp.init, antes de que se renderice cualquier vista - Cada tip tiene un título claro y conciso (orientado a la acción, menos de ~40 caracteres)
- Los tips se invalidan cuando el usuario realiza la acción descubierta
- Reglas configuradas para que los tips aparezcan en el momento adecuado (no inmediatamente en el primer lanzamiento para todos los tips)
- Se usa
TipGroupcuando hay múltiples tips en una sola vista - Las utilidades de prueba (
showAllTipsForTesting,resetDatastore) están protegidas detrás de#if DEBUG - Sincronización con CloudKit configurada si la aplicación soporta múltiples dispositivos
- Frecuencia de visualización establecida adecuadamente (
.dailyo.weeklypara la mayoría de las apps) - Tips usados solo para descubrimiento de funciones, no para información crítica
- Se aplica
TipViewStylepersonalizado de manera consistente si el estilo predeterminado no coincide con el diseño de la aplicación - Las acciones del tip se manejan y el tip se invalida en el controlador de acciones
- Las donaciones de eventos se colocan en los puntos correctos de la acción del usuario
- Asegúrate de que los tipos de Tip personalizados sean Sendable; configura Tips en @MainActor
Referencias
- Consulta references/tipkit-patterns.md para patrones de implementación completos, incluidos estilos personalizados, reglas basadas en eventos, grupos de tips, estrategias de prueba, flujos de onboarding y configuración de previsualización en SwiftUI.