【iOS】记录widget开发流程及遇到的问题

【iOS】记录widget开发流程及遇到的问题,第1张

写在前面
1、iOS14后,苹果更新了扩展组件,引入了新的UI组件:WidgetKit 而舍弃了iOS14以下版本的Today Extension组件;
2、WidgetExtension 使用的是新的WidgetKit不同于Today Widget,它只能使用SwiftUI进行开发,所以需要SwiftUI和Swift基础;
3、Widget支持3种尺寸systemSmall (2x2)、 systemMedium (4x2)、 systemLarge(4x4)

文章目录 1、创建 Widget2、结构简述2.1、Provider2.2、SimpleEntry2.3、MyWidgetEntryView2.4、MyWidget2.5、MyWidget_Previews 3、Static 改为 Intent3.1、添加 Intent 文件3.2、添加 Intent3.3、修改 MyWidget.swift3.3.1、Provider3.3.2 SimpleEntry3.3.3 MyWidget3.3.4 MyWidget_Previews 4、自定义可选列表4.1、创建IntentHandle4.2、创建列表文件并添加图片资源4.3、在Intent文件中添加数据4.3.1、添加Type 4.4、修改 IntentHandler 文件4.5、修改 MyWidget 文件注:SwiftUI Image("") 5、刷新小组件5.1、主APP刷新小组件5.2、小组件中的刷新策略5.2.1、atEnd5.2.2、never5.2.3、after 6、小组件与主App数据交互6.1、添加 App Groups6.2、数据共享6.2.1、UserDefaults6.2.2、FileManager 7、小组件跳转主App7.1、widgetURL7.2、Link7.3、主App响应 8、网络请求参考:

1、创建 Widget

首先创建一个项目,取名MyApp;
然后创建Widget,File -> New -> Target

iOS -> Application Extension -> Widget Extension

输入项目名;
这里要注意下,Widget 分为 Static 和 Intent 两种模式。Intent 模式长按可编辑,Static 模式没有编辑选项。下图为 Intent 模式:

取名 MyWidget,这里先不选 Include Configuration Intent,不选则创建静态小组件,选中则创建可编辑小组件。
注:这里不能取名Widget,系统有这个文件,会报错。
可以看这里

点击创建,有个d窗,直接点击Activate

现在可以运行下,在模拟器上会显示一个只有时间文本的小组件。

2、结构简述

MyWidget.swift 文件里有5个结构体

2.1、Provider

管理时间线的地方,数据绑定处理。

2.2、SimpleEntry

时间线入口,默认只生产一个date属性,需要自定义字段的话在这里声明。

2.3、MyWidgetEntryView

小组件视图布局在这里实现,处理数据展示到视图。

2.4、MyWidget

小组件加载入口,静态的调用StaticConfiguration,可编辑的调用IntentConfiguration。

2.5、MyWidget_Previews

视图预览。

这里介绍比较详细:iOS 14 WidgetKit开发

3、Static 改为 Intent 3.1、添加 Intent 文件

我们创建的静态的,如果需要改为可编辑的,需要手动添加Intent文件。
File -> New -> File (cmd + N)
iOS -> Resource -> SiriKit Intent Definition File

取名Custom.intentdefinition,这里Targets要选中小组件,默认没有选中。

3.2、添加 Intent

取名MyIntent,Category选View,选中 user confirmation required,其他不选。
添加之后编译下项目(cmd + B)会生成一个 MyIntentIntent.swift 文件。MyIntent是我们自定义的Intent的名字。

文件路径

3.3、修改 MyWidget.swift 3.3.1、Provider

TimelineProvider 改为 IntentTimelineProvider

