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 DispatchQueuesetc
- implement Codeableconformance 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)
	}