App Intents y Siri Shortcuts: automatización avanzada de iOS 2026

Guía completa de App Intents y Siri Shortcuts para iOS 18+. Crear acciones personalizadas para Siri, integrar Apple Intelligence y automatizar la app Swift en 2026.

App Intents y Siri Shortcuts para automatización iOS avanzada con Swift y Apple Intelligence

En 2026, con Apple Intelligence y el framework App Intents, las aplicaciones iOS entran en una era donde la intención del usuario importa más que la interfaz gráfica. Las apps que no exponen intents se vuelven invisibles en un sistema operativo orientado a la IA. App Intents constituye la base que permite a Siri, Spotlight, los widgets y el Action Button interactuar con las funcionalidades de la aplicación.

Lo que cubre este artículo

Este artículo presenta la creación completa de App Intents y Siri Shortcuts para iOS 18+, desde los conceptos fundamentales hasta la integración con Apple Intelligence y los App Intent Domains.

Comprender el framework App Intents

El framework App Intents, introducido con iOS 16, moderniza la construcción de intents en Swift al reemplazar el antiguo SiriKit Intents framework. Esta arquitectura declarativa permite crear acciones detectables por el sistema: Spotlight, app Shortcuts, Siri y el Action Button.

TaskIntent.swiftswift
import AppIntents

// Un AppIntent representa una acción que el usuario puede ejecutar
struct CreateTaskIntent: AppIntent {
    // Título mostrado en Shortcuts y Siri
    static var title: LocalizedStringResource = "Crear una tarea"

    // Descripción para accesibilidad y sugerencias
    static var description = IntentDescription(
        "Crea una nueva tarea en la aplicación."
    )

    // Parámetro con validación automática
    @Parameter(title: "Título de la tarea")
    var taskTitle: String

    // Parámetro opcional con valor por defecto
    @Parameter(title: "Prioridad", default: .medium)
    var priority: TaskPriority

    // Ejecución de la acción
    func perform() async throws -> some IntentResult & ReturnsValue<TaskEntity> {
        // Crear la tarea mediante el servicio
        let task = TaskService.shared.createTask(
            title: taskTitle,
            priority: priority
        )

        // Retornar la entidad creada para encadenar
        return .result(value: TaskEntity(task: task))
    }
}

El intent declara sus parámetros mediante el property wrapper @Parameter, permitiendo a Siri solicitar los valores faltantes. El método perform() ejecuta la lógica de negocio y retorna un resultado tipado.

Definir App Entities para los datos

Las App Entities representan los "sustantivos" de la aplicación: los objetos sobre los que operan los intents. Permiten a Siri comprender y manipular los datos de la app.

TaskEntity.swiftswift
import AppIntents

// Modelo de datos interno
struct Task: Identifiable, Codable {
    let id: UUID
    var title: String
    var priority: TaskPriority
    var isCompleted: Bool
    var dueDate: Date?
}

// Entidad expuesta al sistema
struct TaskEntity: AppEntity {
    // Identificador único requerido
    var id: UUID

    // Propiedades visibles
    var title: String
    var priority: TaskPriority
    var isCompleted: Bool

    // Configuración de visualización en el sistema
    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Tarea"

    // Representación visual de la instancia
    var displayRepresentation: DisplayRepresentation {
        DisplayRepresentation(
            title: "\(title)",
            subtitle: "\(priority.rawValue)",
            image: .init(systemName: isCompleted ? "checkmark.circle.fill" : "circle")
        )
    }

    // Query por defecto para buscar entidades
    static var defaultQuery = TaskEntityQuery()

    // Inicializador desde el modelo interno
    init(task: Task) {
        self.id = task.id
        self.title = task.title
        self.priority = task.priority
        self.isCompleted = task.isCompleted
    }
}

// Query para buscar y filtrar entidades
struct TaskEntityQuery: EntityQuery {
    // Búsqueda por identificadores
    func entities(for identifiers: [UUID]) async throws -> [TaskEntity] {
        TaskService.shared.fetchTasks()
            .filter { identifiers.contains($0.id) }
            .map { TaskEntity(task: $0) }
    }

