Category Archives: Uncategorized

@MainActor – The Rules!

I’m going to have a shot at defining what @MainActor does, and what it does not guarantee.

Why?

I keep seeing tutorials and talks explaining @MainActor with something like the following quote

Isolation to the main actor is expressed with the MainActor attribute.

This attribute can be applied to a function or closure to indicate that the code must run on the main actor.

Then, we say that this code is isolated to the main actor.

The Swift compiler will guarantee that main-actor-isolated code will only be executed on the main thread, using the same mechanism that ensures mutually exclusive access to other actors.

– Eliminate data races using Swift Concurrency – WWDC 2022

This quote is simply untrue.

The Swift compiler absolutely 100% does not guarantee that main-actor-isolated code will only be executed on the main thread.

An untrue ‘promise’ is much more dangerous than no promise at all.

There is no real documentation on what @MainActor actually does. The best you can get is probably to look at the Swift Evolution proposals.

Two relevant ones are the proposal for actors (SE-0306) and global actors (SE-0316)
I have no idea whether these are the only relevant ones, or whether they have been superseded.

There is no good way that I’m aware of to tell – other than having an intimate knowledge of all the proposals (which I do not).

I have been banging on for ages about how Apple should document this critical part of Swift, but they haven’t.

So I’m going to give it a go.

This will undoubtably be wrong in subtle and important ways!

Please let me know when you discover more edge cases, and I’ll update accordingly.

Current update: XCode 14.1, 21 Nov 2022

What is considered @MainActor

Starting with the obvious, any method or variable directly annotated with @MainActor

Next up – any method or variable in a class which is marked @MainActor – or in it’s extensions

Similarly – any method or variable in a class where one of it’s parents is marked @MainActor
(This happens a lot when you’re using UIKit and AppKit)

What is considered @MainActor – Protocols…

Protocols are more subtle…

A method is treated as @MainActor if

Declaration – Both of these apply:
a) The protocol declares the method as @MainActor
b) The method is declared in the same block where the protocol conformance is declared

Usage:
c) The object being called is ‘seen’ as an instance of the protocol – and not the original class

In the example below, ProtOne and ProtThree meet the ‘Declaration’ rules, so the relevant methods are always considered @MainActor

The more subtle case is illustrated with ProtTwo

The conformance is declared as an empty extension,
The method is declared in the main block, so the ‘Declaration’ rules do not make it @MainActor

If you call two on a Foo object, it is treated as NOT @MainActor
If you call two on a Foo object which is being ‘seen’ as a ProtTwo – then it IS treated as @MainActor

When is @MainActor ignored?

The swift compiler tries to figure out when @MainActor methods are being called – and tries to ensure that they are called on the main thread.

Because this is a compiler technology – it can only work at compile time.

Because it is extremely complex – it probably misses edge cases.

I’m not knowledgable enough to say for sure exactly why the following cases are actually ignored.

Some seem like dynamic code which is effectively running after the compiler checks…

For the examples, I use the two classes below.

Bar is the ‘pure swift’ version, OBar is annotated to allow ObjC interoperability.

I have separated Bar and OBar to illustrate that the issues don’t only happen when you use @ObjC interoperability, but all the examples work fine with OBar.

mainVar and mainFunc() are intended to be called on the main thread only.

In each case, I show code where mainVar or mainFunc are not called on the main thread.


(please let me know if you find more cases!)

– Keypaths

Keypaths completely ignore Swift Concurrency.
This will call mainVar on a background thread

– Selectors

Calling a selector directly bypasses any Swift Concurrency Checks
This calls mainVar on a background thread

– Objective C

Any time your @MainActor variable or method is called by ObjectiveC, concurrency rules are ignored

In this example, Swift calls MyObjC on the background thread – but the same problem exists if Swift calls an ObjC method, and _that_ method then moves to a background thread.

You can even pass pure swift code to ObjectiveC as a block – and it will be run.
This one does at least generate a warning message

