Programación en Swift y SwiftUI para iOS Developers

EventKit en SwiftUI

Como iOS Developer, tarde o temprano te enfrentarás al reto de integrar tu aplicación con el calendario o los recordatorios del dispositivo del usuario. Ya sea que estés construyendo una app de productividad, un gestor de reservas o una plataforma de fitness, acceder a la agenda del usuario es una funcionalidad crítica. Aquí es donde entra en juego EventKit.

Históricamente, EventKit fue diseñado en la era de Objective-C y UIKit. Sin embargo, con el paradigma moderno de Apple, integrar EventKit en SwiftUI se ha convertido en una habilidad esencial en la programación Swift.

En este tutorial extenso y detallado, aprenderás desde cero qué es EventKit, cómo configurar tu proyecto en Xcode, cómo gestionar los permisos modernos de privacidad, y cómo crear una aplicación completa usando Swift y SwiftUI que funcione de manera impecable en iOS, macOS y watchOS.


1. ¿Qué es EventKit?

EventKit es el framework nativo de Apple que proporciona acceso a los datos del calendario (Eventos) y a la aplicación de Recordatorios. Al utilizar EventKit, tu aplicación de SwiftUI no tiene que crear una base de datos propia para gestionar fechas y citas; en su lugar, lee y escribe directamente en la base de datos centralizada de iOS, macOS o watchOS.

Esto significa que cualquier evento que tu app cree usando EventKit, aparecerá automáticamente en la app oficial de Calendario de Apple, en Google Calendar (si el usuario lo tiene sincronizado) o en Outlook.

La Arquitectura Principal

Para dominar la programación Swift con EventKit, debes entender sus tres pilares:

  1. EKEventStore: Es el motor principal. Piensa en él como la conexión a la base de datos. Solo debes tener una instancia de EKEventStore viva en tu app durante todo su ciclo de vida.
  2. EKCalendar: Representa un calendario específico (por ejemplo, “Trabajo”, “Familia” o “Cumpleaños”).
  3. EKEvent y EKReminder: Son los objetos individuales que contienen los datos (título, fecha de inicio, fecha de fin, ubicación).

2. Configuración en Xcode: La barrera de los Permisos

Antes de escribir una sola línea de código en Swift, debemos configurar nuestro proyecto en Xcode. Apple es extremadamente estricta con la privacidad de los usuarios. Si intentas acceder al calendario sin declarar por qué lo necesitas, tu app sufrirá un crash inmediato.

A partir de iOS 17 y macOS 14, Apple introdujo niveles de permisos granulares para los calendarios:

  • Acceso de solo escritura (Write-only): Permite a tu app añadir eventos sin poder ver lo que el usuario ya tiene en su calendario.
  • Acceso completo (Full access): Permite leer, editar y borrar cualquier evento.

Añadiendo las claves al Info.plist

Abre tu proyecto en Xcode, ve al archivo Info.plist (o a la pestaña “Info” en la configuración del target) y añade las siguientes claves, dependiendo del acceso que necesites:

  1. Para Acceso Completo (Lectura y Escritura):
    • Key: Privacy - Calendars Full Access Usage Description (NSCalendarsFullAccessUsageDescription)
    • Value: “Necesitamos acceso completo para leer tus eventos y mostrar tu agenda diaria.”
  2. Para Solo Escritura (Añadir eventos sin leer):
    • Key: Privacy - Calendars Write Only Access Usage Description (NSCalendarsWriteOnlyAccessUsageDescription)
    • Value: “Necesitamos acceso para guardar esta reserva en tu calendario.”

Nota: Si tu app también requiere recordatorios, añade NSRemindersFullAccessUsageDescription.


3. Creando el Gestor de EventKit en Swift

Dado que trabajamos con SwiftUI, la mejor práctica es crear una clase que actúe como un ObservableObject para gestionar toda la lógica de EventKit. Esto mantiene nuestra vista limpia y separa la lógica de negocio de la interfaz.

Crea un nuevo archivo de Swift llamado CalendarManager.swift:

import Foundation
import EventKit

