Click here to Skip to main content
15,885,032 members
Articles / Mobile Apps / iOS

How to Add Live Activity to Your Existing Project - Part I

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
5 Jan 2023CPOL8 min read 11.8K   3   1
First part of the ultimate guide on how to add a Live Activity feature to your project
iOS developer Azizbek Ismailov told how his team added Live Activity to the inDrive iOS app. He also answered all the questions the teammates had during the development process.

Image 1

The Live Activity feature was presented by Apple when launching iOS 16 and the new iPhone with Dynamic Island. The upgrade is only available to those who have upgraded to iOS 16.1.

My name is Aziz and I work as an iOS developer at inDrive. In this article, I will tell you how we added Live Activity to our iOS app. Here, I’ll try to answer all the questions that we had during the development process.

Why Add Live Activity in the First Place?

The basic idea is that, as a user, you don’t have to open the app whenever you need to check crucial relevant information at a particular point in time.

Here’s a simple example of how your Live Activity works. Let’s say you have ordered a service. In addition to the general status of “Your order is on the way,” a widget will be displayed on your lock screen with all the necessary information such as order status, delivery time, details about the courier/driver, etc.

Если у вас iPhone 14 Pro или 14 Pro Max, вам доступна функция Dynamic Island

The Dynamic Island feature is available to you if you have an iPhone 14 Pro or 14 Pro Max.

При нажатии на фичу появляется информационный блок. В нашем случае это статус поездки, сколько до вас ехать водителю, номер и цвет машины

The relevant information block appears once you click on the feature. In our case, this is the status of the trip, how far away you are from the driver, and the vehicle’s registration number and color.

Adding to Production

There are many articles out there that describe the process of adding Live Activity to a project. The only difference is that none of them shares the hands-on experience of adding the feature to a current project. That is what I want to tell you about in this article.

Our process started off with a demonstration of the Live Activity feature to businesses. The point was to “sell” the idea. We created a demo app loosely based on the relevant articles and official documentation. This helped us demonstrate the basic idea quite clearly.

Image 4

That very demo

After a short approval period, we went on to integrate the Live Activity feature into the inDrive project. We put together an action group, including, besides myself, another two iOS developers.

When integrating into a finished project, we came up against a number of issues that had to be addressed:

  1. XcodeGen and the first run — how to add a new target to project.yml, especially when it has to come complete with Live Activity support?
  2. An in-depth understanding of how Push Notification works with the Live Activity feature.
  3. We have our own design system, how can this be used within Live Activity?
  4. How to connect up translation resources.
  5. How to link the UDF to Live Activity.

XcodeGen and the First Run

In our project, we use XcodeGen to generate the *.xcodeproj file. That was a bit of a challenge, as we hadn’t used widgets in the project before. We had to add specific templates to the project.yml file of our app’s main module. A flag had to be added to the info section of the main target:

Swift
NSSupportsLiveActivities: true

Then we need to create a template for the Live Activity widget itself:

Swift
LiveActivity:
  type: app-extension
  platform: iOS
  info:
    path: "${target_name}/SupportingFiles/Info.plist"
    properties:
      CFBundleDisplayName: ${target_name}
      CFBundleShortVersionString: *cfBundleShortVersionString
      NSExtension:
        NSExtensionPointIdentifier: "com.apple.widgetkit-extension"
  settings:
    base:
      TARGETED_DEVICE_FAMILY: "$(inherited)"
      PRODUCT_BUNDLE_IDENTIFIER: ${bundleId}
    configs:
      debug:
        PROVISIONING_PROFILE_SPECIFIER: "match Development ${bundleId}"
        CODE_SIGN_IDENTITY: ""
        DEBUG_INFORMATION_FORMAT: ""
      release:
        PROVISIONING_PROFILE_SPECIFIER: "match AppStore ${bundleId}"
        CODE_SIGN_IDENTITY: ""
  dependencies:
    - framework: SwiftUI.framework
      implicit: true
    - framework: WidgetKit.framework
      implicit: true

And not to forget the following in the main target:

Swift
dependencies:
  - target: LiveActivity

Be sure to specify the appropriate bundleId which should be first linked to the Provision Profile (also separate for this bundle).

After working through all the moves and getting the long-awaited make finished successfully 🎉, your Live Activity feature has to be presented for the first time. Here, we will leave out the part about configuring Content State, as well as defining static properties and those that should be updated. It’s important to remember to add “main” because you won’t be able to run the widget without it.

Swift
@available(iOSApplicationExtension 16.1, *)
@main
struct Widgets: WidgetBundle {
    var body: some Widget {
        LiveActivityWidgetView()
    }
}

 See below for how to launch Live Activity (iOS 16.1): 
