Xcode 15 iOS 17小组件适配必看:Widget needs to adopt container background

当你更新了 Xcode 15,假如你的 app 中有小组件的代码,在 preview 的页面就会呈现上图的预览过错。提示你:Widget needs to adopt container background。尽管这种 breaking change 看起来很吓人,可是适配起来仍是很容易的,下面将逐个列出 iOS17 小组件有关的 适配计划。

containerBackground

Xcode 15 iOS 17小组件适配必看:Widget needs to adopt container background

iOS 17 中新增了一个形式 stand by。这个形式下手机横屏,能够并排显现两个小号的小组件。由于手机此时归于息屏状态,因而苹果建议小组件的布景图层躲藏,这样整体的风格也更搭。

Xcode 15 iOS 17小组件适配必看:Widget needs to adopt container background

一起锁屏小组件也带到了iPadOS 17 上。相比 iPhone 的锁屏小组件,iPad 由于尺度更大,因而锁屏小组件的尺度也支持了一个更大的尺度。能够显现小号的方形小组件。

Xcode 15 iOS 17小组件适配必看:Widget needs to adopt container background

苹果为了强推小组件这两个功能,要求一切小组件都必须声明适配接口告知体系小组件的布景图层。这样当小组件显现在 standby 和 iPad 锁屏上时,烘托时能够躲藏布景图层。

假如你的 app 只需求支持 iOS 17(不会真有人这么美好吧),那么你只需求在 view 实现这个 containerBackground 就能够了,把布景图层像 background 相同放在 content 闭包里。

.containerBackground(for: .widget) {
            // 布景view
            Color.black
    }

可是做 iOS 的开发者运气都不会太差,你大概率会得到一个 error:

Xcode 15 iOS 17小组件适配必看:Widget needs to adopt container background

所以你需求自定义一个相似的方法,判别体系版本以向前兼容:

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 中不会展现。

Xcode 15 iOS 17小组件适配必看:Widget needs to adopt container background

因而需求再加一层判别,假如在 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。

Xcode 15 iOS 17小组件适配必看:Widget needs to adopt container background

原因和上一节讲的相同,由于小号小组件会呈现在 standby 中,可是 standby 的尺度更大。因而为了让小组件能够适配不同的尺度,体系一致给小组件加了一个 safeArea。因而咱们的小组件变小了。

Xcode 15 iOS 17小组件适配必看:Widget needs to adopt container background

假如你的小组件能够针对尺度巨细自适应的话,或者不在乎 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 全局变量来判别。

以我的小组件周五日历为例,本来有布景中间的标题文字视觉便是居中的。可是假如没有布景,标题文字的视觉平衡就不在中间了。并且我的标题文字本来有一个透明度,可是在锁屏上由于没有布景了,有透明度反而让文字看不清了。

Xcode 15 iOS 17小组件适配必看:Widget needs to adopt container background
Xcode 15 iOS 17小组件适配必看:Widget needs to adopt container background

因而我需求针对在锁屏上做一点区分处理。假如在锁屏上就在布景上画一个边框。

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)
    }
}

Xcode 15 iOS 17小组件适配必看:Widget needs to adopt container background

特大喜讯,苹果工程师良心发现这个全局变量能够向前兼容。

下图是适配今后的样式。

Xcode 15 iOS 17小组件适配必看:Widget needs to adopt container background

containerBackgroundRemovable

小号的方形小组件能够在展现在锁屏上又引入了另外一个问题,锁屏中体系会对图片进行是非处理,某些小组件的核心内容是图片的话不适合展现在图片上。

Xcode 15 iOS 17小组件适配必看:Widget needs to adopt container background

下面的示例图是我开发的打工人小组件,能够看到显现在 iPad 锁屏上显现效果差到无法用。

Xcode 15 iOS 17小组件适配必看:Widget needs to adopt container background
Xcode 15 iOS 17小组件适配必看:Widget needs to adopt container background

为了解决这个问题,需求在小组件装备中声明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 锁屏小组件列表中。

Xcode 15 iOS 17小组件适配必看:Widget needs to adopt container background

widgetRenderingMode

假如要针对图片在不同场景中做单独处理,也能够经过 widgetRenderingMode 这个全局变量判别当时的烘托形式。坏消息:兼容性iOS 16 +。

Xcode 15 iOS 17小组件适配必看:Widget needs to adopt container background

@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