当你更新了 Xcode 15,假如你的 app 中有小组件的代码,在 preview 的页面就会呈现上图的预览过错。提示你:Widget needs to adopt container background。尽管这种 breaking change 看起来很吓人,可是适配起来仍是很容易的,下面将逐个列出 iOS17 小组件有关的 适配计划。
containerBackground
iOS 17 中新增了一个形式 stand by。这个形式下手机横屏,能够并排显现两个小号的小组件。由于手机此时归于息屏状态,因而苹果建议小组件的布景图层躲藏,这样整体的风格也更搭。
一起锁屏小组件也带到了iPadOS 17 上。相比 iPhone 的锁屏小组件,iPad 由于尺度更大,因而锁屏小组件的尺度也支持了一个更大的尺度。能够显现小号的方形小组件。
苹果为了强推小组件这两个功能,要求一切小组件都必须声明适配接口告知体系小组件的布景图层。这样当小组件显现在 standby 和 iPad 锁屏上时,烘托时能够躲藏布景图层。
假如你的 app 只需求支持 iOS 17(不会真有人这么美好吧),那么你只需求在 view 实现这个 containerBackground 就能够了,把布景图层像 background 相同放在 content 闭包里。
.containerBackground(for: .widget) {
// 布景view
Color.black
}
可是做 iOS 的开发者运气都不会太差,你大概率会得到一个 error:
所以你需求自定义一个相似的方法,判别体系版本以向前兼容:
extension View {
@ViewBuilder
func widgetBackground(_ backgroundView: some View) -> some View {
if #available(iOS 17.0, *) {
containerBackground(for: .widget) {
backgroundView
}
} else {
background(backgroundView)
}
}
}
假如你的小组件view不在 app 中展现,那么上述的方法已经足够用了。可是假如你的小组件要在 app 中展现,比如我现在的状况,小组件会在 app 中展现以让用户进行一些主题设置。那么你就会发现 containerBackground 的布景 view 在 app 中不会展现。
因而需求再加一层判别,假如在 app 中正常显现布景图层。
extension View {
@ViewBuilder
func widgetBackground(_ backgroundView: some View) -> some View {
if Bundle.main.bundlePath.hasSuffix(".appex"){
if #available(iOS 17.0, *) {
containerBackground(for: .widget) {
backgroundView
}
} else {
background(backgroundView)
}
} else {
background(backgroundView)
}
}
}
contentMarginsDisabled
在装备完 containerBackground 后小组件能够正常运行了,可是很快你就发现一个问题:小组件尺度变小了。比如下图里黑色是小组件的布景色,外围的一圈是 safeArea。
原因和上一节讲的相同,由于小号小组件会呈现在 standby 中,可是 standby 的尺度更大。因而为了让小组件能够适配不同的尺度,体系一致给小组件加了一个 safeArea。因而咱们的小组件变小了。
假如你的小组件能够针对尺度巨细自适应的话,或者不在乎 standby 中的样式,能够直接在 WidgetConfiguration 中装备关闭体系一致发放的边距。需求留意的是这个装备在 widget 上,不在 view 上。
StaticConfiguration(kind: WorkerWidgetKind.workerSticker.rawValue,
provider: WorkerStickerProvider()) { entry in
WorkerStickerEntryView(entry: entry)
}
.contentMarginsDisabled()
假如你打算针对不同的 margin 处理布局,你也能够经过全局变量获取到 margin 值。
@Environment(\.widgetContentMargins) var margins
extension EnvironmentValues {
/// A property that identifies the content margins of a widget.
///
/// The content margins of a widget depend on the context in which it appears. The
/// system applies default content margins. However, if you disable automatic application of
/// default content margins with ``WidgetConfiguration/contentMarginsDisabled()``, the
/// system uses the `widgetContentMargins` property in combination with ``View/padding(_)``
/// to selectively apply default content margins.
///
@available(iOS 17.0, watchOS 10.0, macOS 14.0, *)
@available(tvOS, unavailable)
public var widgetContentMargins: EdgeInsets { get }
}
可是用这个值会有点痛苦,由于苹果常规操作这个全局变量 iOS 17 only。View 相关全局变量的假如要向前兼容需求包在一个 container view 里,有些小麻烦。
showsWidgetContainerBackground
假如你的小组件某些 UI 要针对在无布景场景做调整,需求经过 showsWidgetContainerBackground 全局变量来判别。
以我的小组件周五日历为例,本来有布景中间的标题文字视觉便是居中的。可是假如没有布景,标题文字的视觉平衡就不在中间了。并且我的标题文字本来有一个透明度,可是在锁屏上由于没有布景了,有透明度反而让文字看不清了。
因而我需求针对在锁屏上做一点区分处理。假如在锁屏上就在布景上画一个边框。
struct FridayWidgetView: View {
@Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground
var body: some View {
ZStack {
if !showsWidgetContainerBackground {
RoundedRectangle(cornerRadius: 12)
.stroke(Color.black, lineWidth: 3)
}
}
.widgetBackground(viewModel.config.theme.coverView)
}
}
特大喜讯,苹果工程师良心发现这个全局变量能够向前兼容。
下图是适配今后的样式。
containerBackgroundRemovable
小号的方形小组件能够在展现在锁屏上又引入了另外一个问题,锁屏中体系会对图片进行是非处理,某些小组件的核心内容是图片的话不适合展现在图片上。
下面的示例图是我开发的打工人小组件,能够看到显现在 iPad 锁屏上显现效果差到无法用。
为了解决这个问题,需求在小组件装备中声明containerBackgroundRemovable(false)
。
struct WeekCalendarWidget: Widget {
var body: some WidgetConfiguration {
IntentConfiguration(kind: WorkerWidgetKind.weekCalendar.rawValue,
intent: WeekCalendarIntent.self,
provider: WeekCalendarTimelineProvider()) { entry in
WeekCalendarEntryView(entry: entry)
}
.configurationDisplayName("打工人周历")
.description("熬夜能够,熬夜工作可不行")
.containerBackgroundRemovable(false)
.contentMarginsDisabled()
}
}
装备了这个选项后小组件就不会呈现在 iPad 锁屏小组件列表中。
widgetRenderingMode
假如要针对图片在不同场景中做单独处理,也能够经过 widgetRenderingMode 这个全局变量判别当时的烘托形式。坏消息:兼容性iOS 16 +。
@available(iOS 16.0, watchOS 9.0, macOS 13.0, *)
@available(tvOS, unavailable)
extension EnvironmentValues {
/// The widget's rendering mode, based on where the system is displaying it.
///
/// You can read the rendering mode from the environment values using this
/// key.
///
/// ``` swift
/// @Environment(\.widgetRenderingMode) var widgetRenderingMode
/// ```
///
/// Then modify the widget's appearance based on the mode.
///
/// ``` swift
/// var body: some View {
/// ZStack {
/// switch renderingMode {
/// case .fullColor:
/// Text("Full color")
/// case .accented:
/// ZStack {
/// Circle(...)
/// VStack {
/// Text("Accented")
/// .widgetAccentable()
/// Text("Normal")
/// }
/// }
/// case .vibrant:
/// Text("Full color")
/// default:
/// ...
/// }
/// }
/// }
/// ```
public var widgetRenderingMode: WidgetRenderingMode
}
Reference
Bring widgets to new places