    // Sugerencias mostradas en la interfaz
    func suggestedEntities() async throws -> [TaskEntity] {
        TaskService.shared.fetchTasks()
            .filter { !$0.isCompleted }
            .prefix(5)
            .map { TaskEntity(task: $0) }
    }
}

La EntityQuery define cómo el sistema busca y sugiere entidades. Los métodos entities(for:) y suggestedEntities() alimentan las interfaces de Siri y Shortcuts.

App Enums vs App Entities

Usar AppEnum para tipos con un conjunto fijo de valores (prioridad, estado), y AppEntity para tipos dinámicos creados por el usuario (tareas, notas, contactos).

Crear App Enums para valores fijos

Los App Enums exponen tipos enumerados al sistema, permitiendo a Siri ofrecer opciones contextuales.

TaskPriority.swiftswift
import AppIntents

// Enum expuesto al sistema
enum TaskPriority: String, AppEnum, Codable {
    case low
    case medium
    case high

    // Nombre del tipo mostrado
    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Prioridad"

    // Representación de cada caso
    static var caseDisplayRepresentations: [TaskPriority: DisplayRepresentation] = [
        .low: DisplayRepresentation(
            title: "Baja",
            image: .init(systemName: "arrow.down.circle")
        ),
        .medium: DisplayRepresentation(
            title: "Media",
            image: .init(systemName: "minus.circle")
        ),
        .high: DisplayRepresentation(
            title: "Alta",
            image: .init(systemName: "exclamationmark.circle")
        )
    ]
}

// Enum para el estado de la tarea
enum TaskStatus: String, AppEnum, Codable {
    case pending
    case inProgress
    case completed

    static var typeDisplayRepresentation: TypeDisplayRepresentation = "Estado"

    static var caseDisplayRepresentations: [TaskStatus: DisplayRepresentation] = [
        .pending: "Pendiente",
        .inProgress: "En progreso",
        .completed: "Completada"
    ]
}

Las representaciones visuales (iconos SF Symbols) enriquecen la visualización en Shortcuts y en las sugerencias de Siri.

Implementar AppShortcutsProvider

El AppShortcutsProvider expone los App Shortcuts al sistema, dejándolos disponibles inmediatamente sin configuración del usuario. Estos atajos aparecen en Spotlight, Siri y el Action Button.

ShortcutsProvider.swiftswift
import AppIntents

// Provider que declara todos los shortcuts de la app
struct TaskAppShortcutsProvider: AppShortcutsProvider {
    // Máximo 10 shortcuts por aplicación
    @AppShortcutsBuilder
    static var appShortcuts: [AppShortcut] {
        // Shortcut para crear una tarea
        AppShortcut(
            intent: CreateTaskIntent(),
            phrases: [
                // El placeholder .applicationName es OBLIGATORIO
                "Crear una tarea con \(.applicationName)",
                "Nueva tarea en \(.applicationName)",
                "Añadir una tarea a \(.applicationName)"
            ],
            shortTitle: "Crear tarea",
            systemImageName: "plus.circle"
        )

        // Shortcut para listar tareas
        AppShortcut(
            intent: ListTasksIntent(),
            phrases: [
                "Mostrar mis tareas en \(.applicationName)",
                "Ver tareas de \(.applicationName)",
                "Cuáles son mis tareas \(.applicationName)"
            ],
            shortTitle: "Mis tareas",
            systemImageName: "list.bullet"
        )

        // Shortcut con parámetro dinámico
        AppShortcut(
            intent: CompleteTaskIntent(),
            phrases: [
                "Completar \(\.$taskName) en \(.applicationName)",
                "Marcar \(\.$taskName) como hecha con \(.applicationName)"
            ],
            shortTitle: "Completar tarea",
            systemImageName: "checkmark.circle"
        )
    }
}

Las frases de voz deben incluir el placeholder \(.applicationName) para que Siri identifique la app destino. Los parámetros dinámicos como \(\.$taskName) permiten comandos contextuales.

Límite de 10 shortcuts

