swift项目中添加iOS14 Widget小组件

iOS14发布后新增加了很多的功能(屏幕小组件widget、App Library 页面、「App Clips」苹果版的「小程序」......),可能对开发者来说,最关注的新功能就是widget小组件和App Clips。今天主要来说说如何在自己的项目中添加widget小组件,喔,对了话说我从事iOS开发多年.....


叨逼叨.jpeg

悠悠记得那是一个月黑风高的夜晚,我默默的按下了笨重机箱的开机按钮,轻车熟路的点开了D盘,然后打开了加密的文件夹....


精神抖擞.jpeg

Double click 打开了. xcodeproj项目!


忍着.png

喔,对了是在swift4.0的时候,就开始放弃了OC语言开发,转而拥抱swift语言开发项目,关于swift这门语言怎么说呢?(哎呀,真香!)可能使用过的人才有评价的资格吧。现在swift5.xABI稳定了,拥抱她的人应该更多了吧。到目前为止我的项目中一直是swift(90%)+ OC(10%)这样的方式来开发项目,等有时间想把OC相关的都去掉,完全用swift来构建项目(我是个追求完美的人),喔,对了后来有一次....


忍不了了.gif

嗯(不喔了- -!)现在来说说项目中添加widget小组件(Follow Me)!

此处说明:(项目中添加Widget我是借鉴了这个作者的文章,包括下面的这些方法和截图 作者:2狗子你变了 链接:https://www.jianshu.com/p/55dce7a524f5,他的文章很详细如果想更清晰的了解添加过程及方法可以去他那儿溜达一会会儿,跟优秀的人学习也会变得很优秀,比如我!🤣)

1.打开Xcode -> File -> New -> Target菜单路径找到 Widget Extension,双击创建


新建widget.png

输入Product Name(我用的是TestWidget)其他的都默认选择,点击Finish!


widget添加后的样子.png

编译一下没问题后,运行在模拟器上看下Xcode为我们生成的默认效果(默认三个样式)。
widget样式.png

看一下Xcode生成的默认的Widget源码:

Provider:为小组件展示提供一切必要信息的结构体,实现TimelineProvider协议
placeholder:提供一个默认的视图,当网络数据请求失败或者其他一些异常的时候,用于展示
getSnapshot:为了在小部件库中显示小部件,WidgetKit要求提供者提供预览快照,在组件的添加页面可以看到效果
getTimeline:在这个方法内可以进行网络请求,拿到的数据保存在对应的entry中,调用completion之后会到刷新小组件

struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date())
    }

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date())
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        var entries: [SimpleEntry] = []
        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)
    }
}

实现TimelineEntry协议,保存所需要的数据

struct SimpleEntry: TimelineEntry {
    let date: Date
}

用来展示的视图View,可以进行自己想要的界面搭建

struct TestWidgetEntryView : View {
    var entry: Provider.Entry
    var body: some View {
        Text(entry.date, style: .time)
    }
}

@main struct TestWidget: Widget
@main:代表着Widget的主入口,系统从这里加载
kind:是Widget的唯一标识
StaticConfiguration:初始化配置代码
configurationDisplayName:添加编辑界面展示的标题
description:添加编辑界面展示的描述内容
supportedFamilies这里可以限制要提供三个样式中的哪几个

@main
struct TestWidget: Widget {
    let kind: String = "TestWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            TestWidgetEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")
        .description("This is an example widget.")
        //supportedFamilies不设置的话默认三个样式都实现
        .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
    }
}

其实根据自己的需要可以设置多个样式,但是不变的就是固定的三个样式([.systemSmall, .systemMedium, .systemLarge])
由于我是在我的swift+OC项目中添加的,所以对swiftUI还不太熟悉(widget必须要用swiftUI实现),所以只能简单的实现一下功能(太复杂的UI样式还搞不定,等swiftUI熟悉后再折腾吧),为了简单方便一点 我自定义的样式就一个.systemMedium

下面这个是我在自己的项目中添加的 >>>>>>

wishesWidget.swift

import WidgetKit
import SwiftUI
import Intents

struct wishesModel {
    let title: String //标题
    let content: String // 内容
}

struct wishesRequest {
    
    static func request(completion: @escaping (Result<wishesModel, Error>) -> Void) {
        let url = URL(string:"自己的url链接地址")
        guard let requestUrl = url else { fatalError() }
        var request = URLRequest(url: requestUrl)
        request.httpMethod = "POST"
        let postString = "自己的参数值"
        request.httpBody = postString.data(using: String.Encoding.utf8)
        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in  
            guard error == nil else {
                completion(.failure(error!))
                return
            }
            if let data = data, let dataString = String(data: data, encoding: .utf8) {
                let model = modelFromJson(fromData: data)
                completion(.success(model))
            }
        }
        task.resume()
    }
    
    static func modelFromJson(fromData data: Data) -> wishesModel {
        let json = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
        guard let data = json["data"] as? [String: Any] else {
            return wishesModel(title:NSLocalizedString("sq_send_world_title", comment: ""),content: NSLocalizedString("sq_req_fail", comment: ""))
        }
        let title = data["title"] as! String
        let content = data["content"] as! String
        return wishesModel(title: title, content: content)
    }
}