@MainActor
class CalendarManager: ObservableObject {
    // La única instancia del store que usaremos
    let store = EKEventStore()
    
    // Lista de eventos publicados para que SwiftUI reaccione
    @Published var events: [EKEvent] = []
    @Published var isAccessGranted: Bool = false
    
    // Método para solicitar permisos
    func requestAccess() async {
        do {
            // Solicitamos acceso completo para eventos
            let granted = try await store.requestFullAccessToEvents()
            self.isAccessGranted = granted
            
            if granted {
                fetchEvents()
            }
        } catch {
            print("Error al solicitar permisos del calendario: \(error.localizedDescription)")
            self.isAccessGranted = false
        }
    }
    
    // Método para obtener los eventos del próximo mes
    func fetchEvents() {
        guard isAccessGranted else { return }
        
        let calendars = store.calendars(for: .event)
        let now = Date()
        
        // Calculamos la fecha de dentro de 30 días
        guard let nextMonth = Calendar.current.date(byAdding: .day, value: 30, to: now) else { return }
        
        // Creamos un predicado (una consulta de búsqueda)
        let predicate = store.predicateForEvents(withStart: now, end: nextMonth, calendars: calendars)
        
        // Obtenemos los eventos y los asignamos a nuestra variable publicada
        self.events = store.events(matching: predicate)
    }
}

Explicación del Código:

  • @MainActor: Asegura que cualquier cambio en las propiedades @Published se realice en el hilo principal, lo cual es obligatorio en SwiftUI para actualizar la interfaz.
  • requestFullAccessToEvents(): Esta es la API moderna y asíncrona introducida recientemente. Reemplaza a las antiguas funciones basadas en closures (requestAccess(to:completion:)).
  • predicateForEvents: Es la forma eficiente de buscar en EventKit. En lugar de traer todos los eventos de la historia (lo cual colapsaría la memoria), definimos un rango de fechas.

4. Construyendo la Interfaz en SwiftUI

Ahora que tenemos nuestro cerebro (el CalendarManager), vamos a crear el cuerpo. Vamos a diseñar una vista en SwiftUI que primero solicite permisos y luego muestre una lista de eventos.

import SwiftUI
import EventKit

struct ContentView: View {
    @StateObject private var calendarManager = CalendarManager()
    
    var body: some View {
        NavigationView {
            Group {
                if calendarManager.isAccessGranted {
                    EventsListView(events: calendarManager.events)
                } else {
                    PermissionView(manager: calendarManager)
                }
            }
            .navigationTitle("Mi Agenda")
        }
        .task {
            // Verificamos el estado actual del permiso al cargar la vista
            let status = EKEventStore.authorizationStatus(for: .event)
            if status == .fullAccess {
                calendarManager.isAccessGranted = true
                calendarManager.fetchEvents()
            }
        }
    }
}

// Sub-vista para solicitar permisos
struct PermissionView: View {
    @ObservedObject var manager: CalendarManager
    
    var body: some View {
        VStack(spacing: 20) {
            Image(systemName: "calendar.badge.exclamationmark")
                .font(.system(size: 60))
                .foregroundColor(.orange)
            
            Text("Se requiere acceso al calendario")
                .font(.title2)
                .bold()
            
            Text("Para mostrar tus próximas citas, necesitamos tu permiso para leer el calendario.")
                .multilineTextAlignment(.center)
                .foregroundColor(.secondary)
                .padding(.horizontal)
            
            Button(action: {
                Task {
                    await manager.requestAccess()
                }
            }) {
                Text("Conceder Permiso")
                    .bold()
                    .frame(maxWidth: .infinity)
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }
            .padding(.horizontal, 40)
        }
    }
}

// Sub-vista para mostrar la lista de eventos
struct EventsListView: View {
    var events: [EKEvent]
    
    var body: some View {
        List {
            if events.isEmpty {
                Text("No tienes eventos próximos.")
                    .foregroundColor(.secondary)
            } else {
                ForEach(events, id: \.eventIdentifier) { event in
                    EventRowView(event: event)
                }
            }
        }
        // Recomendable para una buena experiencia de usuario en iOS
        .refreshable {
            // Aquí llamarías de nuevo a fetchEvents si estuviera directamente accesible
        }
    }
}

