Introduction

I was curious about how to add Dynamic Island and implement it into a Video Streaming App. Here are a few steps on how you can achieve this.

Caveats

  • Debugging Dynamic Island can be a bit tricky; it only works when the main app is running. If you try to run it separately, you will encounter the error SendProcessControlEvent:toPid: encountered an error: Error Domain=com.apple.dt.deviceprocesscontrolservice Code=8 "Failed to show Widget". The solution is to configure live activities and run them through the main app.
  • Be aware that when you add a widget to the project, in some cases, it adds all main target files to Compile Sources.

Implementation

Dynamic Islands are divided into different sizes: minimal, compactTrailing, compactLeading, and expanded. Before proceeding, you need to add LiveActivityManager to be able to display Dynamic Islands.

import Foundation
import ActivityKit

struct VideoStreamingWidgetActivityAttributes: ActivityAttributes {
    struct ContentState: Codable, Hashable {
        var isPlaying: String = "0"
    }
}

final class LiveActivityManager {

    @discardableResult
    static func startActivity(isPlaying: String) throws -> String {

        var activity: Activity<VideoStreamingWidgetActivityAttributes>?
        let initialState = VideoStreamingWidgetActivityAttributes.ContentState(isPlaying: isPlaying)

        do {
            activity = try Activity.request(attributes: VideoStreamingWidgetActivityAttributes(), contentState: initialState, pushType: nil)

            guard let id = activity?.id else { throw
                LiveActivityErrorType.failedToGetID }
            return id
        } catch {
            throw error
        }
    }

}

enum LiveActivityErrorType: Error {
    case failedToGetID
}

UI

compactTrailing

compactTrailing: {
                Text("0:33")
                    .foregroundColor(.red)
                    .padding(.trailing, 8)
                } 

compactLeading

compactLeading: {
                Image(systemName: "waveform")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .foregroundColor(.red)
                    .padding(.leading, 8)
                } 

expanded

  DynamicIsland {
                DynamicIslandExpandedRegion(.center) {
                    HStack {
                        Text("0:33")
                            .foregroundStyle(.gray)
                            .frame(height: 4)
                        ProgressView(value: 33, total: 344)
                            .progressViewStyle(.linear)
                        Text("-2:33")
                            .foregroundStyle(.gray)
                            .frame(height: 4)
                    }
                }
                DynamicIslandExpandedRegion(.bottom) {
                        HStack(spacing: 24) {
                            ForEach(Command.allCases) { command in
                                Button(intent: ButtonIntent(id: command.id)) {
                                    Image(systemName: imageSystemName(isPlaying: true, command: command))
                                }
                            }
                        }
                }
            } 

Helpers

enum Command: String, CaseIterable {
    case previous
    case playPause
    case next
}
extension Command: Identifiable {
    var id: String {
        rawValue
    }
}
func imageSystemName(isPlaying: Bool, command: Command) -> String {
    switch command {
    case .playPause:
        if isPlaying {
            return "pause.fill"
        } else {
            return "play.fill"
        }
    case .next:
        return "forward.fill"
    case .previous:
        return "backward.fill"
    }
}
import AppIntents

struct ButtonIntent: AppIntent {

    static let title: LocalizedStringResource = "ButtonIntent"

    @Parameter(title: "id")
    var id: String

    init(id: String) {
        self.id = id
    }

    init() {}

    func perform() async throws -> some IntentResult {
        if id == Command.playPause.rawValue {

        }
        return .result()
    }

}

Thank you for reading! 😊