esbenr
esbenr

Reputation: 1524

How to update view from @ObservableObject without using it?

I have an ObservableObject that fetches feature toggles async from our API. The feature toggle is, amongst other things, used to switch between views in my Tab View.

The selected tab view doesen't reevaluate when feature toggles are done loading.

FeatureToggle is an enum: string.

And are fetched like this:

class FeatureToggleController: ObservableObject {
    @Published var featureToggles: [FeatureToggle] = []
    
    private var networkController: NetworkController = Container.networkController
    private var errorHandler = Container.errorHandler
    
    func isFeatureToggleOn(_ featureToggle: FeatureToggle) -> Bool {
        return featureToggles.contains(featureToggle)
    }
    
    func fetchFeatureToggles() async -> Void {
        let result: Result<[String], OperatorAppError> = await networkController.getUrlDataResult(.featureToggles)
        switch result {
        case .success(let fts):
            DispatchQueue.main.async {
                self.featureToggles = fts.compactMap { FeatureToggle(rawValue:$0) }
            }
        case .failure(let error):
            errorHandler.handleError(title: "Error while fetching feature toggles", error: error)
        }
    }
}

My tab view looks like this:

    var body: some View {
        TabView(selection: selectedTab) {
            ForEach(Tab.allCases) { tab in
                tab.destination
                    .tag(tab as Tab?)
                    .tabItem { tab.getIcon()  }
                    
            }
        }
    }

In my TabView I have this switch:

enum Tab: Hashable, Identifiable, CaseIterable {
    case otherTabs
    case tabInQuestion
    
    var id: Tab { self }
}

extension Tab {
    @ViewBuilder
    var destination: some View {
        switch self {
        case .otherTabs:
            OtherView()
        case .tabInQuestion:
            if (Container.featureToggleController.isFeatureToggleOn(.isFeature1Enabled)) 
            {
                FeatureToggledView()
            } else {
                NormalView()
            }
        }
    }
}

The FeatureToggleController is declared as singleton and injected via a simple poor mans injection / container:

class Container {
    
    // Mark: - Single instance members
    public static var networkController: NetworkController = NetworkControllerImplementation()
    public static var featureToggleController: FeatureToggleController = FeatureToggleController()
    // Mark: - Multiple instance members    
    public static var errorHandler: ErrorHandler {
        ErrorHandlerImplementation(
            logController: logController,
            alertController: alertController)
    }
}

After the feature toggles finish loading, this tab view is not re-evaluated. Since the isFeatureToggledOn(...) is using the published property on the observable object FeatureToggleController, I would assume so.

But maybe I'm loosing the binding somewhere?

Update: So, the observable object FeatureToggleController is instantiated by my poor mans injection Container and there is no state held in any view. This is why there is not subscribed to the published list.

Holding a state object in the enum extension is not possible, so I added a View to do the view switching and accessed the published list directly instead of a wrapper function:

struct FeatureToggledSwitchView: View {
    @StateObject private var featureToggleController = Container.featureToggleController
    
    var body: some View {
        if (featureToggleController.featureToggles.contains(.isFeature1Enabled)) {
            FeatureToggledView()
        } else {
            NormalView()
        }
    }
}

Instantiating this in my Tab's destination extension triggers re-evaluation of the whole FeatureToggledSwitchView content as expected.

extension Tab {
    @ViewBuilder
    var destination: some View {
        switch self {
        case .otherTabs:
            OtherView()
        case .tabInQuestion:
            FeatureToggledSwitchView()
        }
    }
}

Which makes perfectly sense since the feature toggle controller state is now anchored in a view.

Only problem left is that i get a navigation error, but I guess that's unrelated to this and related to mu NavigationStack implementation.

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Layout requested for visible navigation bar, <SwiftUI.UIKitNavigationBar: 0x105b558e0; baseClass = UINavigationBar; frame = (0 53.6667; 393 96); opaque = NO; autoresize = W; layer = <CALayer: 0x6000002db5a0>> delegate=0x10795be00, when the top item belongs to a different navigation bar. topItem = <UINavigationItem: 0x105b64c90> title='Feature Toggled View' style=navigator leftItemsSupplementBackButton trailingItemGroups=0x600000cbd200 largeTitleDisplayMode=always, navigation bar = <SwiftUI.UIKitNavigationBar: 0x105b6cb50; baseClass = UINavigationBar; frame = (0 0; 393 96); opaque = NO; autoresize = W; layer = <CALayer: 0x6000002bab40>> delegate=0x10782b800, possibly from a client attempt to nest wrapped navigation controllers.'

Upvotes: 0

Views: 79

Answers (0)

Related Questions