今天要来自定义一个展示诗词的小组件Widget
,它显示的内容包括:诗词名字、作者、前两句(为什么只显示前两句呢,因为我找的免费API
它只有给前两句😎)。切入正题,接下里开始实现。
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
小组件的实现就完成了,然后就让代码跑起来,看看效果。