-1

I have a feeling that Swift Concurrency is fighting agains me all the time. It is really frustrating.

My problem. I need to create Combine publisher to emit battery state on iOS device.

So far I have this:


extension Notification.Name {
    var publisher: NotificationCenter.Publisher {
        return NotificationCenter.default.publisher(for: self)
    }
}
func createDefaultPublisher() -> AnyPublisher<Float, Never> {
    UIDevice.batteryLevelDidChangeNotification
        .publisher
        .compactMap { $0.object as? UIDevice }
        .map { device in
            Future<Float, Never> { promise in
                Task { @MainActor in
                    promise(.success(device.batteryLevel)) // Sending 'promise' risks causing data races
                }
            }
        }
        .switchToLatest()
        .eraseToAnyPublisher()
}

So the tricky part for me is how to implement map operator. I need to switch to the MainActor there so I'm using Future with promise callback. The problem here is: Sending 'promise' risks causing data races. So I wondering how to do it properly in Swift 6 in project configured with Complete Strict Concurrency Checking

4
  • The Swift 6 way is to use pure async/await and not use Combine. Also note that notifications are a bit problematic in Swift 6 (though a fix might be upcoming in a later version of Swift), because the UserInfo means a notification is not Sendable; however, there are easy workarounds for this (which have been explained here on Stack Overflow already). Commented Aug 8 at 19:01
  • I agree. I would look at generating an AsyncSequence of your own Sendable type Commented Aug 9 at 0:03
  • In this case, he really doesn’t need to introduce a new Sendable type. The batteryLevel is already Sendable. Commented Aug 11 at 15:34
  • My problem. I need to create Combine publisher to emit battery state on iOS device Commented Oct 8 at 23:44

1 Answer 1

0

There are two approaches:

  1. Use Combine, but remain within Combine patterns. If createDefaultPublisher is isolated to the main actor, you can do:

    extension Notification.Name {
        func publisher() -> NotificationCenter.Publisher {  // rather than hiding a factory behind a computed property, a function might make this more explicit, minimizing potential misuse
            NotificationCenter.default.publisher(for: self)
        }
    }
    
    func createDefaultPublisher() -> AnyPublisher<Float, Never> {
        UIDevice.batteryLevelDidChangeNotification
            .publisher()
            .receive(on: DispatchQueue.main)
            .compactMap { $0.object as? UIDevice }
            .map(\.batteryLevel)
            .eraseToAnyPublisher()
    }
    

    Note, rather than introducing unstructured concurrency, I am just using receive(on:) to make sure it is on the main thread.

    And then:

    cancellable = createDefaultPublisher()
        .sink { [weak self] level in
            // do something with `level` here
        }
    

    Note, if createDefaultPublisher is not a member of a type isolated to the main actor already (and therefore is not main actor isolated, itself), you can let the compiler know that you are on the main thread and therefore enjoy MainActor isolation with MainActor.assumeIsolated {…}:

    func createDefaultPublisher() -> AnyPublisher<Float, Never> {
        UIDevice.batteryLevelDidChangeNotification
            .publisher()
            .receive(on: DispatchQueue.main)
            .compactMap { $0.object as? UIDevice }
            .map { device in MainActor.assumeIsolated { device.batteryLevel } }
            .eraseToAnyPublisher()
    }
    

    Using MainActor.assumeIsolated is the standard way of explicitly specifying main actor isolation when you are already on the main thread (which we are here).

  2. The other approach is to use Swift concurrency:

    extension Notification.Name {
        func notifications() -> NotificationCenter.Notifications {
            NotificationCenter.default.notifications(named: self)
        }
    }
    
    @MainActor
    func monitorBattery() async {
        for await notification in UIDevice.batteryLevelDidChangeNotification.notifications() {
            if let device = notification.object as? UIDevice {
                // do something with `device.batteryLevel` here
            }
        }
    }
    
Sign up to request clarification or add additional context in comments.

2 Comments

First approach is generating errors on map(\.batteryLevel) - Cannot form key path to main actor-isolated property 'batteryLevel'
OK. Your createDefaultPublisher must not be a member of a type isolated to the main actor. In that case, use MainActor.assumeIsolated as shown in my revised answer. Or you could isolate createDefaultPublisher to the main actor, itself, and use the \.batteryLevel pattern.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.