Categories
iOS Swift

Swift 6 suitable notification observers in iOS

The author discusses challenges managing side projects, specifically updating SignalPath to Swift 6. They encountered errors related to multiple notification observations but resolved them by shifting to publishers, avoiding sendable closure issues. Although the new approach risks background thread notifications, the compiler is satisfied with the adjustments made to the code.

I have a couple of side projects going on, although it is always a challenge to find time of them. One of them, SignalPath, is what I created back in 2015. Currently, I have been spending some time to bump the Swift version to 6 which brought a quite a list of errors. In many places I had code what dealt with observing multiple notifications, but of course Swift 6 was not happy about it.

let handler: (Notification) -> Void = { [weak self] notification in
self?.keyboardInfo = Info(notification: notification)
}
let names: [Notification.Name] = [
UIResponder.keyboardWillShowNotification,
UIResponder.keyboardWillHideNotification,
UIResponder.keyboardWillChangeFrameNotification
]
observers = names.map({ name -> NSObjectProtocol in
return NotificationCenter.default.addObserver(forName: name,
object: nil,
queue: .main,
using: handler)
// Converting non-sendable function value to '@Sendable (Notification) -> Void' may introduce data races
})
view raw Observer.swift hosted with ❤ by GitHub

After moving all of the notification observing to publishers instead, I can ignore the whole sendable closure problem all together.

Publishers.Merge3(
NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification),
NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification),
NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)
)
.map(Info.init)
.assignWeakly(to: \.keyboardInfo, on: self)
.store(in: &notificationCancellables)
view raw Observer.swift hosted with ❤ by GitHub

Great, compiler is happy again although this code could cause trouble if any of the notifications are posted from a background thread. But since this is not a case here, I went for skipping .receive(on: DispatchQueue.main). Assign weakly is a custom operator and the implementation looks like this:

public extension Publisher where Self.Failure == Never {
func assignWeakly<Root>(to keyPath: ReferenceWritableKeyPath<Root, Self.Output>, on object: Root) -> AnyCancellable where Root: AnyObject {
return sink { [weak object] value in
object?[keyPath: keyPath] = value
}
}
}

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.