Last active
March 26, 2023 09:04
-
-
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`
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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