Thanks!
For better or worse, that's not necessarily reflective of the situation we have here.
The "brave new world of Swift 6" is much less new than we might think: it's an improved formalization of the model that we already have — warts, backwards compatibility concerns, and all.
I personally find that the constraints you're running into make a lot more sense when I dig deeper into the promises that your types are making:
- By declaring a class which is neither
Sendable
nor actor-constrained, you are declaring: "this is a type which is safe to use from any individual actor or isolation domain, but not safe to pass between domains/threads/etc." - By constraining some of those types' properties to
@MainActor
, you are declaring: "this property is only safe to access from the main actor"- Note that this can already conflict with (1): you can create
Doc
on a non-main-actor, but in order to access any of its@MainActor
properties, you'd need to switch to the main actor to do so — except you cannot transfer the value to the main actor because it's notSendable
- Note that this can already conflict with (1): you can create
- Existing protocols like
Encodable
/Decodable
,Equatable
, etc., make no promises about domain isolation, and for backwards compatibility, cannot change. This is a two-way street:- You cannot constrain a protocol like
Equatable
to require@MainActor
, for instance, because existing types which are notSendable
or are already isolated to a different actor can no longer conform to it, which would break adopters - You also can't change
Equatable
to require@MainActor
because==
is already called from all sorts of isolation domains, so this would break callers
- You cannot constrain a protocol like
- By adopting
Codable
on these types, you are guaranteeing that it's safe to callinit(from:)
andencode(to:)
on your type from any isolation domain, because that's what the protocols promise — but this isn't true
How do you square away all of these constraints without changing (1), (2), (3), or (4)? Unfortunately, we can't change (3) without breaking the world, though we could introduce new protocols which make different promises that help you express these types more correctly (though this doesn't help you in the short term).
If you're intent on solving the problem in the short term, you're going to need to make some changes to (1), (2), or (4).
Swift 5 tells you: "hey, what you're doing is fundamentally broken, but we can't prevent you from doing it yet". Swift 6 tells you: "hey, what you're doing is fundamentally broken, and we're now preventing you from doing it".
This situation isn't any safer or more correct in Swift 5, but the language doesn't yet prevent you from making these incompatible promises. Swift 6 does.
I fully sympathize and empathize with the fact that none of this helps you solve your problems right now. Changing your existing types when they work succinctly is a HUGE pain, and adopting a ton of boilerplate for no "real benefit" right now feels like a losing deal.
But: consider the promises your types are currently making, and whether you're comfortable making them. All of this was never safe to do; the language just didn't help diagnose it, or try to prevent it.
Stick to Swift 5 for now if you can live with the warnings, and maybe mull this over. Improvements to isolation are constantly coming, and as @hborla says, there are always plans to make expressing your true intent safer, easier, and require less boilerplate.

Existing protocols like
Encodable
/Decodable
,Equatable
, etc., make no promises about domain isolation, and for backwards compatibility, cannot change. This is a two-way street:
- You cannot constrain a protocol like
Equatable
to require@MainActor
, for instance, because existing types which are notSendable
or are already isolated to a different actor can no longer conform to it, which would break adopters- You also can't change
Equatable
to require@MainActor
because==
is already called from all sorts of isolation domains, so this would break callers
I do get the logic that leads here - but that doesn't mean swift can't evolve to solve the same problems in the new world.
for example, we can well imagine AsyncCodable which solves the same problem as Codable - just in an async world. The conforming class only has to provide
required init(from decoder: Decoder) async throws
on some isolation domain. The encoder calls the method asynchronously and all is good. (and in fact - in most cases, then compiler can generate the implementation just like it does today for Codable)
Sure - it isn't Codable, but it solves the problem that Codable solves in an async world.

By adopting
Codable
on these types, you are guaranteeing that it's safe to callinit(from:)
andencode(to:)
on your type from any isolation domain, because that's what the protocols promise — but this isn't true
In my specific case, it is true that you can safely call
@MainActor required init(from decoder: Decoder) throws
and most classes/actors could provide that method correctly on some isolation domain.
The rest is implementation details right ;)
Same thing with AsyncEquatable
. You have to provide a method of checking equality in some isolation region. You can't check across regions. If you're checking from a different region, then the equality check is asynchronous.
I get that Swift 6 is different, and I get that there are necessary consequences. But it is NOT necessary to throw out support for serialising/deserializing or checking equality checking as 'things Swift makes easy'
The point here is the functionality. Swift 5 provides a bunch of super-useful developer functionality (like native support for encoding/decoding & equality checking with compiler-built implementations). I don't care exactly how Swift 6 provides that functionality - but I want it to be there.

