I actually solved this problem myself just the other day. I wrote a blog post on it, as well as a Gist
I'll embed the blog post and final code just in case the blog or Gist ever go away. Note: This is a very long post that goes into detail about how the class in constructed and what you can do to call other methods in your App's delegate. If all you want is the finished product (the MediaApplication class), head towards the bottom. It's just above the XML and the Info.plist informaton.
For starters, to get the key events from the media keys you need to create a class that extends NSApplication
. This is as simple as
import Cocoa
class MediaApplication: NSApplication {
}
Next, we need to override the sendEvent()
function
override func sendEvent(event: NSEvent) {
if (event.type == .SystemDefined && event.subtype.rawValue == 8) {
let keyCode = ((event.data1 & 0xFFFF0000) >> 16)
let keyFlags = (event.data1 & 0x0000FFFF)
// Get the key state. 0xA is KeyDown, OxB is KeyUp
let keyState = (((keyFlags & 0xFF00) >> 8)) == 0xA
let keyRepeat = (keyFlags & 0x1)
mediaKeyEvent(Int32(keyCode), state: keyState, keyRepeat: Bool(keyRepeat))
}
super.sendEvent(event)
}
Now, I don't pretend to entirely understand what is going on here, but I think I have a decent idea. NSEvent
objects contain several key properties: type
, subtype
, data1
, and data2
. Type
and subtype
are fairly self-explanatory, but data1
and data2
are extremely vague. Since the code only uses data1
, that's what we'll be looking at. From what I can tell, data1
contains all of the data surrounding a key event. That means it contains the key code and any key flags. It appears that key flags contain information about the key's state (Is the key pressed down? Has the key been released?) as well as whether or not the key is being held down and repeating the signal. I'm also guessing that the key code and key flags both take up half of the data contained in data1
and the bitwise operations are separating that data out into appropriate variables. After we get the values we need, we call mediaKeyEvent()
which I will get to in a moment. Regardless of what events get sent to our MediaApplication
, we do want the default NSApplication
to handle all events as well. To do this, we call super.sendEvent(event)
at the end of the function. Now, let's take a look at mediaKeyEvent()
.
func mediaKeyEvent(key: Int32, state: Bool, keyRepeat: Bool) {
// Only send events on KeyDown. Without this check, these events will happen twice
if (state) {
switch(key) {
case NX_KEYTYPE_PLAY:
// Do work
break
case NX_KEYTYPE_FAST:
// Do work
break
case NX_KEYTYPE_REWIND:
// Do work
break
default:
break
}
}
}
This is where things start to get fun. First things first, we only want to check what key is being pressed if state
is true, which in this case is whenever the key is pressed down. Once we get into checking the keys, we look for NX_KEYTYPE_PLAY
, NX_KEYTYPE_FAST
, and NX_KEYTYPE_REWIND
. If their functions aren't obvious, NX_KEYTYPE_PLAY
is the play/pause key, NX_KEYTYPE_FAST
is the next key, and NX_KEYTYPE_REWIND
is the previous key. Right now, nothing happens when any of those keys is pressed down, so lets go over some possible logic. We'll start with a simple scenario.
case NX_KEYTYPE_PLAY:
print("Play")
break
With this code in place, when your application detects that the play/pause key has been pressed you will see "Play" printed out to the console. Simple, right? Let's up the ante by calling functions in your application's NSApplicationDelegate
. First we will assume that your NSApplicationDelegate
has a function called printMessage
. We will be modifying it as we go, so pay close attention to the changes. They will be minor, but the changes will impact how you call them from mediaEventKey
.
func printMessage() {
print("Hello World")
}
This is the simplest case. When printMessage()
is called, you will see "Hello World" in your console. You can call this by calling performSelector
on your NSApplicationDelegate
which is accessible through the MediaApplication
. performSelector
takes in a Selector
which is just the name of the function in your NSApplicationDelegate
.
case NX_KEYTYPE_PLAY:
delegate!.performSelector("printMessage")
break
Now, when your application detects that the play/pause key has been pressed, you will see "Hello World" printed to the console. Let's kick things up a notch with a new version of printMessage
that takes in a parameter.
func printMessage(arg: String) {
print(arg)
}
The idea is now that if printMessage("Hello World")
is called, you will see "Hello World" in your console. We can now modify the performSelector
call to handle passing in a parameter.
case NX_KEYTYPE_PLAY:
delegate!.performSelector("printMessage:", withObject: "Hello World")
break
There are a few things to note about this change. First, it's important to notice the :
that was added to the Selector
. This separates the function name from the parameter when it gets sent to the delegate. How it works isn't too important to remember, but it's something along the lines of the delegate calling printMessage:"Hello World"
. I'm fairly certain that is not 100% correct as it would likely use an object ID of some sort, but I haven't done any extensive digging into the specifics. Either way, the important thing to remember is to add :
when passing in a parameter.. The second thing to note is that we added a withObject
parameter. withObject
takes an AnyObject?
as a value. In this case, we just pass in a String
because that's what printMessage
is looking for. When your application detects that the play/pause key has been pressed, you should still see "Hello World" in the console. Let's look at one final use-case: a version of printMessage
that takes in not one, but two parameters.
func printMessage(arg: String, _ arg2: String) {
print(arg)
}
Now, if printMessage("Hello", "World")
is called, you will see "Hello World" in your console. We can now modify the performSelector
call to handle passing in two parameters.
case NX_KEYTYPE_PLAY:
delegate!.performSelector("printMessage::", withObject: "Hello", withObject: "World")
break
As before, there are two things to notice here. First, we now add two :
to the end of the Selector
. Like before, this is so that the delegate can pass information along that contains the parameters. At a very basic level, it would look something like printMessage:"Hello":"World"
, but again I don't know what it really looks like at a deeper level. The second thing to notice is that we have added a second withObject
parameter to the performSelector
call. Like before, this withObject
takes an AnyObject?
as a value and we're passing in a String
because that's what printMessage
wants. When your application detects that the play/pause key has been pressed, you should still see "Hello World" in the console.
One final thing to note is that performSelector
can only accept up to two parameters. I'd really like to see Swift add concepts like splatting or varargs so that this limitation eventually goes away, but for now just avoid trying to call functions that require more than two parameters.
This is what a very simple MediaApplication
class that just prints out some text would look like once you are done with everything above:
import Cocoa
class MediaApplication: NSApplication {
override func sendEvent(event: NSEvent) {
if (event.type == .SystemDefined && event.subtype.rawValue == 8) {
let keyCode = ((event.data1 & 0xFFFF0000) >> 16)
let keyFlags = (event.data1 & 0x0000FFFF)
// Get the key state. 0xA is KeyDown, OxB is KeyUp
let keyState = (((keyFlags & 0xFF00) >> 8)) == 0xA
let keyRepeat = (keyFlags & 0x1)
mediaKeyEvent(Int32(keyCode), state: keyState, keyRepeat: Bool(keyRepeat))
}
super.sendEvent(event)
}
func mediaKeyEvent(key: Int32, state: Bool, keyRepeat: Bool) {
// Only send events on KeyDown. Without this check, these events will happen twice
if (state) {
switch(key) {
case NX_KEYTYPE_PLAY:
print("Play")
break
case NX_KEYTYPE_FAST:
print("Next")
break
case NX_KEYTYPE_REWIND:
print("Prev")
break
default:
break
}
}
}
}
Now, I should also add that, by default, your application is going to use the standard NSApplication
when it runs. If you want to use the MediaApplication
that this whole post is about, you'll need to go ahead and modify your application's Info.plist
file. If you're in the graphical view, it will look something like this:
(source: sernprogramming.com)
Otherwise, it will look something like this:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIconFile</key>
<string></string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key&