StateObject alternative for iOS 13
Tags: swift, swiftui, ios
Published: 2020-10-12 04:08:37

The addition of [StateObject](https://developer.apple.com/documentation/swiftui/stateobject) in iOS 14 is wonderful, because it enables to easily connect a view-model (or controller or whatever name you want to give to that stateful piece of code that manages the view) without having to use any global object.

Before it, and even nowadays, I see many examples where an `ObservableObject` is created this way:

```swift
// Don't do this!!
@ObservedObject var foo = Foo()
```

The issue there is that a new instance of `Foo` is created every time the containing view is created. And I'm not talking about the appearance of that view in the screen. SwiftUI instantiates the view's struct every time it needs to re-render. The whole process is really fast because views are structs, so no heap allocations is involved. Still the variable declarations are invoked each time, so `Foo()` is called every time as well.

In iOS 14 the solution is to use `@StateObject`. The initializer value is captured with an `@autoclosure` so it doesn't get invoked every time. And the SwiftUI runtime will make sure that the value created the first time is available on every render of the view.

Now, here's the issue. Even though iOS 14 adoption is growing really fast, it's very unlikely that anyone wants to publish an app without iOS 13 compatibility yet.

So, here's an idea that I came up to workaround this problem. Since in SwiftUI 1.0 the only tool we have to conserve a value between renders is a `@State`, we must store the object within a variable declared with that attribute. But `@State` is only meant for simple types and thus it doesn't care much about observing changes in an `ObservableObject`. That means we need to replicate the value in another variable that is actually observed by the framework. Fortunately there is another attribute we could use: `@EnvironmentObject`. This attribute expects an instance of an `ObservableObject`. Just what we need!

The solution then keeps the value within the state of a "wrapper" view, and injects the object to be available to the wrapped view. Instead of writing this pattern over and over, I propose this reusable view:


```swift
struct Observer<Obs, Content>: View where Obs: ObservableObject, Content: View {
    @State private var obs: Obs?
    private var content: Content
    private var initializer: () -> Obs
    
    init(_ initializer: @autoclosure @escaping () -> Obs, @ViewBuilder content: () -> Content) {
        self.content = content()
        self.initializer = initializer
    }
    
    var body: some View {
        if let obs = obs {
            content.environmentObject(obs)
        } else {
            Color.clear.onAppear(perform: initialize)
        }
    }
    
    private func initialize() {
        obs = initializer()
    }
}
```

The whole purpose of `Color.clear` here is to make the first appearance to trigger the initialization of the `ObservableObject`, but once it's created the `content` is rendered with the object injected.

This can be easily used within other views like this:

```swift
Observer(MyModel()) { MyView() }
```

Although I would recommend to hide the details and declare a static value instead:


```swift
// Just use `MyView` within other views
let MyView = Observer(MyModel()) { MyViewContent() }
```

The view must be declared like this:

```swift
struct MyViewContent: View {
  @EnvironmentObject var model: MyModel
}
```

In case you need to pass extra arguments, it can be declared as a closure instead:

```swift
// Use as `MyView(arg1, arg2)` this time
let MyView = { arg1, arg2 in Observer(MyModel(arg)) { MyViewContent(arg: arg2) }
```

I hope you find this useful and helps calm the anxiety until iOS 13 is at thing of the past and we can use `StateObject` on every project.