but that doesn't mean swift can't evolve to solve the same problems in the new world.
Right! No one is saying that — and in fact, we want Swift to continue evolving, hence the continuous work to make things better.

we can well imagine AsyncCodable

Same thing with
AsyncEquatable
.
Absolutely! There's nothing at all preventing these from existing: it's just work. Someone needs to figure out the semantics required, put together a pitch, write the implementation, and get it through Swift Evolution. Not impossible, and in fact, likely quite necessary. But all of this takes time and effort that someone needs to put in. Until they do, we don't have these tools.

But it is NOT necessary to throw out support for serialising/deserializing or checking equality checking as 'things Swift makes easy'
To be clear: no one is saying "Swift 6 shouldn't make this possible, or shouldn't make this easy", but also, Swift 6 doesn't throw out support for anything. It's reflecting to you a situation that was already in place. It's just that:

I don't care exactly how Swift 6 provides that functionality - but I want it to be there.
getting to this point will require time and effort on someone else's behalf. If you can wait for that, then great; if you need solutions in the short term, you have the tools to put them in place. And if you want to be the person to put in the time and effort to make this easier for everyone else and write some pitches and implementations, you can do that too!

getting to this point will require time and effort on someone else's behalf.
This is entirely fair! I am asking for a lot of work from other people!
The counterpoint I'd put here is that I feel that the pressure to move to Swift 6 is building up.
I think I have the minimal settings for the transition in Xcode 16, and I'm inundated with warnings.
Swift 6 isn't ready for me and there doesn't seem to be a good way for me to stay off the treadmill until this kind of functionality is built out.
(and yes, of course, I can stay on Xcode 15 for a bit - but 16 is so much better in ways unrelated to Swift 6)

Stick to Swift 5 for now if you can live with the warnings, and maybe mull this over.
historically languages that required a lot of deep architectural refactoring to upgrade to new language versions have struggled with adoption of the new major version, and ended up having to support the old series for far longer than anticipated.
part of me wonders if it’s still too early to pull the “warnings to errors” trigger. by Swift Package Index’s census, only 42 percent of public Swift packages compile in Swift 6 mode, and that statistic already excludes some 4,000 packages that are considered “abandoned”.

I think I have the minimal settings for the transition in Xcode 16, and I'm inundated with warnings.
Swift 6 isn't ready for me and there doesn't seem to be a good way for me to stay off the treadmill until this kind of functionality is built out.
From your original post, it looks like you have explicit @MainActor
annotations in your code. That's why you're getting warnings and errors related to concurrency. Minimal checking means that the compiler won't tell you about concurrency issues caused by retroactively-added annotations in libraries that you're using if you're not actually explicitly trying to use concurrency in your project. As soon as you explicitly introduce an actor, a @MainActor
annotation, an async
function, etc, the compiler will tell you when you're using the annotations incorrectly. You can't turn off diagnostics for a feature that you are explicitly using in your code.

This works, but now gives me the warning
Main actor-isolated initializer 'init(from:)' cannot be used to satisfy nonisolated protocol requirement; this is an error in the Swift 6 language mode
Is there a solution here to have Codable and @MainActor properties?
I did not see this suggestion from a skim of the thread, but there is a way to defer the actor isolation checking to runtime in the case where you know the Codable
conformance is only used on the main actor, like this:
class Doc: @preconcurrency Codable {
var foo:String
@MainActor var title:String
@MainActor required public init(from decoder: Decoder) throws { ... }
@MainActor public func encode(to encoder: Encoder) throws { ... }
}
This feature is from SE-0423: Dynamic actor isolation enforcement from non-strict-concurrency contexts.

in the case where you know the
Codable
conformance is only used on the main actor
And to make this more explicit: for types like JSONEncoder
and JSONDecoder
, which don't make use of any concurrency features under the hood, all this would mean is ensuring that you only call the top-level encode(_:)
and decode(_:from:)
methods on those types in a main-actor-isolated context.