Una aplicación puede declarar como máximo 10 App Shortcuts. Conviene priorizar las acciones más frecuentes y útiles para los usuarios.

Intents con parámetros complejos

Los intents pueden aceptar parámetros complejos, incluyendo entidades y opciones de configuración.

CompleteTaskIntent.swiftswift
import AppIntents

struct CompleteTaskIntent: AppIntent {
    static var title: LocalizedStringResource = "Completar una tarea"

    static var description = IntentDescription(
        "Marca una tarea como completada."
    )

    // Parámetro tipo entidad con búsqueda automática
    @Parameter(title: "Tarea")
    var task: TaskEntity

    // Parámetro opcional con fecha
    @Parameter(title: "Fecha de finalización")
    var completionDate: Date?

    // Configuración del diálogo Siri
    static var parameterSummary: some ParameterSummary {
        Summary("Completar \(\.$task)") {
            \.$completionDate
        }
    }

    func perform() async throws -> some IntentResult & ProvidesDialog {
        // Actualiza la tarea
        TaskService.shared.completeTask(
            id: task.id,
            completionDate: completionDate ?? Date()
        )

        // Feedback de voz para Siri
        return .result(
            dialog: "La tarea \(task.title) ha sido marcada como completada."
        )
    }
}

// Intent que retorna una lista de entidades
struct ListTasksIntent: AppIntent {
    static var title: LocalizedStringResource = "Listar tareas"

    // Filtro opcional
    @Parameter(title: "Estado", default: nil)
    var statusFilter: TaskStatus?

    @Parameter(title: "Prioridad", default: nil)
    var priorityFilter: TaskPriority?

    func perform() async throws -> some IntentResult & ReturnsValue<[TaskEntity]> {
        var tasks = TaskService.shared.fetchTasks()

        // Aplicar filtros
        if let status = statusFilter {
            tasks = tasks.filter {
                switch status {
                case .completed: return $0.isCompleted
                case .pending, .inProgress: return !$0.isCompleted
                }
            }
        }

        if let priority = priorityFilter {
            tasks = tasks.filter { $0.priority == priority }
        }

        let entities = tasks.map { TaskEntity(task: $0) }
        return .result(value: entities)
    }
}

El ParameterSummary define cómo Siri presenta el intent durante la ejecución por voz, ofreciendo un feedback natural.

¿Listo para aprobar tus entrevistas de iOS?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

Integración con Apple Intelligence

iOS 18 introduce los App Intent Domains, colecciones de APIs diseñadas para funcionalidades específicas. Estos dominios permiten a Apple Intelligence comprender y ejecutar acciones con mayor precisión.

BookmarkDomainIntents.swiftswift
import AppIntents

// Conformidad con el dominio Bookmarks para integración Apple Intelligence
struct SaveBookmarkIntent: AppIntent {
    static var title: LocalizedStringResource = "Guardar un marcador"

    // Parámetro URL con validación automática
    @Parameter(title: "URL")
    var url: URL

    @Parameter(title: "Título", default: nil)
    var title: String?

    @Parameter(title: "Carpeta", default: nil)
    var folder: BookmarkFolderEntity?

    // Abrir la app si es necesario
    static var openAppWhenRun: Bool = false

    func perform() async throws -> some IntentResult & ProvidesDialog {
        let bookmark = BookmarkService.shared.save(
            url: url,
            title: title,
            folder: folder?.id
        )

        return .result(
            dialog: "Marcador guardado: \(bookmark.title)"
        )
    }
}

// Intent con conciencia del contenido en pantalla (iOS 18.4+)
struct AnalyzeScreenContentIntent: AppIntent {
    static var title: LocalizedStringResource = "Analizar contenido"

    // Acceso al contexto de pantalla vía Apple Intelligence
    @Parameter(title: "Contexto")
    var screenContext: String?

    func perform() async throws -> some IntentResult & ProvidesDialog {
        guard let context = screenContext else {
            return .result(dialog: "No hay contenido que analizar.")
        }

        // Procesar contenido extraído por Apple Intelligence
        let analysis = ContentAnalyzer.analyze(context)

        return .result(dialog: analysis.summary)
    }
}

