Written by Natalia Kurek
Published March 14, 2024

Creating Real-Time News Experience with iOS Live Activities

 

Introduction

As iOS users, we are all familiar with them. When we buy a cup of coffee using one of the ordering apps, they pop up. Live activities show on the lock screen and dynamic island. They display the most recent data from our apps and update their content.

But are they exclusive to delivery apps? I don’t think so. In the fast-paced world of news media, there’s a place for them. Could live activities revolutionize how we consume news? We’ll check it out!

In this article, we will focus on live activities from a technical perspective. But don’t be overwhelmed by it. I prepared for you some other content you can enjoy: troubleshooting tips, testing, limitations, and some cool live activity screenshots. It doesn’t matter if you are an iOS developer, designer, or anyone else. If you are looking for a live activities inspiration – this is a good place.

 

How can we use it in news apps?

In news apps, there are many ways we can use them. In this article, we will focus on adding live activity to the live blog.

A live blog gives real-time updates on events. Imagine for example a popular artist’s concert or a presidential election. Journalists constantly update the live blog to keep users updated. Our live activity will display the two most recent pieces of information.

Now, let’s imagine this story.

Warner Bros decided to produce a Harry Potter TV series. Fans would go crazy about it! Then there is a big press event for reporters where all the details will be revealed. Harry Potter fans won’t have to do anything to be updated. As soon as there is a new detail, it will appear on their lock screen and dynamic island.

In this article, we’ll use that Harry Potter example to show how users can get immediate updates about things they care about, even during the busiest moments of their day.

 

Constraints

Given that we’re all part of the Apple ecosystem, it’s not surprising for us that Live Activities come with limitations.

These limitations start with the ActivityKit framework, which is available from iOS 16.1 onwards. Live Activities pop up on compatible devices, appearing on the dynamic island and at the top of the lock screen notifications list. For devices that don’t have a dynamic island, they appear on the lock screen only.

The only way to update live activity UI is by ActivityKit framework or by push notifications. It means we cannot perform any operations inside live activity e.g. network requests and then update the UI.

The live activity image size must not exceed the whole size of the presentation mode it’s displayed in. In minimal presentation mode, the image must have the maximum size of the minimal presentation view. If it’s bigger, it won’t be displayed.

Live activities have a maximum lifespan. If the user or the app doesn’t end the live activity, it remains visible for 8 hours. After that, the system removes them from the dynamic island. On the lock screen, the live activity can be displayed for an additional 4 hours if the user doesn’t manually remove it.

The live activity dynamic data, which represents the updated data, must be kept small, with a maximum size of 4 KB.

 

Before our hands get dirty: Initial set up

The first step in introducing the live activity to our app is to create the widget extension. If you already have it – you can use it for live activities.

To create a new widget extension, follow these steps: File → New → Target → Widget extension.

 

The next step is to look into the Info.plist configuration. The Support live activities option in the Widget target and App target has to be enabled.

Support Live Activities configuration in Xcode

Our live activity supports push notifications updates. We have to make sure that push notifications are configured properly. We won’t cover this topic, so if you have any doubts, don’t hesitate to refer to this Apple’s article: https://developer.apple.com/documentation/usernotifications/registering_your_app_with_apns

 

What to display?

In our scenario, the goal is to display the most recent data from a big press event about the Harry Potter TV series. For our live activity, this data consists of: title, category name, start time, brand logo, number of unread live blog entries, and the two newest entries.

To display this data we create a struct LiveBlogAttributes that conforms to ActivityAttributes protocol. Within the live activity we have two kinds of data: 

  • Static data – once is displayed never changes.
  • Dynamic data – can be updated.

The dynamic data is defined by using an inner type of the ActivityAttributes protocol, which we call ContentState. This ContentState type needs to be a struct that conforms to the Codable and Hashable protocols. So, to handle dynamic data, we create a struct of this type within the implementation of ActivityAttributes.

The static data is any information within LiveBlogAttributes that isn’t part of the ContentState.

In our case, we want to have static: title, category name, and start time. The rest of the data should be dynamic. This is what our live activity data representation looks like:

struct LiveBlogAttributes: ActivityAttributes {
    let title: String
    let categoryName: String
    let startTime: String 
   
   struct ContentState: Codable, Hashable{
        let numberOfUnreadEntries: Int
        let entries: [LiveBlogEntry]
   }
 
    struct LiveBlogEntry: Codable, Hashable {
        let id: String
        let time: String
        let description: String
    }
}

