iOS14小组件(Widget)实战三

2,173 阅读4分钟

iOS14小组件(Widget)实战三

前言

前面两章我们了解小组件的创建和小组件内部的工作机制,现在我们自己动手开发我们小组件,希望通过这一节大家都能很好的理解和掌握小组件的使用。项目demo我已经上传到码云,项目链接小组件demo

重新定义@main入口

  1. 创建一个文件MyMainWidget 图像2021-4-7 下午1.50.jpg 图像2021-4-7 下午1.50 (1).jpg
  2. 去掉系统默认的小组件@main入口 图像2021-4-7 下午1.50 (2).jpg
  3. MyMainWidget中添加自定义入口代码 图像2021-4-7 下午1.50 (3).jpg
  4. 重新运行主程序,小组件能正常显示说明@main入口替换成功了

新建自定义小组件

  1. 创建一个文件MyDIYWidget 图像2021-4-7 下午1.50 (4).jpg
  2. 创建Widget的方式有两种
  • 一种是动态配置创建,系统默认提供的就是这种! 图像2021-4-7 下午1.50 (5).jpg
  • 第二种是静态配置创建,今天我们就先用静态配置,后续再回过头来讨论两者创建方式的区别 图像2021-4-7 下午1.50 (6).jpg
  1. 我们用静态初始化配置创建一个Widget,然后定义MyDIYWidgetProvider实现TimelineProvider协议,方法中用到的数据模型MyDIYWidgetEntry需要遵循TimelineEntry协议,编写后的代码如图,同时附上代码
import SwiftUI
import WidgetKit

struct MyDIYWidgetEntry: TimelineEntry {
    let date: Date
}

struct MyDIYWidgetProvider: TimelineProvider {
    // 默认占位数据
    func placeholder(in context: Context) -> MyDIYWidgetEntry {
        return MyDIYWidgetEntry(date: Date())
    }
    // 快照
    func getSnapshot(in context: Context, completion: @escaping (MyDIYWidgetEntry) -> Void) {
        completion(MyDIYWidgetEntry(date: Date()))
    }
    // 根据时间线刷新,这里可以进行网络请求
    func getTimeline(in context: Context, completion: @escaping (Timeline<MyDIYWidgetEntry>) -> Void) {
        var entries: [MyDIYWidgetEntry] = []
        let currentDate = Date()
        for hourOffset in 0 ..< 60 {
            let entryDate = Calendar.current.date(byAdding: .second, value: hourOffset, to: currentDate)!
            let entry = MyDIYWidgetEntry(date: entryDate)
            entries.append(entry)
        }
        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

// 用于显示的UI
struct MyDIYWidgetEntryView: View {
    // 接收数据
    var entry: MyDIYWidgetEntry
    // UI展示
    var body: some View {
        Text("我的小组件\(entry.date)")
    }
}

struct MyDIYWidget: Widget {
    let kind = "MyDIYWidget"
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: MyDIYWidgetProvider()) { entry in
            MyDIYWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("自定义小组件")
        .description("输入你想描述的说明文案")
    }
}

图像2021-4-7 下午1.50 (7).jpg 图像2021-4-7 下午1.50 (8).jpg 如果你的代码跟我的写的一样,那么把刚才我们编写的MyDIYWidget小组件添加到@main入口处,再到桌面上添加上我们自定义的小组件就可以看到了 图像2021-4-7 下午1.50 (9).jpg 运行项目后,添加我们的小组件,变成如下

图像2021-4-7 下午1.50 (10).jpg 图像2021-4-7 下午1.51.jpg

卡住不动,因为我们只创建了60个数据,系统根据每秒刷新一个数据后,一分钟过后就没有数据了。我们设置的刷新机制是.atEnd,也就是说没有数据系统就会调用getTimeline方法重新填充数据,但是由于1分钟刷新一次太频繁,苹果会自动屏蔽掉,按照网上的说法是小于5分钟的刷新就会屏蔽掉,这里我也没探讨过,只知道这里频繁刷新会被屏蔽掉。

需求分析

我们先把我们需要做的样式先摆出来,让大家看看是个什么样式;我们要做四个item,横向排列,点击任意一个item跳转到指定的页面

图像2021-4-7 下午1.51 (1).jpg

这样分析我们的数据模型一个字段是title, 另外一个就是跳转链接urlScheme。

网络请求

  1. 创建一个网络请求类 图像2021-4-7 下午1.51 (2).jpg 添加网络请求方法,数据解析
import SwiftUI

![图像2021-4-7 下午1.51.jpg](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/69bf09c25b1e472499ebc0201b4616b3~tplv-k3u1fbpfcp-watermark.image)
// 需求分析中定义的两个字段
struct MyDIYWidgetModel {
    var title: String
    var urlScheme: String
}

struct MyNetWorkHelper {
    // 请求url(这里改成你自己的url地址,我这里是模拟网络请求,就随便写一个)
    static let url_str = "https:www.baidu.com"
    // 网络请求方法
    static func request(completion: @escaping (Array<MyDIYWidgetModel>) -> Void) {
        let url = URL.init(string: url_str)
        let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
            // 这里模拟网络请求造的假数据
            let models = modelFromJson()
            completion(models)
        }
        task.resume()
    }
    