Los dominios predefinidos (Books, Camera, Spreadsheets) permiten a Siri responder a las solicitudes con precisión gracias a modelos entrenados en estas tareas específicas.

Acciones Siri con confirmación del usuario

Para acciones sensibles, el sistema puede solicitar confirmación antes de la ejecución.

DeleteTaskIntent.swiftswift
import AppIntents

struct DeleteTaskIntent: AppIntent {
    static var title: LocalizedStringResource = "Eliminar una tarea"

    @Parameter(title: "Tarea")
    var task: TaskEntity

    // Requiere confirmación del usuario
    static var isDiscoverable: Bool = true

    func perform() async throws -> some IntentResult & ProvidesDialog {
        // Solicitar confirmación mediante diálogo
        try await requestConfirmation(
            result: .result(
                dialog: "¿Confirmas que quieres eliminar \(task.title)?"
            )
        )

        // Eliminar tras confirmación
        TaskService.shared.deleteTask(id: task.id)

        return .result(
            dialog: "La tarea \(task.title) ha sido eliminada."
        )
    }
}

// Intent con varias etapas de diálogo
struct ScheduleTaskIntent: AppIntent {
    static var title: LocalizedStringResource = "Programar una tarea"

    @Parameter(title: "Tarea")
    var task: TaskEntity

    @Parameter(title: "Fecha")
    var scheduledDate: Date

    @Parameter(title: "Recordatorio", default: true)
    var setReminder: Bool

    static var parameterSummary: some ParameterSummary {
        When(\.$setReminder, .equalTo, true) {
            Summary("Programar \(\.$task) para \(\.$scheduledDate) con recordatorio")
        } otherwise: {
            Summary("Programar \(\.$task) para \(\.$scheduledDate)")
        }
    }

    func perform() async throws -> some IntentResult & ProvidesDialog {
        TaskService.shared.schedule(
            taskId: task.id,
            date: scheduledDate,
            reminder: setReminder
        )

        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .medium
        dateFormatter.timeStyle = .short

        let formattedDate = dateFormatter.string(from: scheduledDate)

        return .result(
            dialog: "Tarea programada para el \(formattedDate)."
        )
    }
}

El método requestConfirmation pausa la ejecución hasta la validación del usuario, protegiendo contra acciones accidentales.

Intents en widgets interactivos

Los App Intents se integran de forma natural con WidgetKit para crear widgets interactivos en iOS 17+.

TaskWidgetIntents.swiftswift
import AppIntents
import WidgetKit

// Intent optimizado para widget (ejecución rápida)
struct ToggleTaskFromWidgetIntent: AppIntent {
    static var title: LocalizedStringResource = "Alternar tarea"

    @Parameter(title: "ID de la tarea")
    var taskID: String

    init() {}

    init(taskID: UUID) {
        self.taskID = taskID.uuidString
    }

    // Sin diálogo para los widgets
    func perform() async throws -> some IntentResult {
        guard let uuid = UUID(uuidString: taskID) else {
            return .result()
        }

        TaskService.shared.toggleCompletion(taskId: uuid)

        // Recarga inmediata del widget
        WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")

        return .result()
    }
}

// Vista del widget con botón interactivo
import SwiftUI

struct TaskWidgetView: View {
    let task: Task

    var body: some View {
        Button(intent: ToggleTaskFromWidgetIntent(taskID: task.id)) {
            HStack {
                Image(systemName: task.isCompleted ? "checkmark.circle.fill" : "circle")
                    .foregroundStyle(task.isCompleted ? .green : .secondary)

                Text(task.title)
                    .strikethrough(task.isCompleted)
            }
            .padding()
        }
        .buttonStyle(.plain)
    }
}

Los widgets utilizan la sintaxis Button(intent:) para conectar la interacción directamente con el App Intent, sin abrir la aplicación.

Configuración del Action Button

El Action Button del iPhone 15 Pro y posteriores puede disparar App Shortcuts directamente.

ActionButtonIntent.swiftswift
import AppIntents

// Intent optimizado para Action Button
struct QuickCaptureIntent: AppIntent {
    static var title: LocalizedStringResource = "Captura rápida"

    static var description = IntentDescription(
        "Crea rápidamente una tarea con un título."
    )

    // Abre la app para entrada
    static var openAppWhenRun: Bool = true

    func perform() async throws -> some IntentResult & OpensIntent {
        // Notificación para abrir la pantalla de captura rápida
        NotificationCenter.default.post(
            name: .quickCaptureTriggered,
            object: nil
        )

        return .result(opensIntent: ShowQuickCaptureViewIntent())
    }
}

// Declarar en AppShortcutsProvider para Action Button
extension TaskAppShortcutsProvider {
    @AppShortcutsBuilder
    static var appShortcuts: [AppShortcut] {
        // ... otros shortcuts

        AppShortcut(
            intent: QuickCaptureIntent(),
            phrases: [
                "Captura rápida \(.applicationName)",
                "Nota rápida \(.applicationName)"
            ],
            shortTitle: "Capturar",
            systemImageName: "bolt.circle"
        )
    }
}

Los usuarios pueden configurar el Action Button para disparar este shortcut desde Ajustes > Botón de acción.

Macros iOS 18+

iOS 18 introduce los macros @DeferredProperty y @ComputedProperty para reducir el boilerplate. Los App Intents también pueden residir en Swift Packages para reutilización multiplataforma.

Intents en Swift Packages

Los App Intents pueden definirse en Swift Packages para compartirlos entre iOS, macOS y watchOS.

Package.swiftswift
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "TaskIntents",
    platforms: [
        .iOS(.v17),
        .macOS(.v14),
        .watchOS(.v10)
    ],
    products: [
        .library(name: "TaskIntents", targets: ["TaskIntents"])
    ],
    dependencies: [],
    targets: [
        .target(
            name: "TaskIntents",
            dependencies: []
        )
    ]
)

// Sources/TaskIntents/SharedIntents.swift
import AppIntents

// Intent compartido multiplataforma
public struct SharedCreateTaskIntent: AppIntent {
    public static var title: LocalizedStringResource = "Crear una tarea"

    @Parameter(title: "Título")
    public var taskTitle: String

    public init() {}

    public func perform() async throws -> some IntentResult {
        // Lógica compartida
        await TaskRepository.shared.create(title: taskTitle)
        return .result()
    }
}

// Extensión específica de plataforma en la app
#if os(iOS)
extension SharedCreateTaskIntent {
    // Comportamiento específico iOS
    static var openAppWhenRun: Bool = false
}
#endif

Esta arquitectura mantiene un código único para los intents adaptando el comportamiento por plataforma.

Probar los App Intents

Los App Intents pueden testearse de forma unitaria como cualquier código Swift.

TaskIntentTests.swiftswift
import XCTest
import AppIntents
@testable import TaskApp

final class TaskIntentTests: XCTestCase {

    override func setUp() {
        super.setUp()
        // Resetear el servicio para tests aislados
        TaskService.shared.reset()
    }

    func testCreateTaskIntent() async throws {
        // Given
        var intent = CreateTaskIntent()
        intent.taskTitle = "Tarea de prueba"
        intent.priority = .high

        // When
        let result = try await intent.perform()

        // Then
        let tasks = TaskService.shared.fetchTasks()
        XCTAssertEqual(tasks.count, 1)
        XCTAssertEqual(tasks.first?.title, "Tarea de prueba")
        XCTAssertEqual(tasks.first?.priority, .high)
    }

    func testCompleteTaskIntent() async throws {
        // Given
        let task = TaskService.shared.createTask(
            title: "Tarea por completar",
            priority: .medium
        )

        var intent = CompleteTaskIntent()
        intent.task = TaskEntity(task: task)

        // When
        _ = try await intent.perform()

        // Then
        let updatedTask = TaskService.shared.fetchTask(id: task.id)
        XCTAssertTrue(updatedTask?.isCompleted ?? false)
    }