Building live activity complete UI

Now, let’s go over the process of creating the live activity interface. Our goal is to display updates about the Harry Potter TV series directly on each fan’s lock screen and dynamic island. Hence, let’s walk through the steps to create this presentation for both locations.

Dynamic island

Dynamic island has three presentation modes:

  • Compact – This is the standard presentation. Live activity occupies leading and trailing space between the true depth camera. 
  • Minimal – If multiple live activities are running, the system decides which two live activities display. One is displayed on the leading side, attached to the dynamic island. The second is displayed on the trailing side, detached from the dynamic island.
  • Expanded – If the compact or minimal mode is long-tapped this mode is shown.


Live activity compact presentation


Live activity minimal presentation on the leading side (the microphone icon is the live activity from another app)


Live activity minimal presentation on the trailing side (the microphone icon is the live activity from another app)


Live activity expanded presentation

Let’s discuss how to build all those presentations. We create a structure that conforms to Widget protocol. Let’s name it LiveBlogLiveActivity. To set up presentations we use the ActivityConfiguration structure and specify ActivityAttributes type, in our case, it’s LiveBlogAttributes. As you can see in the code, each presentation has access to context. This gives us static and dynamic data of the live activity.

struct LiveBlogLiveActivity: Widget {

    var body: some WidgetConfiguration {
        ActivityConfiguration(for: LiveBlogAttributes.self) { context in
            //Lock screen presentation
        } dynamicIsland: { context in
            // Expanded presentation displayed after a long tap on the compact or minimal 
        } compactLeading: {
            //Compact presentation displayed on the leading side
        } compactTrailing: {
            // Compact presentation displayed on the trailing side
        } minimal: {
            // Minimal presentation displayed on the trailing or leading side
        }
    }
}

We can specify how our live activity looks when expanded, compact and minimal. We define the compact view using the compactLeading and compactTrailing closures. We should pass the SwiftUI view declarations here. It’s the same for the minimal presentation – we defined it within the minimal closure. 

For the expanded presentation, things get a little more complex. We divide the view into different regions and decide what goes where. For example, we might want to put our brand logo in one region and the main content in another. Check out those regions in the image below.

Source: https://developer.apple.com/documentation/activitykit/displaying-live-data-with-live-activities

We use the DynamicIslandExpandedRegion structure. In this structure, we define the region of the view and its priority. If a view has the highest priority, it gets stretched out and takes the whole width of the live activity. In the case of a live blog, we use the leading region for displaying the brand logo and the bottom region for the rest of the data. 

struct LiveBlogLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: LiveBlogAttributes.self) { context in
            // Lock screen presentation
        } dynamicIsland: { context in
            dynamicIsland(context: context)
        }
    }

    func dynamicIsland(context: ActivityViewContext) -> DynamicIsland {
        DynamicIsland {
            DynamicIslandExpandedRegion(.leading) {
                Image(.aftonbladetLogo)
                    .resizable()
                    .scaledToFit()
                    .frame(width: 100.0)
                    .fixedSize(horizontal: false, vertical: true)
            }
            DynamicIslandExpandedRegion(.bottom) {
                  VStack(alignment: .leading, spacing: .zero) {
                    Text(context.attributes.title)
                        .font(.system(size: 17.0, weight: .bold))
                    ForEach(context.state.entries) { entry in
                        LiveBlogEntry(entry: entry, showTimeline: true)
                    }
                    .padding(.top, 6.0)
                }
            }
        } compactLeading: {
            Image(.aftonbladetMiniLogo)
        } compactTrailing: {
            liveDot(text: "\(context.state.numberOfUnreadEntries)\(String.plus)")
        } minimal: {
            liveDot(text: .plus)
        }
        .contentMargins(.all, 24.0, for: .expanded)
    }
    private func liveDot(text: String) -> some View {
        HStack(alignment: .center, spacing: 2.0) {
            Image(.live)
            Text(text)
        }
        .foregroundColor(.darkRed)
    }
}

struct LiveBlogEntry: View {
    let entry: LiveBlogAttributes.LiveBlogEntry

    var body: some View {
        HStack(alignment: .top, spacing: .zero) {
            LiveBlogTime(time: entry.time, showTimeline: true)
            Text(entry.description)
                .font(.system(size: 13.0))
                .padding(.leading, 4.0)
            Spacer()
        }
        .fixedSize(horizontal: false, vertical: true)
    }
}

