SwiftUI-Widget 使用及避坑指南

iOS Widget简单介绍( 只介绍iOS 14 以后Widget相关内容):

Widget 是 iOS 14 重磅推出的新功能,使得用户可以在主屏幕添加小组件,快速浏览 app 提供的重要信息。
用户可以通过 Widget 对主屏幕进行个性化定制,但是 iOS 14 的 Widget 跟其他系统上的小组件有很大的区别。在 Widget 的设计上苹果也保持了一贯的克制,定位于轻量化、仅用作关键信息的展示。比如系统自带 Widget 中的股票、天气、电量、运动信息,他们的共同特征是更新频率高、提供的信息重要,让用户不用打开 app 就可以浏览关心的内容。

相关限制:

苹果基于上面的设计定位,同时也为了节省系统资源保证续航,对 Widget 的做了一些限制:
不支持动画,仅支持静态页面展示。
更新频率由系统通过机器学习来动态分配。
不支持拖拽、滚动等复杂的交互,不支持 Switch 等控件。
用户点击 Widget 一定会跳转到 App。
支持三种不同大小的样式

适应不同的屏幕尺寸

iOS Widget简单介绍( 只介绍iOS 14 以后Widget相关内容):

Widget 是 iOS 14 重磅推出的新功能,使得用户可以在主屏幕添加小组件,快速浏览 app 提供的重要信息。
用户可以通过 Widget 对主屏幕进行个性化定制,但是 iOS 14 的 Widget 跟其他系统上的小组件有很大的区别。在 Widget 的设计上苹果也保持了一贯的克制,定位于轻量化、仅用作关键信息的展示。比如系统自带 Widget 中的股票、天气、电量、运动信息,他们的共同特征是更新频率高、提供的信息重要,让用户不用打开 app 就可以浏览关心的内容。

相关限制:

苹果基于上面的设计定位,同时也为了节省系统资源保证续航,对 Widget 的做了一些限制:
不支持动画,仅支持静态页面展示。
更新频率由系统通过机器学习来动态分配。
不支持拖拽、滚动等复杂的交互,不支持 Switch 等控件。
用户点击 Widget 一定会跳转到 App。
支持三种不同大小的样式

适应不同的屏幕尺寸

屏幕尺寸 - portrait 小部件-systemSmall 中型部件-systemMedium 大部件-systemLarge
414x896 pt (XR/XsMax/11/11ProMax) 169x169pt 360x169pt 360x379pt
375x812 pt (X/Xs/11 Pro) 155x155 pt 329x155 pt 329x345 pt
414x736 pt (6p/6sp/7p) 159x159 pt 348x159 pt 348x357 pt
375x667 pt (6/6s/7/8) 148x148 pt 321x148 pt 321x324 pt
320x568 pt (5/5s/SE) 141x141 pt 292x141 pt 292x311 pt

开发要求:

开发工具 Xcode 12 以上版本
开发语言 Swift和SwiftUI
手机系统要求 14以上

Widget 创建:

1.Widget作为项目的一个组件,创建之前需要先创建一个iOS的项目,项目创建成功后点击:File->New->Target添加Widget Extension Target 点击Next。

2.输入Widget组件名,取消勾选,点击Finish就可以了。Include Configuration Intent:是否支持用户配置。

3.关于预览:
本项目会提示需要升级,新创建项目的没有该问题
(运行widget target 模拟器调试打日志有时不显示)

多个Widget和小、大、中页面数据布局

如何定义多个Widget,并且小、中、大的布局完全不同?

iOS14中Widget是支持通过创建一个扩展项目返回一个或多个小部件的,可以使您的应用提供多种小部件选择。并且在项目中视图通过WidgetFamily的枚举自定义自己想要的组件和布局。

WidgetFamily枚举

public enum WidgetFamily : Int, RawRepresentable, CustomDebugStringConvertible, CustomStringConvertible {

    /// A small widget.
    case systemSmall

    /// A medium-sized widget.
    case systemMedium

    /// A large widget.
    case systemLarge
}

默认模版代码,只能支持展示一类型种的一种样式

@main //widget 主入口,系统从这里加载
struct WidgetTest: Widget {
  //kind的值是widget的唯一标识
    let kind: String = "Widget"
    var body: some WidgetConfiguration {//初始化配置代码
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
           WidgeEntryView(entry: entry)
        }
        .configurationDisplayName("My Widget")//编辑页面展示的标题
        .description("This is an example widget.")//编辑页面展示的描述内容
        .supportedFamilies([.systemSmall,.systemMedium,.systemLarge])// 如何实现预览里面small样式展示不同样式
    }
}

可以通过修改原Widget入口文件方法添加更多配置来支持多个Widget,相同类型不同样式。

@main
struct SwiftWidgetsBundle: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        Widget1()
        Widget2()
        Widget3()
        Widget4()
        ...
    }
}

与主应用交互:

根据官方文档的描述,点击Widget窗口唤起APP进行交互指定跳转支持两种方式:

widgetURL:点击区域是Widget的所有区域,代码如下。
if family == .systemSmall {  // 小
  VStack(alignment: .center, spacing: 20, content: {
      Text("\(entry.quotes.date) at \(entry.quotes.place) ")
          .font(.system(size: 9))
          .foregroundColor(.gray)
  })
  .widgetURL(URL(string: "https://www.baidu.com/small"))

}

Link:通过Link修饰,允许让界面上不同元素产生点击响应。

