IOS 14小部件-功能和局限性





今年,iOS开发人员有很多有趣的机会可以消耗iPhone的电池以改善用户体验,其中之一就是新的小部件。当我们都在等待发布该操作系统的发行版本时,我想分享我为“ Wallet”应用程序编写小部件的经验,并告诉您我们的团队在Xcode Beta版本上遇到了哪些机遇和局限性。



让我们从定义开始-小部件是视图,这些视图显示相关信息而无需启动主移动应用程序,并且始终在用户的指尖。从iOS 8开始,使用它们的功能已经存在于iOS(今天的扩展)中,但是我个人使用它们的经验非常可悲-尽管为它们分配了带有小部件的特殊桌面,但我仍然很少到那里,并且没有养成这种习惯。



结果,在iOS 14中,我们看到了小部件的兴起,它们被更多地集成到生态系统中,并且更加用户友好(理论上)。







使用会员卡是我们电子钱包应用程序的主要功能之一。用户有时会在App Store的评论中看到有关向“今天”添加小部件的可能性的建议。结帐时,用户希望尽快出示卡,享受折扣并逃避他们的生意,因为任何时间段的延迟都会导致队列中那些令人讨厌的外观。在我们的情况下,小部件可以保存用于打开卡的多个用户操作,从而使结帐时的商品付款更快。商店也将不胜感激-结帐时的队列减少。



今年,苹果公司在演讲后几乎立即意外地发布了iOS版本,使开发人员有一天在Xcode GM上完成其应用程序的定稿,但是我们已经准备好发布,因为我们的iOS团队开始在Xcode的Beta版上制作自己的小部件版本。 ...该小部件当前正在App Store中进行审查。据统计将设备更新到新的iOS相当快;用户很可能会去检查哪些应用程序已经具有小部件,找到我们的小部件并感到高兴。



将来,我们希望添加更多相关信息-例如余​​额,条形码,来自合作伙伴的最后未读消息和通知(例如,用户需要采取行动-确认或激活卡)。目前,结果如下所示:







将小部件添加到项目



像其他类似的附加功能一样,该小部件被添加为对主项目扩展。添加后,Xcode会为小部件和其他核心类生成代码。这是我们等待的第一个有趣功能-对于我们的项目,此代码未编译,因为在其中一个文件中自动在类名中插入了前缀(是的,那些相同的Obj-C前缀!),但未在生成的文件中插入。俗话说,不是众神在烧锅,显然,苹果内部的不同团队之间并没有达成共识。希望他们会为发行版本修复它。为了自定义项目的前缀,请在应用程序主要目标File Inspector中,填写Class Prefix字段



对于那些关注WWDC新闻的人来说,只有使用SwiftUI才可以实现小部件,这不是秘密。有趣的一点是,Apple以这种方式强制对其技术进行更新:即使主应用程序是使用UIKit编写的,也可以仅使用SwiftUI。另一方面,这是一个很好的机会,可以尝试使用新框架来编写功能,在这种情况下,它很适合流程-无需更改状态,无需导航,只需要声明一个静态UI。也就是说,随着新框架的出现,也出现了新的限制,因为“今日”中的旧小部件可以包含更多的逻辑和动画。



SwiftUI的主要创新之一是无需在模拟器或设备上启动预览功能预览)的能力。一件很酷的事情,但是,不幸的是,在大型项目中(在我们的〜40万行代码中),即使在顶级MacBook上,它的运行速度也极其缓慢,在设备上运行的速度更快。替代方法是准备一个空的项目或游乐场进行快速原型制作。



调试也可以通过专用的Xcode模式进行。在模拟器上,调试甚至在Xcode 12 beta 6之前都是不稳定的,因此最好捐赠其中一台测试设备,升级到iOS 14并在其上进行测试。准备好使该部分在发行版本上无法按预期工作。



接口



用户可以从三种类型的小部件(小,中,大)中选择不同类型的小部件WidgetFamily 要注册,您必须明确指定受支持的:









