Nitesh
Nitesh

Reputation: 862

How do I observe the UIPasteboard in a thread-safe manner?

I have an observable class, which has an observer on UIPasteboard. In an attempt to update to the Swift 6 language mode, I understandably can't mutate the state from the completion handler.

@Observable
final class ImageClipboard {
    var images: [SelectableImage]
    
    init() {
        self.images = []
        
        NotificationCenter.default.addObserver(forName: UIPasteboard.changedNotification, object: nil, queue: .main) { _ in
            if let pasteboardImages = UIPasteboard.general.images {
                let newImages = pasteboardImages.map {
                    SelectableImage(image: $0)
                }
                
                self.images.append(contentsOf: newImages)
            }
        }
    }
}

My preference would be to use an asynchronous mechanism to mutate images, otherwise some synchronization mechanism. What would be the best way to deal with this issue?

Upvotes: 1

Views: 64

Answers (1)

Sweeper
Sweeper

Reputation: 274835

I think getting the notifications as a Combine publisher (publisher(for:object:)) is more convenient in this case.

@Observable
@MainActor
class ImageClipboard {
    var images: [SelectableImage] = []
    @ObservationIgnored
    var cancellable: AnyCancellable?
    
    init() {
        cancellable = NotificationCenter.default.publisher(for: UIPasteboard.changedNotification)
            .receive(on: DispatchQueue.main)
            .sink { _ in
                let images = UIPasteboard.general.images?.map { SelectableImage(image: $0) } ?? []
                self.images.append(contentsOf: images)
            }
    }
}

The publisher is cancelled automatically when ImageClipboard is deinitialised.


There is also notifications(named:object:), which you can use like this:

@Observable
@MainActor
class ImageClipboard {
    var images: [SelectableImage] = []
    
    func startMonitoringClipboard() async {
        for await _ in NotificationCenter.default.notifications(named: UIPasteboard.changedNotification) {
            let images = UIPasteboard.general.images?.map { SelectableImage(image: $0) } ?? []
            self.images.append(contentsOf: images)
        }
    }
}

Then in your view you should call startMonitoringClipboard in a .task:

SomeView()
    .task {
        await clipboard.startMonitoringClipboard()
    }

The task will be cancelled when SomeView disappears.

Upvotes: 1

Related Questions