一、引言

  Widget是一个迷你版的App,iOS有沙盒机制,不同App之间无法直接共享数据。组件和主App之间其实就是不同App的关系,所以也无法通过userdefaults.standard来传数据,苹果为了在不打破沙盒的前提下能够传数据,就想出了App Group的方法。

二、数据共享方式

 可以通过网络和本地数据两种方式进行数据的共享,本地数据共享可以通过 App Groups。

四、App Groups 原理

  它是 iOS 8 之后推出的在 App 之间共享数据的方式,只需要简单的配置就可以实现数据的共享。它主要用于同一group下的app共享同一份读写空间,以实现数据共享。编码 App Groups只能异步同步数据,当Widget读取数据的时候,只能读之前手机App保存的数据,相反也是如此。当手机App有新的数据保存时,不能及时的通知Widget更新数据,只能是Widget下次去主动获取数据。

img

img

五、配置证书

  由于widget项目和主项目其实是两个独立的appID,因为需要单独给widget配置证书,配置证书的过程参考APP证书配置;

开启APP Groups

  开启APP Groups是为了widget和app之间实现数据共享;为了便于后续操作,请先确保你的开发者账号在Xcode上处于登录状态。

在app中开启:

    • TARGETS–>AppExtensionDemo–>Capabilities–>App Groups
    • 找到以后,将App Groups右上角的开关打开,然后选择添加groups,注意命名要规范,比如:group.com.company.app;

在extension中开启

假设创建widget target的名称为TodayExtension,对应的App Group位于

  • TARGETS–>TodayExtension–>Capabilities–>App Groups
  • 开启的方式和APP中一样,注意必须要保证这里的App Groups名称和APP中相同。

六、App Groups特点

  App Group容器只是在宿主app运行期间才存在,其中的容器用于扩展与宿主的文件共享,宿主被关闭了,共享也就没意义了。 以上来自于实际测试,测试过程是:在宿主app运行期间,点击其中的按钮弹出模态视图控制器,进行数据填充。完成后保存数据到App Group容器中的文件中,以供today extension扩展进行数据使用。只要将宿主app杀掉后重启启动宿主app,today extension 中已经显示的数据就完全没有了。单纯将宿主app杀掉不重启,today extension的任然hi显示之前的内容。由于宿主app中显示的数据也是从app group中的文件中取出来的,所以数据也没了。

  由于这个共享机制的特殊性,这个容器不能用来长期保存文件!!!应该将文件存储到宿主app的文件夹中,可以长期存储。today extension展示的数据量较少,在合适的时候将其需要的数据搬运到app group中!

七、示例代码

配置好项目的 group 后,我们开始进行数据的配置。我们采用AppStorage来存储数据。

AppStorageSwift中的一个属性包装器,用于在iOS应用程序中存储和管理用户默认设置。它使开发人员能够轻松地将应用程序的状态持久化到用户的设备上.

要使用AppStorage,首先需要声明一个带有存储属性包装器的属性。例如,您可以将一个布尔值标记为应用程序设置:

1
@AppStorage("isDarkModeEnabled") var isDarkModeEnabled = false

在上面的示例中,我们将名为”isDarkModeEnabled”的属性标记为应用程序设置,并将其初始值设置为false。如果用户更改了这个设置,在应用程序下次启动时,它将自动加载用户上次的选择.

1.在我们的项目中,我们新建一个DataService的结构体,用来管理数据

1
2
3
4
5
6
7
8
9
10
11
import Foundation
import SwiftUI

struct DataService {

@AppStorage("streak", store: UserDefaults(suiteName: "group.com.cft.widgetstudy")) private var streak = 0

func count() -> Int {
return streak
}
}

@AppStorage("streak", store: UserDefaults(suiteName: "group.com.cft.widgetstudy")) private var streak = 0 这行代码的意思是将一个名为”streak”的属性标记为应用程序设置,并将其存储在名为group.com.cft.widgetstudy的共享UserDefaults实例中。

因此,通过传递一个自定义的UserDefaults实例给AppStorage的store参数,我们可以将属性的值存储在特定的UserDefaults实例中,以便跨应用程序组件进行共享。

2.主项目代码

在主项目中,我们创建一个 Text 用来显示计数,创建一个 Button,每点击一下 button,计数值加 1, 同时调用WidgetCenter.shared.reloadAllTimelines()去刷新小组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import SwiftUI
import WidgetKit

struct ContentView: View {

@AppStorage("streak", store: UserDefaults(suiteName: "group.com.cft.widgetstudy")) var streak = 0

var body: some View {
VStack {
Text("Count: \(streak)")
.font(.largeTitle)

Button(action: {
streak += 1
WidgetCenter.shared.reloadAllTimelines()
}) {
Text("Increment Count")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
}
}
}

3.小组件代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
//
// widgetdemo.swift
// widgetdemo
//
// Created by cft on 2023/11/19.
//

import WidgetKit
import SwiftUI

struct Provider: TimelineProvider {

let data = DataService()

//它本质上是向系统提供虚拟数据,以在等待小部件准备就绪的同时呈现占位符 UI。请注意,SwiftUI 会对我们提供的虚拟数据应用编辑效果,因此虚拟数据的实际价值并不重要
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), streak: data.count())
}

//该函数主要提供系统在小部件图库中渲染小部件所需的数据。
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {

let entry = SimpleEntry(date: Date(), streak: data.count())
completion(entry)
}

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [SimpleEntry] = []

// Generate a timeline consisting of five entries an hour apart, starting from the current date.
let currentDate = Date()
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, streak: data.count())
entries.append(entry)
}

let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}

struct SimpleEntry: TimelineEntry {
let date: Date
let streak: Int
}

struct widgetdemoEntryView : View {
var entry: Provider.Entry
let data = DataService()

var body: some View {
VStack {
Text(entry.date, style: .time)

Text("Count:\(data.count())")
}
}
}

struct widgetdemo: Widget {
let kind: String = "widgetdemo"

var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
if #available(iOS 17.0, *) {
widgetdemoEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
} else {
widgetdemoEntryView(entry: entry)
.padding()
.background()
}
}
.configurationDisplayName("My Widget")
.description("This is an example widget.")
}
}

#Preview(as: .systemSmall) {
widgetdemo()
} timeline: {
SimpleEntry(date: .now, streak: 1)
SimpleEntry(date: .now, streak: 3)
}

小组件中的代码比较简单,就是去调用DataService的count()的方法,去获取计数值,显示在小组件中。

widget

代码地址:https://gitee.com/chengft/ios_demos/tree/master/%E5%B0%8F%E7%BB%84%E4%BB%B6/widgetStudy

参考:https://cloud.tencent.com/developer/article/1836554