3

I'm trying to place a StateObject actor whose initializer is async in an App, but I can't find a way to do that.

Let's say I have this actor:

actor Foo: ObservableObject {
    init() async {
        // ...
    }
}

This results in the titular error:

import SwiftUI

struct MyApp: App {
    @StateObject
    var foo = await Foo() //  'async' call cannot occur in a property initializer

    var body: some Scene {
        // ...
    }
}

This results in a similar error:

import SwiftUI

struct MyApp: App {
    @StateObject
    var foo: Foo

    init() async {
        _foo = .init(wrappedValue: await Foo()) //  'async' call in a function that does not support concurrency
    }

    var body: some Scene {
        // ...
    }
}

And even these won't work:

import SwiftUI

struct MyApp: App {
    @StateObject
    var foo: Foo

    init() async {
        Task {
            self._foo = .init(wrappedValue: await Foo()) //  Mutation of captured parameter 'self' in concurrently-executing code
        }
    }

    var body: some Scene {
        // ...
    }
}
import SwiftUI

struct MyApp: App {
    @StateObject
    var foo: Foo

    init() async {
        Task { [self] in
            self._foo = .init(wrappedValue: await Foo()) //  Cannot assign to property: 'self' is an immutable capture
        }
    }

    var body: some Scene {
        // ...
    }
}

It seems no matter what I do, I cannot have a Foo be a member of MyApp. What am I missing here? Surely this is possible.

I run into the same problems with SwiftUI Views too, so any advice that works for both Views and Apps would be spectacular!

Ky -
  • 30,724
  • 51
  • 192
  • 308
  • Does this answer your question? [Initialize app with an Async function | SwiftUI](https://stackoverflow.com/questions/68668770/initialize-app-with-an-async-function-swiftui) – lorem ipsum Aug 22 '21 at 23:38
  • There might be a technical reason, why you can't use an asynchronous function to initialise the property wrapper, but on a semantic level: what is your intended behaviour when asynchronously initialising a StateObject? Showing a loading indicator or a blank screen doesn't work - you don't even have any state that would tell this the view when the view is rendered. So, does this make sense at all? It makes sense to treat a View as _synchronous_ (pure) function. – CouchDeveloper Aug 23 '21 at 13:39
  • @loremipsum no, because the answers there are addressed as unusable in my last 2 snippets. Thank you, though! – Ky - Aug 25 '21 at 13:59
  • @CouchDeveloper I want the app to initialize, while in the background it spins up some services. I would love a way to reflect the spin-up with the UI, but honestly I'd be okay if I can block the main thread to wait for that, only showing the UI once they're done spinning up; that'll be better than what I have now which is just compiler errors – Ky - Aug 25 '21 at 14:02
  • @KyLeggiero you should give it another look. What your snippets show go against the what Apple intends. 1. SwiftUI can reinitialize the `View` at any time, your state object will be [re initialized whenever SwiftUI decides](https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app) because you are setting it in the `init` vs directly in the wrapper, also it can cause leaks. 2. based on your comment above this is the wrong approach too because the whole point of `async` is to not block the main thread. – lorem ipsum Aug 25 '21 at 14:46
  • @loremipsum 1: I tried setting it directly in the wrapper (see my 2nd snippet), but then I get the error in the title of this post. 2: I know, I'm just saying that if that's the only possibility, then that's what I'll resort to; I'm hoping there can be a way to do this, though – Ky - Aug 25 '21 at 15:08
  • Do you control the source code of the actor? Can you modify the actor to provide a synchronous initializer? – Soumya Mahunt Aug 20 '22 at 14:02
  • @SoumyaMahunt It's my actor, but its initialization necessarily must be async due to the nature and purpose of the object – Ky - Aug 22 '22 at 22:33
  • @Ky. can you provide why this initializer has to be async? Can't you just use a synchronous initializer and wrap your async work in top-level task? – Soumya Mahunt Aug 23 '22 at 04:23
  • @SoumyaMahunt Because this is initializing an `actor`, and not all its fields are `nonisolated` – Ky - Aug 24 '22 at 22:24

1 Answers1

3

I would like to give you an idea how one can solve this problem:

When you want to initialise a certain State value which takes a prolonged time and thus you may execute it asynchronously, you can use the following pattern:

You start off with your "State", which is initially not initialised, say undefined. One way to model this is shown below:

struct AppState<Value> {
    enum State {
        case undefined
        case initialising
        case idle(Value)
        case mutating(Value)
    }
    var state: State = .undefined
}

Obviously, your initial state of AppState equals undefined.

Now, let's assume, there is something which initialises it (it will be your View Model later). When it's about being initialised, its state transitions from undefined to initialising.

When it completes, its state transitions from initialising to idle.

Note, that there's nothing "asynchronous" here - all state transitions happen "instantaneously".

You may also notice, that there is some additional value, only occurring in state idle and mutating. This is called "extended state value".

I also added a "mutating" state indicating, that you can do mutations on the AppState and reflect this in your variable appState accordingly.

You may already sense that this kind of modelling leads us to some sort of "Finite State Machine" (FSM), which takes this state and performs mutations when receiving "inputs". One of them is "initialisation".

Your ViewModel may now implement the necessary missing parts of this kind of Finite State Machine:

A set of State (like above), a set of Inputs (an Enum), a set of Outputs (an Enum), a transition function (pure) and an output function (pure), and a function which transform AppState to some ViewState which gets rendered in the View. User actions will be wired up via callbacks which eventually gets routed into the system as Input values. Output values trigger "side effects", which exactly is your asynchronous "App State initialisation" function. Side effects return Events, which again get routed into the Input of the system.

CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
  • I'm familiar with a FSM and enjoy using them in my apps. How does this help me with `actor`s and `async`/`await`? – Ky - Aug 25 '21 at 16:43
  • A FSM is an actor. Also note, the crucial idea is, that you _always_ (at any time) you have a _defined_ state. Thus, this state can be rendered accordingly - at any time. The state machine hides the fact, that there are async functions - actually, "side effects". – CouchDeveloper Aug 25 '21 at 16:44
  • I'm not sure I follow; you don't mention `actor`s in your answer. Can you update your answer to show how I can use this solution to have my `actor Foo` initialized and usable in SwiftUI in a way that I can pass it down the view hierarchy so that its fields can be read and its members called? – Ky - Aug 25 '21 at 16:46
  • 1
    Whether you use "Swift actors" or whether you ensure the thread-safety with using Combine (using receive(on:) and subscribe(on:)) does not really matter. This is an implementation detail, how you implement the FSM - or the ViewModel. Basically, "async init" does not make sense. You can use Actors to modify its state after synchronous init. You _need_ an initial state, just like a FSM requires. – CouchDeveloper Aug 25 '21 at 16:49
  • I'm not sure what you're saying. I'm trying to use an `actor` in a SwiftUI app, and something must initialize that `actor` at some point, but it's unclear to me what can, nor when. In the past I've used GCD, but for this project I'm trying to learn how to use `actor`s. Can you update your answer to include how to use an `actor` in a SwiftUI app, as my question asks? – Ky - Aug 27 '21 at 05:20