// Diseño individual de cada celda de evento
struct EventRowView: View {
    let event: EKEvent
    
    var body: some View {
        VStack(alignment: .leading, spacing: 5) {
            Text(event.title)
                .font(.headline)
            
            HStack {
                // Color del calendario
                Circle()
                    .fill(Color(event.calendar.cgColor))
                    .frame(width: 10, height: 10)
                
                Text(formatDate(event.startDate))
                    .font(.subheadline)
                    .foregroundColor(.secondary)
                
                Spacer()
                
                if let location = event.location, !location.isEmpty {
                    Image(systemName: "mappin.and.ellipse")
                        .foregroundColor(.red)
                        .font(.caption)
                }
            }
        }
        .padding(.vertical, 4)
    }
    
    private func formatDate(_ date: Date) -> String {
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        formatter.timeStyle = .short
        return formatter.string(from: date)
    }
}

Esta implementación demuestra el poder de SwiftUI. Con unas pocas líneas de código, hemos gestionado los estados asíncronos y creado una interfaz reactiva. Si el permiso cambia, la vista se actualizará automáticamente.


5. Añadiendo Eventos de forma Programática y Nativa

Una parte vital para cualquier iOS Developer es permitir que el usuario interactúe con el calendario creando nuevos eventos. Existen dos maneras de hacer esto al usar EventKit en SwiftUI:

Opción A: Programación directa en código (Background)

Si tu app necesita crear un evento automáticamente (por ejemplo, al confirmar la compra de un billete de avión), lo harás puramente en Swift:

extension CalendarManager {
    func addEvent(title: String, startDate: Date, endDate: Date) {
        let newEvent = EKEvent(eventStore: self.store)
        newEvent.title = title
        newEvent.startDate = startDate
        newEvent.endDate = endDate
        
        // Asignamos el calendario por defecto del usuario
        newEvent.calendar = store.defaultCalendarForNewEvents
        
        do {
            try store.save(newEvent, span: .thisEvent)
            print("Evento guardado con éxito")
            // Refrescamos la lista para mostrar el nuevo evento
            fetchEvents() 
        } catch {
            print("Error al guardar el evento: \(error.localizedDescription)")
        }
    }
}

Opción B: Usando EKEventEditViewController (Recomendado)

Para una experiencia de usuario superior, debes mostrar la pantalla nativa de creación de eventos de iOS. Como esta es una vista de UIKit, necesitamos un puente hacia SwiftUI usando UIViewControllerRepresentable.

import SwiftUI
import EventKitUI

struct EventEditViewController: UIViewControllerRepresentable {
    let eventStore: EKEventStore
    let event: EKEvent?
    @Environment(\.presentationMode) var presentationMode
    
    func makeUIViewController(context: Context) -> EKEventEditViewController {
        let controller = EKEventEditViewController()
        controller.eventStore = eventStore
        
        if let event = event {
            controller.event = event
        }
        
        controller.editViewDelegate = context.coordinator
        return controller
    }
    
    func updateUIViewController(_ uiViewController: EKEventEditViewController, context: Context) {}
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    class Coordinator: NSObject, EKEventEditViewDelegate {
        var parent: EventEditViewController
        
        init(_ parent: EventEditViewController) {
            self.parent = parent
        }
        
        func eventEditViewController(_ controller: EKEventEditViewController, didCompleteWith action: EKEventEditViewAction) {
            parent.presentationMode.wrappedValue.dismiss()
            // Aquí podrías notificar a tu CalendarManager para hacer un fetchEvents()
        }
    }
}

Ahora, en tu vista principal de SwiftUI, puedes invocar esto usando un modificador .sheet:

// ... dentro de tu ContentView
@State private var showingAddEvent = false

// ... en el botón de la barra de navegación:
.toolbar {
    Button(action: { showingAddEvent = true }) {
        Image(systemName: "plus")
    }
}
.sheet(isPresented: $showingAddEvent, onDismiss: {
    calendarManager.fetchEvents()
}) {
    EventEditViewController(eventStore: calendarManager.store, event: nil)
}

