I have a Core Data app on the App Store that places its persistent store in the default location within the Application Support directory. At some point, I add widgets to the app while utilizing SwiftData. I create a widget extension and the app group to link both targets. I migrate my persistent store from the default location to the app group container directory. Now, the SwiftData code pointing to the shared container in the widget extension can query data. Here's the sample project for illustration up to this point.
Then, I decided to get rid of the Core Data code entirely. Move everything in the main target to SwiftData. I need to keep my persistence controller object, however, because I can never assume that the entirety of my user base has migrated. I do need to ditch the Core Data model file. Otherwise, my new SwiftData models would conflict with the auto-generated ones from that file. But once I do that, my migration code breaks. I can't set up a persistent container without the model. And I need the container for migration. SwiftData has no native interface to migrate the store to a new location. There is a line in the documentation stating that the framework copies the store to an app group container automatically. But I wasn't able to verify that claim.
Post
Replies
Boosts
Views
Activity
While reading the developer documentation article Adopting SwiftData for a Core Data App, one particular line piqued my interest.
For apps that evolve from a version that doesn’t have any app group container to a version that has one, SwiftData copies the existing store to the app group container.
Given how troublesome it has been to migrate the Core Data persistent store to an app group container, I decided to try this out myself. I created an Xcode project using the default Core Data template. I then added a few Item objects with timestamps. There, I had what we would consider a regular Core Data app.
I then created a widget extension for this app since this is one of the most common uses for adopting an app group in an Xcode project. After that, I linked the main target with the widget extension using an app group. In the widget extension, I tried to fetch the Item objects. I utilized the SwiftData code in the sample project associated with the article above.
struct Provider: TimelineProvider {
private let modelContainer: ModelContainer
init() {
let appGroupContainerID = "group.com.genebogdanovich.CoreDataSwiftDataAppGroup"
guard let appGroupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupContainerID) else {
fatalError("Shared file container could not be created.")
}
let url = appGroupContainer.appendingPathComponent("CoreDataSwiftDataAppGroup.sqlite")
print("\(url)")
do {
modelContainer = try ModelContainer(for: Item.self, configurations: ModelConfiguration(url: url))
} catch {
fatalError("Failed to create the model container: \(error)")
}
}
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
Task { @MainActor in
let fetchDescriptor = FetchDescriptor<Item>()
let items: [Item] = try! modelContainer.mainContext.fetch(fetchDescriptor)
print(items)
let entry = SimpleEntry(date: .now, emoji: "😀", count: items.count)
let timeline = Timeline(entries: [entry], policy: .never)
completion(timeline)
}
}
The fetch yielded no results. However, as I explored the app group directory in the file system, I found a .sqlite file. That is interesting because SwiftData creates .store files by default. So, I am guessing that SwiftData did copy something. Or the ModelContainer initializer just created another empty SQLite file since the fetch returned zero results.
I would highly appreciate someone elaborating on that quote from the documentation.
I am working on a scrollable chart that displays days on the horizontal axis. As the user scrolls, I always want them to be able to snap to a specific day. I implemented the following steps described in this WWDC23 session to achieve this.
I have set the chartScrollTargetBehavior to .valueAligned(matching: DateComponents(hour: 0))
I have set the x value unit on the BarMark to Calendar.Component.day
I ended up with the chart code that looks like this:
Chart(dates, id: \.self) { date in
BarMark(
x: .value("Date", date, unit: Calendar.Component.day),
y: .value("Number", 1)
)
.annotation {
Text(date.formatted(.dateTime.day()))
.font(.caption2)
}
}
.chartXAxis {
AxisMarks(format: .dateTime.day())
}
.chartScrollableAxes(.horizontal)
.chartScrollTargetBehavior(.valueAligned(matching: DateComponents(hour: 0)))
.chartXVisibleDomain(length: fifteenDays)
.chartScrollPosition(x: $selection)
However, this fails to work reliably. There is often a situation where the chart scroll position lands on, for instance, Oct 20, 11:56 PM, but the chart snaps to Oct 21.
I attempted to solve this problem by introducing an intermediate binding between a state value and a chart selection. This binding aims to normalize the selection always to be the first moment of any given date. But this hasn't been successful.
private var selectionBinding: Binding<Date> {
Binding {
Calendar.current.startOfDay(for: selection)
} set: { newValue in
self.selection = Calendar.current.startOfDay(for: newValue)
}
}
It's also worth mentioning that this issue also exists in Apple's sample project on Swift Charts.
How would you approach solving this? How can I find a way to make the chart scroll position blind to time values and only recognize whole days?
Here's the minimal reproducible example project for your reference.