Just what I've been saying in https://forums.swift.org/t/10-years-on-what-would-you-change-about-swift/ before the thread has been closed… cough
I think you also need to generate an object graph from values when the app starts up. In real app, where values may have complex relation, there isn't a general way to build up that object graph and the code will need to make assumptions on the relation.
That's what I had in my mind when I read the original post. But I have always wondered which of the two approaches (putting values in Observable class directly vs using a separate struct to hold them) is more popular in real world development? I think most demo projects by Apple seemed to use the first approach.
Also, while I don't think it matters in practice, your approach is worse in performance because all fields are put in a single value which are depended by various views.
You're not seeing my whole data model here - but please assume that I have a sensible representation of my problem.
I have been building apps for mobile as a full-time professional since well before the iPhone existed, so I'm broadly competent at this.
Even if I'm not - the issue I describe exists for other competent folks!
So, now I have my sensible data model, most of what I need to do with it is power UI. @Observable is fantastic for this.
- mark the class @Observable
- ensure that anything which changes during UI interaction, and which is read by the UI is @MainActor.
So - now I have some classes, in which some of the properties are @MainActor
Now I want to serialize that model for storage.
Important: This is really all I'm asking about. How do I serialize a model in Swift6 when that model has some @MainActor properties.
In Swift5, then answer was fabulously simple.
- Define CodingKeys for the properties you want to save
- Mark class Codable
- (optionally) override encode/decode methods for finesse
I Swift6, I still have a good representation of my problem space.
I still have a fantastic way to let those classes drive UI with minimal code.
I no longer have a way to serialize that class.
So - it seems that to serialize my data, I would need to create an entire mirror representation and methods to map in two directions. Simply to convert my data to JSON.
That may be necessary to avoid compiler warnings, but it creates a massive level of otherwise unnecessary duplication and boilerplate.
And again - it isn't inherently necessary. There is no reason why swift couldn't provide a way to serialize models with a protocol that calls @MainActor rather than non-isolated methods.
Codable
includes both Encodable
and Decodable
!
I assure you my real app (and many others) works great building the objects back from the JSON!
required public init(from decoder: Decoder) throws
allows you to customise the initialisation where necessary.
a) As you say - real world matters in practice. Performance costs here are not an issue for me.
b) @Observable is very efficient here - it has a lovely design which tracks directly which properties are actually in use, and only triggers updates to the UI when those are changed
FWIW I was struggling with this idea the other day, and I realised something that hadn't clicked for me.
Perhaps this same idea could help you.
If you have some @MainActor
isolated types - for example some View types or ViewControllers – you have the freedom to pass around non-isolated types between them without constraining those types to the @MainActor
also. Assuming these non-isolated types aren't Sendable, they are naturally restricted to the concurrency context they were created in.
Practically speaking, in your case, you might try removing the @MainActor
annotation. Then – assuming you initialise Doc
on the @MainActor
and it remains non-Sendable
– you'll be naturally constrained to the @MainActor
anyway.
Of course, how feasible this is depends on your use case, any async operations on Doc
may need to be reworked, and so depending on your implementation could be a non-trivial effort.
The brute force approach to this, and if you really need to pass around Doc
around between concurrency contexts, might be a ViewModel wrapper for Doc.
Firstly, remove the @MainActor
annotation on Doc
.
Then create something like a DocViewModel
@MainActor final class DocViewModel {
let doc: Doc
// ...
init(_ doc: Doc) {
self.doc = doc
}
// etc...
}
This allows you to add all the necessary non-isolated conformances to Doc
, pass it between concurrency contexts, and still keep access constrained to the @MainActor
. However, depending on your app, there may be cleaner ways to do it.
Hi @ConfusedVorlon My comments were about @Dan_Stenmark's approach and @vns's approach, respectively.
I’m not saying you don’t know your problem space. I simply see code and it looks highly complicated to me with many mixed responsibilities and isolation domains. That’s what I am looking at.
But more importantly it prevents you from achieving functionality. If that’s a case in my code, I assume that my approach is wrong and I need to change it. If design decisions make it impossible to implement, that’s a decision is subject to change.
Several comments here saying that thisis not the droids you are looking for :) I believe there is a need of change of angle you are looking at the problem here. Not to bend Swift.
Separate or not depends on a context, there might be cases when separate properties are better, and when single struct that you are able to update at once is actually better.
Yup - I can de-isolate my model, and introduce separate view models which enforce concurrency, initialise from the source and (at some point) write changes back.
This is all solvable - but the ergonomics are terrible.
And it feels like a fundamental disconnect in the language design.
I have some properties which drive UI, so they should only change on @MainActor.
Modern Concurrency provides a fantastic mechanism to enforce that - simply mark the property @MainActor.
So - why would we be talking about design approaches to allow the data to be non-isolated whilst limiting the @MainActor requirements to adaptors of some sort?
I'm happy with marking the property @MainActor! That's correct, and a minimal way to enforce it!
but now "Swift's native serialization method" doesn't work...
Not in Swift 5!
-> Not to bend Swift 6 !
and yes, it seems that Swift 6 has decided that the 'Native Serialisation method' should only support non-isolated classes. That seems like a great shame to me.
I hope that will change before I'm forced onto Swift 6.
(deleted bad suggestion)
Absolutely - and indeed, I use that approach in this specific app for some cases.
What seems bizarre to me is the idea that the language would force me to always use this approach (If I also want to use modern concurrency).
If you follow that idea to its logical conclusion, then we'd need to mark everything as @MainActor
. Arrays, Ints, Strings – the lot.
It does require a bit of mental remodelling, but the the best example I can think of is the idea that was touted one year at WWDC of concurrency islands.
There are boundary types that mark the edges or the borders of concurrency islands by being marked as @MainActor
or actors.
Then, within that island you can play with all the non-isolated types freely. They can't escape the island. Unless they are marked as an actor type or Sendable.
FWIW I think the non-isolated
label is a little confusing as semantically, non-Sendable
, non-isolated types, are very much isolated to the concurrency context within which they were created.
Interesting!
It allows a single class to compile without warnings - but breaks Codable everywhere else!
But that’s the point: if change makes your decision impossible to implement, it is time to review the decision. Changes will be there all the time. If not Swift, Apple could’ve made it (as it did with SwiftUI, and many — myself including — had to rethink approaches). Some rethink completely how they structure apps in Swift 6. Some find out that their concurrency safe code is actually has huge gaps in design. We just need be aware of the problem and think “OK, probably my previous assumptions isn’t working anymore; let’s find new system to operate within”. I believe that the design that fails to changes highly has to be rethinked, with no regards to whether I who made it was right or wrong.
I'll wait a few years and hope that we get a native serialization system which supports concurrency before I'm forced to shift!
BTW - Equatable
has the same issue!
This is even more nuts!
Now, I get that concurrency necessarily changes how equality checking would have to work.
- perhaps it has to be asynchronous
- perhaps it would have to only operate on some defined (per class?) isolation.
- presumably actor equality would operate on the isolation of the actor
But simply saying "We're in the brave new world of Swift 6, Build your own equality checking implementations for anything isolated" seems half-assed.
I don't think anyone is saying that.
They're saying use actors and GAITs (@MainActor
, etc.) as boundary types only, otherwise you're going to have a really rough time.
What is RBI? I have not seen that acronym before (in this context).
Region-based isolation, I'd assume.