Swift Concurrency In Real Apps

If it’s valid to load UI classes from Nibs on background threads then I think the language is ‘right’ here—you are making an assumption about dynamic behavior that can’t be proven safe. It seems to me like MainActor.assumeIsolated is the right tool here.

@hborla If this issue isn't going to be resolved for the GM, perhaps the team could make the compiler errors more helpful.

It's confusing that my class is marked @MainActor, but -awakeFromNib is nonisolated even though I haven't marked it as such. That makes it seem like a bug. If the compiler either:

  1. Complained that func awakeFromNib() is missing nonisolated or
  2. Suggested an appropriate workaround for awakeFromNib() since it's so common in apps

That would be very useful until Apple can implement some kind of improvement here.

2 Likes

That might very well be true for awakeFromNib specifically. I was trying to demonstrate the general problem with isolation mismatches in overrides, which I do think the language can make easier generally (e.g. using something similar to the "isolated conformance" idea but for subclasses). It is also definitely the case that some of these issues would be resolved by @MainActor annotations added to superclass methods.

In the meantime, the workarounds are to wrap the access to main actor state in MainActor.assumeIsolated, or if these checks are getting in the way of you getting your work done, switch back to minimal concurrency checking. The model will get easier to use. It is completely fine to wait until then before you adopt it.

4 Likes

It's "valid" in the sense that 30 years ago when OS X was created, there wasn't a way to enforce the "use this only on the main thread" contract. NextStep engineers didn't have language tools to do that. So it became an informal contract.

Now, in 2024, Swift Concurrency has come along and is formalizing that contract. But because developers technically could use -awakeFromNib() on a background thread, Apple can't just make it main actor. Swift Concurrency is bowing to the one-in-a-billion edge case where someone's doing something weird with Nibs that was never really officially encouraged—and in so doing, producing errors for the 99.999% of normal use-cases.

That tradeoff seems suboptimal to me, but I appreciate the predicament. At the very least, better guidance—at the point of of the errors!—for handling the situation seems wise

1 Like

The owners of awakeFromNib could have marked it MainActor, but have decided that they want (or need) to support the method running on arbitrary threads. Perhaps it's less rare than it seems? I would be maybe a little surprised, but I also assume the App/UIKit folks know best.

This is the reality of opting into using concurrency. It's annoying! I had quite a hard time working with NSDocument myself, which has its own, special concurrency system distinct from OperationQueue/GCD! But, I decided that's what I wanted so I had to deal with it by using dynamic isolation (assumeIsolated and friends). I would have preferred that NSDocument just be updated to better support concurrency. And I would prefer that awakeFromNib be changed too somehow to make this easier.

This stuff can be really rough. When you turn on warnings (or go to Swift 6) you are explicitly asking for these kinds of weirdnesses to be surfaced to you. And boy, are there a bunch, especially in the older frameworks. Getting comformable with dynamic isolation is essential.

5 Likes

Since awakeFromNib goes all the way from NSObject, not App/UIKit, I think it may have uses besides loading UI, therefore cannot be restricted at the top of the hierarchy.

I think it might be nice to have some way to implicitly say in UI/NSResponder (or in your own hierarchy at least) that awakeFromNib here is assumed to isolated to main actor so that this would be transient to any subclasses instead of requiring MainActor.assumeIsolated each time.

Without language changes, I think SDKs could offer (and this is something can be artificially added by developers now) is a "stunt" API for this method that would be explicitly isolated to main actor and called from ObjC awakeFromNib:

open class IsolatedNSViewController: NSViewController {
    public final override func awakeFromNib() {
        MainActor.assumeIsolated {
            super.awakeFromNib()
            isolatedAwakeFromNib()
        }
    }

    @MainActor
    open func isolatedAwakeFromNib() {
    }
}
1 Like

It's not really optional, though. You can turn off all the warnings and stick your head in the sand and continue with GCD and friends, but that's a dead-end. It's like staying on Carbon when Cocoa launched. Or choosing Objective-C for a new project in 2024.

I don't want to re-write vast swaths of my apps in two years. I want to build APIs and foundations that will last for a decade+. That means adopting Swift Concurrency.

My contention is: Apple could have done a better job of managing this rollout so that the frameworks were ready. They can't modify all 45,000 of them on day one, of course, but exceptionally common pain-points like this awakeFromNib() error-avalanche should have been trapped.

4 Likes

My full-time job is helping people adopt Swift concurrency specifically. But I was also a classic macOS developer, and I waited almost 3 years before even looking at Cocoa. I also only began using Swift with Swift 3. I just do not share your opinion here.

I think history has taught us, over and over again, that adopting new foundational technologies in the year they are introduced is going to be painful. But, sometimes you just want to. Or maybe a client is insistant. It was possible to adopt concurrency with Swift 5.5, but wow was it hard. I only did it because I thought I had already waited long enough. It is dramatically improved with Swift 6 and the latest SDKs. But, by no means is it perfect yet.

There are still too many holes in the frameworks, some of which I believe to be unfixable. Language can, and will I'm sure, improve as well. All I'm saying is this actually is optional and waiting, even one year, isn't just viable, I think it could be the only reasonable decision for many projects.

18 Likes

Is there a definitive answer to this question? This seems like the easy solution, given that this will not be fixed for Swift 6 release, but I'm worried it might be a footgun if there is some edge case where awakeFromNib is called by the system from a background thread. It would be great to have clarity here or a direction for how to resolve this for legacy apps that use this a lot.

It's not really a footgun if it's not introducing a new data race. If you're calling MainActor-isolated code from awakeFromNib and now you have to state assumeIsolated to make it compile in Swift 6, and it turns out this assumption is actually violated at runtime, wouldn't that mean the existing code was already incorrect in Swift 5?

