Consider this code, wherein we create a custom NSTableColumn that uses an image instead of a String as its header. (Think: the "heart" column in iTunes.)
final class LockTableColumn: NSTableColumn
{
lazy private var _headerCell: NSTableHeaderCell? = nil
override var headerCell: NSTableHeaderCell
{
get
{
if _headerCell != nil {
return _headerCell
}
_headerCell = super.headerCell // We want all the other default styling that super provides
if let image = NSImage(systemSymbolName: "lock.fill", accessibilityDescription: nil)
{
image.size = CGSize(width: 14, height: 14)
Task { @MainActor [_headerCell] in
_headerCell!.image = image
}
}
return _headerCell
}
set
{
// no-op
}
}
}
Discussion
It took me a long time of blindly stumbling around to do this SUPER SIMPLE thing in a way that shutup the compiler in Xcode 16 beta 5. Consider:
NSTableColumn is unchecked sendable for some ungodly reason, even though it's entirely related to UI. But NSCell is bound to MainActor (of course it is!). So working with these two things together is like mixing oil and water.
You can't make your subclass @MainActor if the superclass is nonisolated. The universe will implode.
You can't make your overridden property (headerCell) @MainActor if the superclass's property is nonisolated. That worked in Beta 4, but fails in Beta 5.
You can't simply do Task { @MainActor in } to set the image property, because then you're implicitly capturing self and the compiler vomits up a completely unhelpful error: "Task or actor isolated value cannot be sent", implying that the trouble is the Task. You have to discover/remember to provide the capture list, which is not AT ALL obvious until you've been clubbed in the face with this problem several times.
Conclusions
In the pursuit of "provable concurrency safety", we have gone too far. Swift is now downright painful to use for very simple things where no practical race conditions will ever occur.
It feels like Swift Concurrency has been rushed out the door. Apple's frameworks aren't ready for it and the piecemeal support makes adopting it much harder than it would be if everything had been shipped together, as a cohesive unit.
The constantly-changing landscape (things that worked in Beta 4 suddenly fail in Beta 5) makes it doubly-hard to learn the correct, idiomatic way to do things.
Question:
The above approach has been how I customize NSTableColumn since...I dunno...OS X Tiger. Is there some better approach I should use that more naturally fits with Swift Concurrency?
The community doesn't really like when people critique Swift. But if you think I'm just being mean, here's an app that had ZERO BUILD ISSUES in Xcode 16 Beta 4, the first time I tried to build it in Beta 5:
The class is already declared @MainActor. I have no idea what has happened or if these 146 new errors are actually my fault or are just the byproduct of some regression that will be fixed in Beta 6.
What I DO know is that this is a giant waste of my productivity and the unstable, unpredictable nature of the compiler/language is really damaging. So while my post may seem rant-y, please try to see this from the perspective of a consumer of the Swift language rather than someone who knows every line of the compiler: the real-world way this is rolling out is incredibly frustrating.
"It's a Beta"
Yea, I get it. Xcode 16 is a "beta". But here's the reality: developers have no choice but to use it because we need the new SDKs to get ahead of the changes coming to iOS/macOS this fall. If we waited until the Xcode GM ships, we'd have like 72 hours to adapt.
This doesn't feel like a beta. 146 sudden new errors between beta 4 and beta 5 makes this seem like an alpha. Something that should have baked internally at Apple for another year.
Thanks @hborla. I’m glad to hear this is just a regression. I’ve rolled back to beta 4 for now.
That said, it still feels like we’re building an airplane while falling out of the sky. NSObject is not what you’d call an “obscure” class, and shipping a massive regression like this (even in a beta) that produces hundreds of errors in developers’ apps and forces them to waste hours googling and troubleshooting (in vain, it turns out!) rather than writing code sends a signal that Apple doesn’t really have this thing under control.
Fundamentally, it seems like Apple shipped Swift 6 before it was fully baked. I know there’s no putting this genie back into the bottle and that the Swift team probably got a mandate from on high to ship, but…this is not a good experience.
To nitpick, Swift 6 hasn't been officially shipped yet, and to nitpick even more, technically NSObject is not part of the language (to my understanding anyway).
Honestly I understand your frustration and it is why I generally stay away from betas when it comes to production code. Even worse, I usually avoid Apple's x.0.0 versions especially for the operating systems and Xcode too. So yeah, I know how bad it can get.
I don’t think this would ever be possible to cover all the nuances of compile-time concurrency checks and only then roll it out. While here we as developers provide feedback allowing to polish it. That sounds like a reasonable approach. And bugs happen, that seems to be a forgivable thing in beta (Xcode itself breaks tons of things in betas).
Yet Swift (and Apple) don’t force anyone to jump straight to the Swift 6 strict checks, to be completely fair. You can use this checks as warnings, as one of the options to migrate gradually.
Sorry to hear about all the troubles. You really piqued my interest when you said that NSTableColumn was @unchecked Sendable and hard to use with MainActor stuff. Because I use NSTableColumn a bunch in my own (relatively) warning-free app, and I didn't recall having any problems at all in this area.
This is saying, in perhaps a weird way, that NSTableColumn does not conform to Sendable. However, it also isn't isolated to the MainActor, which I believe is a bug (FB14712121) and also the root problem here.
Luckly we do not have to just wait for that to get fixed. This is what dynamic isolation is for, and I believe it a much more appropriate solution than using Task. It looks like this:
What this is doing is telling the compiler "I realize you cannot see that this stuff is MainActor-isolated, but I promise it is". But, I completely agree that the need to use the capture list is not obvious. That's a tricky one.
I'd probably wrap up all of the headerCell get/set bodies in this, just for the convenience/safety.
There's a section on dynamic isolation in the migration guide too: Documentation
@mattie - that’s probably a much cleaner approach to handle this; I like it. I’ve used assumeIsolated in a few other contexts such as Actors with a custom executor, so I’m aware of it, but didn’t reach for it here.
That’s kind of my larger point: the CORRECT fix for all of this nonsense is to decorate NSTableColumn as @MainActor. I’ve filed 3 separate radars to request that for months. Crickets. (The only way I’ve ever successfully gotten things addressed is to email Ken directly, which I really dislike doing.)
There are gaps like this all over the SDKs that make adopting Swift Concurrency (which isn’t really optional now—nobody wants to write code that’s outdated from day one and will involve a major refactor) much, much harder than it should have been. You have to remember all the tricky bits and the escape hatches, like nonisolated(unsafe) and assumeIsolated and capture lists in Tasks that are implicitly capturing self, etc.
And I do understand that technically Swift and Apple are two separate things. But let’s be honest: Apple and Swift are married. The language hasn’t caught on in any significant scale outside of Apple’s platforms. (Which makes me sad, because I’d rather write Swift than PHP for my servers!)
At any rate, you’ve given me an answer to the original question and ranting about Swift isn’t a useful endeavor, so I’ll leave it there. I just tend to agree with Chris Lattner’s recent comments: Swift was a beautiful language that has become mired in complexity and hidden compiler magic.
@crontab - yep. See my comment about “it’s a beta” above.
For large, old projects that is a reasonable way to go. That's the whole point of not making Swift 6 default mode. There are cases, when even without Swift 6 having just @MainActor in one place triggers a chain of errors, but that's not that bad from my experience, and if project has been using complete checks for a while prior to that, they unlikely to get massive.
I have attempted recently a migration of one of the projects to Swift 6 in a radical way: turn on Swift 6 mode and went for smashing compiler errors. That didn't end well: the project hasn't been using complete checks at all, and some parts are extremely messed up in concurrency terms — like some type can have up to 5 different isolations logically, so you not only fight compiler, but have a good brain teaser for yourself keeping all this isolation relations.
I've ended up reverting back to 5.10 pretty fast. But that's was really insightful for me, because before I didn't know about such design issues, that now has to be addressed, and more likely will improve overall codebase. That's ain't going to be a fast migration, of course. I bet for a year of iterations over features to have this done for such project, since almost nobody have a luxury to stop development of a features and migrate to new language version for a month or two. Yet this is a reasonable for large projects and large breaking changes.
For me it is better to have new features and adopt, rather than hope it will come out one day in perfect shape (as they never will be perfect). As unrelated example, I've been waiting for wider capabilites of existentials and (not existed back in Swift 2 or 3) opaque types – and there have been too much waiting as for me. Finally SwiftUI pushed one small feature for its needs, and since than we've got so much new capabilites for generics!
@mattie Hilariously, to use MainActor.assumeIsolated in the getter, you have to conform NSTableColumn to @unchecked Sendable or you get an error about "sending self risks data races". Can't use a capture list of just [_headerCell] because then the error is "cannot assign to value, _headerCell is an immutable capture".
So to hack support for Swift Concurrency into this situation, I'm telling the compiler that a class that is explicitly NOT sendable is......sendable.
This is my point. Adopting Swift Concurrency in real apps right now is 90% fighting the compiler to resolve arcane error messages any way you can.
Ohh, I'm sorry. I misunderstood completely! My recommendation of using the assumeIsolated throughout the entire method was the issue, and was just a bad idea. A targeted fix like the code snippet I actally shared really is the only thing you can get away with here.
It's very tough problem because you are trying to work around two bugs simultaneously - the incorrect isolation and the inability to fix it in the subclass. You have to sometimes get pretty surgical when dealing with incorrectly-annotated dependencies.
Another tool you might want to keep in your back pocket is a dedicated module that you can just dump code into that isn't easily migrated right now. I hated doing this, and eventually got rid of it completely, but its easy and can be handy.
Going back to the 5 language mode is also a very reasonable option. I think this is particularly important if you don't have a pressing reason to migrate to 6 now and/or are getting frustrated. There's no reason to punish yourself!
I know this is not related to this particular issue, but having several iOS / MacOS projects built as part of a regression suite using the latest swift toolchain might give early warning on issues like these., therefore avoiding releases (yes even beta) with such impactful bugs.
I've lost a day or two to the cryptic new errors in beta 5 (and many more days earlier in the beta cycle, trying to learn the necessary bondage techniques for Swift 6 in general). I didn't have the luck or smarts to stumble onto this explicit capture trick. I very much wish I had, because a lot of time has gone down the drain while failing to discover it.
I note also that this doesn't appear to be discussed anywhere in the migration guide?
It seems as though beta 5 changed Task's init sig to use sending instead of @Sendable? I'm not yet fully understanding what that chance means, and to be honest I'd rather not have to care. It feels like Swift 6 concurrency is targeting a level of absolute safety/strictness that either the language or the frameworks can't yet comfortably support.
As has been said, if we want to or need to support various new features right out of the gate on launch day, we need to be working with the Xcode betas. And for new projects especially, it doesn't make sense to build without Swift 6 enabled. For those of us with those two project constraints, this situation isn't working.
Perhaps Xcode needs to be more proactive in suggesting appropriate fixes? These new beta 5 errors are at the extreme end of cryptic, but even for the less challenging strictness requirements, better inline fix suggestions / more natural error messages would go a long way.
Yep. Swift has become very unpleasant to use in real apps. And the trouble is that the team building Swift is entirely blind to that reality because they know the language's idiosyncrasies SO WELL that, for them, there is no friction.
And when someone sticks their head up to say, "Hey, this language is getting bloated and complex and full of invisible footguns," they get shot down as incompetent or a troll or accused of being "hurtful".
The idea of "progressive disclosure" was great—Swift starts simple and has powerful features when you're ready for them. But the problem is that building a basic real-world app at this point requires you to know all of the complexity. If you don't know all the tricks and footguns and how to decode the gibberish error messages from the compiler, you're left fighting the language instead of getting work done. And at that point, something incredibly dangerous for Apple happens: "Screw it, I'll just use Electron."
I did not shoot down your feedback. I stated to you clearly that I welcome constructive feedback, and that your constructive feedback here is valid. I also said that there are better ways to communicate your feedback than using hyperbolic language. Those two things are not mutually exclusive.
There is plenty of room for improvement in the clarity of compiler errors and making them actionable, mitigating false-positive reports of data-races, decreasing the annotation burden, and more.
I think fundamentally the problem is with older API's, like in your case too. Upgrading an existing codebase is almost always a pain, I agree with you, but starting a new project from scratch and if you can get away with only the newer API's can actually be a breeze (well, almost).
I'm not sure I've seen this happening on this forum tbh. Do you have examples?