iOS14 Widget小组件开发(Widget Extension)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS14 Widget小组件开发(Widget Extension)相关的知识,希望对你有一定的参考价值。

参考技术A

File -> New -> Target -> Widget Extension

本文主要以 未勾选 用户配置属性的情况说明

为小组件展示提供一切必要信息的结构体,遵守 TimelineProvider 协议,产生一个时间线,告诉 WidgetKit 何时渲染与刷新 Widget,时间线包含一个你定义的自定义TimelineEntry类型。时间线条目标识了你希望WidgetKit更新Widget内容的日期。在自定义类型中包含你的Widget的视图需要渲染的属性。

Timeline的刷新策略是会延迟的,并不一定根据你设定的时间精确刷新。同时官方说明了每个widget窗口小部件每天接收的刷新都会有数量限制
TimelineProvider官方解释

渲染 Widget 所需的 数据模型 ,需要遵守 TimelineEntry 协议。

屏幕上 Widget 显示的内容,可以针对不同尺寸的 Widget 设置不同的 View。

一个Widget只能实现大中小三个不同尺寸的组件形式,如果现有需求要做不同功能并且相同尺寸规格的组件则需要实现多组件

1.通过修改原Widget入口文件方法添加更多配置来支持多个Widget

2.另建SwiftUI文件实现组件功能,并去除 @main ,修改相同函数名

并在Widget页面中 Entry 中绑定对应的模型

如果主APP用的swift编写,可以将网络请求模块文件共享或pods库共享(方法后文会介绍)
posterFromJson 此数据模型转换方法仅适用简易接口(为了偷懒🤷‍♀️),复杂数据模型还是用 HandyJSON 或 KaKaJson 解析
若用第三方模型转换方法,图片的同步请求处理放置于 getTodayPoster 的请求中同步处理即可

然后更新补全 placeholder getSnapshot Previews 处相应的 Entry 即完成Widget内容展示

点击Widget窗口唤起APP进行交互指定跳转支持两种方式:

Widget三种尺寸规格中

接收方式
APPDelegate 中接收返回的URL

如果项目实现了 SceneDelegate 则需要在 SceneDelegate 里面实现跳转处理

由于widget跟APP间相互独立,如果想用相同的数据则需要两者间数据共享,创建 App Group
主APP中 Target -> Signing & Capability -> +Capability -> 添加 App Group

ps:网上说的还需创建申请 APPID 但在开启自动管理 Automatically manage signing 的情况下xcode会自动给你创建相关联的APPID

两者间的数据共享主要通过 UserDefaults 和 FileManager 两种形式。
以OC中使用 UserDefaults 共享数据为例

oc、swift混编调用

完成后即可使用pods中的第三方SDK了

Pods第三方SDK使用错误提示
如果在pods导入共享第三方库,或者使用 [UIApplication sharedApplication] 方法报错如下时

则需要在 pods Target 里面,选中出错的SDK并点击 buildSettings 搜索 Require
然后把 Require Only App-Extension-Safe API 然后把YES改为 NO 即可

iOS Widget小组件大小和位置(透明组件)

## size

小组件获取组件大小
1.在Widget获取比较容易,通过context.displaySize获取

	context.displaySize
	
	struct SmallProvider: IntentTimelineProvider {
    func getTimeline(for configuration: SmallIntent, in context: Context, completion: @escaping (Timeline<SmallEntry>) -> Void) {
    	print(context.displaySize)

 	func placeholder(in context: Context) -> SmallEntry {
        print(context.displaySize)
    }

    func getSnapshot(for configuration: SmallIntent, in context: Context, completion: @escaping (SmallEntry) -> ()) {
        print(context.displaySize)
    }

2.在主程序获取

机型屏幕尺寸(pt)小组件(pt)中组件(pt)大组件(pt)
5 / 5s /SE320x568140x140291x140291x310
6 / 6s / 7 / 8 / SE2375x667148x148321x148321x324
6p / 6sp / 7p / 8p375x812155x155329x155329x345
x / xs / 11pro / 12mini390x844158x158338x158338x354
xr / xsmax / 11 / 11promax414x736157x157348x157348x351
12 / 12pro414x896169x169360x169369x379
12promax428x926170x170364x170364x382

 

小号组件坐标

机型屏幕尺寸(pt)左上(CGPoint)右上(CGPoint)左中(CGPoint)右中(CGPoint)左下(CGPoint)右下(CGPoint)
5 / 5s /SE320x568(x:14,y:30)(x:165,y:30)(x:14,y:200)(x:165,y:200)(x:-,y:-)(x:-,y:-)
6 / 6s / 7 / 8 / SE2375x667(x:27,y:30)(x:200,y:30)(x:27,y:206)(x:200,y:206)(x:27,y:382)(x:200,y:382)
6p / 6sp / 7p / 8p375x812(x:23,y:71)(x:197,y:71)(x:23,y:261)(x:197,y:261)(x:23,y:451)(x:197,y:451)
x / xs / 11pro / 12mini390x844(x:26,y:77)(x:206,y:77)(x:26,y:273)(x:206,y:273)(x:26,y:469)(x:206,y:469)
xr / xsmax / 11 / 11promax414x736(x:33,y:38)(x:224,y:38)(x:33,y:232)(x:224,y:232)(x:33,y:426)(x:224,y:426)
12 / 12pro414x896(x:27,y:76)(x:218,y:76)(x:27,y:286)(x:218,y:286)(x:27,y:496)(x:218,y:496)
12promax428x926(x:32,y:82)(x:226,y:82)(x:32,y:294)(x:226,y:294)(x:32,y:506)(x:226,y:506)

 

中号组件坐标

机型屏幕尺寸(pt)上(CGPoint)中(CGPoint)下(CGPoint)
5 / 5s /SE320x568(x:14,y:30)(x:14,y:200)(x:-,y:-)
6 / 6s / 7 / 8 / SE2375x667(x:27,y:30)(x:27,y:206)(x:27,y:382)
6p / 6sp / 7p / 8p375x812(x:23,y:71)(x:23,y:261)(x:23,y:451)
x / xs / 11pro / 12mini390x844(x:26,y:77)(x:26,y:273)(x:26,y:469)
xr / xsmax / 11 / 11promax414x736(x:33,y:38)(x:33,y:232)(x:33,y:426)
12 / 12pro414x896(x:27,y:76)(x:27,y:286)(x:27,y:496)
12promax428x926(x:32,y:82)(x:32,y:294)(x:32,y:506)

 

大号组件坐标

机型屏幕尺寸(pt)上(CGPoint)下(CGPoint)
5 / 5s /SE320x568(x:14,y:30)(x:-,y:-)
6 / 6s / 7 / 8 / SE2375x667(x:27,y:30)(x:27,y:206)
6p / 6sp / 7p / 8p375x812(x:23,y:71)(x:23,y:261)
x / xs / 11pro / 12mini390x844(x:26,y:77)(x:26,y:273)
xr / xsmax / 11 / 11promax414x736(x:33,y:38)(x:33,y:232)
12 / 12pro414x896(x:27,y:76)(x:27,y:286)
12promax428x926(x:32,y:82)(x:32,y:294)

代码

以下是测量的小组件在屏幕上的几个位置坐标,小组件大小和位置都可以通过以下数据换算得到

数组结构为[X1,X2,X3,Y1,Y2,Y3]

let deviceInfos:[String:Array<Int>] = [
    "320x568":[14,165,305,30,200,370], // 5 5s SE。小组件只能放4个,中组件2个,大组件1个
    "375x667":[27,200,348,30,206,382], // 6 6s 7 8 SE2
    "375x812":[23,197,352,71,261,451], // x xs 11pro 12mini
    "390x844":[26,206,364,77,273,469], // 12 12pro
    "414x736":[33,224,381,38,232,426], // 6p 6sp 7p 8p
    "414x896":[27,218,387,76,286,496], // xr xsmax 11 11promax
    "428x926":[32,226,396,82,294,506], // 12promax
]

获取组件宽度

小组件尺寸为X3-X2的正方形
中组件宽度为X3-X1,高度等同小组件 X3-X2
大组件宽度为X3-X1, 高度为(Y2-Y1) + (X3-X2)

小组件高度为X3-X2

func GetWidgetSize(_ widgetType:WidgetSizeEnum) -> CGSize {
    let wxh:String =  "\\(Int(WIDTH))x\\(Int(HEIGHT))"
    
    // TODO 外层做异常处理。告知用户 “您的机型暂不支持透明组件”
    if !deviceInfos.keys.contains(wxh) {
        return CGSize(width: -1, height: -1)
    }
    switch widgetType {
    case .small:
        let smallWidth = deviceInfos[wxh]![2] - deviceInfos[wxh]![1]
        return CGSize(width: smallWidth, height: smallWidth)
    case .medium:
        let mediumWidth = deviceInfos[wxh]![2] - deviceInfos[wxh]![0]
        let mediumHeight = deviceInfos[wxh]![2] - deviceInfos[wxh]![1]
        return CGSize(width: mediumWidth, height: mediumHeight)
    case .large:
        let largeWidth = deviceInfos[wxh]![2] - deviceInfos[wxh]![0]
        let largeHeight = deviceInfos[wxh]![4] - deviceInfos[wxh]![3] + deviceInfos[wxh]![2] - deviceInfos[wxh]![1]
        return CGSize(width: largeWidth, height: largeHeight)
    }
}

enum WidgetSizeEnum {
    case small
    case medium
    case large
}

获取小组件位置

320x568的机型同屏只有4个位置能放小组件,其他尺寸比例的机型为6个

//小组件位置枚举
enum WidgetSizeEnum {
    case topLeft
    case topRight
    case middleLeft
    case middleRight
    case bottomLeft
    case bottomRight
}
// 小组件。获取在不同位置的x,y坐标
func GetSmallWidgetPos(_ widgetPos:SWidgetPosEnum)->CGPoint{
    let wxh:String =  "\\(Int(WIDTH))x\\(Int(HEIGHT))"
    // TODO 外层做异常处理。告知用户 “您的机型暂不支持透明组件”
    if !deviceInfos.keys.contains(wxh) {
        return CGPoint(x: -1, y: -1)
    }
    
    if(wxh == "320x568") {
        switch widgetPos {
        case .topLeft:
            return CGPoint(x: deviceInfos[wxh]![0], y: deviceInfos[wxh]![3])
        case .topRight:
            return CGPoint(x: deviceInfos[wxh]![1], y: deviceInfos[wxh]![3])
        case .middleLeft, .bottomLeft:
            return CGPoint(x: deviceInfos[wxh]![0], y: deviceInfos[wxh]![4])
        case .middleRight, .bottomRight:
            return CGPoint(x: deviceInfos[wxh]![1], y: deviceInfos[wxh]![4])
    } else {
        switch widgetPos {
        case .topLeft:
            return CGPoint(x: deviceInfos[wxh]![0], y: deviceInfos[wxh]![3])
        case .topRight:
            return CGPoint(x: deviceInfos[wxh]![1], y: deviceInfos[wxh]![3])
        case .middleLeft:
            return CGPoint(x: deviceInfos[wxh]![0], y: deviceInfos[wxh]![4])
        case .middleRight:
            return CGPoint(x: deviceInfos[wxh]![1], y: deviceInfos[wxh]![4])
        case .bottomLeft:
            return CGPoint(x: deviceInfos[wxh]![0], y: deviceInfos[wxh]![5])
        case .bottomRight:
            return CGPoint(x: deviceInfos[wxh]![1], y: deviceInfos[wxh]![5])
        }
        
    }
}

 

获取中组件位置

320x568的机型同屏只有上下2个位置能放中组件,其他尺寸比例的机型为上中下3个

//中组件位置枚举
enum WidgetSizeEnum {
    case top
    case middle
    case bottom
}
// 中组件。获取在不同位置的x,y坐标
func GetMiddleWidgetPos(_ widgetPos:MWidgetPosEnum)->CGPoint{
    let wxh:String =  "\\(Int(WIDTH))x\\(Int(HEIGHT))"
    // TODO 外层做异常处理。告知用户 “您的机型暂不支持透明组件”
    if !deviceInfos.keys.contains(wxh) {
        return CGPoint(x: -1, y: -1)
    }
    
    if(wxh == "320x568") {
        switch widgetPos {
        case .top:
            return CGPoint(x: deviceInfos[wxh]![0], y: deviceInfos[wxh]![3])
        case .middle, .bottom:
            return CGPoint(x: deviceInfos[wxh]![0], y: deviceInfos[wxh]![4])
    } else {
        switch widgetPos {
        case .top:
            return CGPoint(x: deviceInfos[wxh]![0], y: deviceInfos[wxh]![3])
        case .middle:
            return CGPoint(x: deviceInfos[wxh]![0], y: deviceInfos[wxh]![4])
        case .bottom:
            return CGPoint(x: deviceInfos[wxh]![0], y: deviceInfos[wxh]![5])
        }
    }
}

 

获取大组件位置

320x568的机型同屏只有1个位置能放大组件,其他尺寸比例的机型为上中下2个

//大组件位置枚举
enum WidgetSizeEnum {
    case top
    case bottom
}
// 大组件。获取在不同位置的x,y坐标
func GetLargeWidgetPos(_ widgetPos:LWidgetPosEnum)->CGPoint{
    let wxh:String =  "\\(Int(WIDTH))x\\(Int(HEIGHT))"
    // TODO 外层做异常处理。告知用户 “您的机型暂不支持透明组件”
    if !deviceInfos.keys.contains(wxh) {
        return CGPoint(x: -1, y: -1)
    }
    
    if(wxh == "320x568") {
        return CGPoint(x: deviceInfos[wxh]![0], y: deviceInfos[wxh]![3])
    } else {
        switch widgetPos {
        case .top:
            return CGPoint(x: deviceInfos[wxh]![0], y: deviceInfos[wxh]![3])
        case .bottom:
            return CGPoint(x: deviceInfos[wxh]![0], y: deviceInfos[wxh]![4])
        case .unknown,.alpha1:
            return CGPoint(x: 0, y: 0)
        }
    }
}

 
 
 

以上是关于iOS14 Widget小组件开发(Widget Extension)的主要内容,如果未能解决你的问题,请参考以下文章

iOS14 Widget小组件开发实践5——网络图片的加载

iOS Widget小组件大小和位置(透明组件)

iOS Widget小组件大小和位置(透明组件)

iOS Widget小组件大小和位置(透明组件)

Android开发工程师文集-1 小时学会Widget小组件开发

iOS Widget 小组件打开其他APP✨仿TopWidget快捷启动✨ 附常用URL schemes