iOS Live Activities using Elixir and Pigeon

Straight to the code? If just want to see how to use Pigeon and alread have everything setup.

Here is the code!

I am currently working on an iOS App that consumes an Elixir (Phoenix) backend. For one feature, we are currently implementing LiveActivites to give the users an immediate feedback directly on their lock screens. Because the data for our feature is driven by the server, we need a way to send updates from the server to the LiveActivity to keep them in sync.

This is possible through the Apple Push Notification service (APNs) by sending a push notification that gets routed straight to the LiveActivity. This article focuses on the few small details that are needed on the server side to make this work using Pigeon. For a general setup for both the Elixir / Phoenix site and the LiveActivity / iOS app, here are some useful links that helped us get started:

Pigeon Setup

As mentioned, I won't go into full detail about setting up all the parts, see the docs / links above. The only requirement for Live Activities is that you are sending Push Notifications using a push token, not the "regular" certificate-based system. You can see how to configure Pigeon to use a push token here under token-based authentication.

Once you can send push notifications using Pigeon using the token-based approach, the only missing thing is to create a Notification suitable for updating a live view. For all details on which parameters you can send, see Apple | Live Activities Updates via push. The basic requirements are:

  • set the topic to the following format <your bundleID>.push-type.liveactivity
  • set the push_type to liveactivity
  • inside the JSON payload set a timestamp, event and content-state

We could modify the topic and payload by updating a Notification created with the new/3 function from Pigeon. But the push_type is at the time of writing default set to alert and cannot be changed directly. This is why we need to create the struct from scratch, doing this, we can set all the values as needed.

Pigeon Code

notf = %Pigeon.APNS.Notification{
  id: nil,
  device_token: token,
  topic: "<your bundleID>.push-type.liveactivity",
  expiration: nil,
  priority: nil,
  push_type: "liveactivity",
  payload: %{
    "aps" => %{
      "timestamp" => System.system_time(:second),
      "event" => "update",
      "content-state" => %{
        "value" => 7

The example above creates a simple Pigeon.APNS.Notification setting the token, topic, push_type and payload all at once. You could also use the utility functions provided by Pigeon like put_badge/2 to update the notification. In our testing, we had some errors with different expiration and priority values, but the ones above worked great.

With a notification setup like this, and if needed updates using some utility functions from Pigeon, you can send the notification using Pigeon.APNS.send/2 and the notification will be delivered to the LiveActivity. Make sure your content-state is valid and the expected format you defined in your Swift code, if it is different, you should see an error in the Xcode console.

Example Swift Code

Below a simple example of a Live Activity that is basically the 2 Apple tutorials from above combined and adapted to work with the above notification example.

struct ExampleWidgetsAttributes: ActivityAttributes {
    public struct ContentState: Codable, Hashable {
        // Dynamic stateful properties about your activity go here!
        var value: Int
    // Fixed non-changing properties about your activity go here!
    var name: String
struct ExampleWidgetsLiveActivity: Widget {
  var body: some WidgetConfiguration {
      ActivityConfiguration(for: ExampleWidgetsAttributes.self) { context in
          VStack {
              Text("\( \(context.state.value)")
if ActivityAuthorizationInfo().areActivitiesEnabled {
    let initialContentState = ExampleWidgetsAttributes.ContentState(value: 0)
    let activityAttributes = ExampleWidgetsAttributes(name: "Maxime")
    let activityContent = ActivityContent(state: initialContentState, staleDate: .minute, value: 5, to: Date())!)
    let activity = try? Activity.request(attributes: activityAttributes, content: activityContent, pushType: .token)
    if let token = activity?.pushToken {
        let unwrappedToken = { String(format: "%02.2hhx", $0) }.joined()
        print("LiveActivity Token: \(unwrappedToken)")
    } else {
        print("Token not found!")
    Task {
        guard let activity = activity else { return }
        for await data in activity.pushTokenUpdates {
            let token = { String(format: "%02.2hhx", $0) }.joined()
            print("LiveActivity token update: \(token)")

We had and still have some errors where the pushToken is not returned, but the pushTokenUpdates stream is working fine. If you have any idea why this is happening, please shoot me a mail and let me know.