56

I've read Apple's Pasteboard Programming Guide, but it doesn't answer a particular question I have.

I'm trying to write a Cocoa application (for OS X, not iOS) that will keep track of everything that is written to the general pasteboard (so, whenever any application copies and pastes, but not, say, drags-and-drops, which also makes use of NSPasteboard). I could (almost) accomplish this by basically polling the general pasteboard on a background thread constantly, and checking changeCount. Of course, doing this would make me feel very dirty on the inside.

My question is, is there a way to ask the Pasteboard server to notify me through some sort of callback any time a change is made to the general pasteboard? I couldn't find anything in the NSPasteboard class reference, but I'm hoping it lurks somewhere else.

Another way I could imagine accomplishing this is if there was a way to swap out the general pasteboard implementation with a subclass of NSPasteboard that I could define myself to issue a callback. Maybe something like this is possible?

I would greatly prefer if this were possible with public, App Store-legal APIs, but if using a private API is necessary, I'll take that too.

Thanks!

Adrian Petrescu
  • 16,629
  • 6
  • 56
  • 82
  • Not an answer, but something to be aware of if you are monitoring the pasteboard in general: There is an informal protocol to mark Transient and application-generated data on the pasteboard: http://nspasteboard.org/ – Smilin Brian Nov 29 '12 at 19:35

6 Answers6

51

Unfortunately the only available method is by polling (booo!). There are no notifications and there's nothing to observe for changed pasteboard contents. Check out Apple's ClipboardViewer sample code to see how they deal with inspecting the clipboard. Add a (hopefully not overzealous) timer to keep checking for differences and you've got a basic (if clunky) solution that should be App-Store-Friendly.

File an enhancement request at bugreporter.apple.com to request notifications or some other callback. Unfortunately it wouldn't help you until the next major OS release at the earliest but for now it's polling until we all ask them to give us something better.

Joshua Nozzi
  • 60,946
  • 14
  • 140
  • 135
  • 3
    I was afraid of that. Thanks! :) – Adrian Petrescu Feb 17 '11 at 19:22
  • 4
    any changes since 2.5 years ago? – tofutim Aug 24 '13 at 04:16
  • 4
    any changes since 3 years ago? – tofutim Mar 06 '14 at 23:23
  • 1
    I don't see any so far but considering the latest (10.10 Yosemite and iOS 8) the pasteboard may be just fine staying as simple as it is since there is now a more modern, more content-aware mechanism Apple is touting for passing information between apps (and devices). Suddenly the pasteboard seems a lesser target for the kind of apps that are interested in managing temporary ambiguous storage spaces... ;-) – Joshua Nozzi Jul 06 '14 at 23:55
  • @JoshuaNozzi What is this new mechanism? – Miscreant Jul 16 '18 at 14:53
  • @Miscreant Huh... I'm trying to think of what it was I thought would be better for passing data around. I may have been referring to app extensions for sharing to and applying actions from other apps. Depending on what you're trying to accomplish this may or may not help, but this mechanism didn't turn out to be as useful as we'd hoped in cases like these. – Joshua Nozzi Jul 16 '18 at 17:13
  • Still the case in 2020? – Supertecnoboff Mar 03 '20 at 10:52
  • 3
    @Supertecnoboff It appears so, sadly. No new API that allows for callbacks. – Joshua Nozzi Mar 03 '20 at 17:33
  • 1
    haha, here I am again in 2021... I guess there are no changes – tofutim Nov 27 '21 at 07:43
  • (possibly, with Mac Catalyst - https://developer.apple.com/documentation/uikit/uipasteboard/1622104-changednotification) – tofutim Nov 27 '21 at 07:53
14

There was once a post on a mailing list where the decision against a notification api was described. I can't find it right now though. The bottom line was that probably too many applications would register for that api even though they really wouldn't need to. If you then copy something the whole system goes through the new clipboard content like crazy, creating lots of work for the computer. So i don't think they'll change that behavior anytime soon. The whole NSPasteboard API is internally built around using the changeCount, too. So even your custom subclass of NSPasteboard would still have to keep polling.

If you really want to check if the pasteboard changed, just keep observing the changeCount very half second. Comparing integers is really fast so there's really no performance issue here.

Karsten
  • 2,772
  • 17
  • 22
  • Assuming "instantaneous" is even needed. I wonder if it's really necessary to note every single change (and therefore need to ask repeatedly with a child's regard to the obnoxious waste of resources it is to ask every damn second, "ARE WE THERE YET???!!!"). ;-) A good time to look is when the user invokes your service (otherwise you're being a resource-wasting nosey app). If you *must* catch background updates (to journal, perhaps), maybe every few seconds is fine. If the user copies something twice in a few seconds, the first was probably an error they later corrected... – Joshua Nozzi Jul 07 '14 at 00:16
  • honestly, what's wasteful in asking NSPasteboard the the changeCount and comparing two integers? I would agree to that behavior being wasteful back in the early nineties and before, but not nowadays. – Karsten Jul 09 '14 at 12:20
  • Power consumption on modern laptops isn't important to you? Allowing your app to be put to sleep is vital these days and, aside from an app whose purpose is to journal *every single thing* that gets put on the pasteboard no matter how briefly (including accidental and immediately corrected copies), there's *no good reason* to poll *that* often. Poll when the user will actually need the info (like when your app is invoked/foregrounded) unless you can demonstrate a **really, really good reason** why it's necessary for your app to be draining your laptop's battery. – Joshua Nozzi Jul 10 '14 at 14:42
  • 1
    polling every half second is not exactly draining the battery, especially if it's only checking for the change-count. I've just created a test-project and it shows zero energy impact. The only scenario where that'd be useful though is for apps that build a history of your clipboard. – Karsten Jul 12 '14 at 18:20
  • In a related post (which led me to this one) I mentioned much the same thing regarding "the only reason" you might need to poll so often. But a repeating timer will cause the app to be woken periodically for work, never allowing it to "nap" for long. That's what common sense would suggest but you seem to have proven otherwise. Would you mind sharing the project somewhere? (Completely friendly professional interest, not one-upsmanship, I promise. :-)) – Joshua Nozzi Jul 14 '14 at 18:10
  • An update: polling is definitely going to waste battery power on modern Apple devices because it prevents the system entering low-power state. – Joshua Nozzi Apr 10 '21 at 18:13
  • You could set the quality of life of a Thread or queue. That will call your timer every so often but it’ll align this timer with other processes that also need to run and not be the only thing in the system that keeps running. On the other hand, if the system is idle (which you could detect) there’s no reason to watch the clipboard either because no one is actively working with the clipboard. – Karsten Apr 12 '21 at 19:00
  • Do you mean Quality of Service, @Karsten? Could you expand on this idea? A timer will still have to be scheduled. Use of tolerance settings (which do improve power consumption by aligning more with the system) won't help much as it'll still be polling regularly, even if more flexibly. Too long between polling and the user wonders where the thing they just copied is if they try to use it quickly. I'm interested in your idea though -- maybe I've misunderstood your proposal of combining timers with GCD QoS. – Joshua Nozzi Apr 22 '21 at 11:53
  • (Note when I said, "[polling] prevents the system entering low-power state", I didn't mean it keeps the system from sleeping; I meant lower-power but awake modes, i.e. putting cores into standby in low demand situations.) – Joshua Nozzi Apr 22 '21 at 11:56
  • You are both talking about a system that enters a low power state and a system where the user actively copies stuff to the clipboard. I don’t think both states are possible simultaneously. Either the user is using the system, in which case your polling process wouldn’t interrupt anything, or the user is absent, in which case you’re safe to not poll because what changes are you going to find? – Karsten Apr 25 '21 at 17:20
  • You may not be aware of how Apple’s platforms manage app idle states and power. It is absolutely 100% possible to have moments where the system is in use but in a low power state and quite frequently the system will enter a low power state just between user actions (within moments) if it’s got nothing else going on. An app with an active recurring timer (regardless of the queue its events are handled on) is “something going on”. Your suggestion quite frankly is no better than polling because it _is_ polling and will use more power. – Joshua Nozzi Apr 25 '21 at 18:50
  • I just stumbled over the method `tolerance` in NSTimer, which allows your timer to not fire precisely at a given interval but allows the system to better align this timer with other operations of your system. I think that should fix the problem of preventing the system from sleep. – Karsten May 12 '21 at 08:29
  • It will only swing so far (not far enough). See the docs for details. – Joshua Nozzi May 13 '21 at 01:32
12

Based on answer provided by Joshua I came up with similar implementation but in swift, here is the link to its gist: PasteboardWatcher.swift

Code snippet from same:

class PasteboardWatcher : NSObject {

    // assigning a pasteboard object
    private let pasteboard = NSPasteboard.generalPasteboard()

    // to keep track of count of objects currently copied
    // also helps in determining if a new object is copied
    private var changeCount : Int

    // used to perform polling to identify if url with desired kind is copied
    private var timer: NSTimer?

    // the delegate which will be notified when desired link is copied
    weak var delegate: PasteboardWatcherDelegate?

    // the kinds of files for which if url is copied the delegate is notified
    private let fileKinds : [String]

    /// initializer which should be used to initialize object of this class
    /// - Parameter fileKinds: an array containing the desired file kinds
    init(fileKinds: [String]) {
        // assigning current pasteboard changeCount so that it can be compared later to identify changes
        changeCount = pasteboard.changeCount

        // assigning passed desired file kinds to respective instance variable
        self.fileKinds = fileKinds

        super.init()
    }
    /// starts polling to identify if url with desired kind is copied
    /// - Note: uses an NSTimer for polling
    func startPolling () {
        // setup and start of timer
        timer = NSTimer.scheduledTimerWithTimeInterval(2, target: self, selector: Selector("checkForChangesInPasteboard"), userInfo: nil, repeats: true)
    }

    /// method invoked continuously by timer
    /// - Note: To keep this method as private I referred this answer at stackoverflow - [Swift - NSTimer does not invoke a private func as selector](http://stackoverflow.com/a/30947182/217586)
    @objc private func checkForChangesInPasteboard() {
        // check if there is any new item copied
        // also check if kind of copied item is string
        if let copiedString = pasteboard.stringForType(NSPasteboardTypeString) where pasteboard.changeCount != changeCount {

            // obtain url from copied link if its path extension is one of the desired extensions
            if let fileUrl = NSURL(string: copiedString) where self.fileKinds.contains(fileUrl.pathExtension!){

                // invoke appropriate method on delegate
                self.delegate?.newlyCopiedUrlObtained(copiedUrl: fileUrl)
            }

            // assign new change count to instance variable for later comparison
            changeCount = pasteboard.changeCount
        }
    }
}

Note: in the shared code I am trying to identify if user has copied a file url or not, the provided code can easily be modified for other general purposes.

Devarshi
  • 16,440
  • 13
  • 72
  • 125
  • 1
    I just saw this today and upvoted. A nice, simple solution. Suggestion: either require a delegate at initialization (so it’s not optional) or implement `didSet` on the delegate to create/start the timer if a delegate is given or stop/destroy if it’s taken away. You should also probably make the delegate weak to avoid retain cycles. This way you avoid resource consumption if the delegate goes away. (May not be possible on your current use but think “reuse”). – Joshua Nozzi Jul 27 '17 at 16:22
7

For those who need simplified version of code snippet that gets the job done in Swift 5.7,

it just works (base on @Devarshi code):

func watch(using closure: @escaping (_ copiedString: String) -> Void) {
    let pasteboard = NSPasteboard.general
    var changeCount = NSPasteboard.general.changeCount

    Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
        guard let copiedString = pasteboard.string(forType: .string),
              pasteboard.changeCount != changeCount else { return }

        defer {
            changeCount = pasteboard.changeCount
        }
        
        closure(copiedString)
    }
}

how to use is as below:

watch {
    print("detected : \($0)")
}

then if you attempt copy any text in your pasteboard, it will watch and print out to the console like below..

detected : your copied message in pasteboard
detected : your copied message in pasteboard

in case, full code sample for how to use it for example in SwiftUI:

import SwiftUI

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .onAppear {
                    watch {
                        print("detect : \($0)")
                    }
                }
        }
    }
    
    func watch(using closure: @escaping (_ copiedString: String) -> Void) {
        let pasteboard = NSPasteboard.general
        var changeCount = NSPasteboard.general.changeCount

        Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            guard let copiedString = pasteboard.string(forType: .string),
                  pasteboard.changeCount != changeCount else { return }

            defer {
                changeCount = pasteboard.changeCount
            }
            
            closure(copiedString)
        }
    }
}

Swift async await version:

import SwiftUI

@main
struct Test2App: App {
    var isWatch = true
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .task {
                    while true {
                        let copy = await watch()
                        
                        if let copy {
                            print("copy : \(copy)")
                        }
                    }
                }
        }
    }
    
    func watch() async -> String? {
        let pasteboard = NSPasteboard.general
        var changeCount = NSPasteboard.general.changeCount

        try? await Task.sleep(nanoseconds: 500_000_000)
        guard let copyString = pasteboard.string(forType: .string),
              pasteboard.changeCount != changeCount else { return nil }

        changeCount = pasteboard.changeCount
        return copyString
    }
}

Note that these are just the samples.

You can do whatever you want to do on top of that.

boraseoksoon
  • 2,164
  • 1
  • 20
  • 25
2

It's not necessary to poll. Pasteboard would generally only be changed by the current view is inactive or does not have focus. Pasteboard has a counter that is incremented when contents change. When window regains focus (windowDidBecomeKey), check if changeCount has changed then process accordingly.

This does not capture every change, but lets your application respond if the Pasteboard is different when it becomes active.

In Swift...

var pasteboardChangeCount = NSPasteboard.general().changeCount
func windowDidBecomeKey(_ notification: Notification)
{   Swift.print("windowDidBecomeKey")
    if  pasteboardChangeCount != NSPasteboard.general().changeCount
    {   viewController.checkPasteboard()
        pasteboardChangeCount  = NSPasteboard.general().changeCount
    }
}
JustPixelz
  • 49
  • 4
  • This is a great insight for dealing with the find pasteboard. Application text search toolbars/panels/etc (what you see when you type Cmd-F) are supposed to track the global find pasteboard. – MtnViewJohn Sep 01 '18 at 00:58
-2

I have a solution for more strict case: detecting when your content in NSPasteboard was replaced by something else.

If you create a class that conforms to NSPasteboardWriting and pass it to -writeObjects: along with the actual content, NSPasteboard will retain this object until its content is replaced. If there are no other strong references to this object, it get deallocated.

Deallocation of this object is the moment when new NSPasteboard got new content.

Tricertops
  • 8,492
  • 1
  • 39
  • 41
  • This won't work for things that are cached/uniqued in the system. Think `NSString` constants, `NSIndexPath`, etc. They're kept around for the lifetime of the app. There are also many other situations where something is retained elsewhere beyond expected lifetime for "reasons". Please don't do anything that depends on when something _else_ is deallocated. Only when it's within the deallocated instance's dealloc/deinit itself. – Joshua Nozzi Apr 22 '21 at 12:07
  • I was talking about a custom class, not a system class. How could you even override `-dealloc` for a system class, anyway? There are valid use cases for binding a lifetime of an object with a callback. – Tricertops Aug 19 '21 at 11:19
  • My point is you CANNOT RELY ON DEALLOCATION as a sign of when something else should happen. It’s a bad idea for a number of reasons. – Joshua Nozzi Aug 19 '21 at 23:46
  • You can rely on deallocation as a sign that the object is ending its lifetime. This way you can tie lifetime of an object with an entry in database or with external resource lifetime, etc. Memory management of Objective-C is deterministic and can be reliably used this way. – Tricertops Aug 20 '21 at 12:02