掌握SwiftUI中的灵动岛

1,601 阅读3分钟

前言

  1. 灵动岛是从iPhone14 Pro开始出现的,充分利用顶部刘海的一种UI展示
  2. 环境的话,需要至少Xcode14.1以及iOS16.1系统的手机或者模拟器
  3. 灵动岛是Widgets系统的一部分,需要你的App有widget extension或者新建一个Widget Extension的Target

image.png

image.png

image.png 3. 同时需要你的App支持Live Activity,这需要在你主工程的Info.plist中添加一个key(NSSupportsLiveActivities),类型为bool,需要设置为YES

image.png

开始

iOS16.1开始,苹果提供了自定义灵动岛的API,它位于WidgetKit动态库中,通过提供给我们一个叫做 *ActivityConfiguration*的configuration来允许我们定义一个 live activity widget

  1. 首先我们需要先定义一个 ActivityAttributes供Widget使用
struct FastingAttributes: 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
}
  1. 创建Activity所需要的SwiftUIView,其中context的 attributes 就是上面我们定义的 FastingAttributes
@available(iOSApplicationExtension 16.1, *)
struct FastingActivityWidget: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: FastingAttributes.self) { context in
            LiveActivityView(context: context)
                .padding(.horizontal)
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.leading) {
                    LiveActivityView(context: context)
                }
            } compactLeading: {
                Image(systemName: "circle")
                    .foregroundColor(.green)
            } compactTrailing: {
                Text(verbatim: context.state.progress.formatted(.percent.precision(.fractionLength(0))))
                    .foregroundColor(context.state.stage?.color)
            } minimal: {
                Image(systemName: "circle")
                    .foregroundColor(.green)
            }
        }
    }
}
  • 我们在这里使用 ActivityConfiguration 来创建了一个Live Activity Widget,在构造函数里使用 灵动岛字段dynamicIsland 来配置灵动岛UI。
  • dynamicIsland 的构造函数有四个参数,需要我们提供 compact leading,compact trailing,expanded和minimal四种情况下所对应的SwiftUI View。第一个闭包对应的是expanded,其余几个分别对应不同的参数
  1. 通过ActivityKit的API启动这个Activity,并且当我们退出App时会有个灵动岛的动画
import ActivityKit
let attributes = FastingAttributes(name: "unravel")
do {
  _ = try Activity<FastingAttributes>.request(attributes: attributes, contentState: .init(value: 3))
} catch {}

222.gif 4. 可以通过以下API停掉所有的FastingAttributes的Live Activity,或者通过上一步request返回的activity进行停掉

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

展示规则

  1. 无论何时,只要你的App是那一刻运行live activity widget 的唯一应用,系统就会相应的展示 compact leading和compact trailing的Views

image.png

image.png 2. 如果那一刻运行live activity widget的应用不止一个,系统就会展示 minimal view并且将它展示为 单独的trailing view

image.png

  1. 当用户在灵动岛上长按时,就会展示 expanded view

image.png

image.png 这种情况下,灵动岛的空间分为如上的四部分,系统允许我们控制每一个部分的内容并且提供了priority参数去优先展示

@available(iOSApplicationExtension 16.1, *)
struct FastingActivityWidget: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: FastingAttributes.self) { context in
            LiveActivityView(context: context)
        } dynamicIsland: { context in
            // Expanded下的灵动岛
            DynamicIsland {
                // 分别控制不同部分的内容,使用优先级优先展示
                DynamicIslandExpandedRegion(.leading, priority: 1) {
                    LiveActivityView(context: context)
                }
                
                DynamicIslandExpandedRegion(.trailing) {
                    LiveActivityView(context: context)
                }
                
                DynamicIslandExpandedRegion(.center) {
                    LiveActivityView(context: context)
                }
                
                DynamicIslandExpandedRegion(.bottom) {
                    LiveActivityView(context: context)
                }
            } compactLeading: {
                Image(systemName: "circle")
                    .foregroundColor(.green)
            } compactTrailing: {
                Text(verbatim: context.state.progress.formatted(.percent.precision(.fractionLength(0))))
                    .foregroundColor(context.state.stage?.color)
            } minimal: {
                Image(systemName: "circle")
                    .foregroundColor(.green)
            }
        }
    }
}

如果我们的内容太宽的话,系统还提供了dynamicIsland 视图修改器将leadingview放置到摄像头下方

@available(iOSApplicationExtension 16.1, *)
struct FastingActivityWidget: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: FastingAttributes.self) { context in
            LiveActivityView(context: context)
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.leading, priority: 1) {
                    LiveActivityView(context: context)
                        // 灵动岛视图修改器
                        .dynamicIsland(verticalPlacement: .belowIfTooWide)
                }
            } compactLeading: {
                Image(systemName: "circle")
                    .foregroundColor(.green)
            } compactTrailing: {
                Text(verbatim: context.state.progress.formatted(.percent.precision(.fractionLength(0))))
                    .foregroundColor(context.state.stage?.color)
            } minimal: {
                Image(systemName: "circle")
                    .foregroundColor(.green)
            }
        }
    }
}

默认情况下系统使用 黑色 填充 compac和minimal的背景色,我们可以通过 keylineTint这个视图修改器来改变这个颜色

@available(iOSApplicationExtension 16.1, *)
struct FastingActivityWidget: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration(for: FastingAttributes.self) { context in
            LiveActivityView(context: context)
        } dynamicIsland: { context in
            DynamicIsland {
                DynamicIslandExpandedRegion(.leading, priority: 1) {
                    LiveActivityView(context: context)
                        .dynamicIsland(verticalPlacement: .belowIfTooWide)
                }
            } compactLeading: {
                Image(systemName: "circle")
                    .foregroundColor(.green)
            } compactTrailing: {
                Text(verbatim: context.state.progress.formatted(.percent.precision(.fractionLength(0))))
                    .foregroundColor(context.state.stage?.color)
            } minimal: {
                Image(systemName: "circle")
                    .foregroundColor(.green)
            }
            // 修改灵动岛内compact和minimal下的背景色
            .keylineTint(.white)
        }
    }
}

学习链接

  1. developer.apple.com/documentati…
  2. swiftwithmajid.com/2022/09/28/…
  3. nemecek.be/blog/171/dy…
  4. zhuanlan.zhihu.com/p/577728766