Programación en Swift y SwiftUI para iOS Developers

Cómo crear una aplicación basada en documentos en SwiftUI

En el ecosistema de desarrollo de Apple, existen dos tipos principales de arquitecturas de datos: las aplicaciones basadas en bases de datos (como una app de To-Do o una red social) y las aplicaciones basadas en documentos.

Piensa en PagesKeynoteTextEdit o Sketch. No gestionan una base de datos central oculta al usuario; trabajan con archivos. El usuario crea un archivo, le pone nombre, elige dónde guardarlo (iCloud Drive, localmente, en un servidor) y lo comparte.

Durante años, implementar UIDocument (en iOS) o NSDocument (en macOS) era una tarea titánica llena de código repetitivo y manejo de errores complejo. Con la llegada de SwiftUI, Apple nos regaló el protocolo FileDocument y la estructura DocumentGroup, simplificando este proceso en un 90%.

En este tutorial aprenderás a construir una aplicación multiplataforma que gestiona sus propios archivos, define tipos de datos personalizados y se integra nativamente con el explorador de archivos del sistema.


Parte 1: Teoría – ¿Qué es una Document-Based App?

Antes de escribir código, debemos entender el cambio de mentalidad.

  1. El Sistema de Archivos es el Rey: Tu app no es dueña de los datos; el usuario lo es. El usuario decide si el archivo vive en “Mis Documentos” o en una carpeta compartida de Dropbox.
  2. Sandboxing: En iOS, tu app no puede leer cualquier archivo. Solo puede leer los archivos que el usuario abre explícitamente o crea dentro de la app.
  3. Serialización: Tu trabajo principal es convertir tus objetos de Swift (Structs/Classes) en datos binarios (Data) que puedan escribirse en disco, y viceversa.

Los Tres Pilares de SwiftUI

Para lograr esto, SwiftUI nos da tres herramientas:

  • UTType (Uniform Type Identifier): Es el DNI de tu archivo. Define quién es (ej: com.miempresa.notasincredibles) y qué extensión usa (ej: .supernote).
  • FileDocument Protocol: El contrato que enseña a tu estructura de datos cómo empaquetarse (encode) y desempaquetarse (decode).
  • DocumentGroup: El reemplazo de WindowGroup en tu archivo App. Se encarga automáticamente de abrir la ventana de selección de archivos en iOS o la gestión de ventanas/pestañas en macOS.

Parte 2: Configuración del Proyecto en Xcode

Vamos a crear un editor de texto enriquecido ficticio llamado “SwiftWriter”.

  1. Abre Xcode y selecciona Create New Project.
  2. Ve a la pestaña Multiplatform y selecciona Document App.
    • Nota: Podrías seleccionar “App” normal y configurarlo manualmente, pero la plantilla “Document App” nos ahorra configurar los Entitlements iniciales.
  3. Nombra el proyecto “SwiftWriter”.
  4. Asegúrate de que la interfaz sea SwiftUI y el lenguaje Swift.

Al crear el proyecto, verás que Xcode ya ha generado un archivo ContentView, un SwiftWriterApp y un modelo básico. Vamos a borrar el modelo básico para construirlo desde cero y entenderlo.


Parte 3: Definiendo la Identidad del Archivo (UTType)

El sistema operativo necesita saber que un archivo .swriter pertenece a tu aplicación. Esto se hace mediante los Uniform Type Identifiers.

Paso 1: Importar el Framework

Crea un nuevo archivo Swift llamado UTType+Extension.swift.

import UniformTypeIdentifiers

extension UTType {
    // Definimos nuestro tipo personalizado
    static var swiftWriterDoc: UTType {
        // El identificador debe ser único (reverse domain notation)
        UTType(importedAs: "com.tuempresa.swiftwriter.doc")
    }
}

Paso 2: Configurar el Info.plist

Este es el paso donde la mayoría falla. No basta con el código; debes registrar el tipo en el proyecto.

  1. Ve al Target de tu app -> Pestaña Info.
  2. Busca la sección Exported Type Identifiers.
  3. Añade un nuevo ítem:
    • Description: SwiftWriter Document
    • Identifier: com.tuempresa.swiftwriter.doc (Debe coincidir con el código de arriba).
    • Conforms To: public.json (Si vamos a guardar como JSON) o public.data. Para este tutorial usaremos JSON.
    • Extensions: swriter

Parte 4: Creando el Modelo de Datos (FileDocument)

Ahora crearemos la estructura que contendrá los datos. Usaremos FileDocument para structs (tipos por valor), lo cual es ideal para SwiftUI y seguridad de hilos.