    // 数据序列化(解析)
    fileprivate static func modelFromJson() -> Array<MyDIYWidgetModel> {
        var models: [MyDIYWidgetModel] = []
        
        let data = [["title": "快捷选车", "urlScheme": "a"],
                    ["title": "销量排行", "urlScheme": "b"],
                    ["title": "厂家直购", "urlScheme": "c"],
                    ["title": "汽车保养", "urlScheme": "a"]]
        
        for item in data {
            var title = ""
            var urlScheme = ""
            if let n = item["title"] {
                title = n
            }
            if let n = item["urlScheme"] {
                urlScheme = n
            }
            let model = MyDIYWidgetModel(title: title, urlScheme: urlScheme)
            models.append(model)
        }
        return models
    }
}
  1. 给自定义数据模型添加models: [MyDIYWidgetModel]字段,用于存储网络获取到的数据 图像2021-4-7 下午1.51 (3).jpg
  2. getTimeline里用网络请求类请求数据,并把数据添加到entries,再创建时间线回调 图像2021-4-7 下午1.51 (4).jpg
  3. 同时需要更新placeholder和getSnapshot方法里MyDIYWidgetEntry的创建方式,因为多了个modes的字段,总的代码如下
struct MyDIYWidgetEntry: TimelineEntry {
    let date: Date
    let models: [MyDIYWidgetModel]
}

struct MyDIYWidgetProvider: TimelineProvider {
    // 默认占位数据
    func placeholder(in context: Context) -> MyDIYWidgetEntry {
        var models: [MyDIYWidgetModel] = []
        let data = [["title": "快捷选车", "urlScheme": "a"],
                    ["title": "销量排行", "urlScheme": "b"],
                    ["title": "厂家直购", "urlScheme": "c"],
                    ["title": "汽车保养", "urlScheme": "a"]]
        
        for item in data {
            var title = ""
            var urlScheme = ""
            if let n = item["title"] {
                title = n
            }
            if let n = item["urlScheme"] {
                urlScheme = n
            }
            let model = MyDIYWidgetModel(title: title, urlScheme: urlScheme)
            models.append(model)
        }
        return MyDIYWidgetEntry.init(date: Date(), models: models)
    }
    // 快照
    func getSnapshot(in context: Context, completion: @escaping (MyDIYWidgetEntry) -> Void) {
        var models: [MyDIYWidgetModel] = []
        let data = [["title": "快捷选车", "urlScheme": "a"],
                    ["title": "销量排行", "urlScheme": "b"],
                    ["title": "厂家直购", "urlScheme": "c"],
                    ["title": "汽车保养", "urlScheme": "a"]]
        
        for item in data {
            var title = ""
            var urlScheme = ""
            if let n = item["title"] {
                title = n
            }
            if let n = item["urlScheme"] {
                urlScheme = n
            }
            let model = MyDIYWidgetModel(title: title, urlScheme: urlScheme)
            models.append(model)
        }
        completion(MyDIYWidgetEntry.init(date: Date(), models: models))
    }
    // 根据时间线刷新,这里可以进行网络请求
    func getTimeline(in context: Context, completion: @escaping (Timeline<MyDIYWidgetEntry>) -> Void) {
        var entries: [MyDIYWidgetEntry] = []
        // 网络请求数据
        MyNetWorkHelper.request { (models) in
            let entry = MyDIYWidgetEntry.init(date: Date(), models: models)
            entries.append(entry)
            let timeline = Timeline(entries: entries, policy: .never)
        completion(timeline)
        }
    }
}

// 用于显示的UI
struct MyDIYWidgetEntryView: View {
    // 接收数据
    var entry: MyDIYWidgetEntry
    // UI展示
    var body: some View {
        Text("我的小组件\(entry.date)")
    }
}

struct MyDIYWidget: Widget {
    let kind = "MyDIYWidget"
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: MyDIYWidgetProvider()) { entry in
            MyDIYWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("自定义小组件")
        .description("输入你想描述的说明文案")
    }
}

页面展示

  1. 创建UI用于展示获取到的数据,这里直接修改MyDIYWidgetEntryView处的代码,并且添加跳转链接
// 用于显示的UI
struct MyDIYWidgetEntryView: View {
    // 接收数据
    var entry: MyDIYWidgetEntry
    // UI展示
    var body: some View {
        GeometryReader{ geo in
            HStack(spacing: 0) {
                ForEach(entry.models, id: \.title) { model in
                    let url = URL(string: model.urlScheme)
                    // 添加跳转链接url
                    Link(destination: url!) {
                        Text(model.title)
                            .frame(width: geo.size.width / CGFloat(entry.models.count), height: geo.size.height)
                            .font(.system(size: 11, weight: .regular, design: .default))
                            .foregroundColor(.black)
                            .lineLimit(1)
                    }
                }
            }
        }
    }
}
  1. 删除SceneDelegate,设置AppDelegate的根视图为ViewController,添加openURL的方法,代码如下
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    self.window.backgroundColor = [UIColor whiteColor];
    ViewController *vc = [ViewController new];
    self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:vc];
    [self.window makeKeyAndVisible];
    return YES;
}

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
    NSLog(@"%@", url.absoluteURL);
    return YES;
}

这里就可以根据url里边接收到的a,b,c,d来确定跳转到不同的页面了