iOS开发SwiftUI

swift项目中添加iOS14 Widget小组件

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

项目中添加Widget我是借鉴了这个作者的文章:https://www.jianshu.com/p/55dce7a524f5

  • 打开Xcode -> File -> New -> Target菜单路径找到 Widget Extension,双击创建
  • 输入Product Name(我用的是TestWidget)其他的都默认选择,点击Finish!
  • 编译一下没问题后,运行在模拟器上看下Xcode为我们生成的默认效果(默认三个样式)。

看一下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()//寄语组件
    }
}

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

最后在真机上的效果:

End:其实关于项目(swift或oc或swift和oc混合项目)中添加widget项目还有很多问题,比如说如何实现widget项目中Localizable.strings多语言本地化?如果是swiftUI应该可以全局访问到Localizable.strings,但是不是的话需要单独新建Localizable.strings,而且还发现widget(swiftUI如何访问获取swift项目中的方法)无法访问swift项目中的文件和方法.