public func startWith(_ attributes: Attributes?, 
       state: Attributes.ContentState, pushType: PushType?) {
 // 1
    guard ActivityAuthorizationInfo().areActivitiesEnabled,
    let attributes = attributes,
    activity.isNil
    else { return }

    do {
        // 2
        activity = try Activity<Attributes>.request(
            attributes: attributes,
            contentState: state,
            pushType: pushType
        )
        if let token = activity?.pushToken {
            let unwrappedToken = token.map { String(format: "%02x", $0) }.joined()
            logger.debug("🚀 Live Activity token: \(unwrappedToken)")
            // 3
            props.action.execute(with: .didStartActivityWith(token: unwrappedToken))
        } else {
            logger.error("⛔️ Failed Live Activity")
        }
        // 4
        Task {
            guard let activity = activity else { return }
            for await data in activity.pushTokenUpdates {
                let token = data.map { String(format: "%02x", $0) }.joined()
                logger.debug("🤝🏼 Live Activity token updates: \(token)")
                props.action.execute(with: .didPushTokenUpdates(token: token))
            }
        }
    } catch {
        logger.error("⛔️ Failed Live Activity: \(error.localizedDescription)")
        props.action.execute(with: .didFailStartLiveActivity(error: error))
    }
}

To upgrade Activity via the app, if you don’t wish to use Push Notifications (iOS 16.1):

C#
Task {
    await activity.update(using: state)
}

And to end the process:

C#
Task {
    await activity.end(using: state, dismissalPolicy: .immediate)
}
  1. We must ensure that the user can display Live Activity enabled in the settings and that there are no running activities.
  2. When creating a request for an Activity, we need to communicate the pushType:
    • .token— Upgrade Live Activity via Push Notifications
    • nil — Only within the app’s life cycle
  3. If upgraded via .token, the pushToken will come in asynchronously and must be sent to the backend. This will ensure that the backend team knows we are ready to receive updates for Live Activity.
  4. The Live Activity token is updated based on whether the app is running or downloaded. We need to keep track of this and report it to the backend.

After calling the startWith() method, you should see the Live Activity on your screen. If you go down that route, be sure to share your cases in the comments.

An In-Depth Understanding of How Push Notification Works With the Live Activity Feature

Initially, we implemented the basic functionality with no push notifications. The Live Activity feature is designed so that it can be implemented without using any Push Notifications — just based on in-app statuses. Then we came up with these questions:

  • How does Live Activity know which push is intended for it? It’s really all Apple under-the-hood magic. Based on the incoming push notification payload and ID, the system makes its own determination to find out which Live Activity the incoming information refers to.
  • Can these push notifications be captured in the app itself? As of the time of writing, Apple has provided no information on how to catch a push notification in a running app. The guys and I checked to see whether this technique works:
    Swift
    application(_:didReceiveRemoteNotification:fetchCompletionHandler:)

    This method will not be called in the case of Live Activity Push Notifications.

  • How do you test push notifications when there is still no backend? We devised a suitable Payload on our own and implemented the usual push notification flow on the test app. Next, we used this.

Most of the work had to be done on the backend. We sent sample requests to APNS indicating what Payload the client would be waiting for. Stay tuned for further details about our backend implementation efforts in the second part of the article.

Swift
curl -v \
--header "apns-topic:{Your App Bundle ID}.push-type.liveactivity" \
--header "apns-push-type:liveactivity" \
--header "authorization: bearer $AUTHENTICATION_TOKEN" \
--data \
'{"aps": {
   "timestamp":1663300480,
   "event": "update",
   "content-state": {
      "playerOnFirst": "Tony Stark",
      "currentLap": 2
   },
   "alert": {
      "title": "Race Update",
      "body": "Tony Stark is now leading the race!"
   }
}}' \
--http2 \
https://${APNS_HOST_NAME}/3/device/$DEVICE_TOKEN2
Swift
{
  "aps": {
    "timestamp": 1669698850,
    "event": "update",
    "content-state": {
      "rideStatus": "on_ride",
      "time": 5
    }
  }
}

Proprietary Design System and Live Activity

We hadn’t used SwiftUI in the project before, so this was another challenge for us to get our heads around. After a successful launch via the demo app, we got down to ironing out the minor kinks and trying out common components from the design system.

  1. The basic components, such as colors, fonts, and icons, were not a significant problem because starting from iOS 15, Apple has added an easy way to use UIKit components in SwiftUI.

    Image 5

    We took it a little further and implemented it natively. Incidentally, we intend to release the basic components to Open Source so you can take a look.

  2. Apple provides a simple mechanism for wrapping the UIView in SwiftUI.View (UIViewRepresentable), which was supposed to make our lives easier, but it turned out something like this:

    Image 6

    Or like this:

    Image 7

    Although it was planned to be like this. :)

    Image 8

    And like this:

    Image 9

