Redux-style systems are perfect for SwiftUI, but the ability to store both the data for a given screen and its view-route as associated enum cases causes view refreshes on every update, even if the outermost view observing the case is only interested in its route.
E.g. with a view route that looks like this:
enum Navigation: Hashable {
case firstScreen(FirstScreenData)
case secondScreen(SecondScreenData)
}
And a navigation router that looks like this:
struct NavigationRouter: View {
@StateBinding var navigation = GlobalState.shared.lensing(\.navigation)
var body: some View {
switch navigation {
case .firstScreen:
FirstScreen()
case .secondScreen:
SecondView()
}
}
}
NavigationRouter will refresh even if the case hasn't changed.
It is of course possible to store the navigation route as an enum without associated cases and to store the data for each screen in a separate location, but that leaves us with a system that needs to be kept manually in sync, which can be a security concern if any of your screens contain sensitive data that is required to be forgotten once the screen has been navigated away from.
What I'm proposing is very similar to CaseIterable
in nature. For the sake of reference, let's give it the preliminary name of UnassociatedCases
.
An example of what applying UnassociatedCases
would do:
extension Navigation: UnassociatedCases {
// Compiler generated
enum Unassociated: Hashable, CaseIterable {
case firstScreen
case secondScreen
}
// Compiler generated
var unassociatedCase: Unassociated {
switch self {
case .firstScreen:
return .firstScreen
case .secondScreen:
return .secondScreen
}
}
// Compiler generated
func isCase(_ unassociatedCase: Unassociated) -> Bool {
switch (self, unassociatedCase) {
case (.firstScreen, .firstScreen):
return true
case (.secondScreen, .secondScreen):
return true
default:
return false
}
}
}
With these improvements to enums, we could then use GlobalState.shared.lensing(\.navigation. unassociatedCase)
and views would no longer refresh unless the route itself changed.
I'm sure there's many other places such improvements would be useful, I'm just speaking to my personal headaches here, which required me to write a workaround to achieve this functionality using Sourcery.
Problems considered:
In the event that two cases share the same name, a differentiating element would need to be added. The most favourable option would be to include the names of the associated values.
E.g.
case firstScreen_data_counter