iOS16.1灵动岛适配 (ActivityKit 初探)

3,120 阅读4分钟

官方文档

 ActivityKit

简单介绍

ActivityKit是iOS16上提供的新功能:提供实时活动的预览能力,在灵动岛和锁定屏幕、消息中心上显示应用程序的最新数据。

应用场景

应用场景主要是我们的app创建了一个持续性的活动,需要给用户一个入口快速查看活动的进度,使用户不用点进app就能看到这个活动的进度,官方文档是采用一个送披萨的例子,用户外卖点了披萨配送,这时候app可以申请一个 LiveActivity ,通过常驻的消息中心和灵动岛来显示实时进度,并且在披萨到达以后可以推送通知。

使用

ActivityKit是基于WidgetKitSwiftUI 的框架,我们要实现其基本功能和小组件类似,可以理解为展示在通知中心的小组件,并且可以在需要的时候更新。

LiveActivity

LiveActivity是展示在通知中心的实时活动,首先我们需要声明一个实现ActivityAttributes协议对象,这个对象可以理解为是我们展示LiveActivity的数据源,当我们需要更新展示的内容的时候,需要通过它来更新数据

ActivityAttributes的声明

public protocol ActivityAttributes : Decodable, Encodable {
    /// The associated type that describes the dynamic content of a Live Activity.
    ///
    /// The dynamic data of a Live Activity that's encoded by `ContentState` can't exceed 4KB.
    associatedtype ContentState : Decodable, Encodable, Hashable
}

其中要求我们使用的时候必须声明一个ContentState作为这个数据源的动态类型,数据变更依据这个ContentState来改变,在官方的示例中时这样做的

struct PizzaDeliveryAttributes: ActivityAttributes {
    public typealias PizzaDeliveryStatus = ContentState
    public struct ContentState: Codable, Hashable {
        var driverName: String
        var estimatedDeliveryTime: Date
    }
    var numberOfPizzas: Int
    var totalAmount: String
}

这个PizzaDeliveryAttributes,披萨的数量、总价基本是不变的,所以不需要作为动态的类型显示,司机名字和预计交付时间可能会实时变化,所以设定为ContentState,作为目前配送的状态。

按照这个Demo仿写一个属性,假设我在包房进入后台之后需要开启一个LiveActivity,数据源设定以下属性,需要我更新的动态属性写进ContentState

struct KTVLRPlayAttributes: ActivityAttributes {
    public typealias status = ContentState
    
    public struct ContentState: Codable, Hashable {
        var musicName: String
        var singerName: String
        var singerIconUrl: URL
    }
    
    public var roomName: String
    public var roomId: String
}

由于ActivityKit是基于widgetKit的,所以接下来是给我们的项目创建一个小组件extension

然后需要在项目的info.plist文件中添加一个属性 Supports Live Activities 并且设置为YES

这个时候就可以编写LiveActivity的UI了

struct KTVLRPlayLiveActivity: Widget {
    var body: some WidgetConfiguration {
        ActivityConfiguration<KTVLRPlayAttributes> { context in             // LiveActivity code...
        } dynamicIsland: { context in             // dynamicIsland code ...
        }
    }
}

LiveActivity的UI和小组件一样,使用SwiftUI编写即可(没咋写过SwiftUI,api不熟悉,简单示例一下)

struct LiveActivityView: View {
    var context: ActivityViewContext<KTVLRPlayAttributes>
    
    var body: some View {
        HStack {
            Spacer().frame(width: 20)
            VStack(alignment: .leading) {
                HStack {
                    Spacer().frame(width: 10)
                    Label {
                        Text("唱吧")
                    } icon: {
                        Image("Activity_share_icon_changba").resizable().frame(width: 25, height: 25)
                    }
                    .font(.body)
                    Spacer()
                    Label {
                        Text("点击返回房间")
                    } icon: {
                        
                    }
                }
                HStack {
                    Label {
                        Text("当前在(context.attributes.roomName)")
                    } icon: {
                        
                    }
                    Spacer()
                    Label {
                        Text("(context.state.singerName) 正在发言")
                    } icon: {
                        
                    }
                }
            }
            Spacer().frame(width: 20)
        }
    }
}

Dynamic Island

灵动岛状态分为3种:

  1. 单个收起状态

  1. 最小化状态(分两种情况,不止一个灵动岛的时候会触发)

这里灵动岛的顺序是按照这个岛产生的先后顺序来的,先产生的会被放在右边

\

  1. 展开状态

最大高度为160

\

将我们的数据填进去,通过swiftui编写视图:

func createDynamicIslandView(context: ActivityViewContext<KTVLRPlayAttributes>) -> DynamicIsland {
    return DynamicIsland {
        // 展开的区域
        DynamicIslandExpandedRegion(.leading) {
            Spacer().frame(height: 10)
            Image("changba_icon")
        }
        DynamicIslandExpandedRegion(.trailing) {
            Spacer().frame(height: 30)
            Text("返回唱吧")
        }
        DynamicIslandExpandedRegion(.center) {
            Text("您正在(context.attributes.roomName)")
        }
        DynamicIslandExpandedRegion(.bottom) {
            Text("(context.state.singerName) 正在演唱")
        }
    } compactLeading: {
       // 收起时候左侧
        Image("changba_icon")
    } compactTrailing: {
        // 收起时候右侧
        Text("唱吧")
    } minimal: {
        // 最小化UI
        Image("changba_icon")
    }
}

效果如下:

\

结束一个LiveActivity

在合适的地方调用:

Task {
            if let activity = self.activity {
                await activity.end(dismissalPolicy: .immediate)
                self.activity = nil             }
        }

这是我的VC中持有了这个activity,如果没有持有或者要终止某一类所有的activity,可以直接通过下面的方法查询:

for activity in Activity<XXXActivityAttributes>.activities {
            // xxxxxxxxxx
       }

\

更新LiveActivity

本地更新:

还是拿到需要更新的activity,调用update方法即可

Task {
            if let activity = self.activity {
                await activity.update(using: Activity<KTVLRPlayAttributes>.ContentState(musicName: "音乐名字", singerName: "歌手名字"))
            }
        }

远程更新:

Updating and ending your Live Activity with remote push notifications | Apple Developer Documentatio

Tips

不论是通知中心还是灵动岛的UI 都无法响应点击事件,整个区域点击会进入app,经过调研,可以通过Link的形式来对不同区域的点击有不同的响应,通过预定的Url来达到点击进入App后进入不同的页面、调用不同的功能等效果,无论是小组件还是LiveActivity,更多的应该是展示状态,而不能在上面实现更多的功能