struct CardListWidget: Widget {
    public var body: some WidgetConfiguration {
        IntentConfiguration(kind: “CardListWidgetKind”,
                            intent: DynamicMultiSelectionIntent.self,
                            provider: CardListProvider()) { entry in
            CardListEntryView(entry: entry)
        }
        .configurationDisplayName(" ")
        .description(",     ")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}


我和我的团队决定坚持使用中小型产品-为小部件显示一张喜欢的卡片,为中型显示4张。



小部件已从控制中心添加到桌面,用户可以在其中选择所需的类型:







使用Assets.xcassets-> AccentColor自定义“添加小部件”按钮的颜色,带有描述的小部件名称也为(上面的示例代码)。



如果遇到受支持的视图数限制,则可以使用WidgetBundle对其进行扩展



@main
struct WalletBundle: WidgetBundle {
    @WidgetBundleBuilder
    var body: some Widget {
        CardListWidget()
        MySecondWidget()
    }
}


由于窗口小部件显示了某种状态的快照,因此用户交互的唯一可能性是通过单击某些元素或整个窗口小部件来切换到主应用程序。没有动画,导航或过渡到其他视图但是可以将深层链接拖放到主应用程序中。在这种情况下,对于窗口部件,单击区域是整个区域,在这种情况下,我们使用widgetURL(_ :)方法。对于中型和大型浏览器,都可以单击查看,而SwiftUILink结构将帮助我们实现这一点



Link(destination: card.url) {
  CardView(card: card)
}


两种尺寸的小部件的最终视图如下:







在设计小部件的界面时,以下规则和要求会有所帮助(根据Apple准则):

  1. 将小部件集中在一个想法和问题上,不要尝试重复应用程序的所有功能。
  2. 根据大小显示更多信息,而不仅仅是缩放内容。
  3. 显示可能全天变化的动态信息。完全静态信息和每分钟更改的信息的极端形式是不受欢迎的。
  4. 该小部件应向用户提供相关信息,而不是打开应用程序的另一种方式。


外观已定制。下一步是选择向用户显示哪些卡以及如何显示。显然可以有四个以上的卡片。让我们考虑几个选项:

  1. 允许用户选择卡片。谁(如果不是他的话)知道哪张牌更重要!
  2. 显示最近使用过的地图。
  3. 制定一个更智能的算法,例如关注星期几和星期几以及统计信息(如果用户在工作日去家附近的水果店,周末去大卖场,那么您可以在此刻帮助用户并显示所需的卡片)


作为原型的一部分,我们选择了第一个选项,以便同时尝试直接在小部件上配置参数的功能。无需在应用程序内部制作特殊屏幕。正如他们所说,用户是否有足够的经验来找到这些设置?



自定义小部件设置



使用意向生成设置(您好,Android开发人员)-创建新的小部件时,意向文件会自动添加到项目中。代码生成器将准备一个继承INIntent的类,该类SiriKit框架的一部分。 intent参数包含魔术选项“ Intent符合窗口小部件的资格”。有几种类型的参数可用,您可以自定义子类型。由于本例中的数据是动态列表,因此我们还设置了“动态提供选项”项



对于不同类型的窗口小部件,设置列表中的最大项数-对于小1,对于中号4。

此意图类型由窗口小部件用作数据源。







接下来,必须将配置的意图类放入IntentConfiguration配置中

struct CardListWidget: Widget {
    public var body: some WidgetConfiguration {
        IntentConfiguration(kind: WidgetConstants.widgetKind,
                            intent: DynamicMultiSelectionIntent.self,
                            provider: CardListProvider()) { entry in
            CardListEntryView(entry: entry)
        }
        .configurationDisplayName(" ")
        .description(",     .")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}


如果不需要用户设置,则可以使用StaticConfiguration类形式的替代方法,该替代方法无需指定意图即可工作。



标题和描述可在设置屏幕上编辑。

小部件的名称必须在一行中,否则将被截断。同时,添加屏幕和窗口小部件设置的允许长度不同。

某些设备的最大名称长度示例:



iPhone 11 Pro Max
28  
21   

iPhone 11 Pro
25  
19   

iPhone SE
24  
19   


描述为多行。如果设置中的文本很长,则可以滚动内容。但是在添加屏幕上,窗口小部件的预览首先被压缩,然后布局发生了一些糟糕的事情。







您还可以更改背景颜色以及WidgetBackground和AccentColor参数的值-默认情况下它们已在Assets中。如有必要,可以分别在“小部件背景颜色名称”和“全局重音颜色名称”字段的“资产目录编译器-选项”的“构建设置”中的小部件配置中重命名它们







根据“关系”设置中另一个参数中的选定值,可以隐藏(或显示)某些参数

应该注意的是,用于编辑参数的UI取决于其类型。例如,如果我们指定布尔,那么我们将看到UISwitch,如果整数,那么我们已经有两个选项可供选择:通过输入的UITextField或通过一步一步的变化UIStepper







与主应用程序的交互。



该捆绑包已配置完毕,它仍然可以确定意图本身将从何处获取真实数据。在这种情况下,与主要应用程序的桥梁是常规组(App Groups)中的文件。主应用程序编写,小部件读取。

以下方法用于将URL获取到常规组:

FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: “group.ru.yourcompany.yourawesomeapp”)


我们保存所有候选者,因为它们将被用户在设置中用作选择字典。

接下来,操作系统必须找出数据已更新,为此,我们称之为:

WidgetCenter.shared.reloadAllTimelines()
//  WidgetCenter.shared.reloadTimelines(ofKind: "kind")


由于方法调用将重新加载窗口小部件的内容和整个时间轴,因此在实际更新数据时使用它,以免使系统过载。



更新资料



为了照顾用户设备的电量,Apple考虑了一种使用时间轴更新小部件上的数据的机制-一种生成快照的机制开发人员不会直接更新或管理视图,而是提供一个时间表,操作系统将在该时间表的基础上在后台剪切快照。

更新发生在以下事件上:

