widgetAccentedRenderingMode

In lieu of a software update today – ScreenFloat v2.3.8 is taking me a bit longer than anticipated – I thought I’d share a fun 🙃 story about SwiftUI with you.

ScreenFloat offers widgets that give you quick access to different things.
Among others, you have the Capture widget which allows you to start a screen recording, capture your screen, import from your iPhone, etc.
And there’s the Recent Captures widget displays your recent shots so you can easily float them.
Or so I thought. But I’m getting ahead of myself.

The Capture widget
The Recent Captures widget

All the actions in these widgets (when a button or a shot in a widget is clicked) are powered by SwiftUI’s Link(), which opens my app with a custom URL that contains whatever information I need to perform the action the user requested.
For clicked shots, that would be floating them (such a custom URL could look like this: esssf2://widget.at?shotID=<someid>&action=float).

Now, some of these widgets are static (the capture utility widget, for example, can not be configured in any way by the user and always shows the same capture buttons), while others are dynamic (widgets that display shots can be configured to only show images, or only video recordings, for example).

Configuring the dynamic Recent Captures widget

That’s all lovely, but recently I discovered something puzzling.
While my static widgets worked just fine – each button in the Capture widget was individually clickable and did the correct thing in my app –, clicking a shot in a widget to float it would not open my app with the custom URL I specified, but with the dynamic widget’s configuration intent instead.
No matter which shot I clicked, it always did the same, wrong, thing.
(A configuration intent is what internally makes customizing widgets possible; and to make things even more confusing, ScreenFloat’s dynamic widgets are set up internally partially through a .intentdefinition file (Apple’s older way to make widgets configurable), and partially through AppIntents (Apple’s modern approach) ).
So instead of my app receiving the correct custom URL which tells it which shot to float, it receives info on how the widget is configured – information not exactly useful in this context.

So I figured it had to do with the widget’s dynamic nature – a logical assumption, given that my app received the configuration intent instead of my URL.

First, I thought perhaps the .intentdefinition file and the AppIntents somehow confused each other, so I temporarily removed the definition file, but the results were the same. Dead end. (Which is good. Otherwise I would have only been able to offer my widgets on macOS 14 “Sonoma” and later, leaving behind two earlier releases of macOS that ScreenFloat supports).

So I set out to test my assumption that the widget’s dynamic nature was at fault.
I tried yet another dynamic widget, the Folders widget.

The Folders widget

It can be configured to show all, only smart, or only “dumb” folders, and when clicked, ideally opens the clicked folder in ScreenFloat’s Shots Browser.

With a smirk on my face, confidently clicking on a folder in that widget, absolutely certain it would not result in the folder being opened in ScreenFloat’s Shots Browser, what did my tired, shocked eyes perceive?

The freaking folder opened in the freaking Shots Browser! The Link() with my custom URL worked!

 

But why!? I couldn’t make sense of it.
What was the difference between the Link() in my Recent Captures widget, and the Link() in my Folders widget?
Readers dabbling in the arts of SwiftUI might have already noticed that the first consists of Image()s, while the latter is made of Label()s. So I swiftly swapped out the Image() for a Label() to test that this was indeed the culprit, and lo and behold, the widget now worked.
A shot was finally floated correctly!

 

At this point, I was desperate. Close to tears of rage. Ready to rage-quit. Not only Xcode, but also my app’s development. My profession. Coding in general. I was done.
Perhaps Image()s in Link()s are just bugged and there’s nothing I can do about it?
The thing is, I knew it worked at some point, so I fired up my virtual machines in UTM and went back to see when it started to break.
macOS 12? Fine.
macOS 13? Fine.
macOS 14? Fine.
macOS 15? Bingo.

Something on macOS 15 and later broke my widget.

So the first thing I did was to remove all modifiers from my Image() and try again. If it still didn’t work then, I’d have to find a different way to make it work. Maybe hack my own Image() implementation together. Fun times ahead.
But I got lucky for once. Removing all modifiers from the Image() did the trick.
Now all that was left to do was to go through each modifier and see which one broke things.

“How many lines of code are there?”
“About two million…”

Ready?
.widgetAccentedRenderingMode(). It was .widgetAccentedRenderingMode() that messes with Link()s.
Only specific configurations, though: While .fullColor and .accented work fine, the other two (.desaturated and .accentedDesaturated) render the Link() defunct. Guess which ones I used.

Now why is this happening? Nobody knows. I doubt Apple knows. Or cares.
Is any of this documented? LOL.
How long has this bug been around for? Since macOS 15, so for about 2 years.

Now that I knew where the bug was, out of interest I googled it, and sure enough found others with the same issue. Not something I found before, because who’d think to google for that particular setup? Nobody, that’s who.

So, there you have it. A Link() containing an Image() with .widgetAccentedRenderingMode() set to .desaturated or .accentedDesaturated will render said Link() defunct 🤷‍♂️ .
I found two ways around this: Don’t use those two properties, or don’t embed the Image() in the Link(), but have the Link() be an .overlay() modifier of the Image(), like so:

var body: some View {
    Image(...)
        .overlay {
            Link(destination: ...) {
                Rectangle()
                    .foregroundStyle(.clear)
                    .frame(...)
            }
        }
}
The Shot floats when I click it in the widget!

A rdar’s been filed and is now ready to gather dust in the forgotten depths that is Apple’s Feedback Assistant, never to be looked at again, until some digital archaeologist far into the future stumbles upon it and finally marks it as “works as intended”.

I’ll go now and scream into a pillow some more.


Read more