Reputation: 1524
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