  1. 调用先前使用的WidgetCenter.shared.reloadAllTimelines()
  2. 当用户将小部件添加到桌面时
  3. 编辑设置时。


另外,开发人员具有三种更新时间轴的策略(TimelineReloadPolicy):

atEnd-在显示最后一个快照后更新,

永不更新-仅在

(_ :)之后强制调用的情况下更新-在一定时间后更新。



在我们的情况下,要求系统拍摄一张快照,直到在主应用程序中更新卡数据为止就足够了:



struct CardListProvider: IntentTimelineProvider {
    public typealias Intent = DynamicMultiSelectionIntent
    public typealias Entry = CardListEntry

    public func placeholder(in context: Context) -> Self.Entry {
        return CardListEntry(date: Date(), cards: testData)
    }

    public func getSnapshot(for configuration: Self.Intent, in context: Self.Context, completion: @escaping (Self.Entry) -> Void) {
        let entry = CardListEntry(date: Date(), cards: testData)
        completion(entry)
    }

    public func getTimeline(for configuration: Self.Intent, in context: Self.Context, completion: @escaping (Timeline<Self.Entry>) -> Void) {
        let cards: [WidgetCard]? = configuration.cards?.compactMap { card in
            let id = card.identifier
            let storedCards = SharedStorage.widgetRepository.restore()
            return storedCards.first(where: { widgetCard in widgetCard.id == id })
        }

        let entry = CardListEntry(date: Date(), cards: cards ?? [])
        let timeline = Timeline(entries: [entry], policy: .never)
        completion(timeline)
    }
}

struct CardListEntry: TimelineEntry {
    public let date: Date
    public let cards: [WidgetCard]
}


如果使用自动算法根据星期几和时间选择卡,则更灵活的选项将很有用。



另外,值得注意的是,如果小部件位于小部件堆栈Smart Stack)中,则显示小部件。在这种情况下,我们可以使用两个选项来管理优先级:Siri建议或通过使用TimelineEntryRelevance类型设置TimelineEntry的相关性值TimelineEntryRelevance包含两个参数:score-当前快照相对于其他快照的优先级;duration是直到小部件保持相关并且系统可以将其放置在堆栈顶部的时间。







