Skip to content
kylebrowningPublic

About

📱HTTP/2 Apple Push Notification Service built with swift - send push notifications to iOS, iPadOS, tvOS, macOS, watchOS, visionOS, and Safari!

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

844 stars

Watchers

15 watching

Forks

sswg:graduated|94x20 Build Documentation

APNSwift

A non-blocking Swift module for sending remote Apple Push Notification requests to APNS built on AsyncHttpClient.

Installation

To install APNSwift, just add the package as a dependency in your Package.swift.

dependencies: [
    .package(url: "https://github.com/kylebrowning/APNSwift.git", from: "8.0.0"),
]

Getting Started

APNSwift aims to provide semantically correct structures to sending push notifications. You first need to setup a APNSClient. To do that youll need to know your authentication method

let client = APNSClient(
    configuration: .init(
        authenticationMethod: .jwt(
            privateKey: try .init(pemRepresentation: privateKey),
            keyIdentifier: keyIdentifier,
            teamIdentifier: teamIdentifier
        ),
        environment: .development
    ),
    responseDecoder: JSONDecoder(),
    requestEncoder: JSONEncoder()
)

// Shutdown the client when done
try await client.shutdown()

Running as a service

With the ServiceLifecycleSupport trait, which is enabled by default, APNSClient, APNSBroadcastClient and APNSClients conform to swift-service-lifecycle's Service. Add a client to your ServiceGroup and it is shut down when the group shuts down, so you never call shutdown() yourself. This is how you use APNSwift from Hummingbird, Vapor 5, or any other ServiceGroup based app.

Create the client once at startup:

let client = APNSClient(
    configuration: .init(
        authenticationMethod: .jwt(
            privateKey: try .init(pemRepresentation: privateKey),
            keyIdentifier: keyIdentifier,
            teamIdentifier: teamIdentifier
        ),
        environment: .production
    ),
    eventLoopGroupProvider: .shared(MultiThreadedEventLoopGroup.singleton),
    responseDecoder: JSONDecoder(),
    requestEncoder: JSONEncoder()
)

let notification = APNSAlertNotification(
    alert: .init(title: .raw("Hello")),
    expiration: .immediately,
    priority: .immediately,
    topic: "com.example.app",
    payload: EmptyPayload()
)

Hummingbird

import APNS
import Hummingbird

let router = Router()
router.post("devices/:token/notify") { request, context in
    let token = try context.parameters.require("token")
    try await client.sendAlertNotification(notification, deviceToken: token)
    return HTTPResponse.Status.accepted
}

var app = Application(router: router)
app.addServices(client)
try await app.runService()

Vapor 5

import APNS
import Vapor

let app = try Application()
app.post("devices", ":token", "notify") { req in
    let token = try req.parameters.require("token")
    try await client.sendAlertNotification(notification, deviceToken: token)
    return HTTPResponse.Status.accepted
}

app.addService(client)
try await app.start()

Other apps

Add the client to your own ServiceGroup:

let serviceGroup = ServiceGroup(
    services: [server, client],
    gracefulShutdownSignals: [.sigterm],
    logger: logger
)
try await serviceGroup.run()

Without the trait

If you don't want the dependency, disable the trait and shut clients down yourself:

.package(url: "https://github.com/kylebrowning/APNSwift.git", from: "8.0.0", traits: []),

Production and development clients

A device token only works against the environment the app was built for, so most servers need a production client and a development client. APNSClients holds a set of clients looked up by ID and registers both from one key:

let apns = APNSClients()
apns.configure(.jwt(
    privateKey: try .init(pemRepresentation: privateKey),
    keyIdentifier: keyIdentifier,
    teamIdentifier: teamIdentifier
))

try await apns.client(.development).sendAlertNotification(notification, deviceToken: token)
try await apns.client(.production).sendAlertNotification(notification, deviceToken: token)

APNSClients is also a Service, so add it to your group instead of the individual clients. For custom setups, use(_:as:) registers a client under any ID and default(to:) picks which one client returns.

Sending a simple notification

All notifications require a payload, but that payload can be empty. Payload just needs to conform to Encodable

struct Payload: Codable {}

try await client.sendAlertNotification(
    .init(
        alert: .init(
            title: .raw("Simple Alert"),
            subtitle: .raw("Subtitle"),
            body: .raw("Body"),
            launchImage: nil
        ),
        expiration: .immediately,
        priority: .immediately,
        topic: "com.app.bundle",
        payload: Payload()
    ),
    deviceToken: "device-token"
)

Sending Live Activity Start

To successfully start a live activity:

  • Attributes and ContentState must match the live activity attributes provided in attributesType
  • alert must contain a title, body, and sound
    • title and body are visible to the device's paired Apple Watch when starting the live activity
let response = try await client.sendStartLiveActivityNotification(
    .init (
        expiration: .immediately,
        priority: .immediately,
        appID: "com.app.bundle",
        contentState: contentState,
        timestamp: Int(Date().timeIntervalSince1970),
        attributes: attributes,
        attributesType: "YourActivityAttributes",
        alert: .init(
            title: .raw("Your title"),
            body: .raw(Your body),
            sound: .fileName("default.aiff")
        )
    ),
    pushToStartToken: pushToStartToken
)

Sending Live Activity Update / End

It requires sending ContentState matching with the live activity configuration to successfully update activity state. ContentState needs to conform to Encodable and Sendable.

try await client.sendLiveActivityNotification(
    .init(
          expiration: .immediately,
          priority: .immediately,
          appID: "com.app.bundle",
          contentState: ContentState,
          event: .update,
          timestamp: Int(Date().timeIntervalSince1970)
    ),
    deviceToken: activityPushToken
)
try await client.sendLiveActivityNotification(
    .init(
          expiration: .immediately,
          priority: .immediately,
          appID: "com.app.bundle",
          contentState: ContentState,
          event: .end,
          timestamp: Int(Date().timeIntervalSince1970),
          dismissalDate: .immediately // Optional to alter default behaviour
    ),
    deviceToken: activityPushToken
)

Authentication

APNSwift provides two authentication methods. jwt, and TLS.

jwt is preferred and recommend by Apple These can be configured when created your APNSClientConfiguration

Notes: jwt requires an encrypted version of your .p8 file from Apple which comes in a pem format. If you're having trouble with your key being invalid please confirm it is a PEM file

openssl pkcs8 -nocrypt -in /path/to/my/key.p8 -out ~/Downloads/key.pem

Server Example

Take a look at Program.swift

iOS Example

For an iOS example, open the example project within this repo.

Once inside configure your App Bundle ID and assign your development team. Build and run the ExampleApp to iOS Simulator, grab your device token, and plug it in to server example above. Background the app and run Program.swift

Original pitch and discussion on API

About

📱HTTP/2 Apple Push Notification Service built with swift - send push notifications to iOS, iPadOS, tvOS, macOS, watchOS, visionOS, and Safari!

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

844 stars

Watchers

15 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages