Home > OS >  Swift Concurrency: Notification Callbacks on @MainActor Objects
Swift Concurrency: Notification Callbacks on @MainActor Objects

Time:12-09

Context

In a Mac app built with Swift 5.x and Xcode 14, I have a controller object. This object has several @Published properties that are observed by SwiftUI views, so I have placed the object on @MainActor like this:

@MainActor
final class AppController: NSObject, ObservableObject
{
    @Published private(set) var foo: String = ""
    @Published private(set) var bar: Int = 0

    private func doStuff() {
        ...
    }
}

Problem

This app needs to take certain actions when the Mac goes to sleep, so I subscribe to the appropriate notification in the init() method, but because AppController is decorated with @MainActor, I get this warning:

override init()
{
    NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.willSleepNotification, object: nil, queue: .main) { [weak self] note in
        self?.doStuff()     // "Call to main actor-isolated instance method 'doStuff()' in a synchronous nonisolated context; this is an error in Swift 6"
    }
}

So, I attempted to isolate it. But (of course) the compiler has something new to complain about. This time an error:

override init()
{
    NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.willSleepNotification, object: nil, queue: .main) { [weak self] note in
        Task { @MainActor in
            self?.doStuff()    // "Reference to captured var 'self' in concurrently-executing code
        }
    }
}

So I did this to solve that:

override init()
{
    NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.willSleepNotification, object: nil, queue: .main) { [weak self] note in
      
        let JUSTSHUTUP: AppController? = self 
        Task { @MainActor in
            JUSTSHUTUP?.doStuff()
        }
    }
}

Question

The last bit produces no compiler errors and seems to work. But I have NO idea if it's correct or best-practice.

I do understand why the compiler is complaining and what it's trying to protect me from, but attempting to adopt Swift Concurrency in an existing project is...painful.

CodePudding user response:

You can use your Task { @MainActor in ... } pattern, but add the [weak self] capture list to the Task:

NSWorkspace.shared.notificationCenter.addObserver(
    forName: NSWorkspace.willSleepNotification,
    object: nil,
    queue: .main
) { [weak self] note in
    Task { @MainActor [weak self] in
        self?.doStuff()
    }
}

CodePudding user response:

An alternative is to use Combine

import Combine

@MainActor
final class AppController: NSObject, ObservableObject
{
    @Published private(set) var foo: String = ""
    @Published private(set) var bar: Int = 0
    
    private var cancellable : AnyCancellable?
    
    private func doStuff() {
        //
    }
    
    override init()
    {
        super.init()
        cancellable = NSWorkspace.shared.notificationCenter
            .publisher(for: NSWorkspace.willSleepNotification)
            .receive(on: DispatchQueue.main)
            .sink { [weak self] note in
                self?.doStuff()
            }
    }
}
  • Related