WWDC会话 中详细讨论了这两种方法以及小部件的配置选项



您还需要讨论如何使日期和时间保持最新。由于我们无法定期更新小部件的内容,因此为Text组件添加了几种样式。使用样式时,当窗口小部件在屏幕上时,系统会自动更新组件的内容。也许将来相同的方法会扩展到其他SwiftUI组件。



文本支持以下样式:

相对-当前日期与指定日期之间的时差。在这里值得注意:如果指定了将来的日期,则倒计时开始,然后显示从零开始的日期。接下来的两种样式将具有相同的行为。

偏移量-与上一个偏移量相似,但是带有带±的前缀形式的指示;

计时器-计时器的类似物;

日期-日期显示;

时间-时间显示。



另外,可以通过简单指定间隔来显示日期之间的时间间隔。



let components = DateComponents(minute: 10, second: 0)
 let futureDate = Calendar.current.date(byAdding: components, to: Date())!
 VStack {
   Text(futureDate, style: .relative)
      .multilineTextAlignment(.center)
   Text(futureDate, style: .offset)
      .multilineTextAlignment(.center)
   Text(futureDate, style: .timer)
      .multilineTextAlignment(.center)
   Text(Date(), style: .date) 
      .multilineTextAlignment(.center)
   Text(Date(), style: .time)
      .multilineTextAlignment(.center)
   Text(Date() ... futureDate)
      .multilineTextAlignment(.center)
}






小部件预览



首次显示时,小部件将以预览模式打开,为此,我们需要在占位符(:)方法中返回TimeLineEntry在我们的例子中,它看起来像这样:

func placeholder(in context: Context) -> Self.Entry {
        return CardListEntry(date: Date(), cards: testData)
 }


之后,带有占位符参数的编辑(reason :)修饰符应用于view 在这种情况下,小部件上的元素显示模糊。 我们可以使用unacted()修饰符从某些元素中删除此效果文档还说,对占位符(in :)方法的调用是同步的,并且结果应尽快返回,这与getSnapshot(in:completion :)getTimeline(in:completion :)不同













舍入元素



在准则中,建议将元素的舍入与小部件的舍入匹配;为此iOS 14中添加ContainerRelativeShape结构,该结构允许您将容器的形状应用于视图。



.clipShape(ContainerRelativeShape()) 


Objective-C支持



如果您需要向小部件添加Objective-C代码(例如,我们已经在其上编写了条形码图像的生成),则通过添加Objective-C桥接标头以标准方式进行所有操作。我们遇到的唯一问题是,在构建时,Xcode停止查看自动生成的意图文件,因此我们也将它们添加到了桥接头文件中



#import "DynamicCardSelectionIntent.h"
#import "CardSelectionIntent.h"
#import "DynamicMultiSelectionIntent.h"


申请规模



测试是在Xcode 12 beta 6上进行的,

没有小部件:61.6 MB

有小部件:62.2 MB我将



总结本文中讨论的要点:

  1. 小部件是在实践中感受SwiftUI的好方法。即使最低支持版本低于iOS 14,也要将它们添加到您的项目中。
  2. WidgetBundle用于增加可用小部件的数量,这是ApolloReddit有多少个不同小部件的一个很好的示例
  3. 如果不需要自定义设置,则IntentConfiguration或StaticConfiguration将有助于在小部件本身上添加自定义设置。
  4. 共享应用程序组中文件系统上的共享文件夹将帮助将数据与主应用程序同步。
  5. 为开发人员提供了一些更新时间线的策略(atEnd,从不,在(_ :)之后)。


在此基础上,在Xcode的Beta版上开发小部件的棘手路径可以被认为是完整的,只剩下一个简单的步骤-在App Store中进行审查。



PS带小部件的版本已通过审核,现在可以在App Store中下载!



感谢您阅读到最后,我将很高兴提出建议和意见。请进行简短调查,以了解小部件在用户和开发人员中的流行程度。



All Articles