Programación en Swift y SwiftUI para iOS Developers

Contacts en SwiftUI

Como iOS Developer, en algún momento de tu carrera te enfrentarás a la necesidad de interactuar con la agenda de tus usuarios. Ya sea para construir una red social que busque amigos, una aplicación de mensajería o una herramienta de productividad, el acceso a la libreta de direcciones es una característica fundamental.

En el mundo de la programación Swift, Apple nos proporciona un framework nativo, potente y seguro para esta tarea: el framework Contacts. Aunque fue diseñado en la época de Objective-C y UIKit, su integración con las interfaces declarativas modernas es totalmente viable.

En este extenso tutorial, vamos a sumergirnos en la teoría y la práctica sobre cómo implementar Contacts en SwiftUI. Aprenderás cómo solicitar permisos respetando la privacidad del usuario, cómo leer datos, cómo crear nuevos contactos y cómo estructurar todo este código en Swift utilizando Xcode para aplicaciones en iOS, macOS y watchOS.


1. ¿Qué es el framework Contacts?

El framework Contacts (que reemplazó al antiguo y obsoleto AddressBook) es la API oficial de Apple para acceder y modificar la base de datos de contactos del usuario de forma centralizada.

Este framework está construido sobre una arquitectura robusta orientada a objetos y diseñada para ser segura en entornos multihilo. En la programación Swift, interactuarás principalmente con tres clases fundamentales:

  • CNContactStore: Es el motor central. Imagínalo como la conexión a la base de datos de la agenda. Se utiliza para solicitar permisos de acceso, buscar contactos (fetch) y guardar cambios.
  • CNContact: Representa un contacto individual. Es una clase inmutable (de solo lectura). Contiene propiedades como el nombre, apellidos, números de teléfono, correos electrónicos e incluso la imagen de perfil.
  • CNMutableContact: Es la versión mutable de CNContact. Utilizarás esta clase cuando necesites crear un contacto nuevo desde cero o cuando vayas a actualizar la información de un contacto existente.

2. Privacidad Primero: Configurando Xcode

Antes de escribir una sola línea de código en Swift, debemos abordar la privacidad. Apple es extremadamente estricto con el acceso a los datos personales. Si tu aplicación intenta leer la agenda sin declarar explícitamente por qué lo necesita, el sistema operativo cerrará la app inmediatamente (crash).

Para evitar esto, debes configurar el archivo Info.plist en Xcode:

  1. Selecciona tu proyecto en el navegador de archivos de Xcode.
  2. Ve a la pestaña Info.
  3. Añade una nueva fila y busca la clave Privacy - Contacts Usage Description (su nombre interno en código es NSContactsUsageDescription).
  4. En el campo de valor (Value), escribe un mensaje claro y honesto explicando por qué necesitas el acceso. Por ejemplo: “Nuestra app necesita acceder a tus contactos para ayudarte a encontrar amigos que ya utilizan la plataforma.”

Este es el mensaje que el usuario verá en el diálogo de alerta del sistema cuando tu aplicación solicite permiso por primera vez.


3. Solicitando Permisos en la Programación Swift Moderna

Con el Info.plist configurado, el siguiente paso es pedirle permiso al usuario. En el entorno de SwiftUI, lo ideal es manejar esto en un modelo de vista (ViewModel) utilizando las capacidades asíncronas de Swift (async/await).

import Foundation
import Contacts

@MainActor
class ContactsViewModel: ObservableObject {
    @Published var contacts: [CNContact] = []
    @Published var permissionGranted: Bool = false
    
    // Instancia central del almacén de contactos
    private let contactStore = CNContactStore()
    
    func requestAccess() async {
        do {
            // Solicitamos acceso de tipo lectura (o escritura)
            let granted = try await contactStore.requestAccess(for: .contacts)
            self.permissionGranted = granted
            
            if granted {
                await fetchContacts()
            } else {
                print("El usuario denegó el acceso a los contactos.")
            }
        } catch {
            print("Error al solicitar acceso: \(error.localizedDescription)")
            self.permissionGranted = false
        }
    }
}

En este fragmento, utilizamos CNContactStore().requestAccess(for: .contacts). Al estar marcado con @MainActor, garantizamos que cualquier actualización a nuestras propiedades @Published se refleje inmediatamente en la interfaz de SwiftUI sin causar problemas de hilos.


