I am trying to create a full SwiftUI Document app (using swift 6, as it matters).

Apple doc says that the Document init(configuration:)and fileWrapper(configuration:)are called on a background thread - and I checked they are.

However, the Document holds a model which is @ObservableObjecttherefore bound to @MainActor.

To write to a file, because fileWrapper(configuration:)is called directly without any chance to serialise the modelin the MainActorI used a function that goes back to the main actor, serialises the data, and then comes back to the background thread for saving.

Clearly cumbersome, not ideal, but works fine.

Here is the code of that function:

nonisolated
public func runOnMainAndWait<T: Sendable>(_ operation: @escaping @Sendable @MainActor () -> T) -> T {
    //  We are in the caller execution context, create a semaphore to wait for
    let semaphore = DispatchSemaphore(value: 0)
    
    //  Declare the variable to store the  result
    var result: T! = nil
    
    //  Create a task that will execute in the caller execution context, whenever the caller
    //  suspends itself, a bit later on.
    Task {
        result = await MainActor.run {
            //  Execute the operation to convert MainActor bounded
            return operation()
        }
        
        //  We're back into caller execution context, we can manipulate the semaphore, and
        //  unblock the second part of the method
        semaphore.signal()
    }
    
    //  Here, we suspend the caller, so the above task will execute, storing its computation in
    //  `result`, and unblock us.
    semaphore.wait()
    
    //  Return the result
    return result
}

To read a file, however, the things get even more complex. I can't just read the file data in the background, and then use runOnMainAndWaitbecause I need a Factory... which I can't pass as a parameter...

So, I store the file data in the document, and transform it into modelon the main actor later on when I get a chance... which is the creation of the main view.

Surely, this is by far too complex and cumbersome. There must be something I missed.

What is the pattern to use for SwiftUI Document App ?

2 Replies 2

I believe you went wrong when you put the ObservableObject in the document struct. To fix, simply move the properties from the object into the document struct and move the funcs (change func to mutating func). The reason the document needs to be a value and not an object is so it can be simply copied to be used on a background thread.

For those that really do need a reference type as the document, e.g. the document itself does asyncronus work or has complicated relations there is ReferenceFileDocument however then you need to rely on the UndoManager for marking the document dirty instead of it being automatic (it can simply compare document values).

@malhal: To fix, simply move the properties from the object into the document struct

Well, that is far from being that simple.

This makes the very strong assumption that the model can be transformed in a bunch of value types. Let's take the example of a graph; any node can refer to several nodes, and those nodes can even refer back to the first node... you need reference types for that (and that is what there're for).

This is exactly what Apple does in its SwiftUI document example : it over-simplifies the model to... a single String, which of course doesn't stand real life...

You can also inherit types from packages you don't own.

So you can't really rule out ObservableObject reference types (classes) in your model.

This said, your reference to ReferenceFileDocument is very interesting.

Apple documentation says:

(ReferenceFileDocument protocol)
If you store your document as a value type — like a structure — use FileDocument instead.

(FileDocument protocol)
If you store your document as a reference type — like a class — use ReferenceFileDocument instead.

I thought Apple was talking about the "document" (the type that represents the document), not the "model" (the document is the link between the file and the model). But maybe, I am mis-understanding, or having a too strong interpretation.

If we say "reference type for model" => ReferenceFileDocument (and "value type for model" => FileDocument), then we benefit from the func snapshot(contentType: UTType) throws -> Self.Snapshotmethod.

This method is called in the MainActor context, and let the app convert the model to a Sendable data version which will be passed to func fileWrapper(snapshot: Self.Snapshot, configuration: Self.WriteConfiguration) throws -> FileWrapper.

Basically, this is doing exactly what my runOnMainAndWaitmethod is doing (most probably in a better and out-of-the box way). With, as you very correctly mention, the drawback of having to rely on Undo Manager.

So that solves the write to a file, but we still have the read a file case, which I don't see that ReferenceFileDocumentsolves...

Your Reply

By clicking “Post Your Reply”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.