6. Sincronización Multiplataforma: iOS, macOS y watchOS

La belleza de la programación Swift moderna es su capacidad de portabilidad. El código que hemos escrito para integrar EventKit en SwiftUI funcionará casi sin modificaciones en todo el ecosistema de Apple. Sin embargo, hay consideraciones clave que todo iOS Developer debe conocer al abrir su proyecto de Xcode a otras plataformas:

Consideraciones para macOS

En macOS, el uso de EventKit requiere habilitar un “Capability” en Xcode (App Sandbox). Debes marcar las casillas correspondientes a Calendar y Reminders en la sección “App Sandbox” -> “App Data” dentro de las configuraciones del target. Sin esto, tu aplicación en Mac fallará silenciosamente o lanzará excepciones de Sandbox, independientemente de lo que hayas puesto en el Info.plist.

Consideraciones para watchOS

En el Apple Watch, el framework EventKit está disponible, pero la interfaz nativa EKEventEditViewController (que pertenece a EventKitUI) no lo está.

Para resolver esto en un proyecto universal, debes utilizar compilación condicional (#if) en Swift:

// En tu archivo de vista...
.toolbar {
    Button(action: { showingAddEvent = true }) {
        Image(systemName: "plus")
    }
}
#if os(iOS)
.sheet(isPresented: $showingAddEvent) {
    EventEditViewController(eventStore: calendarManager.store, event: nil)
}
#elseif os(watchOS)
.sheet(isPresented: $showingAddEvent) {
    // Para watchOS, debes crear tu propio formulario en SwiftUI
    CustomWatchEventForm(manager: calendarManager)
}
#endif

Además, recuerda que watchOS está diseñado para interacciones rápidas. Realizar peticiones (fetch) de rangos de fechas muy extensos en el reloj drenará la batería. Limita tus búsquedas mediante el predicateForEvents a un par de días hacia el futuro.


7. Escuchando Cambios Externos (Observación Avanzada)

Un error común que comete un iOS Developer principiante es asumir que el usuario solo modificará su calendario a través de su app. ¿Qué pasa si el usuario abre la app nativa de Apple, borra un evento y vuelve a tu app? Tu interfaz de SwiftUI mostrará datos desactualizados.

Para solucionar esto, EventKit emite una notificación cada vez que la base de datos cambia. Debemos suscribirnos a ella en nuestro CalendarManager:

import Combine

class CalendarManager: ObservableObject {
    // ... propiedades anteriores ...
    private var cancellables = Set<AnyCancellable>()
    
    init() {
        // Escuchamos los cambios en la base de datos de EventKit
        NotificationCenter.default.publisher(for: .EKEventStoreChanged, object: store)
            // Aseguramos que recibimos la actualización en el hilo principal
            .receive(on: RunLoop.main) 
            .sink { [weak self] _ in
                // Si la base de datos externa cambia, recargamos nuestra lista
                self?.fetchEvents()
            }
            .store(in: &cancellables)
    }
    // ... resto del código ...
}

Con este sencillo bloque, tu aplicación se mantendrá mágicamente sincronizada con iCloud y otras aplicaciones de calendario en tiempo real.


Conclusión

Integrar EventKit en SwiftUI es un paso fundamental para crear aplicaciones de alto valor en el ecosistema Apple. A lo largo de esta guía, hemos explorado cómo configurar Xcode, solicitar permisos de privacidad estrictos y escribir un código limpio y reactivo en Swift.

Al dominar el EKEventStore y utilizar puentes como UIViewControllerRepresentable, puedes combinar la solidez de las APIs subyacentes de Apple con la increíble velocidad de desarrollo de SwiftUI. Recuerda siempre probar tus aplicaciones en dispositivos físicos, ya que el simulador a menudo no refleja con precisión el comportamiento real de los calendarios sincronizados con iCloud.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

TabView Bottom Accessory en SwiftUI

Next Article

CloudKit en SwiftUI

Related Posts