Crea un archivo llamado SwiftWriterDocument.swift.

import SwiftUI
import UniformTypeIdentifiers

// 1. El modelo de datos debe ser Codable para facilitar la serialización
struct WriterContent: Codable {
    var text: String = ""
    var creationDate: Date = Date()
    var fontSize: Double = 12.0
}

// 2. Conformamos a FileDocument
struct SwiftWriterDocument: FileDocument {
    
    // Propiedad que almacena nuestros datos
    var content: WriterContent
    
    // A. Definimos qué tipos de archivos puede leer este documento
    static var readableContentTypes: [UTType] {
        return [.swiftWriterDoc]
    }
    
    // B. Inicializador para un documento nuevo (vacío)
    init() {
        self.content = WriterContent()
    }
    
    // C. Inicializador para LECTURA (Abrir archivo)
    init(configuration: ReadConfiguration) throws {
        // Intentamos obtener los datos del archivo
        guard let data = configuration.file.regularFileContents else {
            throw CocoaError(.fileReadCorruptFile)
        }
        
        // Decodificamos el JSON a nuestro Struct
        self.content = try JSONDecoder().decode(WriterContent.self, from: data)
    }
    
    // D. Método para ESCRITURA (Guardar archivo)
    func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
        // Codificamos nuestro Struct a JSON
        let data = try JSONEncoder().encode(content)
        
        // Devolvemos un FileWrapper (la cajita que contiene los bytes)
        return FileWrapper(regularFileWithContents: data)
    }
}

Análisis del código:

  • ReadConfiguration: SwiftUI nos da acceso al archivo en disco. Leemos los bytes y usamos JSONDecoder.
  • WriteConfiguration: SwiftUI nos pide un FileWrapper. Nosotros convertimos nuestro struct a JSON y lo envolvemos.
  • Ventaja: SwiftUI maneja automáticamente el guardado automático, el control de versiones (en macOS) y el guardado en la nube (iCloud). Tú solo te preocupas del JSON.

Parte 5: La Interfaz de Usuario (Binding)

Aquí es donde conectamos el documento con la pantalla. La clave es que la vista no posee el documento, sino que recibe un Binding a él.

Abre ContentView.swift:

import SwiftUI

struct ContentView: View {
    // Recibimos el documento como un Binding.
    // Esto permite que al editar, los cambios fluyan hacia arriba hasta el DocumentGroup
    @Binding var document: SwiftWriterDocument

    var body: some View {
        VStack {
            HStack {
                Text("Tamaño fuente: \(Int(document.content.fontSize))")
                Slider(value: $document.content.fontSize, in: 8...36)
            }
            .padding()
            
            // TextEditor se enlaza directamente al contenido del documento
            TextEditor(text: $document.content.text)
                .font(.system(size: document.content.fontSize))
                .padding()
                .background(Color.gray.opacity(0.1))
                .cornerRadius(8)
        }
        .padding()
        #if os(iOS)
        .navigationTitle("Editor")
        .navigationBarTitleDisplayMode(.inline)
        #endif
    }
}

Parte 6: El Punto de Entrada (DocumentGroup)

Finalmente, conectamos todo en el archivo principal de la App (SwiftWriterApp.swift).

import SwiftUI

@main
struct SwiftWriterApp: App {
    var body: some Scene {
        // En lugar de WindowGroup, usamos DocumentGroup
        DocumentGroup(newDocument: SwiftWriterDocument()) { file in
            // file.document es un Binding<SwiftWriterDocument>
            // file.$document nos da el Binding que necesita ContentView
            ContentView(document: file.$document)
        }
    }
}

¿Qué ocurre aquí?

  1. En iOS: Al lanzar la app, verás automáticamente el Gestor de Archivos (similar a la app “Archivos”). Podrás navegar por iCloud Drive, crear carpetas y tocar el botón “+” para crear un nuevo documento. Al tocar un archivo, se abrirá ContentView.
  2. En macOS: La app se lanza vacía (o con un diálogo de “Abrir”). Tienes menú nativo: Archivo -> Nuevo, Archivo -> Guardar, etc. Soporta múltiples ventanas y pestañas automáticamente.

Parte 7: Diferencias por Plataforma (iOS vs macOS vs watchOS)

Desarrollar una Document App es un sueño multiplataforma, pero hay matices.

iOS y iPadOS

El DocumentGroup en iOS envuelve tu vista en una estructura de navegación. Automáticamente proporciona el botón de “Cerrar” (o “Atrás”) que guarda y cierra el documento.

  • Reto: Gestión del teclado y toolbars. Debes asegurarte de usar .toolbar para añadir acciones específicas del documento.

