I'm currently working on a SwiftUI component whose state needs to be reset when one of its properties changes. However, to keep the component as generic as possible I want that property to be generic, i.e., let someProperty: any Equatable
.
My initial attempt was:
struct MyMenu: View {
let items: [MenuItem]
let resetOnChange: any Equatable
var body: some View {
ScrollViewReader { proxy in
ScrollView {
// scroll view content
}
.onChange(of: resetOnChange) { _ {
if let first = items.first {
proxy.scrollTo(first, anchor: .leading)
}
}
}
}
}
However, this approach seems to cause an issue with the View
not knowing a concrete type. Ultimately resulting in an error on the line of ScrollViewReader
, "Type 'any View' cannot conform to 'View'"
An approach that did work, was to define a generic in the component:
struct MyMenu<ResetParameter: Equatable>: View {
....
let resetOnChange: ResetParameter
...
}
// Ex. where User is an Equatable model.
let menu = MyMenu<User>(items: photoCategories, resetOnChange: viewModel.user)
The problem I have with this is that this type declaration is now required (we may want to use this menu in the future w/o a reset parameter). Not only that, it makes initializing the menu confusing. For example, if I want the reset parameter to be an unrelated type ... i.e., User
. This results in the menu looking like it's a user menu, when in fact the menu shows different photo categories. The only relation of User
is that we want the menu to reset if the User
model changes.
Another approach I tried was using AnyPublisher<Void, Never>
(instead of an Equatable property) and then using .onReceive(..)
instead of onChange(...)
. The problem there is that onReceive
was called far more often than expected.
One thing to note: This doesn't work using items
as the change because their values often stay the same from User
to User
, so runtime doesn't see them as actually changing.
Is there a clean way to do this, or am I stuck with the less-than-ideal working solution I found above? Thanks in advance.
SwiftUI requires concrete types for just about everything, especially when it comes to Views.
A more versatile solution to generics is using where
to declare the required conformance. It allows for the type to be determine on init
struct MyMenu<E>: View where E: Equatable{
let items: [MenuItem]
let resetOnChange: E
init(items: [MenuItem], resetOnChange: E) {
self.items = items
self.resetOnChange = resetOnChange
}
init(items: [MenuItem]) where E == String {
self.items = items
self.resetOnChange = "" //Nothing from parent just set to blank
}
var body: some View {
ScrollViewReader { proxy in
ScrollView {
// scroll view content
}
.onChange(of: resetOnChange) { _ in
if let first = items.first {
proxy.scrollTo(first, anchor: .leading)
}
}
}
}
}
Or make it optional and set to nil
struct MyMenu<E>: View where E: Equatable{
let items: [MenuItem]
let resetOnChange: E?
init(items: [MenuItem], resetOnChange: E) {
self.items = items
self.resetOnChange = resetOnChange
}
init(items: [MenuItem]) where E == Optional<String> {
self.items = items
self.resetOnChange = nil
}
var body: some View {
ScrollViewReader { proxy in
ScrollView {
// scroll view content
}
.onChange(of: resetOnChange) { _ in
if let first = items.first {
proxy.scrollTo(first, anchor: .leading)
}
}
}
}
}