struct LiveBlogTime: View {
    let time: String
    let showTimeline: Bool

    var body: some View {
        HStack(alignment: .top, spacing: 4.0) {
            circleWithLine
            Text(time)
                .foregroundColor(.gray)
                .font(.system(size: 13.0))
                .fixedSize(horizontal: true, vertical: false)
            Spacer()
        }
        .frame(width: 54.0, alignment: .leading)
    }

    private var circleWithLine: some View {
      VStack(spacing: .zero) {             
          Circle()                 
              .foregroundColor(.darkRed)               
              .frame(width: 12.0, height: 12.0)             
          if showTimeline {                 
              Rectangle()                      
                  .foregroundColor(.gray)
                  .frame(width: 1.0)                  
                  .padding(.top, 2.0)             
        }         
    }         
    .offset(y: 2.0)
  }
}

Here is the live activity transition from compact to expanded mode:

Lock screen

The lock screen presentation is just a SwiftUI view declaration. We create the LockScreen SwiftUI view. The LockScreen structure has access to the context instance, obtained during the creation of the ActivityConfiguration.

struct LockScreen: View {
    let context: ActivityViewContext
    
    var body: some View {
        VStack(alignment: .leading, spacing: .zero) {
            topView(title: context.attributes.categoryName)
            LiveBlogTime(time: context.attributes.startTime, showTimeline: false)
                .padding(.top, 4.0)
            Text(context.attributes.title)
                .font(.system(size: 17.0, weight: .bold))
                .padding(.bottom, 2.0)
            ForEach(context.state.entries) { entry in
                LiveBlogEntry(entry: entry)
            }
            .padding(.top, 6.0)
        }
        .padding(.all, 16.0)
    }

    private func topView(title: String) -> some View {
        HStack(alignment: .center) {
            Text(title)
                .foregroundColor(.darkRed)
                .font(.system(size: 10.0, weight: .semibold))
            Spacer()
            Image(.aftonbladetLogo)
                .resizable()
                .scaledToFit()
                .frame(width: 100.0)
                .fixedSize(horizontal: false, vertical: true)
        }
    }
}

Live activity lock screen presentation

 

Integration of Live Activity in the widget extension 

All the presentations are ready. We can add them to the LiveBlogLiveActivity body:

struct LiveBlogLiveActivity: Widget {

    var body: some WidgetConfiguration {
        ActivityConfiguration(for: LiveBlogAttributes.self) { context in
            LockScreen(context: context)
        } dynamicIsland: { context in
            dynamicIsland(context: context)
        }
    }
}

We need to ensure that the widget extension recognises our newly created widget.

If the widget is the only one in the extension, we can add the @main attribute to it. This informs the extension that the widget is the only one and serves as the starting point. In case of multiple widgets, we create a WidgetBundle. This bundle comprises all the widgets we have, including the live activity one.

@main
struct WidgetExtension: WidgetBundle {  
 
    @WidgetBundleBuilder
    var body: some Widget {
        ElectricityPriceWidget()        
        LiveBlogLiveActivity()
   }
}

Managing Live Activity lifecycle

Now, imagine that during this big press event, new information about the Harry Potter TV series is revealed. They announced the casting. Journalists share this information quickly. Let’s take a closer look at how we can share it on fans’ devices immediately. 

Managing the lifecycle of a live activity involves starting, updating, and ending. This can be done within the app using the ActivityKit framework or through push notifications. 

For devices running iOS 17.2 and newer, we can start a live activity through push notifications. On older iOS versions, it can only be started using the ActivityKit framework.

Using ActivityKit framework for lifecycle operations

To start a live activity with the ActivityKit framework, we use the request function from the Activity class. We provide the static data as the first parameter. The second parameter is the contentState, which is the dynamic data initial state. Lastly, we provide the optional pushType parameter. If we want to enable push notifications updates, it should be set to PushType.token

let title = "Warner Bros vill göra Harry Potter-serie"
let attributes = LiveBlogAttributes(title: title, categoryName: "Senaste nytt", startTime: "09.00")
let initialState = LiveBlogAttributes.ContentState(
    numberOfUnreadEntries: 1,
    entries: [
        LiveBlogAttributes.LiveBlogEntry(
            id: "id1", 
            time: "11.09",
            description: "Bakom kulisserna: Potentiella wändor och karaktärer."
        )
    ]
)
let activity = try Activity.request(attributes: attributes, contentState: initialState, pushType: .token)

We should use the update function on the activity instance to update the live activity. We got it when the live activity started. When updating, the new dynamic data should be specified.

let contentState = LiveBlogAttributes.ContentState(
    numberOfUnreadEntries: 12,
    entries: [
        LiveBlogAttributes.LiveBlogEntry(
            id: "id1", 
            time: "11.09",
            description: "Bakom kulisserna: Potentiella wändor och karaktärer."
        ),
        LiveBlogAttributes.LiveBlogEntry(
            id: "id2",
            time: "09.23",
            description: "Rollrykten! Vem som ska spela dina favoritkaraktärer?"
        )
    ]
)

await activity.update(using: contentState)

The end can be done by calling the end on the activity instance. You can also provide the final state for the activity, which will be shown to the user before the system removes the live activity.

The dismissal policy determines when the live activity gets removed. The default dismissal policy allows the live activity to remain on the lock screen for a maximum of 4 hours if not manually removed by the user.

Additionally, you can specify the after(_ date: Date) policy, which removes the live activity at a specified date but within a 4-hour window.

let contentState = LiveBlogAttributes.ContentState(
    numberOfUnreadEntries: 24,
    entries: [
        LiveBlogAttributes.LiveBlogEntry(
            id: "liveBlogEntryId2",
            time: "11.09",
            description: "Bakom kulisserna: Potentiella wändor och karaktärer."
        ),
        LiveBlogAttributes.LiveBlogEntry(
            id: "liveBlogEntryId1",
            time: "09.23",
            description: "Rollrykten! Vem som ska spela dina favoritkaraktärer?"
        )
    ]
)

await activity.end(using: contentState, dismissalPolicy: .immediate)

Suppose the live activity was initiated through the ActivityKit, but we don’t have the activity instance in the current context. In that case, it’s possible to find this activity instance by iterating through the app’s current live activities of the given type:

for activity in Activity<LiveBlogAttributes>.activities {
    await activity.end(dismissalPolicy: .immediate)
}

 

Using push notifications for lifecycle operations

The real magic of live activities is the ability to control them using push notifications.

We don’t have to do anything special when a device receives the live activity push notification.  The system manages it automatically. Our sole responsibility is to  closely cooperate with the backend team to ensure that push notifications have properly configured payload and headers.

Push token

We need to send the special push token to our backend to receive live activity updates. After starting the live activity using the ActivityKit, receiving this push token is feasible. The activity instance has an asynchronous sequence, pushTokenUpdates, which emits the push token every time it is updated.

let activity = try Activity.request(attributes: attributes, contentState: contentState, pushType: .token)
for await pushToken in activity.pushTokenUpdates {
    // Here, we have the live activity push token, which needs to be sent to the backend.
}

Once we receive the new push token, we should send it to our backend. It’s important to note that this push token can change during the live activity’s lifecycle, so we need to keep observing the push token updates and send it every time it changes.

Push token on iOS 17.2 

On devices with iOS 17.2 or newer, it’s possible to obtain the push token without starting the live activity. To do so, we need to observe the pushToStartTokenUpdates asynchronous sequence. Once we receive it and send it to the backend, it’s possible to start the live activity anytime using push notification!

for try await pushToken in Activity<LiveBlogAttributes>.pushToStartTokenUpdates {
    // Here we can send the push token to the backend
}

The push token is now in the hands of the backend team. They can now update, end, and start (iOS 17.2) the live activity by simply sending the proper payload and headers in the push notification request

Push notification paylods

Although my primary focus is on the iOS app, and my backend expertise may be limited, it remains important to understand how the whole system works. That’s why we will talk about what should be sent in the request payload to Apple Push Notification service (APNs).

Below, you can see is the payload needed to start a live activity (for iOS 17.2).

  • The event field has a start value.
  • The content-state field should exactly match the LiveBlogAttributes.ContentState struct defined in our code.
  • The attributes-type field defines the type of live activity we would like to start; in our case, it’s LiveBlogAttributes.
  • The alert section makes the device highlight like a standard notification. It’s recommended to  include it when starting the live activity to grab user attention that new live activity started.
{
   "aps": {
      "timestamp":1234,
      "event": "start",
      "content-state": {
         "numberOfUnreadEntries": 100,
         "entries": [
            {
               "id": "liveBlogEntryId1",
               "time": "09.23",
               "description": "Rollrykten! Vem som ska spela dina favoritkaraktärer?",
            },
            {
               "id": "liveBlogEntryId2",
               "time": "11.09",
               "description": "Bakom kulisserna: Uppenbarade potentiella handlingar och karaktärer."
            }
         ]
      },
      "attributes-type": "LiveBlogAttributes",
      "attributes":{
         "title": "Warner Bros planerar Harry Potter-serie.",
         "categoryName":  "Senaste nytt",
         "startTime": "09.00"
      },
      "alert": {
         "title":  "Warner Bros planerar Harry Potter-serie.",
         "body": "Bakom kulisserna: Potentiella wändor och karaktärer."
      }
   }
}

