Widget showsWidgetContainerBackground fallback

To bring my widgets to iPad lock screen, i need to detect my widget that has a background to display different contents, watch the wwdc 23 video it tells we can use .showsWidgetContainerBackground environment to apply that goal, but the problem is this code wouldn't compile because .showsWidgetContainerBackground only available in iOS 17

    @available(iOS 14.0, *)
    struct MyWidgetView: View
    {
        @Environment(\.showsWidgetContainerBackground) var showsBackground
        
        var body: some View {
            ...
        }
    }

So, my solution is use a environment wrapper, like this:

    @available(iOS 17.0, *)
    struct EnvironmentWrapper: View
    {
        @Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground
                
        var body: some View {
            VStack(alignment: .leading) {
                Text("Wrapper Has Background: \(showsWidgetContainerBackground ? "YES" : "NO")")
            }
        }
    }

    extension View
    {    
        var showsWidgetContainerBackground: Bool {
            if #available(iOS 17.0, *)
            {
                return EnvironmentWrapper().showsWidgetContainerBackground
            }
            else
            {
                return true
            }
        }
    }

But it didn't work, this is the example:

    @available(iOS 17.0, *)
    struct EnvironmentWrapper: View
    {
        @Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground
                
        var body: some View {
            VStack(alignment: .leading) {
                Text("Wrapper has background: \(showsWidgetContainerBackground ? "YES" : "NO")")
            }
        }
    }
    
    @available(iOS 17.0, *)
    struct ExampleView: View
    {
        @Environment(\.showsWidgetContainerBackground) var showsBackground
        
        var body: some View {
            VStack(alignment: .leading) {
                Text("Read from self: \(showsBackground ? "YES" : "NO")")
                EnvironmentWrapper()
                Text("Read from wrapper: \(EnvironmentWrapper().showsWidgetContainerBackground ? "YES" : "NO")")
            }
            .font(.system(size: 12, weight: .medium))
        }
    }

As you can see, if i read it directly from self, it works, but if i read from outside, it always return true

i also try to load a view first in the wrapper, but it didn't work either

        func getShowsBackground() -> Bool
        {
            let _ = body
            return showsWidgetContainerBackground
        }
Answered by Apple dicker in 759064022

Ok, after a little digging(actually a lot...), i think i finally have a solution, just in case anyone dealing the some issue:

// First we create a fallback variable environment
struct WidgetEnvShowsContainerBackgroundKey: EnvironmentKey
{
    static let defaultValue: Bool = true
}

extension EnvironmentValues
{
    var widgetEnvShowsContainerBackground: Bool {
        set { self[WidgetEnvShowsContainerBackgroundKey.self] = newValue }
        get { self[WidgetEnvShowsContainerBackgroundKey.self] }
    }
}

extension View
{
    func envShowsContainerBackground(_ value: Bool) -> some View
    {
        environment(\.widgetEnvShowsContainerBackground, value)
    }
}

// Magic happens here
struct WidgetEnvironmentReader<Content: View>: View
{
    var content: () -> Content

    init(@ViewBuilder content: @escaping () -> Content)
    {
        self.content = content
    }

    var body: some View {
        if #available(iOS 17.0, *)
        {
            WidgetEnvironmentViewBuilder(content: content)
        }
        else
        {
            content()
        }
    }
}

extension WidgetEnvironmentReader
{
    @available(iOS 17.0, *)
    struct WidgetEnvironmentViewBuilder<C: View>: View
    {
        @Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground
        
        var content: () -> C

        init(@ViewBuilder content: @escaping () -> C)
        {
            self.content = content
        }

        var body: some View {
            content()
                .envShowsContainerBackground(showsWidgetContainerBackground)
        }
    }
}

// Now we are free to use .showsWidgetContainerBackground in the older os version
@available(iOS 14.0, *)
struct WidgetView: View
{
    @Environment(\.widgetEnvShowsContainerBackground) var widgetEnvShowsContainerBackground: Bool
    
    var body: some View {
        Text("Background: \(widgetEnvShowsContainerBackground ? "YES" : "No"), \(Date(), style: .relative)")
    }
}

@available(iOS 14.0, *)
struct WidgetEntryView: View
{
    let entry: Entry

    var body: some View {
        WidgetEnvironmentReader {
            WidgetView()
        }
        .makeContainerBackground()
    }
}

Check your runtime warnings in Xcode. I have seen it tell me the SwiftUI environment variables can only be used in a view.

In my widgets I already have a wrapper view to handle a lot of common things & bootstrap. I have a view "WidgetSetStuff" that sets global variables -- or it could adjust a class object passed in.

struct WidgetEntryView : View
{
    ...
    @Environment(\.widgetFamily) var widgetFamily
    @Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground

    var body: some View
    {
        ...
                WidgetSetStuff(
                    family: widgetFamily,
                    noMargins: showsWidgetContainerBackground == false,
                    ...
                )
       ...
}

WidgetSetStuff constructor sets some globals use elsewhere and has EmptyView() body.

@t9mike thanks, how do you compile your WidgetEntryView because .showsWidgetContainerBackground only available in iOS 17? and i have see those Xcode runtime error, so i tried a new approach but it couldn't work neither:

final class WidgetEnvironmentContext
{
    static let shared = WidgetEnvironmentContext()
    
    var showsWidgetContainerBackground: Bool = true
}

@available(iOS 17.0, *)
struct WidgetEnvironmentReader: View
{
    @Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground: Bool
    
    var body: some View {
        
        WidgetEnvironmentContext.shared.showsWidgetContainerBackground = showsWidgetContainerBackground
        
        return Color.clear
    }
}

struct ExampleView: View
{
    var body: some View {
        ZStack {
            if #available(iOS 17, *)
            {
                WidgetEnvironmentReader()
            }
            
            Text("Shows Background: \(WidgetEnvironmentContext.shared.showsWidgetContainerBackground ? "YES" : "NO")")
        }
    }
}

showsWidgetContainerBackground variable is randomly wrong in this way, i am so confuse now, i think i just use separate view in my code

struct WidgetView: View
{
    var body: some View {
        if #available(iOS 17, *)
        {
            HeaderView()
        }
        else
        {
            FallbackHeaderView()
        }
        
        ...
    }
}

showswidgetcontainerbackground is iOS15+ according to Apple's docs. But I am seeing crash "Thread 1: EXC_BAD_ACCESS (code=1, address=0x0)" when run on iOS16. I have FB open on this:

FB12554533 (showsWidgetContainerBackground Environment variable crashes widget extension on iOS16 with Xcode 15)

I was tabling this issue for a few weeks in hopes it gets resolved. Feels like a bug that would hit a lot of people. But perhaps how I am using it is triggering the crash.

Accepted Answer

Ok, after a little digging(actually a lot...), i think i finally have a solution, just in case anyone dealing the some issue:

// First we create a fallback variable environment
struct WidgetEnvShowsContainerBackgroundKey: EnvironmentKey
{
    static let defaultValue: Bool = true
}

extension EnvironmentValues
{
    var widgetEnvShowsContainerBackground: Bool {
        set { self[WidgetEnvShowsContainerBackgroundKey.self] = newValue }
        get { self[WidgetEnvShowsContainerBackgroundKey.self] }
    }
}

extension View
{
    func envShowsContainerBackground(_ value: Bool) -> some View
    {
        environment(\.widgetEnvShowsContainerBackground, value)
    }
}

// Magic happens here
struct WidgetEnvironmentReader<Content: View>: View
{
    var content: () -> Content

    init(@ViewBuilder content: @escaping () -> Content)
    {
        self.content = content
    }

    var body: some View {
        if #available(iOS 17.0, *)
        {
            WidgetEnvironmentViewBuilder(content: content)
        }
        else
        {
            content()
        }
    }
}

extension WidgetEnvironmentReader
{
    @available(iOS 17.0, *)
    struct WidgetEnvironmentViewBuilder<C: View>: View
    {
        @Environment(\.showsWidgetContainerBackground) var showsWidgetContainerBackground
        
        var content: () -> C

        init(@ViewBuilder content: @escaping () -> C)
        {
            self.content = content
        }

        var body: some View {
            content()
                .envShowsContainerBackground(showsWidgetContainerBackground)
        }
    }
}

// Now we are free to use .showsWidgetContainerBackground in the older os version
@available(iOS 14.0, *)
struct WidgetView: View
{
    @Environment(\.widgetEnvShowsContainerBackground) var widgetEnvShowsContainerBackground: Bool
    
    var body: some View {
        Text("Background: \(widgetEnvShowsContainerBackground ? "YES" : "No"), \(Date(), style: .relative)")
    }
}

@available(iOS 14.0, *)
struct WidgetEntryView: View
{
    let entry: Entry

    var body: some View {
        WidgetEnvironmentReader {
            WidgetView()
        }
        .makeContainerBackground()
    }
}

I'm having the same issue with the same crash log. I filled a feedback with an example widget: FB12702894

I'm unable to support standby mode because of this too

Same problem here. The proposed solution works fine for now. Thanks

The below provides a quick and simple solution:

private struct _ShowsWidgetContainerBackground: EnvironmentKey {
    static let defaultValue: Bool = true
}


extension EnvironmentValues {
    var showsWidgetContainerBackgroundWithFallback: Bool {
        get {
            if #available(iOSApplicationExtension 17.0, *) {
                self.showsWidgetContainerBackground
            } else {
                self[_ShowsWidgetContainerBackground.self]
            }
        }
        set {}
    }
}

You can use the new environment key showsWidgetContainerBackgroundWithFallback in place of showsWidgetContainerBackground.

Widget showsWidgetContainerBackground fallback
 
 
Q