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 *)URLContexts {
    /// 根据不同的URL回调做出响应
    NSLog(@"%@",URLContexts);
}

或 AppDelegate 中的

- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)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踩坑

你可能感兴趣的:(SwiftUI-Widget 使用及避坑指南)