How to create a document-based app with SwiftData
How to create a document-based app with SwiftData 관련
Updated for Xcode 15
SwiftData and SwiftUI combine to make document-based apps work with almost no extra work from us. Behind the scenes, SwiftData silently creates separate storage for each new document along with any associated files used in your data models, and SwiftUI takes care of presenting the document browser then opening files the user selects.
Important
Unlike regular SwiftUI document-based apps, SwiftData-backed apps don't use the concept of a single document object. Instead, you use @Query
and similar to read collections of data, so
unless you write some code to load a document singleton manually you'll need to use collection-based data.
A simple example of this might be an app to track feedback reports for iOS apps you've built in the past. Each app would be its own document, and inside there would be a collection of bugs, suggestions, ideas, and more for that app.
To create this in SwiftData we'd start by defining a model such as this:
@Model
class FeedbackReport {
var title: String
var content: String
var priority: Int
init(title: String, content: String, priority: Int) {
self.title = title
self.content = content
self.priority = priority
}
}
Next you'd build one SwiftUI view to edit your data, and another to show your data.
To follow our FeedbackReport
example, an editing view might look like this:
struct EditReportView: View {
@Bindable var report: FeedbackReport
var body: some View {
Form {
TextField("Issue title", text: $report.title)
TextField("Issue description", text: $report.content, axis: .vertical)
Picker("Priority", selection: $report.priority) {
Text("Low").tag(3)
Text("Medium").tag(2)
Text("High").tag(1)
}
}
.navigationTitle("Edit Report")
}
}
And a view to list all reports might look like this:
struct ContentView: View {
@Query(sort: \FeedbackReport.priority, order: .reverse) var reports: [FeedbackReport]
@Environment(\.modelContext) var modelContext
@State private var navigationPath = [FeedbackReport]()
var body: some View {
NavigationStack(path: $navigationPath) {
List(reports) { report in
NavigationLink(value: report) {
VStack(alignment: .leading) {
Text(report.title)
.font(.headline)
Text(report.content)
.lineLimit(2)
}
}
}
.navigationTitle("Feedback Assistant")
.navigationDestination(for: FeedbackReport.self, destination: EditReportView.init)
.toolbar {
Button("New report", systemImage: "plus", action: createNew)
}
}
}
func createNew() {
let report = FeedbackReport(title: "", content: "", priority: 1)
modelContext.insert(report)
navigationPath = [report]
}
}
So far this is a regular SwiftData app, and if you only wanted to have a single store for all your feedback reports then you'd be done.
However, with a few small changes we can make this into a document-based app.
First, you need to go to the Info tab for your target and add an entry under Exported Type Identifiers. For this project I would use the following values:
- Description: Feedback report
- Identifier:
com.yoursite.feedbackreport
- Conforms To:
com.apple.package
- Extension: feedback
Important
All of those are customizable to fit your app's needs, apart from the "Conforms To" value, which must always be "com.apple.package".
Second, you need to add an entry to the list of custom target properties. This means right-clicking on an existing row and selecting Add Row, then using that new row to see Supports Document Browser to YES.
Third, you need to create a custom UTType
object using the same identifier you just declared for your target. Something like this is enough:
import UniformTypeIdentifiers
extension UTType {
static var feedbackReport = UTType(exportedAs: "com.yoursite.feedbackreport")
}
And finally, you need to edit your App
struct to replace this line of code:
WindowGroup {
With a DocumentGroup
like this one:
DocumentGroup(editing: FeedbackReport.self, contentType: .feedbackReport) {
Important
You must not use the modelContainer()
modifier when working with a SwiftData document-based app.
With those four changes done the app is fully upgraded to work with documents. Next time you run it you'll see the app launches straight to the standard document browser interface, where you can press [+]
to create a new document, then create reports inside there.
As I said earlier, SwiftData silently creates separate storage for each new document along with any associated files used in your data models, meaning that if you use @Attribute(.externalStorage)
on any of your model properties those external files will be stored in your document. This is why it's so important to conform to "com.apple.package" – your "document" is really a directory of data containing the underlying SQLite database and all its external files.