macOS

Aquí FileDocument brilla. Tienes soporte gratuito para:

  • Undo/Redo: Si cambias tu struct a ReferenceFileDocument (clases en lugar de structs) y usas UndoManager, obtienes Cmd+Z gratis. Con structs (FileDocument) es un poco más manual, pero el sistema de guardado versionado funciona.
  • Represented Filename: El icono del archivo en la barra de título de la ventana (proxy icon) funciona nativamente. Puedes arrastrar ese icono a un correo para adjuntar el archivo.

watchOS: La Excepción

Aquí debemos ser honestos. watchOS no soporta DocumentGroup de la misma manera que iOS o macOS. No existe un “Finder” o “Archivos” en el Apple Watch donde el usuario navegue carpetas y abra un JSON.

¿Cómo se maneja en watchOS? Si quieres que tu app funcione en el reloj, no puedes usar la plantilla “Document App” tal cual para el target del reloj.

  1. Sincronización: Usarás WatchConnectivity para enviar el contenido del archivo desde el iPhone al Watch.
  2. Vista: En el Watch, crearás una WindowGroup normal que muestre los datos recibidos.
  3. Edición: El Watch suele actuar como un visualizador o un control remoto. Si permites editar (dictado de voz, por ejemplo), deberás enviar los datos de vuelta al iPhone para que este actualice el archivo físico.

Nota técnica: Aunque watchOS tiene un sistema de archivos local para cachés, no está expuesto al usuario como “Documentos”.


Parte 8: Temas Avanzados – FileWrapper y Directorios

Hasta ahora hemos guardado un solo archivo JSON (un regularFile). Pero, ¿qué pasa si tu documento es complejo? Imagina que tu documento .swriter debe contener el texto Y varias imágenes adjuntas.

Para esto, el FileWrapper puede ser un Directorio.

// Ejemplo conceptual para paquetes (Bundles)
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
    let rootDirectory = FileWrapper(directoryWithFileWrappers: [:])
    
    // 1. Añadimos el JSON de datos
    let jsonData = try JSONEncoder().encode(content)
    let jsonWrapper = FileWrapper(regularFileWithContents: jsonData)
    jsonWrapper.preferredFilename = "data.json"
    rootDirectory.addFileWrapper(jsonWrapper)
    
    // 2. Añadimos una imagen (si existiera en nuestro modelo)
    if let imageData = content.imageData {
        let imageWrapper = FileWrapper(regularFileWithContents: imageData)
        imageWrapper.preferredFilename = "image.png"
        rootDirectory.addFileWrapper(imageWrapper)
    }
    
    return rootDirectory
}

En el Info.plist, deberías marcar tu tipo exportado con la propiedad LSTypeIsPackage (Is a Package) en YES. Para el usuario, en Finder/Files, parecerá un solo archivo, pero en realidad es una carpeta (como los archivos .pages o .key).


Parte 9: Solución de Problemas Comunes

1. “El archivo aparece gris y no puedo abrirlo”

Este es el error número 1. Significa que el sistema operativo no asocia la extensión del archivo con tu aplicación.

  • Solución: Revisa minuciosamente el Info.plist. Asegúrate de que el Identifier en “Exported Type Identifiers” sea idéntico al que definiste en tu extensión de UTType. Asegúrate de que Conforms To incluya public.content o public.data.

2. “Los cambios no se guardan”

SwiftUI guarda automáticamente cuando detecta un cambio en el Binding y la app entra en segundo plano o se cierra el documento.

  • Solución: Verifica que tu modelo content es un Value Type (Struct) y que estás modificando el Binding real ($document.content.text) y no una copia local @State desconectada.

3. Conflicto de Versiones en iCloud

Si editas el archivo en Mac y iPad a la vez, ¿qué pasa? FileDocument intenta resolverlo, pero para un control robusto, necesitarás implementar lógica personalizada de resolución de conflictos examinando configuration.file.fileURL y las versiones del archivo, aunque esto es material para un tutorial avanzado.


Conclusión

Crear una Document Based App en SwiftUI es una de las habilidades más potentes que puedes adquirir como desarrollador de Apple. Te permite crear herramientas profesionales que se sienten nativas, respetan la privacidad del usuario (sandbox) y se integran perfectamente con iCloud Drive.

Hemos pasado de tener que escribir cientos de líneas de código delegando métodos en UIKit a una estructura declarativa elegante donde defines tus datos, tu tipo de archivo y tu vista.

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

Picker en SwiftUI

Next Article

Notificación Local vs In App vs Push en iOS y SwiftUI

Related Posts