@MainActor – not guaranteed

tldr marking methods with @MainActor does not guarantee that they run on the main thread.


Swift has a magical new structured concurrency model.

‘using Swift’s language-level support for concurrency in code that needs to be concurrent means Swift can help you catch problems at compile time’

-the swift programming language

One of the new features is @MainActor. You can use this to annotate functions, classes, properties, etc.


this proposal introduces MainActor as a global actor describing the main thread. It can be used to require that certain functions only execute on the main thread

link

So – what does @MainActor do?
I hoped to find some documentation, but other than the evolution proposal, I haven’t found any.

There are some tutorials out there which say things like:


all that we really need to know is that this new, built-in actor implementation ensures that all work that’s being run on it is always performed on the main queue.

https://www.swiftbysundell.com/articles/the-main-actor-attribute/

This is what I thought @MainActor did. It is not true.


I created an example to demonstrate.

I have a ViewController with a method and property marked with @MainActor

    @MainActor var mainDate:Date {
        print("@MainActor var - main: \(Thread.isMainThread)")
        return Date()
    }
    
    @MainActor func printDate(_ label:String) {
        print("@MainActor func - \(label) - main: \(Thread.isMainThread)")
    }

My expectation is that calling the function printDate or the property mainDate will always run the code on the main thread. As such, they’ll always print “main: true” in their debug statements.

I created an async function which calls these

    func doWork() async  {
        let _ = await Background().go()

        print("returned from Background - now running off main thread")

        print("calling mainDate in doWork")
        self.storedDate = self.mainDate //sometimes not main thread
        printDate("in doWork") //sometimes not main thread
    }

The first call in the async function is to a background actor.

//Actor that isn't main - it does not run on the main thread
actor Background {
    func go() -> Date {
        print("Background go - main: \(Thread.isMainThread)")
        return Date()
    }
}

The effect of the background actor is simply to return from the await on a background (not main) thread.

This ensures that I’m not simply calling mainDate and printDate on the main thread by default.

I call doWork from a task responding to a button click

    @IBAction func doWorkInAsyncFunction(_ sender: Any) {

        Task { @MainActor in
            await doWork()
        }
    }

For good measure – I mark the task as @MainActor.
My expectation was the following

  1. in doWork(), it would return off the main thread after Background().go()
  2. @MainActor annotation would ensure that calling self.mainDate would happen on the main thread – or there would be a compiler error
  3. @MainActor annotation would ensure that calling self.storedDate would happen on the main thread – or there would be a compiler error

#2 and #3 are false. I get the following output when I run the code

returned from Background - now running off main thread
calling mainDate in doWork
@MainActor var - main: false
@MainActor func - in doWork - main: false

Note the last two lines print main:false – @MainActor isn’t keeping me on the main thread here…

Bizarrely – If include a print statement in my task, then everything _does_ run on the main thread!!!

    @IBAction func doWorkInAsyncFunctionWithPrint(_ sender: Any) {

        Task { @MainActor in
            //Adding this print statement, magically makes everything in doWork run on the main thread!!!
            print("Srsly?")
            await doWork()
        }
    }

gives the following “correct” output

Srsly?
Background go - main: false
returned from Background - now running off main thread
calling mainDate in doWork
@MainActor var - main: true
@MainActor func - in doWork - main: true

Similarly – if I call identical functions, but within a Task (and not even one annotated as @MainActor), then I get the “correct” results

    @IBAction func doInTask(_ sender: Any) {
        Task {
            let _ = await Background().go()

            print("returned from Background - now running off main thread")

            print("calling mainDate doInTask")
            self.storedDate = self.mainDate //main thread
            printDate("in doInTask") // main thread
        }
    }

gives the following…

Background go - main: false
returned from Background - now running off main thread
calling mainDate doInTask
@MainActor var - main: true
@MainActor func - in doInTask - main: true

Is this even wrong?

Swift isn’t behaving as I would expect it to. That’s hardly damning!

However:

  • I can’t find any formal documentation to compare against.
  • I can’t look up what the guarantee (if any) is that Swift provides when you annotate something with @MainActor

I have mostly learned from the brilliant resources provided by sites like swiftbysundell

all that we really need to know is that this new, built-in actor implementation ensures that all work that’s being run on it is always performed on the main queue.

-swiftbysundell

and Hacking with Swift

The magic of @MainActor is that it automatically forces methods or whole types to run on the main actor, a lot of the time without any further work from us. 

-hacking with swift


It seems that I’m not alone in what I expected @MainActor to do. Perhaps this is a bug. Perhaps it is expected behaviour. In the absence of clear documentation showing what @MainActor should to, I can’t tell.


Either way – marking methods with @MainActor does not ensure that they run on the main thread.

Update: Warnings Available…

returned from Background - now running off main thread
calling mainDate in doWork
warning: data race detected: @MainActor function at MainActorExample/ViewController.swift:26 was not called on the main thread
2022-01-17 14:43:40.786070+0000 MainActorExample[4407:7439502] warning: data race detected: @MainActor function at MainActorExample/ViewController.swift:26 was not called on the main thread
@MainActor var - main: false
warning: data race detected: @MainActor function at MainActorExample/ViewController.swift:31 was not called on the main thread
2022-01-17 14:43:40.811866+0000 MainActorExample[4407:7439502] warning: data race detected: @MainActor function at MainActorExample/ViewController.swift:31 was not called on the main thread
@MainActor func - in doWork - main: false

This does show runtime a bunch of warnings.

‘cannot use parameter ‘self’ with a non-sendable type ‘ViewController’ from concurrently-executed code’
and

‘cannot call function returning non-sendable type ‘Date’ across actors’

I’ll experiment some more…

Update 2: Models get errors!

Moving the exact same code out of the NSViewController and into a separate class (that inherits from nothing) causes the expected compiler warnings to kick in.

It seems that my model is ‘Sendable’ – so now the compiler can do magic.
This feels like an important limitation that should be in BIG BOLD LETTERS in the documentation…

Interestingly, when this code is moved to a separate model, calling self.mainDate without async causes an error.
However in the original ViewController version – adding an async (async self.mainDate) generates a warning

‘no ‘async’ operations occur within ‘await’ expression’

Update 3

This bug looks like the same setup

Update 4 – May 2022

Running the example code in Xcode 13.3.1 on OSX 12.3.1 and the code behaves as expected.

I don’t know if this is a compiler fix (so newly compiled apps are fine), or an OS fix (so your app will only behave unexpectedly on older OS’s.)


Example code available at https://github.com/ConfusedVorlon/MainActorExample

Fool me once… Reporting bugs to Apple

About a year ago, I decided to give up reporting bugs to Apple. Like many others, I got fed up of them simply being ignored.

At WWDC in in June of 2021, I signed up for a SwiftUI lab, and showed the two Apple engineers a bug that had been annoying me. The bug is small enough to fit in a tweet:

struct ContentView: View {
    
    @State var mode:Int = 0
    
    var body: some View {
        
        HStack(spacing:10) {
            
            Button("Button A") {}
 
            Button("Button B") {}
            
            Picker(selection: $mode, label: EmptyView()) {
                Text("1").tag(0)
                Text("2").tag(1)
            }
            .pickerStyle(SegmentedPickerStyle())
            
        }
        
    }
}

That code should produce two buttons and a segmented picker.

However, if you run it on a Mac in Catalyst, without the ‘optimise for Mac’ option, then as soon as you click on Button A – the picker becomes completely unresponsive.

We poked about a bit, confirmed that it really was a bug, and dug into the root cause (the button is leaving a gesture recogniser in an incorrect state)

There was no fix – but the pair told me that I should definitely submit a bug report.

When I explained that I had given up on bug reports – they assured me that their team didn’t ignore reports.
They love simple reproducible cases like this, and I should absolutely submit.

Fool me twice

I decided to give them the benefit of the doubt.

I went back to feedback reporter and found that I had actually reported this bug in December 2020. Nonetheless, I updated it to confirm that it was still broken on the latest OS and actually resubmitted under my company account with some discussion of the conversation I had at the lab.

The bug (FB8925084) includes the relevant code and a sample project.

The re-submission has never had any response at all.

My initial personal submission has the following history:

  • 9/Feb – I confirm that it is broken in Monterey beta
  • 2/July – Apple report that this is fixed in 11.3.1 (it isn’t)
  • 3/July – I confirm it is not fixed. Still broken in 11.4 and latest Monterey beta
  • 26/July – I confirm still broken in latest Monterey beta
  • 12/Aug – I confirm still broken in Monterey beta 5

What now?

SwiftUI & Catalyst are important platforms. This bug is remarkably simple to trigger.

It feels like the kind of thing that Apple should care about.

I certainly care about it – the underlying problem of bad gesture handling causes a bunch of issues in one of my apps.

Clearly reporting bugs is a waste of time though. I guess we just have to wait until Apple hit this in one of their own apps and have cause to care.

Update 31 Aug

Stil broken in Beta 6 (reported)

Update 4th Sept from Apple:


