|
5 | 5 | // Copyright © 2025 DittoLive Incorporated. All rights reserved. |
6 | 6 | // |
7 | 7 |
|
8 | | -import DittoSwift |
| 8 | +@preconcurrency import DittoSwift |
9 | 9 | import Foundation |
10 | 10 | import UserNotifications |
11 | 11 |
|
@@ -84,42 +84,45 @@ final class ChatNotificationManager { |
84 | 84 | private func startObserving(room: Room) { |
85 | 85 | guard let ditto, roomObservers[room.id] == nil else { return } |
86 | 86 |
|
87 | | - // Query the most recent 50 messages; no attachment tokens needed for text preview. |
88 | 87 | let query = """ |
89 | 88 | SELECT * FROM `\(room.messagesId)` |
90 | 89 | WHERE roomId == :roomId |
91 | 90 | ORDER BY createdOn DESC |
92 | 91 | LIMIT 50 |
93 | 92 | """ |
94 | 93 |
|
95 | | - let roomId = room.id |
96 | | - let roomName = room.name |
97 | | - |
98 | 94 | do { |
99 | | - // deliverOn: .global() — deliver on a background thread so the callback fires |
100 | | - // immediately when Ditto's internal sync engine receives data, without needing |
101 | | - // the main RunLoop to iterate first. This is critical when the app is backgrounded: |
102 | | - // iOS keeps the main RunLoop alive for BLE-mode apps, but delivering directly on a |
103 | | - // global queue means callbacks are not queued behind any pending main-thread work. |
104 | | - // |
105 | | - // Message parsing (compactMap) happens on the background thread; only the actor- |
106 | | - // isolated state mutations hop to .main via DispatchQueue.main.async, which is |
107 | | - // lightweight and safe while the app has any background execution time. |
108 | | - let observer = try ditto.store.registerObserver( |
109 | | - query: query, |
110 | | - arguments: ["roomId": roomId], |
111 | | - deliverOn: .global(qos: .utility) |
112 | | - ) { [weak self] result in |
113 | | - // Parse off the main thread — no actor-isolated state touched here. |
114 | | - let messages = result.items.compactMap { Message(value: $0.value) } |
115 | | - // Hop to main for @MainActor state mutations and UNUserNotificationCenter. |
116 | | - DispatchQueue.main.async { [weak self] in |
117 | | - self?.handle(messages: messages, roomId: roomId, roomName: roomName) |
118 | | - } |
119 | | - } |
| 95 | + // registerObserver is called from a nonisolated helper so that the closure passed |
| 96 | + // to Ditto is created in a nonisolated context. Swift 6.3 injects |
| 97 | + // _swift_task_checkIsolatedSwift into the prologue of any closure created inside an |
| 98 | + // @MainActor method — even if all captures are nonisolated(unsafe) — crashing when |
| 99 | + // Ditto delivers the callback on utility-qos. Moving closure creation into a |
| 100 | + // nonisolated method removes that coloring entirely. |
| 101 | + let observer = try makeObserver(store: ditto.store, query: query, |
| 102 | + roomId: room.id, roomName: room.name) |
120 | 103 | roomObservers[room.id] = observer |
121 | 104 | } catch { |
122 | | - print("ChatNotificationManager: failed to register observer for room \(roomId): \(error)") |
| 105 | + print("ChatNotificationManager: failed to register observer for room \(room.id): \(error)") |
| 106 | + } |
| 107 | + } |
| 108 | + |
| 109 | + /// Creates and registers a Ditto store observer from a `nonisolated` context. |
| 110 | + /// Closures defined here carry no `@MainActor` coloring, so Swift 6.3 does not inject |
| 111 | + /// actor-isolation checks into their prologues. |
| 112 | + nonisolated private func makeObserver( |
| 113 | + store: DittoStore, |
| 114 | + query: String, |
| 115 | + roomId: String, |
| 116 | + roomName: String |
| 117 | + ) throws -> DittoStoreObserver { |
| 118 | + nonisolated(unsafe) weak var weakSelf: ChatNotificationManager? = self |
| 119 | + return try store.registerObserver(query: query, arguments: ["roomId": roomId]) { result in |
| 120 | + let messages = result.items.compactMap { Message(value: $0.value) } |
| 121 | + DispatchQueue.main.async { |
| 122 | + MainActor.assumeIsolated { |
| 123 | + weakSelf?.handle(messages: messages, roomId: roomId, roomName: roomName) |
| 124 | + } |
| 125 | + } |
123 | 126 | } |
124 | 127 | } |
125 | 128 |
|
|
0 commit comments