struct wishesProvider: IntentTimelineProvider {
    func placeholder(in context: Context) -> wishesEntry {
        let model = wishesModel(title: NSLocalizedString("sq_send_world_title", comment: ""), content: NSLocalizedString("sq_default_des", comment: ""))
        return wishesEntry(date: Date(), item: model, configuration: ConfigurationIntent())
    }
    
    func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (wishesEntry) -> ()) {
        let model = wishesModel(title: NSLocalizedString("sq_send_world_title", comment: ""), content: NSLocalizedString("sq_default_des", comment: ""))
        let entry = wishesEntry(date: Date(), item: model, configuration: configuration)
        completion(entry)
    }
    
    func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<wishesEntry>) -> ()) {
        let currentDate = Date()
        // 下一次更新间隔以小时为单位,间隔1小时请求一次新的数据
        let updateDate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)
        wishesRequest.request { result in
            let model: wishesModel
            if case .success(let response) = result {
                model = response
            } else {
                model = wishesModel(title: NSLocalizedString("sq_send_world_title", comment: ""), content: NSLocalizedString("sq_req_fail", comment: ""))
            }
            let entry = wishesEntry(date: updateDate!, item: model, configuration: configuration)
            let timeline = Timeline(entries: [entry], policy: .after(updateDate!))
            completion(timeline)
        }
    }
}

struct wishesEntry: TimelineEntry {
    let date: Date
    let item: wishesModel 
    let configuration: ConfigurationIntent
}

struct wishesWidgetEntryView : View {

    var entry: wishesEntry
  
    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            HStack(alignment: .lastTextBaseline){
                Text(entry.item.title)
                    .font(.title2)
                    .bold()
                Spacer()
                VStack(alignment: .trailing){
                    Image("item_logo_icon")
                        .resizable()
                        .frame(width: 30, height: 30, alignment: .center)
                }
            }
            Spacer()
            Text(entry.item.content)
            .font(.system(size: 13))
            .bold()
            Spacer()
        }
        .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .leading)
        .padding()
        //widget背景图片
        .background(
            Image("item_bg_icon")
                .resizable()
                .scaledToFill()
        )
        .widgetURL(URL(string: "url://123"))//获取点击标记 需要在SceneDelegate里面实现跳转处理,因为iOS13后,APP的UI生命周期交由SceneDelegate管理
        /*
         func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
         for context in URLContexts {
         print(context.url) //获取widget点击标记 url://123
         }
         }
         */

         /*注意⚠️:由于我的项目是swift+OC 所以,点击widget控件打开响应在AppDelegate 中,
            func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
              if url.relativeString == "url://123" {
                  //TODO: 

                    return true
                }
            }
          */
    }
}

//systemMedium 中样式
struct wishesWidget: Widget {
    let kind: String = "wishesWidget"
    
    var body: some WidgetConfiguration {
        IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: wishesProvider()) { entry in
            wishesWidgetEntryView(entry: entry)
        }
        .configurationDisplayName(LocalizedStringKey("sq_title_widget"))
        .description(LocalizedStringKey("sq_intro_widget"))
        .supportedFamilies([.systemMedium])//我只需要一个中样式所以这个位置放置了一个.systemMedium,如果需要多个样式可以从这三个样式([.systemSmall, .systemMedium, .systemLarge])里面选择,也可以重复使用属性值,展示多个样式
    }
}

wwsqWidget.swift添加MyWidgetBundle,关于这个MyWidgetBundle名字应该可以随意主要是@main主入口(如果你是用上面的步骤的话是这个文件TestWidget.swift)

@main
struct MyWidgetBundle: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        wishesWidget()//寄语组件
    }
}

这是我项目中添加的文件目录(由于这是公司的项目,贴图的话需要打码~)


widget目录列表.png

最后在真机上的效果:


添加弹出.PNG

主屏上展示.png

End:其实关于项目(swift或oc或swift和oc混合项目)中添加widget项目还有很多问题,比如说如何实现widget项目中Localizable.strings多语言本地化?如果是swiftUI应该可以全局访问到Localizable.strings,但是不是的话需要单独新建Localizable.strings,而且还发现widget(swiftUI如何访问获取swift项目中的方法)无法访问swift项目中的文件和方法,或许愚钝的我没有发现互相调用的方法,大家有知道的可以给我留言告诉我,我是个爱学习的好孩子,你懂的!

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