As we had not managed to find an explanation for this online, we decided to drop some of the features until we switch components to native SwiftUI. If you have any ideas as to why this is the case, we’d love to hear from you.

Vehicle Colors

In our main app, we don’t change the color of the vehicle that is en route to the user’s location. This icon is embedded in the app and we just substitute it. For LiveActivity, I took an icon from Figma, placed it where I wanted it, and forgot all about it.

Then a colleague of mine came over and said, “Why don’t we paint the vehicles the color that comes in from the backend?” We found out that the color comes in simply as text: Gold, Blue, etc. We asked the backend team to send hex, too, so we could paint as well.

We had to use some clever tricks here, though. We broke the image down into several layers: the body that we would paint, and the base of the vehicle which would remain unchanged. The result was something like this:

Image 10

This simple technique helped us make the widget more informative.

How to Connect Translation Resources?

We operate in 47 countries and support multiple languages. When launching a feature, we must ensure that all languages are supported, including Arabic (written from right to left).

We use the Crowdin platform to make it convenient to handle translations, and for the widget and the island, the settings must be adjusted depending on the locale involved. For this purpose, we used the “joys” that SwiftUI brings us in the form of Environment.

Swift
@Environment(\.layoutDirection) var direction

UDF + Live Activity

We can use the UDF to implement the service component that will “monitor” the change events of a certain State, as well as generate our Live Activity and update it if necessary.

As shown above, we can wrap up the start, update, and end of Live Activity through the service component. This will be useful if you wish to update the status through the running app (with no remote push notifications).

It’s essential to keep this in mind. To update Live Activity via push notifications, it has to be explicitly run via the active app. From then on, Apple takes over to work all the magic needed.

Swift
import UDF
import ActivityKit

@available(iOS 16.1, *)
public typealias LiveActivityAttributes = ActivityAttributes & Equatable

@available(iOS 16.1, *)
open class LiveActivityServiceComponent<Attributes: LiveActivityAttributes> 
    public var activity: Activity<Attributes>?
    public let disposer = Disposer()
    public var props: Props = .init() {
        didSet {
            render(props)
        }
    }

    public init() { }

    open func render(_: Props) {
        // the logic we implement here is based on the props that come in
        // or you can explicitly override this method in the descendants
        // call the startWith(),updateWith(), endWith() methods when needed
    }
}

// MARK: - LiveActivityServiceComponent

@available(iOS 16.1, *)
extension LiveActivityServiceComponent: ViewComponent {
    public func startWith(_ attributes: Attributes?, 
           state: Attributes.ContentState, pushType: PushType?) {
        ...
    }

    public func updateWith(_ state: Attributes.ContentState) {
        ...
    }

    public func endWith(_ state: Attributes.ContentState) {
       ...
    }
}

// MARK: - Props

@available(iOS 16.1, *)
public protocol LiveActivityProps: Equatable {
    associatedtype Attributes: ActivityAttributes & Equatable
    var attributes: Attributes? { get }
    var contentState: Attributes.ContentState? { get }
    var pushType: PushType? { get }
    var action: CommandOf<LiveActivityAction> { get }

    init(
        attributes: Attributes?,
        contentState: Attributes.ContentState?,
        pushType: PushType?,
        action: CommandOf<LiveActivityAction>
    )
}

@available(iOS 16.1, *)
extension LiveActivityProps {
    init() {
        self.init(attributes: nil, contentState: nil, pushType: nil, action: .nop)
    }
}

@available(iOS 16.1, *)
public extension LiveActivityServiceComponent {
    struct Props: LiveActivityProps {
        public let attributes: Attributes?
        public let contentState: Attributes.ContentState?
        public let pushType: PushType?
        public let action: CommandOf<LiveActivityAction>

        public init(
            attributes: Attributes? = nil,
            contentState: Attributes.ContentState? = nil,
            pushType: PushType? = nil,
            action: CommandOf<LiveActivityAction> = .nop
        ) {
            self.attributes = attributes
            self.contentState = contentState
            self.pushType = pushType
            self.action = action
        }
    }
}

// MARK: - Action

public enum LiveActivityAction: Action {
    case didStartActivityWith(token: String)
    case didPushTokenUpdates(token: String)
    case didFailStartLiveActivity(error: Error)
}

Then you can capture LiveActivityAction in the desired reducer and send requests to the backend with the token to receive push notifications. You can use them to both refresh and end a Live Activity. This process is described in sufficient detail in Apple’s documentation.

The first part of the article is quite a long read and gives you enough material to process, but soon I’ll be sure to share some further observations about Live Activity. I would like to say special thanks to Lyosha Kakoulin and Petya Kazakov because without these guys, nothing would have happened.

Useful Links

History

  • 5th January, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
PraiseNice.! Pin
xpsintel10-Jan-23 2:24
xpsintel10-Jan-23 2:24 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.