How to best isolate spinning pickers from view updates #1072
-
I'm working on a view that uses picker wheels to select some values and uses those values in a calculation (assume it has to be picker wheels, and not any other picker variety). If I build this the naive way using SwiftUI, any spinning picker wheels will reset to their last selection value, if the view updates. e.g. if both wheels are spinning, when one picker stops the answer is calculated and any other spinning pickers snap back. I have a demo solution, but I'd like to know if there is a better way, as it seems awful. Am I missing a nicer way to achieve the same outcome? e.g.
Any pointers greatly appreciated. :) |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 3 replies
-
Why do you think this is awful? The correct way to isolate state changes from unrelated parts of the view is to divide your view up and scope down the observed view state which is exactly what you're doing here. You could clean this up by extracting child views for each picker and initialise them with a scoped store and move the WithViewStores to the body of the new child view. Don't be afraid to break your views and state up into smaller components - there isn't usually a performance penalty for doing so (it can often help with performance) and decomposing your views in this way is an encouraged design pattern with SwiftUI and TCA gives you the tools to support this approach. |
Beta Was this translation helpful? Give feedback.
-
Hey Ryan, This is a cool example showing how scoping can be used to better control view updates :)
Depending on your needs, one option could be to extract the func picker<Content: View>(
_ title: String, store: Store<Int, Int>, @ViewBuilder content: @escaping () -> Content
) -> some View {
WithViewStore(store) { viewStore in
Picker(title, selection: viewStore.binding(send: { $0 }), content: content)
}
} Then instead of scoping to picker("A", store: store.scope(state: \.a, action: { .set(\.$a, $0) })) {
ForEach(0...1000, id: \.self) {
Text($0, format: .number)
}
} If you intended to do this frequently, you could add some extensions to extension Store where Action: BindableAction, Action.State == State {
func scope<Value: Equatable>(
binding keyPath: WritableKeyPath<State, BindableState<Value>>
) -> Store<Value, Value> {
self.scope(state: { $0[keyPath: keyPath].wrappedValue }, action: { .set(keyPath, $0) })
}
}
extension ViewStore where State == Action {
func binding() -> Binding<State> { self.binding(send: { $0 }) }
} Which would let you do this: WithViewStore(store.scope(binding: \.$a)) { viewStore in
Picker("A", selection: viewStore.binding()) {
ForEach(0...1000, id: \.self) {
Text($0, format: .number)
}
}
} Hope that helps a little bit. |
Beta Was this translation helpful? Give feedback.
-
Apparently you don't have to do any low-level coding ... you can solve the issue of multiple wheel Pickers by placing each individual Picker in its own top-level View. Here's a code snippet that illustrates the difference (if you spin multiple Pickers simultaneously in the top grouping, then run the same test with the bottom grouping). import SwiftUI
struct FiftySpinner: View {
@State var value: Int = 0
var body: some View {
Picker("", selection: $value) {
ForEach (0..<51) {
Text("\($0)")
}
}
}
}
struct ContentView: View {
@State var value1: Int = 0
@State var value2: Int = 0
@State var value3: Int = 0
var body: some View {
VStack {
HStack {
Picker("", selection: $value1) {
ForEach (0..<51) {
Text("\($0)")
}
}
.pickerStyle(.wheel)
Picker("", selection: $value2) {
ForEach (0..<51) {
Text("\($0)")
}
}
.pickerStyle(.wheel)
Picker("", selection: $value3) {
ForEach (0..<51) {
Text("\($0)")
}
}
.pickerStyle(.wheel)
}
Text("Within Same View")
Divider().background(.black).padding(EdgeInsets(top: 20, leading: 0, bottom: 20, trailing: 0))
Text("Within Different Views")
HStack {
FiftySpinner()
.pickerStyle(.wheel)
FiftySpinner()
.pickerStyle(.wheel)
FiftySpinner()
.pickerStyle(.wheel)
}
}
}
}
#Preview {
ContentView()
} |
Beta Was this translation helpful? Give feedback.
Hey Ryan,
This is a cool example showing how scoping can be used to better control view updates :)
Depending on your needs, one option could be to extract the
Picker
to a helper functionThen instead of scoping to
\.$a
and.binding
you could scope directly tostate: \.a, action: { .set(\.$a, $0) }
. Resulting in the following: