swiftswiftuicombineuipasteboard

SwiftUI onReceive don't work with UIPasteboard publisher


I would like to subscribe to UIPasteboard changes in SwiftUI with onReceive. pHasStringsPublisher will not be updated as soon as something in the clipboard changes and I don't understand why.

import SwiftUI

struct ContentView: View {
    let pasteboard = UIPasteboard.general
    
    @State var pString: String = "pString"
    @State var pHasStrings: Bool = false
    @State var pHasStringsPublisher: Bool = false

    var body: some View {
        VStack{
            Spacer()
            Text("b: '\(self.pString)'")
                .font(.headline)
            Text("b: '\(self.pHasStrings.description)'")
                .font(.headline)
            Text("p: '\(self.pHasStringsPublisher.description)'")
                .font(.headline)
            Spacer()
            Button(action: {
                self.pString = self.pasteboard.string ?? "nil"
                self.pHasStrings = self.pasteboard.hasStrings
            }, label: {
                Text("read pb")
                    .font(.largeTitle)
            })
            Button(action: {
                self.pasteboard.items = []
            }, label: {
                Text("clear pb")
                    .font(.largeTitle)
            })
            Button(action: {
                self.pasteboard.string = Date().description
            }, label: {
                Text("set pb")
                    .font(.largeTitle)
            })
            
        }
        .onReceive(self.pasteboard
                    .publisher(for: \.hasStrings)
                    .print()
                    .receive(on: RunLoop.main)
                    .eraseToAnyPublisher()
                   , perform:
                    { hasStrings in
                        print("pasteboard publisher")
                        self.pHasStringsPublisher = hasStrings
                    })
    }

}

Solution

  • As far as I know, none of UIPasteboard's properties are documented to support Key-Value Observing (KVO), so publisher(for: \.hasStrings) may not ever publish anything.

    Instead, you can listen for UIPasteboard.changedNotification from the default NotificationCenter. But if you are expecting the user to copy in a string from another application, that is still not sufficient, because a pasteboard doesn't post changedNotification if its content was changed while your app was in the background. So you also need to listen for UIApplication.didBecomeActiveNotification.

    Let's wrap it all up in an extension on UIPasteboard:

    extension UIPasteboard {
        var hasStringsPublisher: AnyPublisher<Bool, Never> {
            return Just(hasStrings)
                .merge(
                    with: NotificationCenter.default
                        .publisher(for: UIPasteboard.changedNotification, object: self)
                        .map { _ in self.hasStrings })
                .merge(
                    with: NotificationCenter.default
                        .publisher(for: UIApplication.didBecomeActiveNotification, object: nil)
                        .map { _ in self.hasStrings })
                .eraseToAnyPublisher()
        }
    }
    

    And use it like this:

        var body: some View {
            VStack {
                blah blah blah
            }
            .onReceive(UIPasteboard.general.hasStringsPublisher) { hasStrings = $0 }
        }