struct Provider: TimelineProvider {
...
改为
struct Provider: IntentTimelineProvider {
...

getSnapshot 方法修改

func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
...
改为
func getSnapshot(for configuration:MyIntentIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
...

getTimeline 方法修改

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
...
改为
func getTimeline(for configuration:MyIntentIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
...
3.3.2 SimpleEntry

添加 configuration 属性

let configuration: MyIntentIntent

修改调用 SimpleEntry 的地方

Provider -> placeholder
SimpleEntry(date: Date(), configuration: MyIntentIntent())

Provider -> getSnapshot
let entry = SimpleEntry(date: Date(), configuration: configuration)

Provider -> getTimeline
let entry = SimpleEntry(date: entryDate, configuration: configuration)

MyWidget_Previews
MyWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: MyIntentIntent()))
3.3.3 MyWidget

StaticConfiguration 改为 IntentConfiguration

StaticConfiguration(kind: kind, provider: Provider()) { entry in
...
改为
IntentConfiguration(kind: kind, intent: MyIntentIntent.self, provider: Provider()) { entry in
...
3.3.4 MyWidget_Previews

上面已修改,参看3.3.2。

这里有个问题,因为是静态修改为可编辑的,点击编辑不会出现编辑界面。删除小组件重新添加才会有。
添加一个可选项试一下吧。

在 MyWidgetEntryView 中获取参数值显示在屏幕上

Text(entry.configuration.parameter ?? "")


4、自定义可选列表


4.1、创建IntentHandle

添加 Target:iOS -> Application Extension -> Intents Extension

取名 IntentHandle。这个绑定Intent文件和数据模型的,这里要设置下 target 选中小组件,就可以在小组件中调用了,Intent 的 target 要选中 Intenthandle。MyWidget.swift文件不动。

4.2、创建列表文件并添加图片资源

创建MenuJson.swift文件,这里注意target选中小组件和handle

添加图片

import Foundation

struct MenuJson: Codable {
    let id: String
    let name: String
    let image: String
    
    static func createMenuList() -> [MenuJson] {
        var list = [MenuJson]()
        list.append(.init(id: "1", name: "航拍", image: "aerial.jpg"))
        list.append(.init(id: "2", name: "城市", image: "city.jpg"))
        list.append(.init(id: "3", name: "人物", image: "figure.jpg"))
        list.append(.init(id: "4", name: "宠物", image: "pet.jpg"))
        return list
    }
}

这里要选中小组件的target

4.3、在Intent文件中添加数据 4.3.1、添加Type

添加一个type属性,Type 选择 Add Type… ,Type取名MenuList。

Type展示名称可自定义

4.4、修改 IntentHandler 文件

添加代理 MyIntentIntentHandling
添加方法 provideTypeOptionsCollection
provideTypeOptionsCollection 中 绑定数据

class IntentHandler: INExtension, MyIntentIntentHandling {
    
    func provideTypeOptionsCollection(for intent: MyIntentIntent, with completion: @escaping (INObjectCollection<MenuList>?, Error?) -> Void) {
        let list = MenuJson.createMenuList().map { (item) -> MenuList in
            .init(identifier: item.id, display: item.name)
        }
        completion(.init(items: list), nil)
    }
    ...
4.5、修改 MyWidget 文件
struct Provider: IntentTimelineProvider {
    
    let list = MenuJson.createMenuList().map { (item) -> MenuList in
        .init(identifier: item.id, display: item.name)
    }
    
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(
            date: Date(),
            configuration: MyIntentIntent(),
            menu: list[0]
        )
    }

    func getSnapshot(for configuration:MyIntentIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        
        var firstItem = list.filter { (item: MenuList) -> Bool in
            item.identifier == configuration.type?.identifier
        }
        if firstItem.count == 0 {
            firstItem = list
        }
        
        let entry = SimpleEntry(
            date: Date(),
            configuration: configuration,
            menu: firstItem[0]
        )
        completion(entry)
    }

    func getTimeline(for configuration:MyIntentIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        
        var firstItem = list.filter { (item: MenuList) -> Bool in
            item.identifier == configuration.type?.identifier
        }
        if firstItem.count == 0 {
            firstItem = list
        }
        
        let entry = SimpleEntry(
            date: Date(),
            configuration: configuration,
            menu: firstItem[0]
        )

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

struct SimpleEntry: TimelineEntry {
    let date: Date
    let configuration: MyIntentIntent
    let menu: MenuList
}

struct MyWidgetEntryView : View {
    var entry: Provider.Entry

    var body: some View {
        
        let item = MenuJson.createMenuList().first { (subItem: MenuJson) -> Bool in
            subItem.id == entry.menu.identifier
        }
        let defaultText = "剑舞鸿门能赦汉,船沉巨鹿竞亡秦。\n-- 清 • 严遂成"
        ZStack {
            
            Image(uiImage: UIImage(imageLiteralResourceName: item?.image ?? "figure.jpg"))
                .resizable()
                .aspectRatio(contentMode: .fit)
            VStack {
                Spacer()
                Text(item?.name ?? defaultText)
            }
            
        }
        .widgetURL(URL(string: "widget://tap"))
    }
}

@main
struct MyWidget: Widget {
    let kind: String = "MyWidget"

    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: MyIntentIntent.self, provider: Provider()) { entry in
            MyWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
    }
}

struct MyWidget_Previews: PreviewProvider {
    static var previews: some View {
        let list = MenuJson.createMenuList().map { (item) -> MenuList in
            .init(identifier: item.id, display: item.name)
        }
        MyWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: MyIntentIntent(), menu: list[0]))
            .previewContext(WidgetPreviewContext(family: .systemSmall))
    }
}
注:SwiftUI Image("")

SwiftUI Image("")添加的图片只能放在Assets里面,直接拖入项目的图片需要调用UIImage来加载

Image(uiImage: UIImage(imageLiteralResourceName: item?.image ?? "figure.jpg"))
5、刷新小组件 5.1、主APP刷新小组件
WidgetCenter.shared.reloadAllTimelines()WidgetCenter.shared.reloadTimelines(ofKind: "MyWidget")

kind是什么?去看看小组件部分的代码,不是自己手动写的代码所以可能没印象,找过一次就记住了。

@main
struct MyWidget: Widget {
    // kind 在这里定义
}
5.2、小组件中的刷新策略

苹果建议小组件的刷新时间是15分钟,你可以设置的最小刷新时间为5分钟。

5.2.1、atEnd
public static let atEnd: TimelineReloadPolicy

结束时刷新

// 例:
let currentDate = Date()
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let entry = SimpleEntry(date: entryDate, configuration: configuration)
let timeline = Timeline(entries: [entry], policy: .atEnd)
5.2.2、never
public static let never: TimelineReloadPolicy

字面意思,永不刷新

5.2.3、after
public static func after(_ date: Date) -> TimelineReloadPolicy

指定刷新时间间隔

// 例:
let updateDate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!
let timeline = Timeline(entries: [entry], policy: .after(updateDate))
6、小组件与主App数据交互 6.1、添加 App Groups

在Targets里添加 App groups,主App和小组件都要添加

6.2、数据共享 6.2.1、UserDefaults
// 存数据
let userDefaults = UserDefaults.init(suiteName: "group.com.xxx.MyApp")
userDefaults?.setValue("value1", forKey: "first")
// 取数据
let userDefaults = UserDefaults.init(suiteName: "group.com.xxx.MyApp")
let context = userDefaults.value(forKey: "first")

小组件中如果在 view 布局里实现存值要这样写

let _ = userDefaults.setValue("v-v", forKey: "widgetValue")
6.2.2、FileManager
// 存数据
let doc = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.xxx.MyApp")
if let path = doc?.appendingPathComponent("test.plist") {
	let array:NSMutableArray = [
		["name": "Pikachu"],
		["name": "Pikachu"],
		["name": "Pikachu"],
	]
	let _ = array.write(to: path, atomically: true)
}

// 读数据
let doc = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.zyl.MyApp")
if let path = doc?.appendingPathComponent("test.plist") {
	let array = NSArray(contentsOf: path)
	print(array as Any)
}

FileManager 的一些 *** 作:swift之FileManager的 *** 作

7、小组件跳转主App

点击小组件打开App(默认功能)

跳转App传递数据,两种方式:widgetURL 和 Link
systemSmall 样式的 widget 只能用 widgetURL

7.1、widgetURL
ZStack {
	... 
}
.widgetURL(URL(string: "自定义内容"))
7.2、Link
Link(destination: URL(string: "Link")!) {
	Text("Link")
}
7.3、主App响应
// 有 SceneDelegate.swift
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
	let urlContext = URLContexts.map{ $0 }[0]
    print(urlContext.url.absoluteString)
}

// 没有 SceneDelegate.swift 只有 AppDelegate.swift
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    print(url)
	return true
}
8、网络请求
// 添加一个图片字段
struct SimpleEntry: TimelineEntry {
    ...
    var image: UIImage? = UIImage(named: "figure.jpg")
}
// 网络请求
// struct Provider
func getTimeline(for configuration:MyIntentIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
	let url = URL(string: "图片地址")!
	let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
		guard error == nil else {
	        return
		}
	    if let image = UIImage(data: data!) {
	    	...
			let entry = SimpleEntry(
                    date: Date(),
                    configuration: configuration,
                    menu: firstItem[0],
                    image: image
                )

			let timeline = Timeline(entries: [entry], policy: .never)
		    completion(timeline)
	    }
    }
	task.resume()
}

// 展示网络下载的图片
// struct MyWidgetEntryView
var body: some View {
	Image(uiImage: entry.image!)
		.resizable()
		.aspectRatio(contentMode: .fit)
}

参考:

iOS14 Widget小组件开发(Widget Extension)
iOS 14 WidgetKit开发

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/996942.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存