【注意】最后更新于 June 27, 2020,文中内容可能已过时,请谨慎使用。
前言
2020 年 06 月 22 日的 WWDC 上 iOS14 的新特性 - 小部件正式在 iOS 上线,同时 WidgetKit 也正式面向广大开发者使用。
此帖开始记录学习 WidgetKit 的经历, 同时开源了 - iWidget 作为 WidgetKit
的简单演示,看完这篇文章认为有用可点个 Star。
项目地址: https://github.com/Littleor/iWidget
没有看过前一节的建议看看这个: (iOS14)WidgetKit 开发实战 1- 初识 iOS 小部件
效果演示
开发一言小部件
使用 WigetKit 开发 Widget 的主要就是 View、Provider、Data。简单来说,获取到 Data 之后使用 Provider 显示在 View 上就是
Widget 了。
1. 编写 View
一言小部件的 View 可能是这三个当中最简单的部分了,这里为了方便理解直接使用一个 Text 来表示吧。
1
2
3
4
5
6
|
struct OneWordView: View {
var content:String = " 每日一言 "
var body: some View {
Text(content)
}
}
|
如上述代码,content 用来控制显示内容, 一个最简单的 Text 显示即可,还能靠 Swift 实现自动暗黑模式。
这里对 View 就不多谈了,这部分 View 比较容易。
2. 获取 Data
一言部件的数据如何获取?从哪来?
目前 iWidget 采取的方案是通过 Hitokoto 提供的接口来获取对应的数据。
不得不说,Hitokoto 的接口很方便有质量。
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
|
import Foundation
struct OneWord {
let content: String
let length: Int
}
struct OneWordLoader {
static func fetch(completion: @escaping (Result<OneWord, Error>) -> Void) {
let oneWordURL = URL(string: "https://v1.hitokoto.cn/")!
let task = URLSession.shared.dataTask(with: oneWordURL) {(data, response, error) in
guard error == nil else {
completion(.failure(error!))
return
}
let oneWord = getOneWordInfo(fromData: data!)
completion(.success(oneWord))
}
task.resume()
}
static func getOneWordInfo(fromData data: Foundation.Data) -> OneWord {
let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
let content = json["hitokoto"] as! String
let length = json["length"] as! Int
return OneWord(content: content, length: length)
}
}
|
其中 OneWord
声明了解析过后的数据类型,通过 OneWordLoader
的 fetch
方法请求 API 获取 JSON
数据后通过内部的 getOneWordInfo
方法解析 JSON 数据返回 OneWord
数据。
这部分代码我封装到了 iWidget/Data/OneWordData.swift 中, 具体可见 GitHub。
3. 编写 Provider
Provider 的作用主要用于控制 Widget 的刷新
一言的 Provider 中,我们需要在获取 API 数据后再给 Widget 刷新显示,故大概流程为: 获取数据 -> 获取成功后刷新数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
struct OneWordProvider: IntentTimelineProvider {
public func snapshot(for configuration: ConfigurationIntent, with context: Context, completion: @escaping (OneWordEntry) -> ()) {
let entry = OneWordEntry(date: Date(),data: OneWord(content: " 一言 ", length: 2))
completion(entry)
}
public func timeline(for configuration: ConfigurationIntent, with context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let currentDate = Date()
let refreshDate = Calendar.current.date(byAdding: .minute, value: 60, to: currentDate)!
OneWordLoader.fetch { result in
let oneWord: OneWord
if case .success(let fetchedData) = result {
oneWord = fetchedData
} else {
oneWord = OneWord(content: " 获取失败 ", length: 4)
}
let entry = OneWordEntry(date: currentDate,data: oneWord)
let timeline = Timeline(entries: [entry], policy: .after(refreshDate))
completion(timeline)
}
}
}
|
其中
let timeline = Timeline(entries: [entry], policy: .after(refreshDate))
entries 提供了下次更新的数据,policy 提供了下次更新的时间。
其中 policy
可填 .never
永不更新 (可通过 WidgetCenter
更新)、.after(Date)
指定多久之后更新、.atEnd
指定 Widget
通过你提供的 entries 的 Date 更新。
预览的坑
介绍
个人被这个坑卡了很久,后来看了 WidgetKit 的代码才找到原因,这个真心坑。
当使用预览的时候编译会报错:
1
|
reference to invalid associated type 'Entry' of type 'Provider'
|
这是因为你启用了预览:
1
2
3
4
5
6
|
struct MainWidget_Previews: PreviewProvider {
static var previews: some View {
PayToolsEntryView(entry: SimpleEntry(date: Date()))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
|
解决方案
对于这个问题我目前只想到了 2 个办法解决:
1. 直接注释预览
在编译前注释 Preview 的代码即可,需要预览再解除
2.Provider 添加 typealias
直接在 Provider 中添加:
1
|
typealias Entry = SimpleEntry
|
其中 SimpleEntry 需要使用你的 Entry 的变量名替换。
后记
这一次大概整理了下自己开发一个 Widget 的大概过程,还有可配置小部件和小部件 Link 等操作下次再分享,敬请期待。
完整代码见 GitHub
后续还会慢慢完善 WidgetKit 开发的文章,同时 iWiget 也会不断完善,这篇文章对你有用就点个
Star 吧!
项目地址: https://github.com/Littleor/iWidget