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 Pages, Keynote, TextEdit 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.
- 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.
- 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.
- 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
WindowGroupen tu archivoApp. 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”.
- Abre Xcode y selecciona Create New Project.
- 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
Entitlementsiniciales.
- Nota: Podrías seleccionar “App” normal y configurarlo manualmente, pero la plantilla “Document App” nos ahorra configurar los
- Nombra el proyecto “SwiftWriter”.
- 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.
- Ve al Target de tu app -> Pestaña Info.
- Busca la sección Exported Type Identifiers.
- 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) opublic.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í?
- 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. - 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
.toolbarpara 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 usasUndoManager, obtienesCmd+Zgratis. 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.
- Sincronización: Usarás
WatchConnectivitypara enviar el contenido del archivo desde el iPhone al Watch. - Vista: En el Watch, crearás una
WindowGroupnormal que muestre los datos recibidos. - 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 elIdentifieren “Exported Type Identifiers” sea idéntico al que definiste en tu extensión deUTType. Asegúrate de queConforms Toincluyapublic.contentopublic.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
contentes unValue Type(Struct) y que estás modificando el Binding real ($document.content.text) y no una copia local@Statedesconectada.
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










