今天要来自定义一个展示诗词的小组件
Widget
,它显示的内容包括:诗词名字、作者、前两句(为什么只显示前两句呢,因为我找的免费API
它只有给前两句😎)。切入正题,接下里开始实现,还不了解
Widget
的同学可以先看看这里iOS14 Widget小组件开发实践1
1、新建一个Widget Extension
,命名PoetryWidget
。
2、获取网络数据请求处理
先看下免费的API
接口:https://v1.alapi.cn/api/shici?type=shuqing的返回参数:
{
"code": 200,
"msg": "success",
"data": {
"content": "范增一去无谋主,韩信原来是逐臣。",
"origin": "乌江项王庙",
"author": "严遂成",
"category": "古诗文-抒情-爱国"
},
"author": {
"name": "Alone88",
"desc": "由Alone88提供的免费API 服务,官方文档:www.alapi.cn"
}
}
声明一个诗词需要的数据model
:
struct Poetry {
let content: String // 内容
let origin: String // 名字
let author: String // 作者
}
既然是请求API
接口,那就需要一个请求函数,并且回调请求参数,声明一个请求工具,实现数据请求以及json
转model
:
struct PoetryRequest {
static func request(completion: @escaping (Result<Poetry, Error>) -> Void) {
let url = URL(string: "https://v1.alapi.cn/api/shici?type=shuqing")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard error == nil else {
completion(.failure(error!))
return
}
let poetry = poetryFromJson(fromData: data!)
completion(.success(poetry))
}
task.resume()
}
static func poetryFromJson(fromData data: Data) -> Poetry {
let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
//因为免费接口请求频率问题,如果太频繁了,请求可能失败,这里做下处理,放置crash
guard let data = json["data"] as? [String: Any] else {
return Poetry(content: "诗词加载失败,请稍微再试!", origin: "耐依福", author: "佚名")
}
let content = data["content"] as! String
let origin = data["origin"] as! String
let author = data["author"] as! String
return Poetry(content: content, origin: origin, author: author)
}
}
3、搭建展示界面
数据有了,就可以实现界面搭建了,这里必须用SwiftUI
实现:
struct PoetryWidgetView: View {
let entry: PoetryEntry
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(entry.poetry.origin)
.font(.system(size: 20))
.fontWeight(.bold)
Text(entry.poetry.author)
.font(.system(size: 16))
Text(entry.poetry.content)
.font(.system(size: 18))
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .leading)
.padding()
.background(LinearGradient(gradient: Gradient(colors: [.init(red: 144 / 255.0, green: 252 / 255.0, blue: 231 / 255.0), .init(red: 50 / 204, green: 188 / 255.0, blue: 231 / 255.0)]), startPoint: .topLeading, endPoint: .bottomTrailing))
}
}
这里用了三个Text
文本组件来展示三部分内容,很明显这里没有分别处理三种样式的UI
,因此在编辑预览界面,三种样式的Widget
能提供的内容是一样的,如果要实现三种样式各显示不同的内容,需要获取WidgetFamily
的实例属性,实现不同的界面展示。
@Environment(\.widgetFamily) var family: WidgetFamily
来看看苹果官方文档的相关实现代码片段例子:
struct GameStatusView : View {
@Environment(\.widgetFamily) var family: WidgetFamily
var gameStatus: GameStatus
@ViewBuilder
var body: some View {
switch family {
case .systemSmall: GameTurnSummary(gameStatus)
case .systemMedium: GameStatusWithLastTurnResult(gameStatus)
case .systemLarge: GameStatusWithStatistics(gameStatus)
default: GameDetailsNotAvailable()
}
}
}
4、实现PoetryEntry
让结构体PoetryEntry
遵守TimelineEntry
协议:
struct PoetryEntry: TimelineEntry {
var date: Date
let poetry: Poetry // 可以理解为绑定了Poetry模型数据
}
5、实现PoetryProvider
让结构体PoetryProvider
遵守TimelineProvider
协议,同时实现:
struct PoetryProvider: TimelineProvider {
func placeholder(in context: Context) -> PoetryEntry { ...... }
func getSnapshot(in context: Context, completion: @escaping (PoetryEntry) -> Void) { ...... }
func getTimeline(in context: Context, completion: @escaping (Timeline<PoetryEntry>) -> Void) { ...... }
}
placeholder
:实现默认视图
func placeholder(in context: Context) -> PoetryEntry {
let poetry = Poetry(content: "床前明月光,疑似地上霜", origin: "耐依福", author: "佚名")
return PoetryEntry(date: Date(), poetry: poetry)
}
getSnapshot
:在组件的添加页面可以看到效果
func getSnapshot(in context: Context, completion: @escaping (PoetryEntry) -> Void) {
let poetry = Poetry(content: "床前明月光,疑似地上霜", origin: "月光光", author: "佚名")
let entry = PoetryEntry(date: Date(), poetry: poetry)
completion(entry)
}
getTimeline
:在这个方法内可以进行网络请求,拿到的数据保存在对应的entry
中,调用completion
之后会到刷新小组件
这里有坑高能!!!
这里关于刷新策略,根据官方文档来看,Timeline
的刷新策略是会延迟的,并不一定根据你设定的时间来。同时官方规定每个配置的窗口小部件每天都接收有限数量的刷新,具体的详情说明请看官方解释:TimelineProvider
func getTimeline(in context: Context, completion: @escaping (Timeline<PoetryEntry>) -> Void) {
let currentDate = Date()
// 下一次更新间隔以分钟为单位,间隔5分钟请求一次新的数据
let updateDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)
PoetryRequest.request { result in
let poetry: Poetry
if case .success(let response) = result {
poetry = response
} else {
poetry = Poetry(content: "诗词加载失败,请稍微再试!", origin: "耐依福", author: "佚名")
}
let entry = PoetryEntry(date: currentDate, poetry: poetry)
let timeline = Timeline(entries: [entry], policy: .after(updateDate!))
completion(timeline)
}
}
实现PoetryWidget
@main
:代表着Widget
的主入口,系统从这里加载
kind
:是Widget
的唯一标识
StaticConfiguration
:初始化配置代码
configurationDisplayName
:添加编辑界面展示的标题
description
:添加编辑界面展示的描述内容
supportedFamilies
这里可以限制要提供三个样式中的哪几个
@main
struct PoetryWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "PoetryWidget", provider: PoetryProvider()) { entry in
PoetryWidgetView(entry: entry)
}
.configurationDisplayName("每日一湿")
.description("默读并背诵全文")
}
}
到这里一个完整的Widget
小组件的实现就完成了,然后就让代码跑起来,看看效果。
参考资料
creating-a-widget-extension
TimelineProvider
https://swiftrocks.com