SwiftUI

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

今天要来自定义一个展示诗词的小组件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接口,那就需要一个请求函数,并且回调请求参数,声明一个请求工具,实现数据请求以及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小组件的实现就完成了,然后就让代码跑起来,看看效果。