This situation is very common whenever you have a delegate pattern and ObjC code

This is a massively simplified example, but it is essentially what lots of Apple frameworks do. You call a class, and it calls you back via your delegate.

The Swift code here generates no warnings – but calls mainFunc on a background thread

– Any Library/Framework with a delegate, or a block callback (possibly)

This isn’t technically a separate case, but it is important enough to be worth emphasising.

Whenever you call a Framework or Library, you don’t know how they have implemented their code.

That means that any callback may be using Objective C, or might be ‘seeing’ your class as a Protocol, or could be using some other dynamic coding approach.

One of the first times I came across this in my own code was when using NSAlert in MacOS.

Clicking the ‘ok’ button sometimes called back on a background thread.

Apple coding is full of examples where this problem _might_ exist; CoreBluetooth, NSNotificationCentre, any third party Networking Framework just to take some super-obvious examples.

And the behaviour in those libraries/frameworks can change from version to version.

If you need to be sure – then you need to manually dispatch your code to the main queue

Conclusion

Swift Concurrency is GREAT.

I use it in all my code now, and it massively simplifies async work.

It is a highly impressive technical achievement. But it doesn’t magically fix everything – and the perception that it might is dangerous.

I think it is shameful that Apple has not provided documentation on exactly what it guarantees. This should be covered in the swift guide, with perhaps a tech note on the more subtle details.

Apple should not be making false claims about what it does in WWDC talks.

My hope is that they’ll provide proper documentation and I can deprecate this article…

PS: Top Tip…

Update – Add flag for warnings…

You can add a build flag to generate warnings when a @MainActor func/var is called on a background thread.
It doesn’t stop @MainActor methods from being called in the background – but it at least generates a purple runtime warning for you to investigate.

The flag is -Xfrontend -enable-actor-data-race-checks

Add it to ‘Other Swift Flags’

Swift Concurrency Protocol & @MainActor

At this stage, I’m just riffing on different ways to ‘break’ swift concurrency.

I wondered if protocols would provide an opportunity, and indeed they do!

Take this scenario. We have a simple protocol – but the class implements it with an @MainActor annotation.

If you do this all within the main class definition, then you get a warning


However, you can declare conformance in an extension and make it @MainActor
No warning here

Or perhaps more confusingly – you can declare your whole class @MainActor, then just conform ‘normally’ in the extension

– in this last case, I wasn’t sure whether it would actually be treated as an @MainActor variable – but I tested, and indeed it is.

So – we have three ways of conforming to a protocol which _doesn’t_ require @MainActor – but in each case, we have required @MainActor in our implementation

Swift Concurrency will use the MainThread for an instance which it recognises as Bar, and won’t bother for an instance it recognises as Thing

I can see the logic here – but once again, @MainActor isn’t guaranteeing anything. The actual call depends subtly on what kind of thing the compiler recognises.

So – we have three subtly different ways of generating exactly the same code.
One of them generates a compile-time warning, the other two do not.

I generally prefer declaring conformance to a protocol in an extension – I had no idea that meant I would get different compile-time warnings.

Sometimes the method is called on Main – Sometimes not!

    @MainActor
    func makeBar() -> Bar {
        return Bar()
    }
    
    func breakWithProtocol() {
        Task {
            var bar:Bar = await makeBar()
            //The compiler recognises a Bar and insists we call asynchronously
            //It will call on the main thread
            _ = await bar.thing
        }

        Task {
            let thing:Thing = await makeBar()
            //The compiler just sees a Thing here - so it calls directly on a background thread
            _ = thing.thing
        }
    }

Five ways to break Swift Concurrency

I really like Swift Concurrency.

I just wish Apple would clearly document what it actually guarantees

Generally, sites (including Apple) explain that marking a function with @MainActor guarantees that it will always run on main.

By adding the new @MainActor annotation to Photos, the compiler will guarantee that the properties and methods on Photos are only ever accessed from the main actor.

