iOS锁屏Widget

1,073 阅读4分钟

简介

从iOS16和watchOS9开始,WidgetKit 小组件同时能够在iPhone锁屏和watch表盘上展示。显示与你的App最相关的,可浏览的内容,并让人们快速访问App的更多细节。 屏幕小组件和watch表盘应用使用WidgetKit和SwiftUI创建和开发,使我们能够:

  1. 更新现有的 iOS 主屏幕和watch上今日视图小部件的代码以支持 iPhone 上的锁屏小部件。
  2. watchOS 应用程序中使用WidgetKit替换ClockKit,让我们的iOS和watchOS 应用程序之间复用更多的代码
  3. 可以创建同时支持iPhone 锁屏和watch小组件
  4. 在应用程序中添加对iOS或者watchOS的支持,并创建小组件

Widget种类

image.png WidgetFamily.accessoryCircular (iOS和WatchOS都支持) image.png WidgetFamily.accessoryRectangular(iOS和WatchOS) image.png WidgetFamily.accessoryInline(iOS和WatchOS) image.png WidgetFamily.accessoryCorner(仅支持WatchOS)

Widget创建

  1. File - New - Target
  2. 选择 Widget Extension image.png
  3. 输入Widget Target信息 image.png

相关教程 Creating a Widget Extension and SwiftUI

具体Demo

Main App配置

  • 配置主App和Widget App的App Group
    • App Group用于在Main App和Widget App间共享数据

image.png

  • Main App保存数据到group user default image.png

Widget App配置

  • 配置入口 image.png
  • 从Group User Default读取数据 image.png
  • Rectangular组件
// 为小组件展示提供一切必要信息的结构体
struct MyWidgeRectangularProvider: IntentTimelineProvider {
    // 占位视图, 例如网络请求失败、发生未知错误、第一次展示小组件都会展示这个view
    func placeholder(in context: Context) -> MyWidgeRectangularEntry {
        MyWidgeRectangularEntry()
    }
    // 快照 编辑屏幕在左上角选择添加Widget  第一次展示时会调用该方法
    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (MyWidgeRectangularEntry) -> ()) {
        completion(MyWidgeRectangularEntry())
    }
    // 生成一个事件线,更新数据&&进行数据的预处理,转化成Entry
    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        let timeline = Timeline(entries: [MyWidgeRectangularEntry()], policy: .atEnd)
        completion(timeline)
    }
}
// Widget 数据源
struct MyWidgeRectangularEntry: TimelineEntry {
    var date: Date = Date()
    var weightValue = ""
    var heightValue = ""
    init() {
        weightValue = appGroupWeight
        heightValue = appGroupHeight
    }
}

private let textDefaultColor = Color(red: 199.0 / 255, green: 203.0 / 255, blue: 231.0 / 255)

// widget 展示视图
struct MyWidgetEntryView : View {
    var entry: MyWidgeRectangularEntry
    @Environment(\.widgetFamily) var family // 尺寸环境变量
    
    var body: some View {
        let imageSize:CGFloat = 20
        let mediumFontSize: CGFloat = 12
        let bigFontSize:CGFloat = 18
        return VStack(alignment: .leading, spacing: 3) {
            Image("widget_logo").resizable().frame(width: 58, height: 20).aspectRatio(contentMode: .fit)
            HStack {
                VStack(alignment: .leading, spacing: 0) {
                    Text("体重").font(.system(size: mediumFontSize)).foregroundColor(textDefaultColor)
                    HStack(alignment:.lastTextBaseline, spacing: 0) {
                        Text(entry.weightValue).font(.system(size: bigFontSize)).foregroundColor(Color.blue)
                    }.frame(height: imageSize + 5)
                }
                VStack(alignment: .leading, spacing: 0) {
                    Text("身高").font(.system(size: mediumFontSize)).foregroundColor(textDefaultColor)
                    HStack(alignment:.lastTextBaseline, spacing: 0) {
                        Text(String(entry.heightValue)).font(.system(size: bigFontSize)).foregroundColor(Color.blue)
                    }
                }
            }
        }
    }
}
struct MyWidgetRectangular: Widget {
    let kind: String = "MyWidgetRectangular"
    var body: some WidgetConfiguration {
        let configuration = IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: MyWidgeRectangularProvider()) { entry in
            MyWidgetEntryView(entry: entry).widgetURL(URL(string:"widget://")!)
        }
        .configurationDisplayName(CommonLocalizabledString(key: "widget_data", comment: "My Data")) // 添加组件时组件title
        return configuration.supportedFamilies([.accessoryRectangular])
    }
}

两个Circular组件

// 为小组件展示提供一切必要信息的结构体
struct MyWidgetCircularProvider: IntentTimelineProvider {
    let index: Int
    init(index: Int) {
        self.index = index
    }
    // 占位视图, 例如网络请求失败、发生未知错误、第一次展示小组件都会展示这个view
    func placeholder(in context: Context) -> MyWidgetCircularEntry {
        MyWidgetCircularEntry(index: index)
    }
    // 快照 编辑屏幕在左上角选择添加Widget  第一次展示时会调用该方法
    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (MyWidgetCircularEntry) -> ()) {
        completion(MyWidgetCircularEntry(index: index))
    }
    // 生成一个事件线,更新数据&&进行数据的预处理,转化成Entry
    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        let timeline = Timeline(entries: [MyWidgetCircularEntry(index: index)], policy: .atEnd)
        completion(timeline)
    }
}

struct MyWidgetCircularEntry: TimelineEntry {
    var date: Date = Date()
    let index: Int
    init(index: Int) {
        self.index = index
    }
}

@available(iOSApplicationExtension 16.0, *)
struct MyWidgetCircularWidgetEntryView : View {
    var entry: MyWidgetCircularEntry
    @Environment(\.widgetFamily) var family // 尺寸环境变量
    
    var body: some View {
        VStack(spacing: 2) {
            HStack() {
                Image(entry.index == 0 ? "widget_activity" : "widget_switch").resizable().frame(width: 20, height: 20).foregroundColor(Color.black)
            }.frame(width: 30, height: 30).background(Color(white: 220.0/255.0)).cornerRadius(15)
            Text(entry.index == 0 ? "活动" : "开关").font(.system(size: 10)).lineLimit(1).padding(.zero).frame(alignment: .center)
        }.frame(width: 120, height: 120).background(Color(white: 40.0/255.0)).cornerRadius(60)
    }
}

@available(iOSApplicationExtension 16.0, *)
struct MyWidgetCircular: Widget {
    init() {
        
    }
    
    var index = 0
    var kind: String = ""
    var body: some WidgetConfiguration {
        // widget URL定义点击widget后的回调,在AppDelegate的application:openURL:中收到回调
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: MyWidgetCircularProvider(index: index)) { entry in
            MyWidgetCircularWidgetEntryView(entry: entry).widgetURL(URL(string:kind)!)
        }
        .configurationDisplayName(index == 0 ? "活动" : "开关")
        .supportedFamilies([.accessoryCircular])
    }
    
    init(index: Int) {
        self.index = index
        kind = "MyWidgetCircularWidget" + String(index)
    }
}

Widget刷新

如果Widget配置没有刷新,需要重启手机来重载下系统SpringBoard桌面进程

更多信息