4. Leyendo Datos: El CNContactFetchRequest

Una vez que el usuario otorga el permiso, podemos proceder a leer la agenda. Sin embargo, hay una regla de oro para cualquier iOS Developer que trabaje con Contacts: Nunca pidas más datos de los que necesitas.

Los objetos CNContact pueden ser enormes (con imágenes en alta resolución, múltiples direcciones, etc.). Leer todos los contactos con todos sus campos saturará la memoria y hará que tu app sea lenta.

Para resolver esto, Apple diseñó el CNContactFetchRequest, donde especificas exactamente qué “llaves” (keys) quieres extraer.

Añadamos la función fetchContacts() a nuestro ContactsViewModel:

    func fetchContacts() async {
        // 1. Definimos qué propiedades queremos recuperar
        let keysToFetch: [CNKeyDescriptor] = [
            CNContactGivenNameKey as CNKeyDescriptor,
            CNContactFamilyNameKey as CNKeyDescriptor,
            CNContactPhoneNumbersKey as CNKeyDescriptor
        ]
        
        // 2. Creamos la solicitud
        let request = CNContactFetchRequest(keysToFetch: keysToFetch)
        
        // 3. Opcional: Ordenar los resultados por el nombre
        request.sortOrder = .givenName
        
        var fetchedContacts: [CNContact] = []
        
        // 4. Ejecutamos la solicitud de forma segura en un hilo secundario
        Task.detached {
            do {
                try self.contactStore.enumerateContacts(with: request) { (contact, stopPointer) in
                    fetchedContacts.append(contact)
                }
                
                // Volvemos al hilo principal para actualizar la UI
                await MainActor.run {
                    self.contacts = fetchedContacts
                }
                
            } catch {
                print("Error al recuperar los contactos: \(error.localizedDescription)")
            }
        }
    }

Al utilizar enumerateContacts(with:), el framework nos devuelve los contactos uno por uno. Agrupamos estos resultados en un array y luego actualizamos nuestra propiedad @Published.


5. Interfaz de Usuario: Contacts en SwiftUI

Ahora que tenemos nuestro ViewModel con la lógica de negocio y la programación Swift estructurada, vamos a crear la interfaz visual en SwiftUI.

Construiremos una lista sencilla que primero pedirá permisos mediante un botón y, si se conceden, mostrará los nombres y números de teléfono.

import SwiftUI
import Contacts

struct ContactsListView: View {
    @StateObject private var viewModel = ContactsViewModel()
    
    var body: some View {
        NavigationView {
            Group {
                if viewModel.permissionGranted {
                    List(viewModel.contacts, id: \.identifier) { contact in
                        VStack(alignment: .leading) {
                            // Formatear el nombre completo
                            Text("\(contact.givenName) \(contact.familyName)")
                                .font(.headline)
                            
                            // Obtener el primer número de teléfono (si existe)
                            if let firstPhone = contact.phoneNumbers.first {
                                Text(firstPhone.value.stringValue)
                                    .font(.subheadline)
                                    .foregroundColor(.gray)
                            } else {
                                Text("Sin número")
                                    .font(.subheadline)
                                    .foregroundColor(.red)
                            }
                        }
                    }
                } else {
                    VStack(spacing: 20) {
                        Image(systemName: "person.crop.circle.badge.questionmark")
                            .font(.system(size: 60))
                            .foregroundColor(.blue)
                        
                        Text("Necesitamos acceso a tus contactos")
                            .font(.title2)
                            .multilineTextAlignment(.center)
                        
                        Button("Conceder Permiso") {
                            Task {
                                await viewModel.requestAccess()
                            }
                        }
                        .buttonStyle(.borderedProminent)
                    }
                    .padding()
                }
            }
            .navigationTitle("Mis Contactos")
        }
    }
}

Esta estructura demuestra el poder de SwiftUI. La vista reacciona automáticamente a los cambios de estado en permissionGranted y contacts. Además, utilizamos la propiedad identifier nativa de CNContact como identificador único para el componente List.


6. Creando y Guardando Nuevos Contactos

Un iOS Developer avanzado no solo lee datos, sino que interactúa bidireccionalmente. ¿Qué pasa si tu aplicación genera un perfil y quieres añadirlo directamente a la agenda nativa del iPhone?

Para ello, utilizamos CNMutableContact y un objeto llamado CNSaveRequest. Vamos a añadir una función a nuestro ViewModel para crear un contacto de prueba.

    func createNewContact(firstName: String, lastName: String, phoneNumber: String) {
        // 1. Creamos una instancia mutable
        let newContact = CNMutableContact()
        
        // 2. Asignamos los valores básicos
        newContact.givenName = firstName
        newContact.familyName = lastName
        
        // 3. Formateamos el número de teléfono
        // CNPhoneNumber requiere un formato específico, y se envuelve en un CNLabeledValue
        let phoneValue = CNPhoneNumber(stringValue: phoneNumber)
        let labeledPhone = CNLabeledValue(label: CNLabelPhoneNumberMobile, value: phoneValue)
        
        newContact.phoneNumbers = [labeledPhone]
        
        // 4. Creamos la solicitud de guardado
        let saveRequest = CNSaveRequest()
        saveRequest.add(newContact, toContainerWithIdentifier: nil)
        
        // 5. Ejecutamos la solicitud a través del store
        do {
            try contactStore.execute(saveRequest)
            print("Contacto guardado exitosamente.")
            
            // Volvemos a leer la lista para que la UI se actualice con el nuevo contacto
            Task {
                await fetchContacts()
            }
            
        } catch {
            print("Error al guardar el contacto: \(error.localizedDescription)")
        }
    }

En Swift, datos complejos como teléfonos, correos o direcciones físicas no se asignan como simples Strings. Se envuelven en CNLabeledValue, lo que permite etiquetarlos como “Móvil”, “Casa”, “Trabajo”, etc.


7. Consideraciones Multiplataforma (iOS, macOS, watchOS)

Una de las maravillas de integrar Contacts en SwiftUI usando Xcode es que gran parte de la lógica que acabamos de escribir es multiplataforma. Sin embargo, existen sutilezas:

  • iOS / iPadOS: La implementación es directa. Asegúrate de tener la descripción de privacidad en el Info.plist y funcionará a la perfección.
  • macOS: Si estás desarrollando para Mac y tu app usa “App Sandbox” (lo cual es obligatorio para la Mac App Store), debes ir a la pestaña “Signing & Capabilities” en Xcode. Allí, bajo “App Sandbox”, debes marcar explícitamente la casilla “Contacts”. Si omites este paso, el framework devolverá errores de lectura silenciosos, aunque tengas el Info.plist configurado.
  • watchOS: La serie Apple Watch tiene acceso limitado. Aunque CNContactStore está disponible, dependes enormemente de que el reloj esté sincronizado con el iPhone. Operaciones pesadas de escritura o lecturas masivas pueden agotar la batería del reloj. Usa esta API en watchOS solo para consultas puntuales.

8. Rendimiento y Mejores Prácticas

Para cerrar esta guía, aquí tienes los consejos más valiosos para dominar el framework Contacts:

  • Paginación no nativa: A diferencia de las bases de datos SQL, CNContactStore no soporta paginación nativa (ej. “dame los primeros 20 contactos, luego los siguientes 20”). El método enumerateContacts trae todo en bloque. Si tienes un usuario con 5000 contactos, esto puede ser lento. Siempre ejecuta el fetch en un bloque concurrente (como hicimos con Task.detached) para no congelar la UI de SwiftUI.
  • Gestión de Excepciones: Si intentas acceder a la propiedad contact.emailAddresses en tu vista de SwiftUI, pero olvidaste incluir CNContactEmailAddressesKey en tu array de keysToFetch, tu aplicación sufrirá un crash inmediato. Siempre verifica las llaves antes de consumir las propiedades utilizando contact.isKeyAvailable().
  • Uso de Predicados: Si solo buscas un contacto específico (por ejemplo, buscar a alguien por nombre), no leas toda la agenda. Utiliza predicados: CNContact.predicateForContacts(matchingName: "Juan"). Pasa este predicado a tu CNContactFetchRequest para que el sistema operativo haga el filtrado a bajo nivel de forma ultra rápida.

Conclusión

Integrar el framework Contacts en SwiftUI demuestra perfectamente cómo la programación Swift puede tender un puente entre las arquitecturas sólidas y antiguas de Apple y el diseño de interfaces modernas, reactivas y multiplataforma que ofrece Xcode hoy en día.

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

Cómo usar SF Symbols en Xcode

Next Article

Cómo usar SecureField en SwiftUI

Related Posts