iOS widget 小组件开辟

程序员 2024-10-2 22:46:34 39 0 来自 中国
iOS widget 小组件开辟

Github地点

项目选择对应语言项目小组件部分 Github地点 https://github.com/HahnLoving/iOS_Study
iOS 多个widget调试题目

iOS 多个widget调试题目
https://www.jianshu.com/p/d0993b2c6e34
iOS widget 小组件 秒级革新
https://www.jianshu.com/p/40d631f32260
创建项目

widget 代码阐明

Provider

struct Provider: TimelineProvider {    // 占位视图,比方网络哀求失败、发生未知错误、第一次展示小组件都会展示这个view    func placeholder(in context: Context) -> SimpleEntry {        SimpleEntry(date: Date())    }    // 界说Widget预览中怎样展示,以是提供默认值要在这里    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {        let entry = SimpleEntry(date: Date())        completion(entry)    }    // 决定 Widget 何时革新    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {        var entries: [SimpleEntry] = []        // Generate a timeline consisting of five entries an hour apart, starting from the current date.        let currentDate = Date()        for hourOffset in 0 ..< 5 {            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!            let entry = SimpleEntry(date: entryDate)            entries.append(entry)        }        let timeline = Timeline(entries: entries, policy: .atEnd)        completion(timeline)    }}SimpleEntry

// 渲染 Widget 所需的数据模子,需要服从TimelineEntry协议struct SimpleEntry: TimelineEntry {    let date: Date}MainWidgetEntryView

// 渲染的viewstruct MainWidgetEntryView : View {    var entry: Provider.Entry    var body: some View {        Text(entry.date, style: .time)    }}MainWidget

@mainstruct MainWidget: Widget {    // 主件唯一标识符    let kind: String = "MainWidget"    var body: some WidgetConfiguration {        StaticConfiguration(kind: kind, provider: Provider()) { entry in            MainWidgetEntryView(entry: entry)        }        // 标题        .configurationDisplayName("My Widget")        // 详情        .description("This is an example widget.")        // 摆列设置        .supportedFamilies([.systemMedium, .systemSmall, .systemLarge])    }}MainWidget_Previews

// SwiftUI Xcode 测试预览视图struct MainWidget_Previews: PreviewProvider {    static var previews: some View {        MainWidgetEntryView(entry: SimpleEntry(date: Date()))            .previewContext(WidgetPreviewContext(family: .systemSmall))    }}效果图

3.jpg widget 分组

widget 有多组和单组区别。
单组包罗 小,中,大,特大(iOS 15)
多组包罗 小,小,小
单组的例子如体系的天气widget包罗小,中,大三个组件
4.jpg 多组以付出宝为例子。包罗两个小的组件
8.jpg 单组件适配开辟

MainWidgetEntryView
// 渲染的viewstruct MainWidgetEntryView : View {    var entry: Provider.Entry        // 判定小组件的范例    @Environment(\.widgetFamily) var family: WidgetFamily    var body: some View {//        Text(entry.date, style: .time)        switch family {        case .systemSmall:            VStack(alignment: .center, spacing: 0) {                Text("小")                Spacer().frame(height: 20)                Text(entry.date, style: .time)            }        case .systemMedium:            VStack(alignment: .center, spacing: 0) {                Text("中")                Spacer().frame(height: 20)                Text(entry.date, style: .time)            }        default:            VStack(alignment: .center, spacing: 0) {                Text("大")                Spacer().frame(height: 20)                Text(entry.date, style: .time)            }        }    }}效果图
多组件开辟 WidgetBundle

多组件开辟使用了 WidgetBundle
下面用了以下5个案例教学多组件开辟
1.网络哀求 NetWorkData
2.网络图片 NetWorkImage
3.编辑小组件 Edit
4.和app当地数据举行交互和点击交互 AppData
MainWidget
先表明全部代码
import WidgetKitimport SwiftUIimport Intentsstruct SimpleEntry: TimelineEntry {    public let date: Date}struct PlaceholderView : View {    //这里是PlaceholderView - 提示用户选择部件功能    var body: some View {        Text("lace Holder")    }}@mainstruct MainWidget: WidgetBundle {    @WidgetBundleBuilder    var body: some Widget {//        OneWordWidget()//        FristWidget(title: "hahn", desc: "hahn1")//        CountDownWidget()//        Demo(title: "Demo", desc: "Demo")        NetWorkData(title: "网络哀求小组件", desc: "网络哀求小组件列表")        NetWorkImage(title: "网络图片小组件", desc: "网络图片小组件列表")        Edit(title: "编辑小组件", desc: "编辑小组件列表")        AppData(title: "app数据交互小组件", desc: "app数据交互小组件列表")    }}新建分组的widget

新建SwiftUI文件
新建 SirKit Intent Definition File 文件
记得勾选这两个,否则会无法编辑的
新建 Intent
14.png Title就是你对应Widget 名字
Category 选择View
勾Widgets
15.png 网络哀求 NetWorkData

先按照上面的步调创建 NetWorkData 的widget 和 Intent文件
注意需要info.plist设置可以网络哀求。如果主项目没有网络哀求,先做一次网络哀求。
我这里使用了Moya 框架举行Api 哀求。
为widget 扩展添加pod
platform :ios, '14.0'use_frameworks!inhibit_all_warnings!source 'https://github.com/CocoaPods/Specs.git'target 'StudySwiftUI' do# swiftend# Extension 名字  在Targets内里可以查察名字target 'MainWidgetExtension' do  pod 'ObjectMapper'  pod 'Moya'  pod 'ObjectMapper'  pod 'HandyJSON'  pod 'SwiftyJSON'end哀求的json 布局如下
{    "items":[        {            "title":"Quiz for Designing",            "subTitle":"834 questions in tottal",            "icon":"tlIconDesign1",            "correctRate":80,            "submit":48,            "starCount":4        },        {            "title":"Quiz for Coding",            "subTitle":"1000 questions in tottal",            "icon":"tlIconMobile1",            "correctRate":98,            "submit":68,            "starCount":5        },        {            "title":"Quiz for Officing",            "subTitle":"569 questions in tottal",            "icon":"tlIconOffice1",            "correctRate":60,            "submit":43,            "starCount":3        },        {            "title":"Quiz for Painting",            "subTitle":"321 questions in tottal",            "icon":"tlIconDesign2",            "correctRate":87,            "submit":48,            "starCount":5        }    ]}使用HandyJSON 创建Model
import Foundationimport HandyJSONstruct ZModel: HandyJSON {    var items: [ZQuiz] = []}struct ZQuiz: HandyJSON {        var title : String = ""    var subTitle : String = ""    var icon : String = ""    var correctRate : Int = 0    var submit : Int = 0    var starCount : Int = 0}我们取json数组第一个字典的title渲染出来
NetWorkData.swift
import WidgetKitimport SwiftUIimport Intentsstruct NetWorkDataProvider: IntentTimelineProvider {    func placeholder(in context: Context) -> NetWorkDataEntry {        NetWorkDataEntry(date: Date(), configuration: NetWorkDataIntent(), displaySize: context.displaySize, model: ZModel())    }    // 界说Widget预览中怎样展示,以是提供默认值要在这里    func getSnapshot(for configuration: NetWorkDataIntent, in context: Context, completion: @escaping (NetWorkDataEntry) -> ()) {        let entry = NetWorkDataEntry(date: Date(), configuration: configuration, displaySize: context.displaySize, model: ZModel())        completion(entry)    }    // 决定 Widget 何时革新    func getTimeline(for configuration: NetWorkDataIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {                // API哀求        NetWorkRequest(API.getQuizListApi, modelType: ZModel.self) { (model, responseModel) in            // 每隔2个小时革新。            let entry = NetWorkDataEntry(date: Date(), configuration: configuration, displaySize: context.displaySize, model: model)                // refresh the data every two hours            let expireDate = Calendar.current.date(byAdding: .hour, value: 2, to: Date()) ?? Date()            let timeline = Timeline(entries: [entry], policy: .after(expireDate))            completion(timeline)        } failureCallback: { (responseModel) in                    }            }}struct NetWorkDataEntry: TimelineEntry {    let date: Date    let configuration: NetWorkDataIntent    let displaySize: CGSize    let model: ZModel}struct NetWorkDataEntryView : View {    var entry: NetWorkDataProvider.Entry    var body: some View {        Text(entry.model.items.first?.title ?? "0")    }}// 单个//@mainstruct NetWorkData: Widget {    let kind: String = "NetWorkData"    var title: String = ""    var desc: String = ""        var body: some WidgetConfiguration {        IntentConfiguration(kind: kind, intent: NetWorkDataIntent.self, provider: NetWorkDataProvider()) { entry in            NetWorkDataEntryView(entry: entry)        }        .configurationDisplayName(title)        .description(desc)        .supportedFamilies([.systemMedium, .systemSmall])//        .supportedFamilies([.systemMedium, .systemLarge, .systemSmall])    }}效果
渲染出来第一个元素的title
16.png 网络图片 NetWorkImage

先按照上面的步调创建 NetWorkImage 的widget 和 Intent文件
注意小组件目前是不支持异步下载的和动画的
以是图片是需要同步下载的
WidgetImageLoader 先对图片下载封装
import Foundationimport SwiftUIenum WidgetError: Error {    case netError //网络哀求堕落    case dataError //数据剖析错误}/*  由于不支持异步加载图片 以是临时在网络哀求好之后,直接下载好全部图片 使用NSCache暂存图片 */class WidgetImageLoader {        static var shareLoader = WidgetImageLoader()    private var cache = NSCache<NSURL, UIImage>()        /// 下载单张图片    /// - Parameters:    ///   - imageUrl: 图片URL    ///   - completion: 乐成的回调    func downLoadImage(imageUrl: String?,completion: @escaping (Result<Image, WidgetError>) -> Void) {        if let imageUrl = imageUrl {            if let cacheImage  = self.cache.object(forKey: NSURL(string: imageUrl)!) {                completion(.success(Image(uiImage: cacheImage)))            } else {                URLSession.shared.dataTask(with: URL(string: imageUrl)!) { (data, response, error) in                    if let data = data,                       let image = UIImage(data: data) {                        self.cache.setObject(image, forKey: NSURL(string: imageUrl)!)                        completion(.success(Image(uiImage: image)))                    } else {                        completion(.failure(WidgetError.netError))                    }                }.resume()            }        } else {            completion(.failure(WidgetError.dataError))        }    }        /// 批量下载图片    /// - Parameters:    ///   - imageAry: 图片数组聚集    ///   - placeHolder: 占位图,可传可不传    ///   - completion: 乐成回调    func downLoadImage(imageAry:[String],placeHolder:Image?,completion: @escaping (Result<[Image], WidgetError>) -> Void) {        let groupispatchGroup = DispatchGroup()        var array = [Image]()        for image in imageAry {            group.enter()            self.downLoadImage(imageUrl: image) { result in                let image : Image                if case .success(let response) = result {                    image = response                } else {                    image = placeHolder ?? Image("")                }                array.append(image)                group.leave()            }        }        group.notify(queue: DispatchQueue.main) {            completion(.success(array))        }    }        /// 获取image    /// - Parameters:    ///   - imageUrl: 图片地点    ///   - placeHolderImage: 占位图,请尽量传入    /// - Returns: 返回效果    func getImage(_ imageUrl:String, _ placeHolderImage:UIImage?) -> UIImage {        if let cacheImage  = self.cache.object(forKey: NSURL(string: imageUrl)!) {            return cacheImage        } else {            if let cacheImag = placeHolderImage {                return cacheImag            } else {                return UIImage()            }        }    }}NetWorkImage
import WidgetKitimport SwiftUIimport Intentsstruct NetWorkImageProvider: IntentTimelineProvider {        var img = Image("widget_background_test")        func placeholder(in context: Context) -> NetWorkImageEntry {        NetWorkImageEntry(date: Date(), configuration: NetWorkImageIntent(), displaySize: context.displaySize, img: img)    }    // 界说Widget预览中怎样展示,以是提供默认值要在这里    func getSnapshot(for configuration: NetWorkImageIntent, in context: Context, completion: @escaping (NetWorkImageEntry) -> ()) {        let entry = NetWorkImageEntry(date: Date(), configuration: configuration, displaySize: context.displaySize, img: img)        completion(entry)    }    // 决定 Widget 何时革新    func getTimeline(for configuration: NetWorkImageIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {                var img = Image("widget_background_test")        // 占位图        WidgetImageLoader.shareLoader.downLoadImage(imageUrl: "https://lmg.jj20.com/up/allimg/tx18/0217202027012.jpg") { result in            switch result {            case .success(let image):                print("乐成 = \(image)")                img = image                // 每隔2个小时革新。                let entry = NetWorkImageEntry(date: Date(), configuration: configuration, displaySize: context.displaySize,img: img)                    // refresh the data every two hours                let expireDate = Calendar.current.date(byAdding: .hour, value: 2, to: Date()) ?? Date()                let timeline = Timeline(entries: [entry], policy: .after(expireDate))                                completion(timeline)            case .failure(let error):                print("失败 = \(error)")            }        }    }}struct NetWorkImageEntry: TimelineEntry {    let date: Date    let configuration: NetWorkImageIntent    let displaySize: CGSize    let img: Image}struct NetWorkImageEntryView : View {    var entry: NetWorkImageProvider.Entry    var body: some View {        entry.img            .resizable()            .frame(width: entry.displaySize.width, height: entry.displaySize.height, alignment: .center)    }}// 单个//@mainstruct NetWorkImage: Widget {    let kind: String = "NetWorkImage"    var title: String = ""    var desc: String = ""        var body: some WidgetConfiguration {        IntentConfiguration(kind: kind, intent: NetWorkImageIntent.self, provider: NetWorkImageProvider()) { entry in            NetWorkImageEntryView(entry: entry)        }        .configurationDisplayName(title)        .description(desc)        .supportedFamilies([.systemMedium, .systemSmall])//        .supportedFamilies([.systemMedium, .systemLarge, .systemSmall])    }}效果
编辑小组件 Edit

先按照上面的步调创建 Edit 的widget 和 Intent文件
这里需要注意要Intent文件一个勾上主项目和你widget,否则小组件编辑无法载入。
在Edit Intent文件创建title字段

Edit
import WidgetKitimport SwiftUIimport Intentsstruct EditProvider: IntentTimelineProvider {    func placeholder(in context: Context) -> EditEntry {        EditEntry(date: Date(), configuration: EditIntent())    }    //    typealias Entry = EditEntry    func getSnapshot(for configuration: EditIntent, in context: Context, completion: @escaping (EditEntry) -> Void) {        let entry = EditEntry(date: Date(), configuration: configuration)        completion(entry)    }    func getTimeline(for configuration: EditIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {        let entry = EditEntry(date: Date(), configuration: configuration)            // refresh the data every two hours        let expireDate = Calendar.current.date(byAdding: .hour, value: 2, to: Date()) ?? Date()        let timeline = Timeline(entries: [entry], policy: .after(expireDate))        completion(timeline)    }}struct EditEntry: TimelineEntry {    public let date: Date    let configuration: EditIntent}struct EditEntryView : View {    //这里是Widget的范例判定    var entry: EditProvider.Entry        @ViewBuilder    var body: some View {        VStack(alignment: .center) {            if entry.configuration.title == "请编辑小组件" {                Text("请编辑小组件吧")                    .font(.headline)                    .foregroundColor(Color.gray)            }else {                Text(entry.configuration.title ?? "")                    .font(.headline)                    .foregroundColor(Color.gray)            }        }        .padding(.all)    }    }struct Edit: Widget {    private let kind: String = "Edit"    var title: String = ""    var desc: String = ""        public var body: some WidgetConfiguration {        IntentConfiguration(kind: kind, intent: EditIntent.self, provider: EditProvider()) { entry in            EditEntryView(entry: entry)        }        .configurationDisplayName(title)        .description(desc)        .supportedFamilies([.systemSmall])            }}效果

21.png 和app当地数据举行交互 AppData

先按照上面的步调创建 AppData 的widget 和 Intent文件
数据交互

小组件和app交互是通过AppGroups的交互的
主项目和子项目都需要创建AppGroups
在主项目生存数据
Swift
// appgrouplet z = UserDefaults.init(suiteName: "你的appgroupid")// 生存数据z?.set("demo", forKey: "demo")let demo = z?.value(forKey: "demo")print("AppGroup交互 = \(String(describing: demo))")// 全部革新WidgetCenter.shared.reloadAllTimelines() // 革新指定的kind// WidgetCenter.shared.reloadTimelines(ofKind: <#T##String#>)OC 需要创建桥接文件
创建WidgetKitManager.swift
第一次的话会自动设置
WidgetKitManager.swift
import WidgetKit@objc@available(iOS 14.0, *)class WidgetKitManager: NSObject {    @objc    static let shareManager = WidgetKitManager()        /// MARK: 革新全部小组件    @objc    func reloadAllTimelines() {       #if arch(arm64) || arch(i386) || arch(x86_64)            WidgetCenter.shared.reloadAllTimelines()            #endif    }    /// MARK: 革新单个小组件    /*     kind: 小组件Configuration 中的kind     */    @objc    func reloadTimelines(kind: String) {          #if arch(arm64) || arch(i386) || arch(x86_64)        WidgetCenter.shared.reloadTimelines(ofKind: kind)            #endif    }}OC
#import "WidgetController.h"#import "StudyOC-Swift.h"@interface WidgetController ()@end@implementation WidgetController- (void)viewDidLoad {    [super viewDidLoad];    // Do any additional setup after loading the view.    [self initWidget];}- (void)initWidget{    UIButton *btn = [UIButton new];    [btn setTitle"革新" forState:UIControlStateNormal];    [btn setTitleColor:[UIColor redColor] forState:UIControlStateNormal];    btn.frame = CGRectMake(0, 100, 100, 100);    [btn addTarget:self actionselector(clicktBtn1) forControlEvents:UIControlEventTouchUpInside];    [self.view addSubview:btn];}- (void)clicktBtn1{//    [WidgetCenter];    //使用 Groups ID 初始化一个供 App Groups 使用的 NSUserDefaults 对象    NSUserDefaults *userDefaults = [[NSUserDefaults alloc] initWithSuiteName"group.zhahntest"];    //写入数据    [userDefaults setValue"123456789" forKey"userID"];    //读取数据    NSString *userIDStr = [userDefaults valueForKey"userID"];    NSLog(@"zzr123 = %@",userIDStr);    [[WidgetKitManager shareManager] reloadAllTimelines];}效果
一开始是空的
执行方法
24.png 革新机制

革新分被动革新和自动革新
自动革新
WidgetCenter.shared.reloadAllTimelines() // 革新指定的kind// WidgetCenter.shared.reloadTimelines(ofKind: <#T##String#>)被动革新,需要就算设置1分钟,小组件也是最快5分钟革新一次的
    // 决定 Widget 何时革新    func getTimeline(for configuration: AppDataIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {                        // 每隔2个小时革新。        let entry = AppDataEntry(date: Date(), configuration: configuration, str: String(describing: AppData))            // refresh the data every two hours        let expireDate = Calendar.current.date(byAdding: .minute, value: 5, to: Date()) ?? Date()//        print("zzr123 = \(expireDate)")        let timeline = Timeline(entries: [entry], policy: .after(expireDate))                completion(timeline)            }点击交互

点击是通过link和widgetURL操纵,格式是URL sheme
widgetUrl
widgetUrl 是针对整个小组件 点击小组件相应(如果有Link 就相应Link)
Link
LinK 给元素添加点击变乱, Link 对 systemSmall样式的组件不收效(systemSmall 样式的小组件只相应widgetUrl)
struct AppDataEntryView : View {    var entry: AppDataProvider.Entry    var body: some View {        VStack        {            // 和App数据交互            Text(entry.str)            Spacer()            // 点击交互            HStack            {                Link(destination: URL(string: "https://www.baidu.com?str=left")!) {                    // 左 View                    leftView()                }                Spacer()                    .frame(width: 20)                // 右 View                Text("right")                .widgetURL(URL(string: "https://www.baidu.com?str=right"))                            }        }    }}struct leftView : View {    var body: some View {                HStack {                        Text("Left")        }    }}监听是通过openUrl
SwiftUI
@mainstruct SwiftUIDemoApp: App {    var body: some Scene {        WindowGroup {            ContentView()                .onOpenURL {                    print("交互 = \($0)")                }        }    }}Swift
// AppDelegatefunc application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {    print("交互 = \(url)")    return true}// SceneDelegate    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {        print("交互 = \(URLContexts.first?.url)")    }OC
// AppDelegate- (BOOL)applicationUIApplication *)app openURLNSURL *)url optionsNSDictionary<UIApplicationOpenURLOptionsKey,id> *)options{    NSLog(@"交互 = %@",url);    return YES;}// SceneDelegate- (void)sceneUIScene *)scene openURLContextsNSSet<UIOpenURLContext *> *)URLContexts{    NSLog(@"交互 = %@",URLContexts.allObjects.firstObject.URL);}
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-10-18 16:44, Processed in 0.127811 second(s), 35 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表