    func testEntityQuery() async throws {
        // Given
        let task1 = TaskService.shared.createTask(title: "Tarea 1", priority: .low)
        let task2 = TaskService.shared.createTask(title: "Tarea 2", priority: .high)

        let query = TaskEntityQuery()

        // When
        let entities = try await query.entities(for: [task1.id, task2.id])

        // Then
        XCTAssertEqual(entities.count, 2)
    }
}

Los tests verifican el comportamiento de los intents independientemente de la interfaz del sistema.

Buenas prácticas y optimizaciones

Varios patrones aseguran App Intents performantes y una experiencia óptima para el usuario.

BestPractices.swiftswift
import AppIntents

// 1. Frases localizadas
struct LocalizedTaskIntent: AppIntent {
    static var title: LocalizedStringResource = "task.create.title"

    static var description = IntentDescription(
        "task.create.description",
        categoryName: "task.category"
    )

    @Parameter(title: "task.parameter.title")
    var taskTitle: String

    func perform() async throws -> some IntentResult {
        // ...
        return .result()
    }
}

// 2. Manejo correcto de errores
enum TaskIntentError: Error, CustomLocalizedStringResourceConvertible {
    case taskNotFound
    case invalidInput
    case serviceUnavailable

    var localizedStringResource: LocalizedStringResource {
        switch self {
        case .taskNotFound:
            return "Tarea no encontrada."
        case .invalidInput:
            return "Datos inválidos."
        case .serviceUnavailable:
            return "Servicio temporalmente no disponible."
        }
    }
}

struct RobustTaskIntent: AppIntent {
    static var title: LocalizedStringResource = "Intent robusto"

    @Parameter(title: "ID")
    var taskId: String

    func perform() async throws -> some IntentResult & ProvidesDialog {
        guard let uuid = UUID(uuidString: taskId) else {
            throw TaskIntentError.invalidInput
        }

        guard let task = TaskService.shared.fetchTask(id: uuid) else {
            throw TaskIntentError.taskNotFound
        }

        return .result(dialog: "Tarea encontrada: \(task.title)")
    }
}

// 3. Optimización de EntityQuery
struct OptimizedTaskQuery: EntityStringQuery {
    // Búsqueda textual optimizada
    func entities(matching string: String) async throws -> [TaskEntity] {
        // Búsqueda en el servicio con límite
        TaskService.shared.search(query: string, limit: 10)
            .map { TaskEntity(task: $0) }
    }

    // Sugerencias limitadas para rendimiento
    func suggestedEntities() async throws -> [TaskEntity] {
        TaskService.shared.fetchRecentTasks(limit: 5)
            .map { TaskEntity(task: $0) }
    }
}

// 4. Focus Filter para los modos de Concentración
struct TaskFocusFilter: SetFocusFilterIntent {
    static var title: LocalizedStringResource = "Filtrar tareas"

    @Parameter(title: "Mostrar solo prioridad alta")
    var showHighPriorityOnly: Bool

    func perform() async throws -> some IntentResult {
        TaskService.shared.setFocusFilter(highPriorityOnly: showHighPriorityOnly)
        return .result()
    }
}

Estos patrones aseguran una integración fluida con el sistema preservando un rendimiento óptimo.

Conclusión

Los App Intents y Siri Shortcuts transforman la manera en que los usuarios interactúan con las aplicaciones iOS. En 2026, con Apple Intelligence, exponer intents ya no es opcional sino esencial para ofrecer una experiencia moderna e intuitiva.

Checklist App Intents iOS 18+

  • ✅ Crear AppIntents para las acciones principales de la app
  • ✅ Definir AppEntities para los datos manipulables
  • ✅ Usar AppEnum para los tipos enumerados
  • ✅ Implementar AppShortcutsProvider con frases de voz
  • ✅ Respetar el límite máximo de 10 App Shortcuts
  • ✅ Incluir \(.applicationName) en todas las frases
  • ✅ Configurar ParameterSummary para feedback Siri
  • ✅ Integrar dominios Apple Intelligence si aplica
  • ✅ Probar los intents con tests unitarios
  • ✅ Localizar títulos y descripciones

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Etiquetas

#app-intents
#siri-shortcuts
#ios
#swift
#apple-intelligence

Compartir

Artículos relacionados