class Doc: @preconcurrency Codable { var foo:String @MainActor var title:String @MainActor required public init(from decoder: Decoder) throws { ... } @MainActor public func encode(to encoder: Encoder) throws { ... } }
I wasn't aware of that. It definitely cleans things up - Thank you.

Practically speaking, in your case, you might try removing the
@MainActor
annotation. Then – assuming you initialiseDoc
on the@MainActor
and it remains non-Sendable
– you'll be naturally constrained to the@MainActor
anyway.
I'm not 100% sure I understand where this landed in the end, but I think this was an under-rated suggestion.
Because the type is non-Sendable, I don't see how the split isolation was helpful. Unless maybe it was being used to work around other isolation mis-matches?
I am mildly surprised that a @MainActor
property on a non-Sendable, non-isolated type is even a legal thing to do. It means that your type can be constructed on any actor, but if you construct it on an actor other than the main actor some of the properties can never be accessed. This is a really weird thing to do and I have trouble imagining where it'd be better than having two different types.

Because the type is non-Sendable, I don't see how the split isolation was helpful. Unless maybe it was being used to work around other isolation mis-matches?
Split isolation isn't the key here.
The same issue exists if you make the whole class @MainActor (or some other actor)
The point is that as soon as you have some (or all) of your class constrained - then "Swift's native serialization mechanism" stops working.
To me - that's a hygene factor for an ergonomic Swift 6.

It means that your type can be constructed on any actor, but if you construct it on an actor other than the main actor some of the properties can never be accessed.
This seems perfectly reasonable to me!
Class is constructed in the background alongside some potentially expensive file access. Indeed -that's what I'm doing in my case. A bunch of files have to be loaded, ML analysed in order to construct the class.
I could do that work on the background - then hop back to main to initialise, but that doesn't gain me anything.
Then it's passed to the @MainActor so that it can be edited and displayed.
Background work (again, expensive calculations) just jump over to @MainActor when they need to write resulting state to vars which are read by the UI.
That flow requires a Sendable type.

Then it's passed to the @MainActor so that it can be edited and displayed.
This isn't valid so long as the type isn't Sendable
, which is part of the problem you're running into here.

I am mildly surprised that a
@MainActor
property on a non-Sendable, non-isolated type is even a legal thing to do.
I suppose with region-based isolation it is OK thing to have if you’ll use it in disconnected region. Because before that, having async functions on non-isolated, non-Sendable
type also didn’t make sense, as you wasn’t able to use such type practically nowhere, but with region-based isolation it now makes sense to have such.

Class is constructed in the background alongside some potentially expensive file access. Indeed -that's what I'm doing in my case. A bunch of files have to be loaded, ML analysed in order to construct the class.
I could do that work on the background - then hop back to main to initialise, but that doesn't gain me anything.
Then it's passed to the @MainActor so that it can be edited and displayed
Maybe region based isolation plus transferring
closures will be the ointment here. You'll still need to remove the @MainActor
annotations on the non isolated protocol conformances, however.
This post gives a good summary:
As a quick example of this in action, consider the following:
class NonSendable { } extension Task where Failure == Never { // Modified Task.init() converting it to a non-Sendable transferring implementation. init(x: (), @_inheritActorContext @_implicitSelfCapture operation: transferring @escaping () async -> Success) { /* ... */ } } func spawn() { let ns = NonSendable() Task.init(x: ()) { _ = ns } }
IIUC, this should allow you to construct your @MainActor
destined class off the main actor, then pass it to the @MainActor
once constructed.
Yes, that's right! This is a mis-match between the protocol and type isolation.
But here's what I'm getting at. Why do you have some properties MainActor? It sounds like this class is being constructed in the background, transferred (via RBI or an explicit sending
return value) to the MainActor, where it then will remain. This flow can be done without any annotations. And if we could remove the isolation, this specific problem would be solved.

IIUC, this should allow you to construct your
@MainActor
destined class off the main actor, then pass it to the@MainActor
once constructed.
But as long as class itself contains only sync methods. If you pass non-Sendable
to, say, view controller to be stored as a property then, and try to call async method on it, then lack of isolation would still be an issue.
Non-Sendables + async methods was definitely a major problem with Swift 5, but SE-0420 provides a source-compatible (but 6-only) way to potentially make this arrangement work.