I'm trying to figure out the correct structure for a macOS document app using SwiftUI and Swift 5.5 concurrency features.
I want to demonstrate updating a document's data asynchronously, in a thread safe manner, with the ability to read / write the data to a file, also thread-safe and in the background. Yet I am struggling to:
- write clean code - some of it looks inelegant at best, more like clunky, compared to my prior apps which used
DispatchQueues
etc - implement
Codeable
conformance for an actor
I'm seeking ideas, corrections and advice on how to improve on this. I've posted the full code over at GitHub, as I will only highlight some particular elements here. This is a minimum viable app, just for proof-of-concept purposes.
The app
The app displays a list of Records
with a button to add more. It should be able to save and reload the list from a file.
Current approach / design
I've chosen the ReferenceFileDocument
protocol for the Document
type, as this is what I would use in a future app which has a more complex data structure. (i.e. I'm not planning on using a pure set of structs
to hold a documents' data)
Document
has a property content
of type RecordsModelView
representing the top-level data structure.
RecordsModelView
is annotated with @MainActor
to ensure any updates it receives will be processed on the main thread.
RecordsModelView
has a property of type RecordsModel
. This is an actor ensuring read/write of its array of Records
are thread safe, but not coordinated via the MainActor for efficiency.
The app assumes that the func to add an item takes a long time, and hence runs it from with a Task
. Although not demonstrated here, I am also making the assumption that addRecord
maybe called from multiple background threads, so needs to be thread safe, hence the use of an actor
.
The code compiles and runs allowing new items to be added to the list but...
Issues
Firstly, I can't annotate Document
with @MainActor
- generates compiler errors I cannot resolve. If I could I think it might solve some of my issues...
Secondly, I therefore have a clunky way for Document
to initialise its content property (which also has to be optional to make it work). This looks nasty, and has the knock on effect of needing to unwrap it everywhere it is referenced:
final class Document: ReferenceFileDocument {
@Published var content: RecordsViewModel?
init() {
Task { await MainActor.run { self.content = RecordsViewModel() } }
}
// Other code here
}
Finally, I can't get the RecordsModel
to conform to Encodable
. I've tried making encode(to encoder: Encoder)
async, but this does not resolve the issue. At present, therefore RecordsModel
is just conformed to Decodable
.
func encode(to encoder: Encoder) async throws { // <-- Actor-isolated instance method 'encode(to:)' cannot be used to satisfy a protocol requirement
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(records, forKey: .records)
}