Concurrency

RSS for tag

Concurrency is the notion of multiple things happening at the same time.

Posts under Concurrency tag

166 Posts
Sort by:

Post

Replies

Boosts

Views

Activity

Sending main actor-isolated value of type 'PurchaseAction' with later accesses to nonisolated context risks causing data races
Trying to migrate to Swift 6. However getting this error when using SwiftUI StoreKit purchase environment. Sending main actor-isolated value of type 'PurchaseAction' with later accesses to nonisolated context risks causing data races @Environment(\.purchase) private var purchase let result = try await purchase(product)
1
2
735
Sep ’24
Live activity sample code for Swift 6?
Hi, I'm updating our app to use Xcode 16 and Swift 6 language mode. I'm stuck on updating our live activity code. I looked at the Emoji Rangers sample project and after switching it to Swft 6 mode, it has the exact same errors as our project. The main problem seems to be that Activity is not Sendable, which prevents us from passing it to child tasks to await things like activityStateUpdates and contentUpdates (those are also not Sendable). Is there any guidance on this? Updated sample code? Another project?
4
0
828
Sep ’24
iOS18 AVPlayerViewController 出现卡住界面
(AVPlayerViewController *)avPlayerVC { if(!_avPlayerVC){ _avPlayerVC =[[AVPlayerViewController alloc] init]; _avPlayerVC.videoGravity = AVLayerVideoGravityResizeAspectFill; _avPlayerVC.showsPlaybackControls = NO; [self addSubview:_avPlayerVC.view]; [_avPlayerVC.view mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.mas_equalTo(0); }]; [self sendSubviewToBack:_avPlayerVC.view]; } return _avPlayerVC; } 我在一个cell里添加这个,界面无法动弹。只有在iOS18会这样
2
1
683
Sep ’24
Swift 6 Migration error in Sample code (Updating an app to use strict concurrency sample code) provided by Apple.
Updating an app to use strict concurrency is not compiling in Swift 6 with strict concurrency enabled. I am getting the following error in Xcode Version 16.0 (16A242d). private func queryHealthKit() async throws -> ( [HKSample]?, [HKDeletedObject]?, HKQueryAnchor? ) { try await withCheckedThrowingContinuation { continuation in // Create a predicate that returns only samples created within the last 24 hours. let endDate = Date() let startDate = endDate.addingTimeInterval(-24.0 * 60.0 * 60.0) let datePredicate = HKQuery.predicateForSamples( withStart: startDate, end: endDate, options: [.strictStartDate, .strictEndDate]) // Create the query. let query = HKAnchoredObjectQuery( type: caffeineType, predicate: datePredicate, anchor: anchor, limit: HKObjectQueryNoLimit ) { (_, samples, deletedSamples, newAnchor, error) in // When the query ends, check for errors. if let error { continuation.resume(throwing: error) } else { continuation.resume(returning: (samples, deletedSamples, newAnchor)) } } store.execute(query) } } The error is on ** continuation.resume(returning: (samples, deletedSamples, newAnchor)) ** and the error is Task-isolated value of type '([HKSample]?, [HKDeletedObject]?, HKQueryAnchor?)' passed as a strongly transferred parameter; later accesses could race. How to solve this error?
2
1
1.5k
Sep ’24
How to fix this Swift 6 migration issue?
Here is a code snippet about AVPlayer. avPlayer.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 60), queue: .main) { [weak self] _ in // Call main actor-isolated instance methods } Xcode shows warnings that Call to main actor-isolated instance method '***' in a synchronous nonisolated context; this is an error in the Swift 6 language mode. How can I fix this? avPlayer.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 60), queue: .main) { [weak self] _ in Task { @MainActor in // Call main actor-isolated instance methods } } Can I use this solution above? But it seems switching actors frequently can slow down performance.
1
0
2.9k
Sep ’24
Live queries on SwiftData DB but without @Query macro?
I switched from using @Query to @ModelActor because of the following reasons: Performance Issues: With @Query, my app became unresponsive with large datasets because data fetching occurred on the main thread. Integration with CKSyncEngine: I needed to implement @ModelActor anyway to allow CKSyncEngine to add data to local persistent storage from the background. In my current setup, the onAppear() method for my view calls the getItems() function on my model actor, which returns [ItemsDTO] and then View renders them. However, I'm now facing a challenge in achieving the same automatic data refreshing and view updates that @Query provided. Here are a few potential solutions I'm considering: Periodic Data Fetching: Fetch data at regular intervals to keep the view updated. But this seems expensive. Local Write Monitoring: Monitor all local writes to automatically trigger updates when changes occur. But this is quite a lot of code I would have to write myself. Switch to manual refresh: Users would have to manually trigger UI updates by pressing button. Reintroduce @Query: Writes would happen from ModelActor, but reads would still happen from Main thread. But then again app would become unresponsive on reads. If you have any additional ideas or best practices for maintaining reactivity with @ModelActor, I'd love to hear them!
1
1
1k
Sep ’24
Issues with @preconcurrency and AVFoundation in Swift 6 on Xcode 16.1/iOS 18 (Worked fine in Swift 5)
Question: I'm working on a project in Xcode 16.1, using Swift 6 with iOS 18. My code is working fine in Swift 5, but I'm running into concurrency issues when upgrading to Swift 6, particularly with the @preconcurrency attribute in AVFoundation. Here is the relevant part of my code: import SwiftUI @preconcurrency import AVFoundation struct OverlayButtonBar: View { ... let audioTracks = await loadTracks(asset: asset, mediaType: .audio) ... // Tracks are extracted before crossing concurrency boundaries private func loadTracks(asset: AVAsset, mediaType: AVMediaType) async -> [AVAssetTrack] { do { return try await asset.load(.tracks).filter { $0.mediaType == mediaType } } catch { print("Error loading tracks: \(error)") return [] } } } Issues: When using @preconcurrency, I get the warning: @preconcurrency attribute on module AVFoundation has no effect. Suggested fix by Xcode is: Remove @preconcurrency. But if I remove @preconcurrency, I get both a warning and an error: Warning: Add '@preconcurrency' to treat 'Sendable'-related errors from module 'AVFoundation' as warnings. Error: Non-sendable type [AVAssetTrack] returned by implicitly asynchronous call to nonisolated function cannot cross actor boundary. (Class AVAssetTrack does not conform to the Sendable protocol (AVFoundation.AVAssetTrack)). This error comes if I attempt to directly access non-Sendable AVAssetTrack in an async context : let audioTracks = await loadTracks(asset: asset, mediaType: .audio) How can I resolve this issue while staying compliant with Swift 6 concurrency rules? Is there a recommended approach to handling non-Sendable types like AVAssetTrack in concurrency contexts? Appreciate any guidance on making this work in Swift 6, especially considering it worked fine in Swift 5. Thanks in advance!
1
0
2.4k
Sep ’24
Handling Main Actor-Isolated Values with `PHPhotoLibrary` in Swift 6
Hello, I’m encountering an issue with the PHPhotoLibrary API in Swift 6 and iOS 18. The code I’m using worked fine in Swift 5, but I’m now seeing the following error: Sending main actor-isolated value of type '() -> Void' with later accesses to nonisolated context risks causing data races Here is the problematic code: Button("Save to Camera Roll") { saveToCameraRoll() } ... private func saveToCameraRoll() { guard let overlayFileURL = mediaManager.getOverlayURL() else { return } Task { do { let status = await PHPhotoLibrary.requestAuthorization(for: .addOnly) guard status == .authorized else { return } try await PHPhotoLibrary.shared().performChanges({ if let creationRequest = PHAssetCreationRequest.creationRequestForAssetFromVideo(atFileURL: overlayFileURL) { creationRequest.creationDate = Date() } }) await MainActor.run { saveSuccessMessage = "Video saved to Camera Roll successfully" } } catch { print("Error saving video to Camera Roll: \(error.localizedDescription)") } } } Problem Description: The error message suggests that a main actor-isolated value of type () -> Void is being accessed in a nonisolated context, potentially leading to data races. This issue arises specifically at the call to PHPhotoLibrary.shared().performChanges. Questions: How can I address the data race issues related to main actor isolation when using PHPhotoLibrary.shared().performChanges? What changes, if any, are required to adapt this code for Swift 6 and iOS 18 while maintaining thread safety and actor isolation? Are there any recommended practices for managing main actor-isolated values in asynchronous operations to avoid data races? I appreciate any points or suggestions to resolve this issue effectively. Thank you!
1
0
2.2k
Sep ’24
Inexplicable Fence Hang
Hello, My App is getting a Fence hang right after install in a specific scenario. Issue1: I attempted to follow the directions, tried to symbolicate the file etc. however did not have much luck. I was able to pinpoint the lines of code where the hang seems to occur. I did this using simple print and comment out/uncomment blocks of code related to the specific scenario. I was able to do so as, not much is happening on the Main thread in this scenario . Issue 2: The following lines of code ( modified var etc. ) seem to cause the hang. Commenting them out gets rid of the hang across devices, while online/offline etc. I am not sure if I need to use a framework other than AVFoundation. Note: The file extension is mpg The music files are static ( included in the Bundle ) and not accessed from user's playlist etc. import var plyr : AVAudioPlayer? let pth = Bundle.main.path(forResource: "MusicFileName", ofType: "mpg")! let url = URL(fileURLWithPath: pth) do [{](https://www.example.com/) plyr = try AVAudioPlayer(contentsOf: url) plyr?.prepareToPlay() plyr?.play() } catch { // print error etc. } Thanks in advance. I would appreciate some help! Close to submission :)
6
0
954
Sep ’24
Xcode 16 beta 6 - unexpected concurrency build issues
The following behavior seems like a bug in the swift compiler that ships with Xcode 16 beta 6. Add the following code snippet to a new iOS app project using Xcode 16 beta 6 and observe the error an warning called out in the comments within the itemProvider() method: import WebKit extension WKWebView { func allowInspectionForDebugBuilds() { // commenting out the following line makes it so that the completion closure argument of the trailing closure // passed to NSItemProvider.registerDataRepresentation(forTypeIdentifier:visibility:loadHandler:) is no longer // isolated to the main actor, thus resolving the build issues. It is unexpected that the presence or absence of // the following line would have this kind of impact. isInspectable = true } } class Foo { func itemProvider() -> NSItemProvider? { let itemProvider = NSItemProvider() itemProvider.registerDataRepresentation(forTypeIdentifier: "", visibility: .all) { completion in Task.detached { guard let url = URL(string: "") else { completion(nil, NSError()) // error: Expression is 'async' but is not marked with 'await' return } let task = URLSession.shared.dataTask(with: url) { data, _, error in completion(data, error) // warning: Call to main actor-isolated parameter 'completion' in a synchronous nonisolated context; this is an error in the Swift 6 language mode } task.resume() } return Progress() } return itemProvider } } Now, comment out the line isInspectable = true and observe that the error and warning disappear. Also filed as FB14783405 and https://github.com/swiftlang/swift/issues/76171 Hoping to see this fixed before Xcode 16 stable.
8
0
1.1k
Sep ’24
withCheckedContinuation crashes on Xcode 16
We are using a 3rd party SDK which crashes on iOS 18 in certain scenarios. They say they need Apple to fix this bug ahead of release https://github.com/swiftlang/swift/issues/75952 but I'm skeptical since it is only a few weeks away most likely. The bug seems pretty bad so is there any chance it will be fixed before iOS 18? We aim for a same-day release so would be great to know if we need to remove the 3rd party SDK or not.
5
1
2.5k
Sep ’24
AVAudioPlayerNode scheduleBuffer & Swift 6 Concurrency
We do custom audio buffering in our app. A very minimal version of the relevant code would look something like: import AVFoundation class LoopingBuffer { private var playerNode: AVAudioPlayerNode private var audioFile: AVAudioFile init(playerNode: AVAudioPlayerNode, audioFile: AVAudioFile) { self.playerNode = playerNode self.audioFile = audioFile } func scheduleBuffer(_ frames: AVAudioFrameCount) async { let audioBuffer = AVAudioPCMBuffer( pcmFormat: audioFile.processingFormat, frameCapacity: frames )! try! audioFile.read(into: audioBuffer, frameCount: frames) await playerNode.scheduleBuffer(audioBuffer, completionCallbackType: .dataConsumed) } } We are in the migration process to swift 6 concurrency and have moved a lot of our audio code into a global actor. So now we have something along the lines of import AVFoundation @globalActor public actor AudioActor: GlobalActor { public static let shared = AudioActor() } @AudioActor class LoopingBuffer { private var playerNode: AVAudioPlayerNode private var audioFile: AVAudioFile init(playerNode: AVAudioPlayerNode, audioFile: AVAudioFile) { self.playerNode = playerNode self.audioFile = audioFile } func scheduleBuffer(_ frames: AVAudioFrameCount) async { let audioBuffer = AVAudioPCMBuffer( pcmFormat: audioFile.processingFormat, frameCapacity: frames )! try! audioFile.read(into: audioBuffer, frameCount: frames) await playerNode.scheduleBuffer(audioBuffer, completionCallbackType: .dataConsumed) } } Unfortunately this now causes an error: error: sending 'audioBuffer' risks causing data races | `- note: sending global actor 'AudioActor'-isolated 'audioBuffer' to nonisolated instance method 'scheduleBuffer(_:completionCallbackType:)' risks causing data races between nonisolated and global actor 'AudioActor'-isolated uses I understand the error, what I don't understand is how I can safely use this API? AVAudioPlayerNode is not marked as @MainActor so it seems like it should be safe to schedule a buffer from a custom actor as long as we don't send it anywhere else. Is that right? AVAudioPCMBuffer is not Sendable so I don't think it's possible to make this callsite ever work from an isolated context. Even forgetting about the custom actor, if you instead annotate the class with @MainActor the same error is still present. I think the AVAudioPlayerNode.scheduleBuffer() function should have a sending annotation to make clear that the buffer can't be used after it's sent. I think that would make the compiler happy but I'm not certain. Am I overlooking something, holding it wrong, or is this API just pretty much unusable in Swift 6? My current workaround is just to import AVFoundation with @preconcurrency but it feels dirty and I am worried there may be a real issue here that I am missing in doing so.
3
0
1k
Aug ’24
SwiftData @ModelActor Memory usage skyrocketing when changing properties
When changing a property of a SwiftData Model from a ModelActor the memory needed slightly increases. Once you do that more often, you can see that the usage is linearly increasing. I modified the Swiftdata template as little as possible. This is the least code I need to reproduce the problem: Changes In the @main struct : ContentView(modelContainer: sharedModelContainer) ContentView: struct ContentView: View { @Query private var items: [Item] let dataHanndler: DataHandler @State var timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: false, block: { t in }) var body: some View { NavigationSplitView { List { ForEach(items) { item in NavigationLink { Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))") } label: { Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard)) } } } .toolbar { ToolbarItem(placement: .navigationBarTrailing) { EditButton() } ToolbarItem { Button(action: addItem) { Label("Add Item", systemImage: "plus") } } ToolbarItem { Button { timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { t in Task { await dataHanndler.updateRandom() // Obviously this makes little sense but I need to update a lot of entities in my actual app. This is the simplest way to demonstrate that. updateRandom() could also be a function of a view but that doesn't make a difference } } } label: { Label("Do a lot of writing", systemImage: "gauge.with.dots.needle.100percent") } } ToolbarItem { Button { timer.invalidate() } label: { Label("Invalidate", systemImage: "stop.circle") } } } } detail: { Text("Select an item") } } private func addItem() { Task { await dataHanndler.insert(timestamp: Date.now) } } init(modelContainer: ModelContainer) { self.dataHanndler = DataHandler(modelContainer: modelContainer) } } ModelActor: @ModelActor actor DataHandler { public func update<T>(_ persistentIdentifier: PersistentIdentifier, keypath: ReferenceWritableKeyPath<Item, T>, to value: T) throws { let model = modelContext.model(for: persistentIdentifier) as! Item model[keyPath: keypath] = value } public func insert(timestamp: Date) { let item = Item(timestamp: timestamp) modelContext.insert(item) } public func updateRandom() { let count = try! modelContext.fetchCount(FetchDescriptor<Item>()) var descriptor = FetchDescriptor<Item>() descriptor.fetchOffset = Int.random(in: 0..<count) descriptor.fetchLimit = 1 let model = try! modelContext.fetch(descriptor) model.first!.timestamp = Date.now } } I filed a bug report FB14876920 but I am looking for other ideas to solve this before it will be fixed in a future update. The modelContext I use is created and managed by the @ModelActor macro. Happy to hear ideas
2
0
683
Aug ’24
Implement UNUserNotificationCenterDelegate in iOS app using Swift6
I've got a problem with compatibility with Swift6 in iOS app that I have no idea how to sort it out. That is an extract from my main app file @MainActor @main struct LangpadApp: App { ... @State private var notificationDataProvider = NotificationDataProvider() @UIApplicationDelegateAdaptor(NotificationServiceDelegate.self) var notificationServiceDelegate var body: some Scene { WindowGroup { TabView(selection: $tabSelection) { ... } .onChange(of: notificationDataProvider.dateId) { oldValue, newValue in if !notificationDataProvider.dateId.isEmpty { tabSelection = 4 } } } } init() { notificationServiceDelegate.notificationDataProvider = notificationDataProvider } } and the following code shows other classes @MainActor final class NotificationServiceDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { var notificationDataProvider: NotificationDataProvider? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -&amp;gt; Bool { UNUserNotificationCenter.current().delegate = self return true } func setDateId(dateId: String) { if let notificationDataProvider = notificationDataProvider { notificationDataProvider.dateId = dateId } } nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { // After user pressed notification let content = response.notification.request.content if let dateId = content.userInfo["dateId"] as? String { await MainActor.run { setDateId(dateId: dateId) } } } nonisolated func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification) async -&amp;gt; UNNotificationPresentationOptions { // Before notification is to be shown return [.sound, .badge, .banner, .list] } } @Observable final public class NotificationDataProvider : Sendable { public var dateId = "" } I have set Strict Concurrency Checking to 'Complete.' The issue I'm facing is related to the delegate class method, which is invoked after the user presses the notification. Current state causes crash after pressing notification. If I remove "nonisolated" keyword it works fine but I get the following warning Non-sendable type 'UNNotificationResponse' in parameter of the protocol requirement satisfied by main actor-isolated instance method 'userNotificationCenter(_:didReceive:)' cannot cross actor boundary; this is an error in the Swift 6 language mode I have no idea how to make it Swift6 compatible. Does anyone have any clues?
1
15
1.4k
Feb ’25
Swift Data + Swift 6 Sendable-ity paradox
Xcode 16 beta 3 Assume a SwiftData model starts like this and has a few more properties like a name and creation date (these are immaterial to my main question. @Model final class Batch: Identifiable, Sendable { @Attribute(.unique) var id: UUID //... more stuff The combination of Swift 6 (or Swift 5 with warnings enabled) and SwiftData seem to provide a paradox: Swift 6 complains when the id is a let: Cannot expand accessors on variable declared with 'let'; this is an error in the Swift 6 language mode Swift 6 complains when the id is a var: Stored property '_id' of 'Sendable'-conforming class 'Batch' is mutable; this is an error in the Swift 6 language mode Removing "Sendable" may be one solution but defeats the purpose and causes warnings elsewhere in the app about the model not being Sendable. Is there an obvious fix? Am I as a newbie (to the combination of Swift 6 and SwiftData) missing an entire architectural step of using ModelActor somewhere?
1
1
3.5k
Aug ’24
App is crashing when using "withCheckedContinuation" in Xcode 16 beta 5
We are experiencing an issue with withCheckedContinuation in our Swift project. Our implementation was working perfectly in a previous version of Xcode and continues to work in the simulator. However, it fails to work on a real device. Here’s a brief description of the problem: • Environment: Xcode Version: Xcode 16.0 Beta 5 Swift Version: Swift 5 OS: IOS18 beta 5 • Problem: The code using withCheckedContinuation behaves as expected in the simulator but fails on a physical device. We are receiving a “bad access to memory” error when running on a real device. • What We’ve Tried: Verified that the code works in previous Xcode versions. Tested on different simulators, where it runs without issues. Checked for any obvious errors in memory handling or threading. Code Example: Here’s a simplified version of the problematic code: var body: some View { VStack { Text("Hello, world!") } .padding() .onAppear { Task { await self.checkTrialOrIntroductoryDiscountEligibilityAsync() } } } func checkTrialOrIntroductoryDiscountEligibilityAsync() async { return await withCheckedContinuation { continuation in checkTrialOrIntroDiscountEligibility() { continuation.resume() } } } func checkTrialOrIntroDiscountEligibility(completion: () -> Void) { completion() } }
18
6
4.4k
Sep ’24
Swift 6, SwiftData modelContext.save() Crashes, Does not AutoSave
I took one of my apps and have gone through the process of making it compatible with Swift 6. I started with Swift 5 and Complete Concurrency Checking and eliminated every warning. I flipped the switch to Swift 6 and I have no compile errors and no warnings. I also created a ModelActor and created async functions to fetch, delete, get an object's persistentID and save model objects. My SwiftData model has a notes property and the user can update or add onto the notes and resave the model object. The app crashes when I save the changes using an explicit save operation. However, the next time the app is launched, the changes did propagate. If I do not use the explicit save operation, the system does not auto-save and does not crash. When I switched back to Swift 5 and tried it out, I was able to save without a crash. I did need to use an explicit save operation though. However, I am now getting 3 warnings like the following. Type 'ReferenceWritableKeyPath<MyModel, Optional>' does not conform to the 'Sendable' protocol; this is an error in the Swift 6 language mode The line of code that produced that warning is `let fetchDescriptor = FetchDescriptor(sortBy: [SortDescriptor(\MyModel.title)]) However, when I flip the switch back to Swift 6, the three warnings go away and I have zero warnings under Swift 6.
5
0
1.1k
Aug ’24
App is crashing when using "withCheckedContinuation" in Xcode 15 beta 5
We are experiencing an issue with withCheckedContinuation in our Swift project. Our implementation was working perfectly in a previous version of Xcode and continues to work in the simulator. However, it fails to work on a real device. Here’s a brief description of the problem: • Environment: - Xcode Version: Xcode 16.0 Beta 5 - Swift Version: Swift 5 - OS: IOS18 beta 5 • Problem: The code using withCheckedContinuation behaves as expected in the simulator but fails on a physical device. We are receiving a “bad access to memory” error when running on a real device. • What We’ve Tried: 1. Verified that the code works in previous Xcode versions. 2. Tested on different simulators, where it runs without issues. 3. Checked for any obvious errors in memory handling or threading. Code Example: Here’s a simplified version of the problematic code: var body: some View { VStack { Text("Hello, world!") } .padding() .onAppear { Task { await self.checkTrialOrIntroductoryDiscountEligibilityAsync() } } } func checkTrialOrIntroductoryDiscountEligibilityAsync() async { return await withCheckedContinuation { continuation in checkTrialOrIntroDiscountEligibility() { continuation.resume() } } } func checkTrialOrIntroDiscountEligibility(completion: () -> Void) { completion() } }
18
9
3.7k
Sep ’24
Swift 6 Concurrency Errors with MKLocalSearchCompleterDelegate results
Has anyone found a thread-safe pattern that can extract results from completerDidUpdateResults(MKLocalSearchCompleter) in the MKLocalSearchCompleterDelegate ? I've downloaded the code sample from Interacting with nearby points of interest and notice the conformance throws multiple errors in Xcode 16 Beta 5 with Swift 6: extension SearchDataSource: MKLocalSearchCompleterDelegate { nonisolated func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { Task { let suggestedCompletions = completer.results await resultStreamContinuation?.yield(suggestedCompletions) } } Error: Task-isolated value of type '() async -> ()' passed as a strongly transferred parameter; later accesses could race and Error: Sending 'suggestedCompletions' risks causing data races Is there another technique I can use to share state of suggestedCompletions outside of the delegate in the code sample?
4
2
2.1k
Dec ’24
Task Isolation Inheritance and SwiftUI
This post discusses a subtlety in Swift concurrency, and specifically how it relates to SwiftUI, that I regularly see confusing folks. I decided to write it up here so that I can link to it rather than explain it repeatedly. If you have a question or a comment, start a new thread and I’ll respond there. Put it in the App & System Services > Processes & Concurrency topic area and tag it with both Swift and Concurrency. Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com" Task Isolation Inheritance By default, tasks inherit their actor isolation from the surrounding code. This is a common source of confusion. My goal here is to explain why it happens, why it can cause problems, and how to resolve those problems. Imagine you have a main actor class like this: @MainActor class MyClass { var counter: Int = 0 func start() { Task { print("will sleep") doSomeCPUIntensiveWork() print("did sleep") } } } In this example the class is a model object of some form, but it could be an @Observable type, a SwiftUI view, a UIKit view controller, and so on. The key thing is that the type itself is isolated to the main actor. Remember that Swift code inherits its isolation from the surrounding code (in compiler author speak this is called the lexical context). So the fact that MyClass is annotated with @MainActor means that both counter and start() are isolated to the main actor. IMPORTANT This model is what allows the compiler to detect concurrency problems at compile time. I’ve found that, whenever I’m confused by Swift concurrency, it helps to ask myself “What does the compiler know?” Folks look at this code and think “But I’ve added a Task, and that means that doSomeCPUIntensiveWork() will run on a secondary thread!” That is not true. There are a couple of easy ways to prove this to yourself: Actually run the code. If you put this code into an app, you’ll find that your app’s UI is unresponsive for the duration of the doSomeCPUIntensiveWork(). Indeed, you can test this example for yourself, as explained below in Example Context. Access a value that’s isolated to the main actor. For example, insert this doSomeCPUIntensiveWork(): self.counter += 1 doSomeCPUIntensiveWork() The compiler doesn’t complain about this access to counter — a main-actor-isolated value — from this context, which tell you that this code will run on the main thread. So, what’s going on? The task is running on the main actor because of a form of isolation inheritance. The mechanics of that are complex, something I’ll explained in the The Gory Details section below. For the moment, however, the key thing to note is that starting a task in this way — using Task.init(…) — causes the task to inherit actor isolation from the surrounding code. In this case the surrounding code is the start() method, which is isolated to the main actor because it’s part of MyClass, and thus the code ends up calling doSomeCPUIntensiveWork() on the main thread. So, how do you prevent this? There are many different ways, but the two most obvious are: Replace Task.init(…) with Task.detached(…): func start() { Task.detached() { print("will sleep") doSomeCPUIntensiveWork() print("did sleep") } } And how does that work? Again, see the The Gory Details section below. Move the code to a non-isolated method: func start() { Task { print("will sleep") await self.myDoSomeCPUIntensiveWork() print("did sleep") } } nonisolated func myDoSomeCPUIntensiveWork() async { doSomeCPUIntensiveWork() } In both cases you can prove to yourself that this has done the right thing: Add code to access counter from the non-isolated context and observe the complaints from the compiler. SwiftUI While my “What does the compiler know?” thought experiment is super helpful, sometimes it’s not easy understand that. Folks are often caught out by the way that the SwiftUI View protocol works. We’ve fixed this problem in Xcode 16, but that change has brought more confusion. In Xcode 15 and earlier the View protocol was defined like this: public protocol View { … @ViewBuilder @MainActor var body: Self.Body { get } } Only the body property is annotated with @MainActor. The view as a whole is not. Consider this view: struct CounterViewOK: View { @State var counter: Int = 0 var body: some View { VStack { Text("\(counter)") Button("Increment") { Task { counter += 1 } } } } } This compiles because the task inherits the main actor isolation from body. But if you make a seemingly trivial change, the compiler complains: struct CounterViewNG: View { @State var counter: Int = 0 var body: some View { VStack { Text("\(counter)") Button("Increment") { increment() } } } func increment() { Task { counter += 1 // ^ Capture of 'self' with non-sendable type 'CounterViewNG' in a `@Sendable` closure } } } That’s because the increment() method is not isolated to the main actor, and thus neither is the task. The compiler thinks you’re trying to pass an instance of the view between contexts, and rightly complains. In contrast, in Xcode 16 (currently in beta) the View protocol looks ilke this: @MainActor @preconcurrency public protocol View { … @ViewBuilder @MainActor @preconcurrency var body: Self.Body { get } } The entire View is now isolated to the main actor. This makes everything easier to understand. Both of the examples above work. Specifically, CounterViewNG works because the task inherits main actor isolation via the increment() > CounterViewNG > View chain. Of course, everything is a trade-off. More of your views are now running on the main actor, which can trigger the CPU intensive work issue that I described above. Other Options When I crafted the doSomeCPUIntensiveWork() example above, I avoided mentioning SwiftUI. There was a specific reason for that: When working with a UI framework, it’s best to avoid doing significant work in your UI types. This is true in SwiftUI, but it’s also true in UIKit and AppKit. Indeed, doing all your app’s work in your view controllers is called the massive view controller anti-pattern. So, if you’re find yourself doing significant work in your UI types, consider some alternatives. You have lots of options: The simplest option is to move the code into an async function. But you might also want to add an abstraction layer. Swift has lots of good options for that (structs, enums, classes, actors). You can also define a new global actor. The best option depends on your specific situation. If you’re looking for further advice, there’s no shortage of it out there on the ’net (-: The Gory Details To understand the difference between Task.init(…) and Task.detached(…), you have to look at their declarations. This is easy to do from Xcode — just command-click on the init or the detached — but that’s misleading. The difference is keyed off a underscore-prefixed attribute and, for better or worse, Xcode won’t show you those. To see the actual difference you have have to open the Swift interface file. Within any given SDK the relevant file is usr/lib/swift/_Concurrency.swiftmodule/arm64e-apple-macos.swiftinterface. Here’s what you’ll see in the macOS SDK within Xcode 16.0b4: @discardableResult @_alwaysEmitIntoClient public init( priority: TaskPriority? = nil, @_inheritActorContext @_implicitSelfCapture operation: __owned @escaping @isolated(any) @Sendable () async -> Success ) {…} @discardableResult @_alwaysEmitIntoClient public static func detached( priority: TaskPriority? = nil, operation: __owned @escaping @isolated(any) @Sendable () async -> Success ) -> Task<Success, Failure> {…} Note I’ve edited this significantly to make things easier to read. The critical difference is the use of @_inheritActorContext in the Task.init(…) case. This tells the compiler that the closure argument should inherit its isolation from the surrounding code. This attribute is underscored, and thus there’s no Swift Evolution proposal for it, but there is some limited documentation. Example Context To run the example in context, create a new command-line tool project, rename main.swift to start.swift, and insert MyClass into this scaffolding: import Foundation @MainActor class MyClass { … code above … } func doSomeCPUIntensiveWork() { sleep(5) } @main struct Main { static func main() { Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in print("tick") } let m = MyClass() m.start() withExtendedLifetime(m) { RunLoop.current.run() } } } In this context: doSomeCPUIntensiveWork() uses the sleep system call to hog the current thread for 5 seconds. The timer tick helps illustrate the unresponsive main thread. It’s also need to ensure that the run loop continues to run indefinitely. More Reading There is a lot of good information available about Swift concurrency. My favourite resources include: The Swift Programming Language > Concurrency Migrating to Swift 6 The Avoid hangs by keeping the main thread free from non-UI work section of Improving app responsiveness WWDC 2023 Session 10248 Analyze hangs with Instruments, especially the section starting at 31:42. Swift Evolution proposals SE-0431 @isolated(any) Function Types which covers another subtle issue with tasks Matt Massicotte blog at https://www.massicotte.org Revision History 2024-08-05 Added the Other Options section. Added some more links to the More Reading section. Made other minor editorial changes. 2024-08-01 First posted.
0
0
3.1k
Aug ’24