Reputation: 1619
I'm struggling to implement a custom view which can take Binding as an argument and implement two-way updates of that value.
So basically I'm implementing my custom slider and want its initializer to be like this:
MySlider(value: <Binding<Float>)
What I'm struggling with:
Here's my current implementation so far which is not perfect.
struct MySlider: View {
@Binding var selection: Float?
@State private var selectedValue: Float?
init(selection: Binding<Float?>) {
self._selection = selection
// https://stackoverflow.com/a/58137096
_selectedValue = State(wrappedValue: selection.wrappedValue)
}
var body: some View {
HStack(spacing: 3) {
ForEach(someValues) { (v) in
Item(value: v,
isSelected: v == self.selection)
.onTapGesture {
// No idea how to do that other way so I don't have to set it twice
self.selection = v
self.selectedValue = v
}
}
}
}
}
Edit 1:
I suppose my problem is that the underlying model object comes from Core Data and wasn't owned by any SwiftUI view which would observe its changes. The model object was owned by the UIKit ViewController and I was passing only a Binding to the SwiftUI view which is not enough.
My solution now is to pass the model object also to the SwiftUI View so that it can marked it as an @ObservedObject
.
struct MySlider<T>: View where T: ObservableObject {
@ObservedObject var object: T
@Binding var selection: Float?
var body: some View {
return ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 3) {
ForEach(values) { (v) in
Item(value: v,
isSelected: v == self.selection)
.onTapGesture {
self.selection = v
}
}
}
}
}
}
Upvotes: 3
Views: 5916
Reputation: 2083
My answer might be a little late, but i stumbled across a similar problem. So here is how i solved it.
struct Item : Identifiable {
var id : Int
var isSelected : Bool
}
class MySliderObserver : ObservableObject {
@Published var values = [Item]()
init() {
values.append(Item(id: 0, isSelected: false))
values.append(Item(id: 1, isSelected: false))
values.append(Item(id: 2, isSelected: false))
}
func toggleSelectForItem(id:Int) -> Void {
guard let index = values.firstIndex(where: { $0.id == id }) else {
return
}
var item = values[index]
item.isSelected.toggle()
values[index] = item
}
}
struct MySlider: View {
@EnvironmentObject var observer : MySliderObserver
var body: some View {
return ScrollView(.horizontal, showsIndicators: false) {
VStack(spacing: 3) {
ForEach(self.observer.values) { v in
Text("Selected: \(v.isSelected ? "true" : "false")").onTapGesture {
self.observer.toggleSelectForItem(id: v.id)
}
}
}
}
}
}
and in the SceneDelegate
you need to set an EnvironmentObject
like:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = MySlider()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView.environmentObject(MySliderObserver()))
self.window = window
window.makeKeyAndVisible()
}
}
Upvotes: 0
Reputation: 3752
The definition of @Binding
is essentially a two-way connection to an underlying data item, such a a @State
variable that's owned by another View. As stated by Apple:
Use a binding to create a two-way connection between a view and its underlying model.
If it's a binding, SwiftUI will already update its views automatically if the value changes; so (in answer to your first question), you don't need to do any sort of subscriptions to update your custom view - that will happen automatically.
Similarly (regarding your second question), because the binding is the state from another view, you shouldn't be declaring it as a state for your view as well, and I also don't believe it's possible. State is something that should be a purely internal value to your view (Apple strongly recommend that all @State
properties are declared private
).
This all ties back to the 'Single Source of Truth' concept Apple stressed when unveiling SwiftUI: The parent view where that binding's already @State
is what owns the information, so it's not something your view should also declare as a state.
For your code, I think all you need to do is take out that second state property, because it's not required. Just make sure the binding you pass is a @State
property in whatever parent view owns your custom view, and pass it in using the $
syntax to create that binding. This article covers the idea in more detail if you need.
struct MySlider: View {
@Binding var selection: Float?
init(selection: Binding<Float?>) {
self._selection = selection
}
var body: some View {
HStack(spacing: 3) {
ForEach(someValues) { (v) in
Item(value: v, isSelected: v == self.selection)
.onTapGesture {
self.selection = v
}
}
}
}
}
Upvotes: 2