Live Activity triggered by AlarmKit remains as an empty state

I configured my app to show a Live Activity when an alarm rings using AlarmKit. However, if I dismiss the Live Activity by tapping somewhere other than the X button, and then long-press the Dynamic Island, a new Live Activity appears that is long but contains no information.

Currently, the only way I can remove this empty Live Activity is to press the X button while the alarm is in the snooze state. Pressing the X button on the initial alarm does not remove it.

Is there any way to prevent this behavior or properly handle / clean up this empty Live Activity?

Thank you for the post and providing detailed information. I am curious about how you are handling Live Activity events in code as you didn’t provide any focused sample. I recommend reviewing our samples to see how it works well to dismiss Live Activities.

Handling Live Activities with AlarmKit and ensuring they behave correctly with the Dynamic Island can be tricky, especially concerning state management and user interactions. Make sure that when your app dismisses the Live Activity, it explicitly ends the activity using the APIs. Before dismissing, ensure the Live Activity’s state is updated to reflect that it is no longer active or relevant. Ensure that your logic for starting a Live Activity does not inadvertently create multiple instances. Use unique identifiers and conditions to check if an activity for a particular alarm is already running before starting a new one.

As an interim measure, clearly communicate to users the intended method to dismiss Live Activities (using the X button) through UI hints or onboarding instructions until any software or logic issues are resolved.

By carefully managing activity states and ensuring clean terminations, you should be able to mitigate the issue of empty Live Activities appearing.

Here is a good resource of updating live activity I personally recommend. https://developer.apple.com/documentation/ActivityKit/displaying-live-data-with-live-activities

Also do not miss the end of the Live Activity API here: https://developer.apple.com/documentation/ActivityKit/displaying-live-data-with-live-activities#End-the-Live-Activity

Albert Pascual
  Worldwide Developer Relations.

final class AlarmScheduler {
  private let manager = AlarmManager.shared
  private let holidayProvider = HolidayProvider.shared

...

  func cancel(_ alarm: AlarmModel) throws {
    try manager.cancel(id: alarm.id)
    Task {
      await AlarmLiveActivityManager.end(alarmID: alarm.id)
    }
  }

...
enum AlarmLiveActivityManager {
  static func upsert(
    alarmID: UUID,
    attributes: AlarmAttributes<EmptyMetadata>,
    content: ActivityContent<AlarmPresentationState>
  ) async {
    let matches = activeActivities(for: alarmID)
    if let activity = matches.first {
      await activity.update(content)
      await endActivities(matches.dropFirst())
      return
    }

    _ = try? Activity.request(
      attributes: attributes,
      content: content,
      pushType: nil
    )
  }

  static func end(alarmID: UUID) async {
    let matches = activeActivities(for: alarmID)
    await endActivities(matches)
  }

  private static func activeActivities(for alarmID: UUID) -> [Activity<
    AlarmAttributes<EmptyMetadata>
  >] {
    Activity<AlarmAttributes<EmptyMetadata>>.activities.filter { activity in
      guard isActive(activity.activityState) else { return false }
      return activity.content.state.alarmID == alarmID
    }
  }

  private static func endActivities<S: Sequence>(
    _ activities: S
  ) async where S.Element == Activity<AlarmAttributes<EmptyMetadata>> {
    for activity in activities {
      await activity.end(nil, dismissalPolicy: .immediate)
    }
  }

  private static func isActive(_ state: ActivityState) -> Bool {
    switch state {
    case .active, .pending, .stale:
      return true
    case .ended, .dismissed:
      return false
    @unknown default:
      return false
    }
  }
}
private func endLiveActivity(alarmID: UUID) async {
  let activities = Activity<AlarmAttributes<EmptyMetadata>>.activities.filter { activity in
    guard isActive(activity.activityState) else { return false }
    return activity.content.state.alarmID == alarmID
  }

  for activity in activities {
    await activity.end(nil, dismissalPolicy: .immediate)
  }
}

The code is shown above.

During testing, I found the following behavior:

When I swipe the Dynamic Island left or right, the Live Activity disappears and the alarm also stops. After that, if I open the app and then return to the Home screen, a visual feedback animation appears as if it is entering the Dynamic Island. In this state, when I long-press the Dynamic Island, it shows an empty, long black Live Activity with no content.

Is there any way to prevent this behavior or properly handle this state so that the empty Live Activity does not appear?

Live Activity triggered by AlarmKit remains as an empty state
 
 
Q