前言

2020 年 06 月 22 日的 WWDC 上 iOS14 的新特性 - 小部件正式在 iOS 上线,同时 WidgetKit 也正式面向广大开发者使用。

此帖开始记录学习 WidgetKit 的经历, 同时开源了 - iWidget 作为 WidgetKit 的简单演示,看完这篇文章认为有用可点个 Star。

项目地址: https://github.com/Littleor/iWidget

没有看过前一节的建议看看这个: (iOS14)WidgetKit 开发实战 1- 初识 iOS 小部件

效果演示

演示 GIF 白天模式 黑夜模式

开发一言小部件

使用 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 声明了解析过后的数据类型,通过 OneWordLoaderfetch 方法请求 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