Hello,
SwiftData is not working correctly with Swift Concurrency. And it’s sad after all this time.
I personally found a regression. The attached code works perfectly fine on iOS 17.5 but doesn’t work correctly on iOS 18 or iOS 18.1.
A model can be updated from the background (Task, Task.detached or ModelActor) and refreshes the UI, but as soon as the same item is updated from the View (fetched via a Query), the next background updates are not reflected anymore in the UI, the UI is not refreshed, the updates are not merged into the main.
How to reproduce:
Launch the app
Tap the plus button in the navigation bar to create a new item
Tap on the “Update from Task”, “Update from Detached Task”, “Update from ModelActor” many times
Notice the time is updated
Tap on the “Update from View” (once or many times)
Notice the time is updated
Tap again on “Update from Task”, “Update from Detached Task”, “Update from ModelActor” many times
Notice that the time is not update anymore
Am I doing something wrong? Or is this a bug in iOS 18/18.1?
Many other posts talk about issues where updates from background thread are not merged into the main thread. I don’t know if they all are related but it would be nice to have
1/ bug fixed, meaning that if I update an item from a background, it’s reflected in the UI, and
2/ proper documentation on how to use SwiftData with Swift Concurrency (ModelActor). I don’t know if what I’m doing in my buttons is correct or not.
Thanks,
Axel
import SwiftData
import SwiftUI
@main
struct FB_SwiftData_BackgroundApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(for: Item.self)
}
}
}
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@State private var simpleModelActor: SimpleModelActor!
@Query private var items: [Item]
var body: some View {
NavigationView {
VStack {
if let firstItem: Item = items.first {
Text(firstItem.timestamp, format: Date.FormatStyle(date: .omitted, time: .standard))
.font(.largeTitle)
.fontWeight(.heavy)
Button("Update from Task") {
let modelContainer: ModelContainer = modelContext.container
let itemID: Item.ID = firstItem.persistentModelID
Task {
let context: ModelContext = ModelContext(modelContainer)
guard let itemInContext: Item = context.model(for: itemID) as? Item else { return }
itemInContext.timestamp = Date.now.addingTimeInterval(.random(in: 0...2000))
try context.save()
}
}
.buttonStyle(.bordered)
Button("Update from Detached Task") {
let container: ModelContainer = modelContext.container
let itemID: Item.ID = firstItem.persistentModelID
Task.detached {
let context: ModelContext = ModelContext(container)
guard let itemInContext: Item = context.model(for: itemID) as? Item else { return }
itemInContext.timestamp = Date.now.addingTimeInterval(.random(in: 0...2000))
try context.save()
}
}
.buttonStyle(.bordered)
Button("Update from ModelActor") {
let container: ModelContainer = modelContext.container
let persistentModelID: Item.ID = firstItem.persistentModelID
Task.detached {
let actor: SimpleModelActor = SimpleModelActor(modelContainer: container)
await actor.updateItem(identifier: persistentModelID)
}
}
.buttonStyle(.bordered)
Button("Update from ModelActor in State") {
let container: ModelContainer = modelContext.container
let persistentModelID: Item.ID = firstItem.persistentModelID
Task.detached {
let actor: SimpleModelActor = SimpleModelActor(modelContainer: container)
await MainActor.run {
simpleModelActor = actor
}
await actor.updateItem(identifier: persistentModelID)
}
}
.buttonStyle(.bordered)
Divider()
.padding(.vertical)
Button("Update from View") {
firstItem.timestamp = Date.now.addingTimeInterval(.random(in: 0...2000))
}
.buttonStyle(.bordered)
} else {
ContentUnavailableView(
"No Data",
systemImage: "slash.circle", //
description: Text("Tap the plus button in the toolbar")
)
}
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
}
}
private func addItem() {
modelContext.insert(Item(timestamp: Date.now))
try? modelContext.save()
}
}
@ModelActor
final actor SimpleModelActor {
var context: String = ""
func updateItem(identifier: Item.ID) {
guard let item = self[identifier, as: Item.self] else {
return
}
item.timestamp = Date.now.addingTimeInterval(.random(in: 0...2000))
try! modelContext.save()
}
}
@Model
final class Item: Identifiable {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}
Post
Replies
Boosts
Views
Activity
Hello,
I'm trying to understand how dangerous it is to read and/or update model properties from a thread different than the one that instantiated the model.
I know this is wrong when using Core Data and we should always use perform/performAndWait before manipulating an object but I haven't found any information about that for SwiftData.
Question: is it safe to pass an object from one thread (like MainActor) to another thread (in a detached Task for example) and manipulate it, or should we re fetch the object using its persistentModelID as soon as we cross threads?
When running the example app below with the -com.apple.CoreData.ConcurrencyDebug 1 argument passed at launch enabled, I don't get any Console warning when I tap on the "Update directly" button. I'm sure it would trigger a warning if I were using Core Data.
Thanks in advance for explaining.
Axel
--
@main
struct SwiftDataPlaygroundApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(for: Item.self)
}
}
}
struct ContentView: View {
@Environment(\.modelContext) private var context
@Query private var items: [Item]
var body: some View {
VStack {
Button("Add") {
context.insert(Item(timestamp: Date.now))
}
if let firstItem = items.first {
Button("Update directly") {
Task.detached {
// Not the main thread, but firstItem is from the main thread
// No warning in Xcode
firstItem.timestamp = Date.now
}
}
Button("Update using persistentModelID") {
let container: ModelContainer = context.container
let itemIdentifier: Item.ID = firstItem.persistentModelID
Task.detached {
let backgroundContext: ModelContext = ModelContext(container)
guard let itemInBackgroundThread: Item = backgroundContext.model(for: itemIdentifier) as? Item else { return }
// Item on a background thread
itemInBackgroundThread.timestamp = Date.now
try? backgroundContext.save()
}
}
}
}
}
}
@Model
final class Item: Identifiable {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}
Hello,
I'm currently developing an app using SwiftData.
I want the app to use CloudKit to sync data, so I made sure all my model properties are optional.
I've defined a Codable enum as follows:
enum Size: Int, Codable {
case small
case medium
case large
}
I've defined a Drink SwiftData model as follows:
@Model
class Drink {
var name: String?
var size: Size?
init(
name: String? = nil,
size: Size? = nil
) {
self.name = name
self.size = size
}
}
In one of my Views, I want to use a @Query to fetch the data, and use a Predicate to filter the data. The Predicate uses the size enumeration of the Drink model. Here is the code:
struct DrinksView: View {
@Query var drinks: [Drink]
init() {
let smallRawValue: Int = Size.small.rawValue
let filter: Predicate<Drink> = #Predicate<Drink> { drink in
if let size: Size = drink.size {
return size.rawValue == smallRawValue
} else {
return false
}
}
_drinks = Query(filter: filter)
}
var body: some View {
List {
ForEach(drinks) { drink in
Text(drink.name ?? "Unknown Drink")
}
}
}
}
The code compiles, but when I run the app, it crashes with the following error:
Thread 1: Fatal error: Couldn't find \Drink.size!.rawValue on Drink with fields [SwiftData.Schema.PropertyMetadata(name: "name", keypath: \Drink.name, defaultValue: nil, metadata: nil), SwiftData.Schema.PropertyMetadata(name: "size", keypath: \Drink.size, defaultValue: nil, metadata: nil)]
How can I filter my data using this optional variable on the Drink model?
Thanks,
Axel
Hello,
I'm trying to clear the purchase history made with a sandbox Apple ID on my test device but it does not work. The past purchases are still returned by StoreKit. I've waited many hours but it seems to persist.
When I use for await result in Transaction.currentEntitlements { in my app, my non-consumable product is still here.
Is it expected?
How long should it take to reset the history?
Is is supposed to work also for non-consumable products?
Thanks
Axel
Hello,
I want to use Automatic Grammar Agreement to localise a string in my app, let say "three remaining activities". The string "three" is obtained by using a NumberFormatter with a numberStyle set to .spellOut (so I'm not using an Integer)
var formatter: NumberFormatter = NumberFormatter()
formatter.numberStyle = .spellOut
let formattedCount: String = numberFormatter.string(from: count as NSNumber)!
Text("key_with_string_\(formattedCount)")
In my string catalog, I have translated the key key_with_string_%@ like this ^[%@ remaining activity](inflect: true), but it does not work.
I've tried to add the integer value used by the number formatter in the key key_with_string_%@_%lld but it does not work.
Should Automatic Grammar Agreement work normally just by using the formatted string provided by the NumberFormatter?
If not, is there a way to specify to use a secondary variable (my count integer) to switch between different categories like one and other automatically?
Thanks !
Axel
Hello,
I want to detect when a ScrollView is scrolled at the top of a specific View in a LazyVStack. Here is the code I use:
struct ContentView: View {
@State private var scrollID: Int?
var body: some View {
HStack {
VStack {
Text(scrollID?.formatted() ?? "Unknown")
Button("Go") {
withAnimation {
scrollID = 7
}
}
Divider()
ScrollView {
LazyVStack(spacing: 300) {
ForEach(0...100, id: \.self) { int in
Text(int.formatted())
.frame(maxWidth: .infinity)
.background(.red)
}
}
.scrollTargetLayout()
}
.scrollPosition(id: $scrollID, anchor: .top)
}
}
}
}
As I specify a top anchor, I was expecting to see the scrollID binding being updated when the red Text View is at the top of the ScrollView. But I noticed that scrollPosition updates the binding way before the red Text View is positioned at the top of the ScrollView, which is not what I want.
In this image, you can see the binding is already at one even though there is a lot of space between the View and the top of the ScrollView. Maybe the Stack spacing is taken into account?
And manually setting the binding scroll at the position I want, just above the red Text for 7, which makes me think the views IDs are correct.
Is my understanding wrong about this modifier?
How can I detect the top (beginning) of the View?
(If this is a SwiftUI bug, I filed #FB13811349)
Hello,
I want to understand how listSectionSpacing with a custom CGFloat value works in SwiftUI.
According to the documentation, we can use different spacing between sections and If adjacent sections have different spacing value, the smaller value on the shared edge is used.
For example, if I have a:
Section A with a listSectionSpacing of 10
Section B with a listSectionSpacing of 200
Section C with a listSectionSpacing of 20
Section D with a listSectionSpacing of 0
I notice the spacing between the sections A+B and B+C is none of 10, 200 or 20. If the documentation is correct, shouldn't it be 10?
If I now have a:
Section A with a listSectionSpacing of 200
Section B with a listSectionSpacing of 200
Section C with a listSectionSpacing of 200
Section D with a listSectionSpacing of 0
I notice the spacing is never 200. Is the spacing capped?
Also, if I specify a header and footer, the spacing doesn't seem to be used at all. Thus the spacing is the same for all sections.
Is my understanding wrong and this API working as expected?
Or is that a bug in how the spacing is used?
I filed #FB13699952 few weeks ago.
Axel
struct ListSections: View {
@State private var header: Bool = false
@State private var footer: Bool = false
private let sections: [ListSection] = [
ListSection(position: "A", spacing: 200),
ListSection(position: "B", spacing: 200),
ListSection(position: "C", spacing: 200),
ListSection(position: "D", spacing: 0)
]
var body: some View {
VStack {
Toggle("Header", isOn: $header)
Toggle("Footer", isOn: $footer)
}
.padding(.horizontal)
List {
ForEach(sections) { section in
Section {
LabeledContent("Section \(section.position)", value: section.spacing.formatted())
} header: {
if header {
Text("Header \(section.position)")
.background(.red.opacity(0.3))
}
} footer: {
if footer {
Text("Footer \(section.position)")
.background(.green.opacity(0.3))
}
}
.listSectionSpacing(section.spacing)
}
}
}
}
#Preview {
ListSections()
}
Hello,
I include a Privacy Manifest file in my app and specify one Privacy Nutrition Label Type (Email Address, for marketing purposes). My app uses some third-party SDKs like RevenueCat that contain Privacy Manifest files with nutrition label types specified (Purchases History for RevenueCat for example).
Xcode can generate a report that aggregates all the data types that are collected by the app.
But is App Store Connect updated when I upload a build? Or do I have to manually setup the App Privacy info?
Thanks
Hello,
I'm trying to understand what happens when a subscribed customer of a subscription A purchases a promotional offer for the same subscription A.
Let's say the product is a yearly subscription priced at $100. When the month 7 starts (6 months remaining in the regular subscription period), I send the user a promotional offer for the same product but priced at $25 for the first year (100$ afterwards) and he accepts the offer.
Is the promotional offer started only at the end of the current year (after the 6 remaining months) or is it started immediately and he gets a pro-rata refund for the 6 remaining months?
Thanks.
I'm creating an app that uses WeatherKit to get weather data. I wanted to confirm if the requests made to the service during app development will be deducted from our monthly usage limit or credit?
Thanks
In UIKit, we can add an insets to a MKMapView with setVisibleMapRect to have additional space around a specified MKMapRect. It's useful for UIs like Apple Maps or FlightyApp (see first screenshot attached). This means we can have a modal sheet above the map but still can see all the content added to the map.
I'm trying to do the same for a SwiftUI Map (on iOS 17) but I can't find a way to do it: see screenshot 2 below. Is it possible to obtain the same result or should I file a feedback for an improvement?
My NavigationSplitView crashes if I select an item in the sidebar after I removed other items: Thread 1: EXC_BREAKPOINT (code=1, subcode=0x1c44d488c)
How to reproduce:
Launch the app from the attached project: https://gist.github.com/alpennec/a45f5ff94382dc922718906a60a35220
Tap on “Add”
Select the new added item in the sidebar
In the detail view, tap “Delete”
Select “All” in the sidebar
The app crashes: Thread 1: EXC_BREAKPOINT (code=1, subcode=0x1c44d488c)
It occurs if I use just Strings or Core Data objects (I tried with plain String because I thought it was maybe an issue with Core Data but it’s not apparently).
What is wrong? Is that a bug?
Filed #FB13561900 for this.
I’m trying to get the previous date that matches the 9nth month in the Gregorian calendar (which is September) from Date.now (which is in December 2023 right now). The expected date is then in 2023. The first date returned is in 1995. Why?
I filed the feedback FB13462533
var calendar: Calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone.autoupdatingCurrent
let matchingDateComponents: DateComponents = DateComponents(month: 09)
let date: Date? = calendar.nextDate(
after: Date.now,
matching: matchingDateComponents,
matchingPolicy: .nextTime,
direction: .backward
)
print(date) // Optional(1995-08-31 23:00:00 +0000)
I want to find the "last Sunday of a month before the current date" in Swift, but using the Calendar nextDate function doesn't work (always returns nil).
var calendar: Calendar = Calendar(identifier: .gregorian)
calendar.timeZone = .gmt
let lastSundayDateComponents: DateComponents = DateComponents(
weekday: 1,
weekdayOrdinal: -1
)
let previousLastSundayDate: Date? = calendar.nextDate(
after: Date.now,
matching: lastSundayDateComponents,
matchingPolicy: .nextTime,
repeatedTimePolicy: .first,
direction: .backward
)
print(previousLastSundayDate ?? "Not found") // "Not found"
If I use a positive weekdayOrdinal, it's working normally and the same nextDate method provides the correct date.
let firstSundayDateComponents: DateComponents = DateComponents(
weekday: 1,
weekdayOrdinal: 1
)
When I check if the date components can provide a valid date for the given calendar, it returns false...
let lastSundayInNovember2023DateComponents: DateComponents = DateComponents(
year: 2023,
month: 11,
weekday: 1,
weekdayOrdinal: -1
)
// THIS RETURNS FALSE
let isValid: Bool = lastSundayInNovember2023DateComponents.isValidDate(in: calendar)
print(isValid) // false
... even if the correct date can be created.
let lastSundayInNovember2023: Date = calendar.date(from: lastSundayInNovember2023DateComponents)!
print(lastSundayInNovember2023) // 2023-11-26 00:00:00 +0000
Is that a bug in Foundation?
I'm using NSPersistentCloudKitContainer with Core Data and I receive errors because my iCloud space is full. The errors printed are the following: <CKError 0x280df8e40: "Quota Exceeded" (25/2035); server message = "Quota exceeded"; op = 61846C533467A5DF; uuid = 6A144513-033F-42C2-9E27-693548EF2150; Retry after 342.0 seconds>.
I want to inform the user about this issue, but I can't find a way to access the details of the error. I'm listening to NSPersistentCloudKitContainer.eventChangedNotification, I receive a error of type .partialFailure. But when I want to access the underlying errors, the partialErrorsByItemID property on the error is nil.
How can I access this Quota Exceeded error?
import Foundation
import CloudKit
import Combine
import CoreData
class SyncMonitor {
fileprivate var subscriptions = Set<AnyCancellable>()
init() {
NotificationCenter.default.publisher(for: NSPersistentCloudKitContainer.eventChangedNotification)
.sink { notification in
if let cloudEvent = notification.userInfo?[NSPersistentCloudKitContainer.eventNotificationUserInfoKey] as? NSPersistentCloudKitContainer.Event {
guard let ckerror = cloudEvent.error as? CKError else {
return
}
print("Error: \(ckerror.localizedDescription)")
if ckerror.code == .partialFailure {
guard let errors = ckerror.partialErrorsByItemID else {
return
}
for (_, error) in errors {
if let currentError = error as? CKError {
print(currentError.localizedDescription)
}
}
}
}
} // end of sink
.store(in: &subscriptions)
}
}