5 Likes

Well I think this is exactly the question I'm trying to ask. Is this assumption valid? And if not, is there a good solution to continue using awakeFromNib code that interacts with the view?

1 Like

One implied thing that seems to be missing from this conversation: MainActor.assumeIsolated { } will trap if run outside the Main Actor. In other words, there will be no invisible undefined behaviour occurring if the assumption proves to be false -- you will know it immediately.

5 Likes

This is perhaps the wrong place to ask for this, but would it make sense to run a survey and ask users of Swift what they think of the recent changes in the language? Maybe this survey could be open for all at Swift.org, as only asking registered Swift forum users might skew the results in a certain direction. It would be interesting to know what the public opinion is overall, and that could help Apple steer decision making in the future.

3 Likes

It’s funny because I’m not sure if this point is raised as a pro or a con of this whole situation!

As a fan of code that works I certainly enjoy the contract of assumeIsolated and love the idea of ferreting out incorrect code so unambiguously.

But in my experience, larger teams eventually converge on a policy of avoiding anything that could possibly crash, enforced via code reviews, etc. Right or wrong, crash rate is a quality metric that is easily tracked :woman_shrugging:

There are a lot of awakeFromNibs out there maintained by a variety of folks under lots of different constraints and with varying levels of experience. It would be nice if there was parity between assumeIsolated and “assertIsolated” for people in this situation.

In other words, it would be good to be able to “assume” isolation and crash if that assumption is wrong, but only in debug builds, for example. Does anyone know if there is a reason that this is not part of the e.g. “assertIsolated” API?

4 Likes

This is an interesting question! I think the relevent proposal is SE-0313. I wasn't able to find any specific reasoning for why there wasn't a version introduced that matches the assert/precondition behavior. Maybe in the review thread?

And while I am personally against introducing code paths that are only possible in production, I also totally agree this is a common practice.

2 Likes

I'm neither for nor against any form of survey, but this sentence highlights something that I think is often somewhat misunderstood by the community at large.

Swift is no longer "just Apple's baby"... kinda? On paper? Or "not just"?

I watched the last SSW stream on youtube and someone in the comments asked an Apple-targeted question, clearly assuming all or at least most people on the stream were Apple employees, which caused some chuckles as in fact, only one of them was (and btw, it was a friendly question without ill intent or something).

Of course one can make the argument that Swift still is Apple's thing to a large extent, the company is obviously the Project Lead (it says so on the site), but the entire evolution process and the various steering groups bring in quite a bit of "outside" influence. I don't want to get political here, and I am sure Apple still has a "long arm" when it comes to decisions and steering, so it is understandable when people phrase or see it that way.
Sometimes, though, it's just based on an assumption that is "technically not correct".

For me the result would be to judge a completely open survey with skepticism, Apple is, after all, often times very polarizing even among its fans (MagSafe... :laughing:)

On a side note

I get the sometimes perceived "hostility" towards "criticism" in certain threads is also rooted in such assumptions. For example, missing @MainActor annotations in Apple-platform-only, closed frameworks are "prematurely" assumed to be oversights or flat out mistakes of the entire "Swift Concurrency feature". While of course knowing that it's more like one hand didn't manage to keep up with what the other produced yet doesn't help you in dealing with that, it would perhaps make it easier to understand it. At least that's the case for me, and I believe many of those who come off as somewhat hostile would agree, but they think it's just "one hand", so they're irritated. And this works the other way around, too: "Why are you shouting at me, I am not the one who missed that!"

3 Likes

If Apple tells Swift to jump, Swift is going to jump. If there’s a single constant about Apple since it fired Scully, it’s this: control. Apple controls all of its core technologies. Cook has literally said that’s an explicit goal.

For political purposes, it’s convenient for Apple to position Swift as an independent project. But the control is there. And Swift has largely failed to gain any meaningful traction outside of Apple’s platforms. Go and Rust have larger footholds.

Swift is Apple and Apple is Swift.

1 Like

Go figure, this kind of illustrates the point: Any survey about Swift as a programming language (which probably includes an inherent judgement about the wider ecosystem, of which not everything is under Apple's direct control) will be heavily biased by people's string opinion about the company itself.

If someone suggesting that the direction it is going is not only influenced by internal decisions of Apple (note that this statement alone does not necessarily mean Apple is not in control) already triggers an emphatic rebuttal, trying to get a survey that won't receive too much noise is probably doomed.

@bdkjones: I hope you will not think I took your post as an insult or considered it misplaced, I totally understand what you mean. We're not really far off from one another, I believe.

None of these languages (and many others) aren’t independent as well. Go is Google, Rust is also Google and Microsoft among founders of Rust Foundation. C# is Microsoft, and also struggled to gain traction for a while outside MS. And all of them are follow some path that is influenced by these stakeholders. So I don’t think it is fair to blame Swift particularly here, especially when latest release doesn’t align well with some Apple SDKs.

I'm not "blaming" Swift. It's literally impossible to build a first-tier language without the financial backing of a massive company. I have no complaints about Apple being involved.

I think this is accurate. I don't see Apple's involvement in Swift as a bad thing; I see it as a necessary thing.

To illustrate my point, consider Reactive Cocoa: they existed long before SwiftUI. They would have loved the features that drive SwiftUI: Combine, Property Wrappers, Function Builder Syntax, etc. But Swift didn't bend the language to accomodate them. The language bent when Apple came along and said, "We're doing SwiftUI. We need X, Y, and Z in Swift. Get it done." That's control.

1 Like