Displaying changes instantly is great, but not always. SwiftUI gave us a great mechanism for this, and we love it.

Often, when we have a deal with a mobile application such behavior is exactly what we want. But imagine a situation, when on such a change u should perform a kind of heavy operation - in this case, the output of such behavior is a bit not what we want.

The real case - imagine that u have a filter, and the user should be able to select 10 or more filter categories and apply them all at once or change his mind and cancel this operation. At the same moment, we want to use SwiftUI and show everything on screen using this great binding system.

BindingWrapper

I am faced with this problem on a current project. As result, I think, that it would be great if we can make kind of @Binding but on additional action.

The regular @Binding we can describe as following:

idea



And the one that we need to solve proble described above:

idea



To do so, we can create data copy, modify it and commit from the user gave it back. To save the same functionality as @Binding we can do next:

  1. store 2 versions of data - initial (input) and the one that is currently active
@Binding private var active: Type
@Binding private var original: Type
  1. create @Published property for displaying any changes and handling proxy state
@Published var proxy: Type
  1. on init View perform a dance
self._original = inputBinding
viewModel.sortBy = inputBinding.wrappedValue
self._active = $viewModel.proxy
  1. on commit send data
_original.wrappedValue = viewModel.proxy

Such an approach work, but… it’s not the best from the code perspective of view - too many things we need to write every time, too many parts separated all over the app.

So I decided to make it compact and wrap it into stuct. The result is next:

struct BindingWrapper<T> {
  private final class Proxy<T>: ObservableObject {
    @Published var proxy: T
    init(value: T) {
      proxy = value
    }
  }
  
  @Binding private var originalValue: T
  @Binding var value: T
  @ObservedObject private var proxy: Proxy<T>
  
  init(value: Binding<T>) {
    _originalValue = value
    _value = value
    
    proxy = .init(value: value.wrappedValue)
    _value = $proxy.proxy
  }
  
  func commit() {
    _originalValue.wrappedValue = proxy.proxy
  }
}

Looks like that everything is ok, but if u try to use this in the code - nothing happens, updates are not received. The reason - because View doesn’t know that u’r struct backed store changed…

A naive approach to fix this would be to use the @State modifier for this property, but in this case, u need to set the default value - this is not the good approach for the @Binding value.

The correct way would be simple conformance to the DynamicProperty.

struct BindingWrapper<T>: DynamicProperty

read more about DynamicProperty on my prev post here.

Now, everything works as expected - View can show binding value instantly, but sending value out of the view - this is a task for a final user.

Here is a quick demo:

idea



Resources