Skip to content

Instantly share code, notes, and snippets.

@pd95
Last active March 26, 2023 09:04
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pd95/6c561bcfbb5edb6e1a972334ae668679 to your computer and use it in GitHub Desktop.
Save pd95/6c561bcfbb5edb6e1a972334ae668679 to your computer and use it in GitHub Desktop.
A `DocumentGroup` demo app (based on the Xcode iOS app template with Core Data enabled) illustrating wrapping `UIManagedDocument` using SwiftUI `ReferenceFileDocument`
/*
How to use the content of this gist:
- Create a new iOS app using the "App" template,
set the product name to "Demo", use "SwiftUI" for interface and enable Core Data.
- go to the apps target (=click the Project "Demo" and select "Demo" as under "Targets")
- select the "Info" tab
- Add an entry for "Document Types":
- Name "Example Document"
- Types "com.example.document"
- Add an entry to "Additional document type properties" (by clicking on the left side of the table,
below the column header):
Key = "LSTypeIsPackage", Type = "String", Value = "1"
- Add an entry for "Exported Type Identifiers"
- Description "Example Document"
- Extensions "example"
- Identifier "com.example.document"
- Conforms To "com.apple.package"
- Remove the content of the "DemoApp.swift", "ContentView.swift" and "Persistence.swift" file
- Paste the content of this gist into "DemoApp.swift"
- Run the app in the simulator and play with it
*/
import UniformTypeIdentifiers
import CoreData
import SwiftUI
@main
struct DemoApp: App {
var body: some Scene {
DocumentGroup(newDocument: { DemoDocument() }) { file in
DocumentWrapperView()
.onAppear {
// the UIManagedDocument needs to know about the file URL...
// so we pass it as soon as we have it and appear on screen
if let url = file.fileURL {
file.document.open(fileURL: url)
}
}
}
}
}
struct DocumentWrapperView: View {
@EnvironmentObject var document: DemoDocument
var body: some View {
if let managedDocument = document.managedDocument {
ContentView()
.environment(\.managedObjectContext, managedDocument.managedObjectContext)
} else {
ProgressView("Loading")
}
}
}
extension UTType {
static var exampleDocument: UTType {
UTType(exportedAs: "com.example.document", conformingTo: .package)
}
}
class DemoDocument: ReferenceFileDocument {
typealias Snapshot = Date
@Published var managedDocument: MyManagedDocument?
static var readableContentTypes: [UTType] = [.exampleDocument]
init() {
// This initializer is used when a new document is created within the DocumentGroup
// We don't do anything here: a UIManagedDocument needs an URL to be instantiated!
}
deinit {
// Close the managed document if necessary
if let managedDocument = managedDocument, managedDocument.documentState.contains(.closed) == false {
DispatchQueue.global().async {
managedDocument.close()
}
}
}
required init(configuration: ReadConfiguration) throws {
// This function is called when a document is opened. We receive the `FileWrapper` on `configuration.file`
// but as we do not have any information about the real location (=no URL yet), we cannot initialize our
// document.
// We simply check here whether we do have a directory as the top FileWrapper
guard configuration.file.isDirectory else {
throw CocoaError(.fileReadUnknown)
}
}
func snapshot(contentType: UTType) throws -> Date {
// This function is only called the first time when we create a new document.
// We return a bogus snapshot as we never use it anywhere.
return Date()
}
func fileWrapper(snapshot: Date, configuration: WriteConfiguration) throws -> FileWrapper {
// This function is called when a new document must be stored. It receives the snapshot previously captured
// We do not write anything here. We simply return the existing or an empty package structure
return configuration.existingFile ?? emptyManagedDocument
}
// An empty document is represented by a directory containing "StoreContent" directory containing the managed document
private var emptyManagedDocument: FileWrapper {
FileWrapper(directoryWithFileWrappers: [
"StoreContent" : FileWrapper(directoryWithFileWrappers: [
MyManagedDocument.persistentStoreName : FileWrapper(regularFileWithContents: Data())
])
])
}
func open(fileURL url: URL) {
guard managedDocument == nil else { return }
// This function can finally create and initialize our UIManagedDocument subclass.
let document = MyManagedDocument(fileURL: url)
DispatchQueue.global().async {
document.open { success in
print("🟡 Managed document has been opened successfully:", success, "state:", document.documentState)
DispatchQueue.main.async {
self.managedDocument = document
}
}
}
}
}
class MyManagedDocument: UIManagedDocument {
// We fetch the ManagedObjectModel only once and cache it statically
private static let managedObjectModel: NSManagedObjectModel = {
guard let url = Bundle(for: MyManagedDocument.self).url(forResource: "Demo", withExtension: "momd") else {
fatalError("Demo.momd not found in bundle")
}
guard let mom = NSManagedObjectModel(contentsOf: url) else {
fatalError("Demo.momd not load from bundle")
}
return mom
}()
// Make sure to use always the same instance of the model, otherwise we get crashes when opening another document
override var managedObjectModel: NSManagedObjectModel {
Self.managedObjectModel
}
}
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)],
animation: .default)
private var items: FetchedResults<Item>
var body: some View {
List {
ForEach(items) { item in
NavigationLink {
Text("Item at \(item.timestamp!, formatter: itemFormatter)")
} label: {
Text(item.timestamp!, formatter: itemFormatter)
}
}
.onDelete(perform: deleteItems)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton()
}
ToolbarItem {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
}
private func addItem() {
withAnimation {
let newItem = Item(context: viewContext)
newItem.timestamp = Date()
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach(viewContext.delete)
do {
try viewContext.save()
} catch {
// Replace this implementation with code to handle the error appropriately.
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
let nsError = error as NSError
fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
}
}
}
}
private let itemFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter
}()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment