7

I'm writing an assistive application for Mac OS 10.10+, using swift. I need to be able to paste the content from the general NSPasteboard into the application which had previously been active.

Just to make it extra clear: I need to paste into another application.

It should work like this:

  1. The user is using some random application

  2. Because the user can't press cmd+v due to disability, they make a gesture which activates my app (this part is done and it's outside the scope of this question)

  3. My app becomes active and now I need to simulate a Paste action in the app the user has been using beforehand. This is the bit I don't know how to do.

  4. Finally the previously active app needs to become active again.

Please bare in mind the app is to be submitted to the AppStore.

20
  • 1
    @Neovibrant: As a heads-up, just because there is another app in the Mac App Store with this functionality, does not mean your app will pass app review. (1): Some older Mac apps were submitted before sandboxing was a requirement. They are "grandfathered" in - as long as only bug fix updates are submitted, they can remain un-sandboxed (currently). (2): New apps must be sandboxed to be submitted to the Mac App Store. No exceptions. (3): It is possible that a developer "snuck" private API use or other functionality past app review. You can't count on being able to do the same. Commented Oct 22, 2016 at 1:18
  • 1
    @mz2: Per the documentation on NSUserAppleScriptTask, "If the application is sandboxed, then the script must be in the applicationScriptsDirectory folder. A sandboxed application may read from, but not write to, this folder." The ability to request security scoped writing access through an open folder panel is due to an implementation detail that could always change to match the documentation (i.e. Apple could decide to block sandboxed apps from requesting access to this folder at some future date to enforce the documented restriction). Enabling core functionality with this is a risk, IMO. Commented Oct 24, 2016 at 17:36
  • 2
    mz2: Indeed, my concern is that this method will not survive for this purpose. (But that largely depends on Apple and the precedence of the various clauses in the documentation.) In the most pessimistic reading, an app-supplied script is not a user-supplied script (strike 1), NSUserAppleScriptTask "is not intended to execute scripts built into an application" (strike 2), a sandboxed application may not write to the applicationScriptsDirectory folder (strike 3). NSUserAppleScriptTask is for user-supplied scripts, not for application scripts - this loophole may be patched. Commented Oct 24, 2016 at 18:43
  • 1
    In a wider context than this question I do of course agree that I would consider twice whether an app which fundamentally needs to know about system state outside of its own sandbox (including the active applications) should be built for Mac App Store distribution; This particular problem can be solved and that is what we should be discussing here, but whether you want to go ahead with it depends on many factors beyond technology (is it worth the risk and hassle monetarily or otherwise – many devs' views differ on this). Goes way beyond this question though, and technically this is solvable. Commented Oct 25, 2016 at 1:41
  • 1
    As an update, please see developer reports of App Review rejections for similar things here: forums.developer.apple.com/thread/67953 Commented Nov 24, 2016 at 5:09

3 Answers 3

8
+50

At its simplest you need to do two things:

  1. Subscribe to notifications of the current application changing so you know what application you should send events to.

  2. Simulate a Cmd+V keypress using System Events.

However, the bad news is, as you say you need to do this in an App Store submitted app, your app needs to be inside the sandbox and requires the temporary entitlement com.apple.security.temporary-exception.apple-events to send events to System Events directly quoting the Apple documentation:

requesting the necessary apple-events temporary exception entitlements for the Finder and System Events will likely result in rejection during the app review process, because granting access to these processes gives your app free rein over much of the operating system. For system-level tasks, use other methods, as discussed above.

There is also a way to do the step #2 using assistive technologies, but that too will not work inside the sandbox. Fundamentally, what you are trying to do (to activate an external arbitrary app, and to control it) is pretty much bang on what the sandbox is designed to prevent you from doing.

Fortunately, since macOS 10.8 there is now NSUserAppleScriptTask, which is executed outside of the sandbox.

There is a twist to NSUserAppleScriptTask: scripts executed with NSUserAppleScriptTask need to be placed inside the application's script directory, and your application cannot write to it, only read from it.

This objc.io article shows an example of how you can request for security scoped writing access to the scripting directory to be able to write your script to it; Annoyingly to yourself and your user you need to bring up an open dialog on the first time, therefore, and you need to store the security scoped bookmark so you don't need to repeat the exercise, but that's the best you'll be able to do inside the sandbox.

Your only other route beside jumping the NSUserAppleScriptTask hoops to my knowledge is to convince Apple that it's a really good idea to accept your app with the temporary entitlement that allows it to script System Events (I would not hold my breath).

Sign up to request clarification or add additional context in comments.

1 Comment

Regarding NSUserAppleScriptTask and prompting the user to install a script from within your app, I just had my app rejected for doing that, citing section 2.4.5 of the App Store Review Guidelines so I guess we're out of luck :(
8

I found these commands to simulate cut/copy/paste using Foundation:

func pastematchstyle () { let event1 = CGEvent(keyboardEventSource: nil, virtualKey: 0x09, keyDown: true); // opt-shft-cmd-v down event1?.flags = [CGEventFlags.maskCommand, CGEventFlags.maskShift, CGEventFlags.maskAlternate] event1?.post(tap: CGEventTapLocation.cghidEventTap); let event2 = CGEvent(keyboardEventSource: nil, virtualKey: 0x09, keyDown: false); // opt-shf-cmd-v up // event2?.flags = [CGEventFlags.maskCommand, CGEventFlags.maskShift, CGEventFlags.maskAlternate] event2?.post(tap: CGEventTapLocation.cghidEventTap); } func paste () { let event1 = CGEvent(keyboardEventSource: nil, virtualKey: 0x09, keyDown: true); // cmd-v down event1?.flags = CGEventFlags.maskCommand; event1?.post(tap: CGEventTapLocation.cghidEventTap); let event2 = CGEvent(keyboardEventSource: nil, virtualKey: 0x09, keyDown: false) // cmd-v up // event2?.flags = CGEventFlags.maskCommand event2?.post(tap: CGEventTapLocation.cghidEventTap) } func pasteresults () { let event1 = CGEvent(keyboardEventSource: nil, virtualKey: 0x09, keyDown: true); // shft-cmd-v down event1?.flags = [CGEventFlags.maskCommand, CGEventFlags.maskShift] event1?.post(tap: CGEventTapLocation.cghidEventTap); let event2 = CGEvent(keyboardEventSource: nil, virtualKey: 0x09, keyDown: false); // shf-cmd-v up // event2?.flags = [CGEventFlags.maskCommand, CGEventFlags.maskShift]; event2?.post(tap: CGEventTapLocation.cghidEventTap); } func cut() { let event1 = CGEvent(keyboardEventSource: nil, virtualKey: 0x07, keyDown: true); // cmd-x down event1?.flags = CGEventFlags.maskCommand; event1?.post(tap: CGEventTapLocation.cghidEventTap); let event2 = CGEvent(keyboardEventSource: nil, virtualKey: 0x07, keyDown: false); // cmd-x up // event2?.flags = CGEventFlags.maskCommand; event2?.post(tap: CGEventTapLocation.cghidEventTap); } func copy() { let event1 = CGEvent(keyboardEventSource: nil, virtualKey: 0x08, keyDown: true); // cmd-c down event1?.flags = CGEventFlags.maskCommand; event1?.post(tap: CGEventTapLocation.cghidEventTap); let event2 = CGEvent(keyboardEventSource: nil, virtualKey: 0x08, keyDown: false); // cmd-c up // event2?.flags = CGEventFlags.maskCommand; event2?.post(tap: CGEventTapLocation.cghidEventTap); } func copystyle() { let event1 = CGEvent(keyboardEventSource: nil, virtualKey: 0x08, keyDown: true); // opt-cmd-c down event1?.flags = [CGEventFlags.maskCommand, CGEventFlags.maskAlternate]; event1?.post(tap: CGEventTapLocation.cghidEventTap); let event2 = CGEvent(keyboardEventSource: nil, virtualKey: 0x08, keyDown: false); // opt-cmd-c up // event2?.flags = CGEventFlags.maskCommand; event2?.post(tap: CGEventTapLocation.cghidEventTap); } func pastestyle() { let event1 = CGEvent(keyboardEventSource: nil, virtualKey: 0x07, keyDown: true); // opt-cmd-v down event1?.flags = [CGEventFlags.maskCommand, CGEventFlags.maskAlternate]; event1?.post(tap: CGEventTapLocation.cghidEventTap); let event2 = CGEvent(keyboardEventSource: nil, virtualKey: 0x07, keyDown: false); // opt-cmd-v up // event2?.flags = CGEventFlags.maskCommand; event2?.post(tap: CGEventTapLocation.cghidEventTap); } 

1 Comment

When giving an answer it is preferable to give some explanation as to WHY your answer is the one.
-4

I'm pretty sure you can do this with Apple Events, but don't forget to set up a temporary exception entitlement to be able to message another app.

1 Comment

What is "Apple Events"? I would appreciate a little more detail, if that's possible, please. Also, I do need to submit the app to the AppStore, if that detail is of any relevance.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.