“Your feedback is important to us”

Update October 2021

It looks like Apple have finally addressed this in a Monterey beta.


I commented earlier:

I guess we just have to wait until Apple hit this in one of their own apps and have cause to care.’

And I suspect that is exactly what happened. The Shortcuts app in Monterey looks like it has been hitting this issue (or a related one).

I certainly don’t think my feedback helped to get things fixed. There has been no update to my report from Apple.

Dynamic Wallpaper Wierdness (again)

tldr; Multi Monitor Wallpaper can give you the daytime images for your dynamic wallpaper even if you use dark mode.

Dynamic Wallpaper

One of the trickier things that Multi Monitor Wallpaper does is pull apart dynamic wallpapers so that it can build separate ones for each of your monitors. It then puts them back together again.

These dynamic wallpapers should change with the sun – but they don’t always.

Sometimes, it is just one of Apple’s many frustrating bugs* around wallpapers.

Sometimes it is something that looks like it was intended as a feature.

One of those is the feature where dynamic wallpapers can declare which images are suitable for dark mode. Sometimes, the operating system will honour that declaration. When you switch to dark mode, even in the middle of the day, it will show you an evening/night image.

I thought Big Sur had stopped doing this, but it turns out the behaviour is just intermittent. On my iMac right now, the Catalina wallpaper shows based on the current time whatever your mode. The Big Sur wallpaper switches to match the mode.

On my Macbook air, they were both switching earlier today – but now neither is switching.

I have no idea what is causing this inconsistent behaviour. I have pulled out the metadata and they both have the same structure.

However – I can fix it so that you can see all the images whatever your mode.

Multi Monitor Wallpaper now has a setting (enabled by default) which simply tells the operating system that every image is suitable for use at night. This is based on the fantastic work by Marcin Czachurski.

You can turn this off if you prefer. In that case, you’ll get Apple’s default and unpredictable behaviour. It might show dark images when you use dark mode. It might not!


*for example the popup display bug reported when Mojave was in beta here, or the bug I have reported where disconnecting and re-connecting screens can corrupt the wallpaper settings database, or the bug where picking a still desktop means that wallpapers set via the API are also stuck in still mode. I could go on…

Apple Reviewers can Punish You

tl;dr – Apple policy may now say they won’t hold up your urgent bugfix, but that doesn’t stop them holding it ‘in review’ for an unusually long time so that it is never actually released…

I have had plenty of ‘robust discussions’ with App Review. Sometimes they come round to my way of thinking – I have had several appeals approved. Sometimes Apple just isn’t budging. Until this occasion though they have always been polite and engaged in good faith. I have never felt that a reviewer was punishing me out of spite. This time, I feel like I really got a ‘Bad Apple’ at Apple.

This particular Apple Review nightmare kicked off when a reviewer decided that my bugfix release:

  1. Enabled auto-launch without the user’s permission
  2. Had an app preview video which breaks the rules

Convincing them that they were wrong about #1 was easy. I just had to show the screenshot where my app asks the user if they want to enable the recommended settings and explicitly lists ‘auto-launch’ as one that will be enabled.

Number 2 was harder. Here is the video causing the problem:

For context, this video has been in place for about 18 months, and has (successfully) gone through about 30 reviews so far. You can see it now in the app store listing.

Multi Monitor Wallpaper is an app that sets the wallpaper across multiple monitors. It really needs a way in the app preview to show the multiple monitors in action. So – it ‘zooms out’ allowing you to see the full effect.

This, my reviewer insists breaches the (unwritten) rule

Your app preview includes content that does not sufficiently reflect the app in use. Specifically, your preview:

– Includes device images and/or device frames.

I understand where the frames thing comes from. Apple don’t want a video of people using their Mac in the coffee shop with the app running. They want to see the app itself.

In my case though – the monitors (and frames) _are_ the app. I think that makes this a reasonable and honest app preview. The reviewers when I first submitted the preview 18 months ago obviously agree, and none of the 30ish reviews since then have had a problem with it.

Nonetheless, my reviewer wasn’t budging. They said:

We advise appeal this review by sending a request to the App Review Board. As you may be aware, the App Review Board was created for developers to appeal an app review they have concerns with.
Once the App Review Board has completed their evaluation, they will contact you directly.

Recently, Apple published new guidelines which promised developers that

Bug Fix Submissions: For apps that are already on the App Store, bug fixes will no longer be delayed over guideline violations except for those related to legal issues.

https://developer.apple.com/news/?id=xqk627qu

I really like the new policy. It seems reasonable. This certainly was a bug fix submission. It fixes a crashing bug when users click on one of my image search providers (Unsplash)

I asked:

I did see your message suggesting that I should appeal to the app review board. I’m will do that today.

In the meantime, there is a significant bug which this version fixes (there is an immediate crash if you click to browse Unsplash).
As per your press release, I understand that ‘bug fixes will no longer be delayed over guideline violations except for those related to legal issues.’, so would appreciate it if you could publish this update as soon as possible.

And, I submitted my appeal, and copied it into the resolution centre to keep my reviewer up to date.

Hello Rob,

At this time, you will need to follow the pending guidance from the App Review Board.

I explained that I wasn’t aware of any pending guidance and asked again

Bug Fix Submissions: For apps that are already on the App Store, bug fixes will no longer be delayed over guideline violations except for those related to legal issues.[…]

this is a bug fix submission. I haven’t changed the AppPreview video. I would like to take advantage of this process.
I will address this issue in my next submission if my appeal is not supported by the Appeal Board

still ‘computer says no’

Hello Rob,

Thank you for your response.

To clarify, the App Review Board will be in contact with you, as you currently have a pending appeal.

Best regards,

I tried again

I’m happy for the review board to be in contact with me.
As you say – I have a pending appeal (at your explicit suggestion)

Apple have communicated publicly (via press release and direct email) that ‘bug fixes will no longer be delayed over guideline violations except for those related to legal issues.’

If this is indeed a guideline violation, it certainly doesn’t relate to legal issues.

I’m asking you to honour that explicit public commitment.

that got me close to what I needed

Thank you for providing this information.

If this is a bug fix submission, you may elect to have it approved at this time.

If you would like to take advantage of this opportunity, please respond in Resolution Center to confirm that you can address these issues in your next submission. Additionally, please let us know if you have any questions about how to resolve these issues. If you believe this decision was made incorrectly, you may appeal your app rejection. You may also provide feedback on our review guidelines.

well – I must admit I thought the reviewer was just throwing up an extra step here to be annoying, but I confirmed. Shortly later, I got the promising response

Thank you for providing this information.

We will continue the review, and we will notify you if there are any further issues.


Now. We need a little diversion. How long does a review take?
I have put quite a few updates on Multi Monitor Wallpaper.

This is how long the recent reviews took from the point where they moved to the ‘In Review’ status.

  • 9-Sept, 5 mins
  • 8 Sept, 16 mins
  • 25 July, 20mins
  • 23 July, 20 mins
  • 21 July, 24 mins
  • 16 July, 7 hrs
  • 24 Jun, 1hr 40mins
  • 11 Jun, 6hrs

Mostly about 20 mins. Sometimes 6 or 7 hours.


How long do you think it takes for a very simple bugfix with no changes to the overall app – where Apple is allowing it through because of their commitment ‘not to hold up bugfix releases’ ?

I don’t know the answer to that.

My app changed status to ‘In Review’ on the 11 Sep 2020 at 23:27

It has been over 60 hours so far and I am still waiting.

After 24 hours, I even submitted a request for an expedited review. That was granted – but nothing has happened yet.

Perhaps I have just been unlucky, and the reviewer who seemed to deliberately throw up steps to avoid allowing my bugfix until I asked whether Apple were good to their word; Perhaps that reviewer just had a family emergency and accidentally left my app in the queue.

I think they’re punishing me.

They’re also punishing our shared customers who currently have to use a crashing app.

I’ll keep this post updated.


Update. 68 hours after moving to ‘In Review’, Multi Monitor Wallpaper was finally approved.

Remember that this was the process to ‘not hold up an urgent bugfix release’


Note – quotes of discussions with App Review are fairly heavily edited for brevity, in that they don’t show everything said. Everything quoted is verbatim though.

If any press want full details, I’ll be happy to share the complete conversation.

Google/Apple. Requiring the Account Holder to do techie things is BAD SECURITY!

tl;dr – in the name of security, you encourage me to share passwords.

Photo by Jon Moore on Unsplash

Both the Apple App Store, and the Google Play store allow business accounts to have multiple users.

This means that my client (the app owner) can own their account, and they can let me (the developer) do what I need to to develop and publish apps.

This means we don’t have to share passwords, and the account holder can limit what I have access to. So, for example I might be able to update apps – but I can’t see their financial reports. This all makes lots of sense.

Unfortunately, both Apple and Google have technical actions where they require the account holder to perform them. Because the actions are pretty technical, if the account holder is a business owner rather than a techie – they probably don’t have the skills to perform them. I hit these both recently.