Below, you can see the payload for updating the live activity.

  • The event field has an update value.
  • The vital thing to note is that the timestamp has to change with each new notification. If we keep sending live activity push updates with the same timestamp, the system won’t update live activity.
  • The content-state field should exactly match the LiveBlogAttributes.ContentState struct defined in our code.
  • There is no need to send attributes representing the static and attributes-type data.
  • It’s good to send the alert when a crucial event grabs the user’s attention by highlighting the device.
{
   "aps":{
      "timestamp":1234,
      "event":"update",
      "content-state":{
         "numberOfUnreadEntries":100,
         "entries":[
            {
               "id":"liveBlogEntryId2",
               "time":"11.09",
               "description":"Bakom kulisserna: Uppenbarade potentiella handlingar och karaktärer."
            },
            {
               "id":"liveBlogEntryId3",
               "time":"12.00",
               "description":"Överraskande fakta! Läs om magiska platser i den nya Harry Potter TV-serien."
            }
         ]
      },
      "alert":{
         "title": "Warner Bros planerar Harry Potter-serie.",
         "body": "Bakom kulisserna: Potentiella wändor och karaktärer."
      }
   }
}

Finally, you can check the payload for ending the live activity here

  • The field event has an end value.
  • The content-state field contains the final live activity state that will be displayed before ending by the system.
  • The dismissal-date field defines when the system should remove the live activity. To remove the live activity immediately, the dismissal date value must be in the past. If no date is provided, it will be removed within a 4-hour window. If the provided date is more than 4 hours later, the date will be ignored, and the removal will occur after 4 hours.
{
   "aps": {
      "timestamp": 1234,
      "event": "end",
      "dismissal-date": 1663177260,
      "content-state": {
         "numberOfUnreadEntries": 100,
         "entries": [
            {
               "id": "liveBlogEntryId2",
               "time": "11.09",
               "description": "Bakom kulisserna: Uppenbarade potentiella handlingar och karaktärer."
            },
            {
               "id": "liveBlogEntryId3",
               "time": "12.00",
               "description": "Överraskande fakta! Läs om magiska platser i den nya Harry Potter TV-serien."
            }
         ]
      }
   }
}

Push notifications headers

To identify the push notification request as a live activity, it must include special headers, and the key headers include: 

  • apns-topic: This should be set to <app_bundle_id>.push-type.liveactivity. 
  • apns-push-type: Set this to liveactivity. 
  • apns-priority: This determines the priority of the push notification. To deliver it immediately, set it to 10. 
  • authorization: This is the authorisation token for the push notification.

 

Testing live activity push notifications

All right, we’ve got everything set up, but the backend team hasn’t finished their work yet. How do we know if the live activity push notifications are actually working?

We can’t just use fancy tricks like APNs files or xcrun simctl push commands. These notifications need specific information in the request headers to be handled properly by the system.

There is no short way, we have to make a real request to the APNs. One way is to make a request to APNs using curl command. That means we have to get our hands dirty preparing the authorization token. You can find more about that process here: https://developer.apple.com/documentation/usernotifications/sending_push_notifications_using_command-line_tools

There is also a quicker way. Apple gifted us with the Apple Push Notifications Console. It’s a  tool that lets us test different notifications, including live activity ones. Right now, that’s good enough for our testing needs.

https://icloud.developer.apple.com/dashboard/notifications/.

Apple Push Notifications Console

You need to login and then select Send on the top navigation bar. After that, you can see the form for sending push notifications.

Let’s look at those fields and give them the proper values:

  • Device Token – This is the push token we get when observing push token updates, it was described earlier. 
  • apns-topic – This field should be the application bundle id.
  • apns-push-type – This should be set to liveactivity. 
  • apns-push-priority – This determines the priority of the push notifications, it’s better to set it to high to ensure the push notification is delivered immediately.
  • payload – In this text field we should just paste the live activity push notification payload with proper content-state value.