Discover Concurrency in SwiftUI – Apple

This simply isn’t true.

Here are five simple functions that cause a @MainActor function to run on a background thread…

class OnMain:NSObject {
    
    @MainActor
    @objc
    func noCanFail() {
        guard Thread.isMainThread else {
            fatalError("not on main")
        }
        print("Did Not Fail!")
    }

    //Just call the selector directly. Swift doesn't care
    func breakWithSelector() {
        Task {
            self.perform(#selector(self.noCanFail))
        }
    }
    
    // Get some Objective C code to call the 'protected' function
    // The objective C code is simply
    // [[OnMain new] noCanFail];
    func breakByCallingObjC() {
        Task {
            Trouble.cause()
        }
    }
    
    //NSNotification centre calls you back on whatever thread the notification was posted
    //It doesn't care about Swift Concurrency
    func breakByNotification() {
        let notification = Notification.Name("Cheeky")
        let center = NotificationCenter.default
        
        center.addObserver(self,
                           selector: #selector(self.noCanFail),
                           name: notification,
                           object: nil)
        Task {
            center.post(name: notification, object: nil)
        }
    }
    
    //Surprisingly, even with block syntax, you get the fatal error
    //At least there is a warning in this one
    func breakByNotification2() {
        let notification2 = Notification.Name("Cheeky2")
        
        let queue = OperationQueue()
        let center = NotificationCenter.default
        
        center.addObserver(forName: notification2,
                           object: nil,
                           queue: queue) { _ in
            self.noCanFail()
        }
        
        //No need to post from a task this time!
        center.post(name: notification2, object: nil)
    }
    
    
    //Objective C will just run the block for us by calling
    //block();
    //At least there is a warning in this one
    func breakWithBlock() {
        Task {
            Trouble.run {
                self.noCanFail()
            }
        }
    }

    func breakFoo() {
        let foo = Foo()
        let library = MyLibrary(delegate: foo)
        library.doWork()
    }
}

Two of these functions generate a warning in XCode 14.1 -three don’t.
They all trigger fatal errors when they end up calling noCanFail on a background thread.

This isn’t by any means an exclusive list.

The most obvious point where bugs like this can bite you is that any time you have a callback function from a system framework (such as bluetooth discovery, or even an Alert), or from a third party framework like a networking library – then it _could_ call back on any thread.

You might get lucky – but in this scenario, @MainActor does precisely nothing.

To my mind, the great sin here is not that Swift Concurrency has edge cases.

The great sin is that Swift Concurrency is presented as if it ‘just works’ – when in fact there are important caveats that you need to manage.

@MainActor is barely documented by Apple. There is a single oblique reference to it in ‘the Swift Programming Language’

It is a core part of the language, it’s ridiculous that Apple doesn’t have clear documentation on what it actually does.

The Rules (?)


My understanding is something like the following:

@MainActor works as long as you…

  1. Are calling from your own code
  2. Are not using selectors
  3. You only use Swift
  4. You don’t use any other form of concurrency (like OperationQueue)

NB – I am not claiming that these rules are 100% correct.
There are probably edge cases even within these rules.

Apple should document what the real rules are.

Rule #1 is trickier than you might think…

It is up to you to ensure that any code which is protected by @MainActor is only called directly from your own code.

If you have a callback from a library/framework, and that calls a function, which calls a function, which eventually calls your protected function – then that’s a problem.

This is true even if you’re providing a callback to an Apple framework like SKStoreKit

It’s _really_ easy to miss something like this.

Imagine the following common pattern

class Foo:NSViewController {

}

//perhaps conforming to some delegate protocol...
extension Foo:MyLibraryDelegate {
    func someCallback() {
        OnMain().noCanFail()
    }
}

You have a UIViewController or an NSViewController.

That does calls a framework/library (perhaps SKStoreKit or a networking library)

The delegate is able to call noCanFail() in the extension here because the whole class has inherited @MainActor from NSViewController

The library is something trivial that ends up calling the delegate off the main thread

protocol MyLibraryDelegate {
    func someCallback()
}

class MyLibrary {
    var delegate:MyLibraryDelegate
    
