iOS14 Widget小组件开发实践2——自定义Widget


今天要来自定义一个展示诗词的小组件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接口,那就需要一个请求函数,并且回调请求参数,声明一个请求工具,实现数据请求以及jsonmodel

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,082评论 5 464
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,231评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,047评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,977评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,893评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,976评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,605评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,888评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,906评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,732评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,513评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,980评论 3 301
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,132评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,447评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,027评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,232评论 2 339