bdkjones
(Bryan)
1
Context
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?
1 Like
bdkjones
(Bryan)
2
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.
</rant>
1 Like
hborla
(Holly Borla)
3
I hear your feedback, but this particular issue:
is a regression in Xcode 16 Beta 5 that's fixed by [6.0][Concurrency] Implement a narrow carve out in the isolation override checking for `NSObject.init()`. by hborla Ā· Pull Request #75749 Ā· swiftlang/swift Ā· GitHub.
9 Likes
bdkjones
(Bryan)
4
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.
1 Like
crontab
(Hovik Melikyan)
5
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.
But still... this is beta! 
1 Like
vns
6
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.
mattie
7
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.
Xcode 16b5 regressions aside...
The header in AppKit looks like this:
@available(*, unavailable)
extension NSTableColumn : @unchecked Sendable {
}
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:
// snip
MainActor.assumeIsolated { { [_headerCell] in
_headerCell!.image = image
}
// snip
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
1 Like
Yes, alas. The extent of this phenomenon sometimes borders on groupthink. While criticism is actually intended to improve things.
1 Like
bdkjones
(Bryan)
9
@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.
2 Likes
bdkjones
(Bryan)
10
@vns I reverted to Swift 5 language mode, which cleared many but not all of the new errors. I had to rollback to beta 4 in order to get working again.
vns
11
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!
1 Like
bdkjones
(Bryan)
12
@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.
1 Like
mattie
13
Ohh Iām very sorry! I could have sworn I tested this. Iāll give it another go a little later on.
mattie
14
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!
3 Likes