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) -> ()) {
        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) -> 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) -> ()) {
        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) {
         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

你可能感兴趣的:(swift项目中添加iOS14 Widget小组件)