Now, press that send button and let’s see the live activity update in action!

 

Troubleshooting

While working on this feature, I encountered issues with live activities not starting or updating. I’ll give you potential solutions and hints on how to tackle these issues.

Live activity is not starting

You’ve called the request function from the ActivityKit framework, but the live activity fails to appear on the lock screen and/or dynamic island.  

First, let’s make sure that the device or simulator we’re using has iOS version 16.1 or later. 

If the iOS version is correct, let’s check if the Support live activities option is enabled for both the widget target and the app target.

If this option is enabled, let’s also check if Live Activities in the app settings are enabled. When the live activity appears for the first time, the system asks you to enable live activities. If you missed it, the option might be turned off. Just turn it on.

Live Activities settings

If that doesn’t do the trick, let’s look at the data we’re trying to show. If we’re using images, make sure they’re not too big. If your image is bigger than the whole available space, the live activity might not show up.

 

Push notifications are not updating live activity

Your live activity is running. Then you send a push notification update and the live activity is not updated.  

There are three potential reasons for this issue: 

  • the request to APNs is incorrect,
  • there is a mistake in the ContentState structure defined in the app,
  • something is wrong with your device (notifications disabled, poor network connection, low battery).

First, let’s check the response we get from APNs. There’s an Apple’s article about APNs responses: https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/handling_notification_responses_from_apns

You can see on the image below that we got an error in the Apple Push Notifications Console – the device token has expired. We need to get a new token. 

Apple Push Notification Console – error message

The Apple Push Notifications Console has another feature – it allows device token validation.

To do so, you go to the Apple Push Notification Console, click on Tools, and then select Device Token Validator.

Apple Push Notification Console – Device Token Validator

If it’s valid, it will show you what type of push notifications it’s valid for, in our case its Live Activity.

Now, if your token is valid but you’re still not receiving the 200 status code response from APNs, you should double-check other headers like: apns-topic and apns-push-type.

The Apple Push Notifications Console displays a delivery log if the request succeeds. The image shows that the request was successful, but the device was offline then at that time. Instead of being delivered right away, it was stored until the device was back online.

Apple Push Notifications Console – Delivery Log

If you see the delivery message saying: Successfully delivered to the target device, but your live activity still isn’t updating, something can be wrong with ContentState structure. This structure should exactly match the content-state from the push notification payload.

If that didn’t help, we should look at the timestamp from the push notification payload. This part is easy to miss. Each timestamp should be different. The next push notification should have a later timestamp. If the timestamps aren’t set up this way, the live activity won’t update properly.

If the issue persists, let’s take a look at the priority from the push notification request. We need to make sure that the notification has a high priority, which is 10. I tested sending live activity updates with a low priority, but I never received them. According to Apple’s documentation: Notifications with apns-priority 5 and 1 might get grouped and delivered in bursts to the user’s device.

We reviewed several aspects. If the problem is still ongoing double-check the device isn’t in focus mode and that notifications are turned on. Check your internet connection. Troubleshooting live activities can feel like a puzzle with pieces that don’t fit together. But don’t worry! Just take it step by step!

 

Conclusions

The goal was to boost the news app user experience. We demonstrated how we can keep users updated about important things, even during their busiests moments. To achieve this, we gave users instant updates on the lock screen and dynamic island.

Fortunately, the development wasn’t too complicated. The main challenges involved close collaboration with the backend team and the integration with Apple Push Notification service. But when you look at what we get in return, it’s definitely worth a shot. Now, it’s time to see if users love it! 

In this article, we discussed the live activities basics. If you’re an iOS developer, you might have questions popping up. How do you create a scalable solution for multiple live activities? How do you handle widget extensions elegantly? And what about animations? There are many other topics I didn’t cover. If after reading it your live activities are running now, I’m satisfied. Maybe we’ll discuss these topics someday. For now, I encourage you to make your own experiments and investigations. Don’t forget to share them!

Before we finish, a big shoutout to our designer, Niclas, for creating the beautiful UI you’ve seen throughout!

 

Sources:

https://developer.apple.com/documentation/activitykit/displaying-live-data-with-live-activities

https://developer.apple.com/documentation/activitykit/starting-and-updating-live-activities-with-activitykit-push-notifications

Written by Natalia Kurek
Published March 14, 2024