In both cases, they’re important security-sensitive actions. For Apple – it was creating Developer ID Certificates to let me upload a MacOS Catalyst app to the store. For Google, it was enabling API access to automate uploading new builds.

Both of these actions are absurdly technical for a non-tech person to complete. Follow the links if you want the gory details!*

This means that the only practical way to perform these necessary actions (if you’re not physically located in the same place) is for the account owner to share their password with me the developer. This is clearly terrible security practice – and exactly what the multi-user system is set up to avoid.

I completely understand that these are security-critical steps. It makes sense that the account holder should have some kind of approval when they happen. This could be an explicit post-action approval:

‘Rob has requested XXXX – this is a critical security issue. Do you want to approve this action’

Or it could be a time-limited user permission:

‘Grant Rob permission to do XXX for the next 24 hours’

The current system achieves the opposite of what it sets out to do. It sets out to keep security-critical actions safe, but what it encourages is that the developer probably gets the account holder’s password and complete access to everything.


*for bonus foolishness – in the case of the Apple action. Xcode will automatically generate your certificates if you add the account holder’s account to Xcode. This means that it is complicated and techie for the business person to perform the actions (they don’t have Xcode installed) – but automated for the developer if they can just get the account holder’s password.

Run Multiple copies of VLC

To my surprise, one of VLC Remote’s users showed me how you actually can run multiple copies of VLC and remote control them with VLC Remote. (thank you Calvin!)

He is using them to stream to 4 zones in his house, but there are plenty of other reasons you might want to do this.

The trick is simply to run VLC from the command line and specify a different http-port value for each.

For Mac os, I used:

/Applications/VLC.app/Contents/MacOS/VLC --http-port 8090
/Applications/VLC.app/Contents/MacOS/VLC --http-port 8091

for Windows, the equivalent would be:

c:\Program Files (X86)\VideoLAN\VLC\VLC -VVV --http-port 8090
c:\Program Files (X86)\VideoLAN\VLC\VLC -VVV --http-port 8091

You’ll then need to add computers manually in VLC remote using the IP address of your computer and port 8090 (or whatever port you use).

If you’re doing this regularly, you’ll probably want to set up a batch file to start up your multiple instances. Calvin was able to set up his batch script so it pointed to individual audio devices in his streaming setup.

cd "c:\Program Files (X86)\VideoLAN\VLC\"


rem VLC session 1 with gigaport1 audio, port 8310, and russound1$

start cmd.exe /k "VLC -VVV --http-port 8310 --directx-audio-device={32776569-ABD9-4C9D-89B1-11E47A93EDEF} --directx-audio-speaker=Stereo \\atomic\russound1$"


rem VLc session 2 with gigaport2 audio, port 8320, and russound2$

start cmd.exe /k "VLC -VVV --http-port 8320 --directx-audio-device={39E33C1B-A451-4185-B470-2CBAEB769018} --directx-audio-speaker=Stereo \\atomic\russound2$"


rem VLC session 3 with  gigaport3 audio, port 8330, and russound3$

start cmd.exe /k "VLC -VVV --http-port 8330 --directx-audio-device={CF04DBB5-952D-4AEE-80F6-AEC2AFDAF671} --direct x-audio-speaker=Stereo \\atomic\russound3$


rem vlc session 4 with gigaport4 audio, port 8340, and russound4$

start cmd.exe /k "VLC -VVV --http-port 8340 --directx-audio-device={EEF16A7C-842C-4DA3-BA65-E8F24C0A0266} --direct x-audio-speaker=Stereo \\atomic\russound4$

Dolby Codecs, Legal Threats: Coda

Back in 2014, I wrote about how Dolby were requiring me to remove ac3 support from VLC Streamer for somewhat questionable legal reasons

I included the email I received from their representative about the price I would have to pay

You should also know that there are fees associated with becoming licensed.  Specifically, there is a $25,000 one-time initial fee and a recurring $5,000 annual maintenance fee.  There is also a per-unit royalty that has a tiered structure, due quarterly, based on annual total usage, as follows:

0-100,000 downloads at $0.99 per download

100,001-1,000,000 downloads at $0.67 per download

1,000,001+ downloads at $0.45 per download

I was interested to get a call from a lawyer working for Adobe recently. They were being sued by Dolby for something related to these patents.

Bizarrely, Dolby were arguing that their pricing levels were so super secret that they couldn’t even disclose them in court.

Adobe found my old post and argued that if they were published on the web – they couldn’t be that secret.

I don’t have any details about the case, but I know they settled and it went away. Hopefully my pricing info made the difference…