Using @StateObject in iOS 14.0 while supporting iOS 13.0

Viewed 873

I need help finding the best way to support the new @StateObject in iOS 14.0 and still supporting some alternative in iOS 13.0. Admittedly, I do not know what is the best approach in iOS 13.0. Below is what I currently have.

Does anyone have ideas on a better approach?

struct HomeView: View {
    let viewModel: HomeViewModel

    var body: some View {
        if #available(iOS 14, *) {
            HomeViewWrapper(viewModel: viewModel)
        } else {
            CompatibleHomeViewWrapper(viewModel: viewModel)
        }
    }
}

@available(iOS 14, *)
private struct HomeViewWrapper: View {
    @StateObject var viewModel: HomeViewModel

    var body: some View {
        CompatibleHomeView(viewModel: viewModel)
    }
}

private struct CompatibleHomeViewWrapper: View {
    @State var viewModel: HomeViewModel

    var body: some View {
        CompatibleHomeView(viewModel: viewModel)
    }
}

struct CompatibleHomeView: View {
    @ObservedObject var viewModel: HomeViewModel

    var body: some View {
        Text(viewModel.someRandomName)
    }
}
1 Answers

You can get the @StateObject behaviour by wrapping a custom propertyWrapper around @State and @ObservedObject like so:

import Combine
import PublishedObject // https://github.com/Amzd/PublishedObject

/// A property wrapper type that instantiates an observable object.
@propertyWrapper
public struct StateObject<ObjectType: ObservableObject>
where ObjectType.ObjectWillChangePublisher == ObservableObjectPublisher {
    
    /// Wrapper that helps with initialising without actually having an ObservableObject yet
    private class ObservedObjectWrapper: ObservableObject {
        @PublishedObject var wrappedObject: ObjectType? = nil
        init() {}
    }
    
    private var thunk: () -> ObjectType
    @ObservedObject private var observedObject = ObservedObjectWrapper()
    @State private var state = ObservedObjectWrapper()
    
    public var wrappedValue: ObjectType {
        if state.wrappedObject == nil {
            // There is no State yet so we need to initialise the object
            state.wrappedObject = thunk()
        }
        if observedObject.wrappedObject == nil {
            // Retrieve the object from State and observe it in ObservedObject
            observedObject.wrappedObject = state.wrappedObject
        }
        return state.wrappedObject!
    }
    
    public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType) {
        self.thunk = thunk
    }
}

I use this myself too so I will keep it updated at: https://gist.github.com/Amzd/8f0d4d94fcbb6c9548e7cf0c1493eaff


Note: The most upvoted comment was that ObservedObject was very similar, which is just not true at all.

StateObject retains the object between view inits AND relays object changes to the view through willChangeObserver.

ObservedObject only relays changes to the view BUT if you create it in the view init, every time the parent view changes the object will be initialised again (losing your state).

This explanation is very crude and there is better out there please read up on it as it is an impactful part of SwiftUI.

Related