    init(delegate:MyLibraryDelegate){
        self.delegate = delegate
    }
    
    func doWork() {
        Task {
            delegate.someCallback()
        }
    }
}

Once again – a fatal error.
No warnings in the compiler (at least not with default XCode settings)

Swift Concurrency is great. I’ll continue to use it.

Carefully.

Update – Add flag for warnings…

Thomas Goyne pointed out that that you can add a build flag to generate warnings in these cases.
It doesn’t stop @MainActor methods from being called in the background – but it at least generates a purple runtime warning for you to investigate.



The flag is -Xfrontend -enable-actor-data-race-checks

Add it to ‘Other Swift Flags’

I’ll be adding this to all my projects.

Update – Keypaths…

Probably not surprising at this point. Keypaths seem to completely ignore Swift Concurrency

No Objective C involved here at all

    @MainActor
    var doNotFail:Int {
        guard Thread.isMainThread else {
            print("Failed")
            return 0
        }
        return 0
    }

    func breakWithKeypath() {    
        Task {
            var array = [OnMain()]
            var sorted = array.map(\.doNotFail)
        }
    }

doNotFail is merrily called on a background thread…

@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

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…

Overreaction vs Sanity. Google Play vs Apple Store.

In the last week or so, both Apple and Google informed me that one of my apps was in a small way breaching store guidelines.

Photo by Jorge Fernandez on Unsplash

I’ll paraphrase both messages here:

One of your apps hasn’t been updated in years. It doesn’t meet current guidelines. If you don’t update it – we’ll remove it in 30 days.

Apple

and

One of your apps breaks the metadata guidelines. It has a quote in the app description.

We have removed it from the store.

Google

Both complaints are probably valid. The iOS app hasn’t been updated since 2015 and hasn’t been optimised for the latest devices. I’ll update it this month.

The Android app did have the following quote in the store description

VLC Remote is the latest Android app to earn my favor, and it’s a beauty

Androidnica

The quote has been there for at least 5 years. I’m not entirely sure whether it should count as a ‘user testimonial’ (banned) or a ‘third party review’ (I think those are ok). But either way – I’m happy to remove it.

Removing my app from the store just seems like a Massive Overreaction when they could simply have emailed me to request a change.

Easier iTunes Connect Sandbox Users with GSuite Routing

Use GSuite Routing to dynamically handle different iTunes Connect test emails.

iTunes connect testing is a pain. One of the pain points is that you have to create a bunch of iTunes Connect accounts when you want to test your purchase flows. Each of those needs a separate email address.

You used to be able to do this with the magic

  • rob+test1@hobbyistsoftware.com
  • rob+test2@hobbyistsoftware.com

And all your emails would be routed through to rob@hobbyistsoftware.com (this works for gmail, and many other email providers)

Sadly Apple disabled this capability some time in 2018 (?) – so now you need a new valid email address for every iTunes Connect sandbox user

GSuite Routing provides a neat way to restore this functionality

  1. Open your GSuite Management Console
  2. Click through to Apps > G Suite > Gmail
  3. Click on ‘Default Routing’
  4. Click ‘Add Setting’ and add something like the following

(note – the regexp is rob_.*@hobbyistsoftware.com)

This redirects all email of the format rob_something@hobbyistsoftware.com to rob@hobbyistsoftware.com

New App for Mac: AV Rules

When I connect my projector, I want my mac to switch to 1080p.

When I connect my TV, I want it to run at 720p

When hdmi audio output is available, I want my mac to use that (and to use all the channels too!)

Previously, my Mac mini didn’t do a great job of switching, so I wrote AV Rules to take charge.

rules


Buy Now