if family == .systemMedium { // 中
  VStack(alignment: .center, spacing: 8, content: {
      Link(destination: URL(string: "https://www.baidu.com/medium/1")!) {
          Text(entry.quotes.content[0])
              .font(.system(size: 17))
              .foregroundColor(.black)
              .frame(maxWidth:.infinity, alignment: .leading)
      }
     
      Text("\(entry.quotes.date) at \(entry.quotes.place) ")
          .font(.system(size: 12))
          .foregroundColor(.gray)
          .frame(maxWidth:.infinity, alignment: .trailing)
          .frame(height: 20, alignment: .bottom)
  })
 
}

注!:systemSmall(小组件)只支持widgetURL,而systemMedium(中组件)和 systemLarge(大组件)则都支持。Link:更希望的是不同元素的点击响应。

在主项目的SceneDelegate代理方法中接收回调

- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts {
    /// 根据不同的URL回调做出响应
    NSLog(@"%@",URLContexts);
}

或 AppDelegate 中的

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<NSString*,id> *)options{
//处理
    return YES;
}

数据更新时机

这里关于刷新策略,根据官方文档来看,Timeline的刷新策略是会延迟的,并不一定根据你设定的时间来。同时官方规定每个配置的窗口小部件每天都接收有限数量的刷新,
(官方文档:https://developer.apple.com/documentation/widgetkit/timelineprovider

导致无法预测何时更新Widget。即使设置了某个时间再次获取时间轴本身进行更新,也无法保证iOS会同时更新视图。从而造成一定的Widget页面更新延迟。

苹果因为提供了一个单独的方法,调用来重新加载所有窗口小部件
/// 控件的所有已配置小部件重新加载时间线
/// 包含应用程序。

WidgetCenter.shared.reloadAllTimelines()

刷新次数限制

Refreshing Widgets Efficiently
Each configured widget receives a limited number of refreshes every day. Several factors affect how many refreshes a widget receives, such as whether the containing app is running in the foreground or background, how frequently the widget is shown onscreen, and what types of activities the containing app engages in.

每个配置的小部件每天都会收到有限的刷新次数。有几个因素会影响小部件接收的刷新次数,例如包含的应用程序是在前台还是后台运行,小部件在屏幕上显示的频率,以及包含的应用程序参与的活动类型。

在Xcode中调试小部件时,WidgetKit不会施加此限制。要验证小部件的行为是否正确,请在Xcode的调试器之外测试应用程序和小部件的行为。

当你的应用程序位于前台、有活动媒体会话或使用标准位置服务时,刷新不计入小部件的每日限制。有关媒体会话和定位服务的更多信息,请参阅doc://com.apple.documentation/documentation/avfoundation/avaudiosession使用标准定位服务。

数据共享:

主要是使用App Group来实现。
如登录态同步等

Swift 与OC 相互调用:

Widget 中的Swift 调用主项目的OC 调用
使用桥接方法,且引入的文件必须 在Target Membership 关联对应的 Widget Target。

注意:

  • 用户初次安装未启动,无法添加Widget组件。
  • 图片加载不支持异步,只能同步加载好后进行显示
  • 在创建文件时一般都会在Target Membership 进行勾选相应的Target,默认生产的模版的入口不需要勾选主target ,否则会报错
duplicate symbol '_main' in:
    /Users/XXX/Library/Developer/Xcode/DerivedData/XXX-hghyirqieliknyckkefqaeomgvms/Build/Intermediates.noindex/XXX.build/Debug-iphoneos/XXX.build/Objects-normal/arm64/main.o
    /Users/XXX/Library/Developer/Xcode/DerivedData/XXX-hghyirqieliknyckkefqaeomgvms/Build/Intermediates.noindex/XXX.build/Debug-iphoneos/XXX.build/Objects-normal/arm64/XXXWidget.o
ld: 1 duplicate symbol for architecture arm64
clang_bk: error: linker command failed with exit code 1 (use -v to see invocation)
[$] waitpid = 64263 
[$] run clang_bk fail,not exist 
nagain clang exit 
Command /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang failed with exit code 255
  • Widget中支持的字体和主工程支持的字体不一样,如果UI中设置的字体没有达到预期效果建议打印一下支持的字体。
    swift:
var familNames:[String] = []
        familNames = UIFont.familyNames
        for fami in familNames {
            print("---familNames",fami)
            let namesArr = UIFont.fontNames(forFamilyName: fami)
            for name in namesArr {
                print("---famil fontname",name)
            }
        }
  print(familNames)
  • 埋点
    Widget 的曝光事件我们是无法感知的,由于点击 Widget 会直接跳转到主 app,所以我们在跳转到主 app 的 URL 上增加了埋点参数,主 app 解析 URL 中的参数调用 UT 来埋点。
  • 包大小
    主工程是使用OC,Widget开发会使用Swift及SwiftUI 会引入新的库,会导致包有一定的增加。
  • swiftUI布局的坑点:
    比如
    Image("imageName")
    .resizable()
    .frame(width: fitWidth(22), height: fitWidth(22))
    图片设置大小要先设置resizable,否则不生效,
    SwiftUI 设置方法先后顺序不同会导致不同的UI效果

参考:
如何用iOS14 Widget小组件自定义玩法

iOS14 Widget初体验

如何进行 iOS Widget 开发?
iOS小组件Widget踩坑

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

推荐阅读更多精彩内容