Building Push-Triggered Sync, Part Two: First Steps

After researching languages and choosing Go to implement a push provider for OmniFocus, we needed to get started writing code. Apple has set out the interface for a provider in the chapter “Provider Communication with Apple Push Notification Service;” our task then became to write a server that could speak in the binary format APNs expected.

Every notification delivered to Apple is built of a series of items. Each item details one part of the notification, such as its JSON payload or metadata like its priority or expiration date. Items have a common format: each begins with one byte giving the item type, then two bytes giving its total length. All the actual information in the item follows that three-byte “header.”

This is pretty straightforward to represent in Go: we just need a struct whose layout matches the expected format of an item. Let’s write one now:

type Item struct {
    ID         byte
    dataLength uint16
    data       []byte
}

From this basic definition, we can represent all the different kinds of item that might appear in an APNs notification. Since the data in an item is stored in its encoded (byte slice) form, we can also write this item to a binary buffer for transmission to Apple very easily:

func (item *Item) Bytes() []byte {
    buf := new(bytes.Buffer)
    binary.Write(buf, binary.BigEndian, item.ID)
    binary.Write(buf, binary.BigEndian, item.dataLength)
    binary.Write(buf, binary.BigEndian, item.data)
    return buf.Bytes()
}

We can also take advantage of Go’s iota construct inside constant declarations to define names for each kind of item that Apple documents. Since the items are 1-indexed, we simply define the first as 1 + iota and let the language go from there:

const (
    DeviceTokenItemIdentifier = 1 + iota
    PayloadItemIdentifier
    NotificationIdentifierIdentifier
    ExpirationDateIdentifier
    PriorityIdentifier
)

With this infrastructure, defining any given kind of item becomes as easy as providing a short function to populate the struct:

func NewDeviceTokenItem(token [32]byte) *Item {
    return &Item{ID: DeviceTokenItemIdentifier, dataLength: 32, data: token[:]}
}

While some items are a little more complex than this one-liner, in general, creating any single item is simply a matter of parsing its data into a byte slice and returning one of these structs. We can then begin composing these items into larger pieces of data for more convenient use.

The next step is to build an entire frame for transmission to Apple. A frame is simply a list of items, all serialized next to each other and wrapped with a few more bytes’ worth of header data. We can take a similar approach for building frames as we did for items: create a struct that contains a slice of Item structs, then implement a Bytes() function for that struct that recursively encodes all the contained Items and adds the required header data.

Strictly speaking, this is all our push provider needs to start sending notifications! We can construct frames full of items, serialize them, and send them to Apple. However, we’ll probably want a little more metadata internally to help our notifications along. At the very least, we’ll need to know the bundle ID associated with each notification – since the connection we establish to APNs is specific to one app’s bundle ID, and we’re providing push services for multiple apps, we’ll need a way to distinguish between different notifications so we know which connection to use.

To handle this, we wrap up our Frame in one more top-level struct: the Notification. All this struct does is provide information that our provider tracks internally – it doesn’t send anything extra to Apple. We define it as:

type Notification struct {
    Frame *Frame
    BundleID string
    UserInfo map[string]interface{}
}

This last field is a little quirky: it maps strings to interface{}, which is Go’s “anything” type. With this field, the provider yields a bit to Objective-C – we can attach any extra bits of data that we want internally in this UserInfo map, then pull them back out later. We’ll see an example of how this is used in a future post.

At this point, we’re ready to start talking to Apple! We have representations of all the different pieces of data that the APNs binary interface expects, at least for sending notifications. In the next post, we’ll explore how the push provider connects to the APNs gateway.