Git Product home page Git Product logo

2waybindinguikit's People

Contributors

gahntpo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

2waybindinguikit's Issues

2-way binding endless loop when changing the value in a sink

Hey,
I am not sure if this is the right place to ask a question, but it is the only place I found, so here we go :)

At first, let me say, that I really like the implementation of 2 way bindings and used it extensively. It worked very well.
Unfortunately I ran into an issue recently where I got into an endless loop when updating a binding inside a sink method of the same binding. Maybe you have an idea of how to solve that ?

The scenario. I have a custom dropdown button which adopts the 2-way binding you proposed here. A dropdown button holds an array of DropdownOption<T> to let the user select a value and a selected value of type DropdownOption<T>?. T is any value which is hashable and can be shown with a textual representation represented by the DropdownDescribable protocol.

The 2-way binding is implemented like this:

// MARK: - Combine Binding
public extension DropdownButton where T: Hashable & DropdownDescribable {
    func bind(to subject: CurrentValueSubject<DropdownOption<T>?, Never>, storeIn subscriptions: CancelBag) {
        subject.sink { [weak self] value in
            if value != self?._selectedOption {
                self?._selectedOption = value
            }
        }.store(in: subscriptions)
        
        self.selectedOption.sink { value in
            if value != subject.value {
                subject.send(value)
            }
        }.store(in: subscriptions)
    }
}

The issue appears in the following case:

let cancelBag: CancelBag = CancelBag()
let selectedSize: CurrentValueSubject<DropdownOption<Int>?, Never> = .init(nil)

let button = DropdownButton<Int>()
button.bind(to: selectedSize, storeIn: cancelBag)

selectedSize.eraseToAnyPublisher()
    .sink { value in
        if value != nil  {
            selectedSize.value = nil
        } 
    }
    .store(in: cancelBag)

print("Send 1")
selectedSize.send(.init(value: 1))

print("Send 2")
selectedSize.send(.init(value: 2))

print("Send 3")
selectedSize.send(.init(value: 3))

print(selectedSize.value)

The original issue appears in a much more complicated context, but broken down it is something like: Whenever a value is selected and some constraint is met, here it would be value != nil I want to set the selected size value to be nil.

What I see now is that the sink closure is executed before the value is set on the selectedSize value. This leads to the checks in the 2-way binding, wether the current value of the CurrentValueSubject equals the selected value on the button to fail and let the button sends a new value to the binding, even though the value will be set on the CurrentValueSubject to the correct one milliseconds afterwards.

The full playground code would be this:

Playground Code
import Foundation
import UIKit
import Combine

/// A convenient wrapper around a set of AnyCancellable
public class CancelBag {
  public var subscriptions = Set<AnyCancellable>()
  
  public init() {}
}

extension CancelBag {
  public func cancelAll() {
      subscriptions.forEach { $0.cancel() }
  }
}

extension AnyCancellable {
  public func store(in cancelBag: CancelBag) {
      cancelBag.subscriptions.insert(self)
  }
}


extension Int: DropdownDescribable {
  public var dropdownText: String {
      return "\(self)"
  }
}


/// A type that can be displayed by a dropdown menu
public protocol DropdownDescribable {
  var dropdownText: String { get }
}

/// A type representation an instance of type `T` that can be selected by a dropdown menu
public struct DropdownOption<T: Hashable & DropdownDescribable>: Hashable {
  
  public var key: Int
  public var value: T
  public var systemImage: String?
  public var imageColor: UIColor?
  
  public init(value: T, systemImage: String? = nil, imageColor: UIColor? = nil) {
      self.value = value
      self.key = value.hashValue
      self.systemImage = systemImage
      self.imageColor = imageColor
  }
  
  public static func == (lhs: DropdownOption, rhs: DropdownOption) -> Bool {
      return lhs.key == rhs.key
          && lhs.systemImage == rhs.systemImage
          && lhs.imageColor == rhs.imageColor
  }
  
  public func hash(into hasher: inout Hasher) {
      hasher.combine(key)
      hasher.combine(systemImage)
      hasher.combine(imageColor)
  }
}

/// A button that present a dropdown menu to select a value from it
///
/// The values inside the dropdown menu represent instances of type `T`
public class DropdownButton<T: Hashable & DropdownDescribable>: UIButton {
  
  // MARK: - TraitCollectionAwareUpdateView
  
  // MARK: - Public properties
  var selectedOption: AnyPublisher<DropdownOption<T>?, Never> {
      return _cvs.eraseToAnyPublisher()
  }
  
  // MARK: - Private properties
  private var _options: [DropdownOption<T>] = [] {
      didSet {
          self.updateDropdownMenu()
      }
  }
  private var _cvs = CurrentValueSubject<DropdownOption<T>?, Never>(nil)
  private var _selectedOption: DropdownOption<T>? = nil {
      didSet {
          self.updateDropdownMenu()
          self.setTitle(_selectedOption?.value.dropdownText, for: .normal)
          self._cvs.send(_selectedOption)
      }
  }
  
  private var placeholder: String = ""
  
  // MARK: - Initializers
  override init(frame: CGRect) {
      super.init(frame: frame)
      configure()
      updateDropdownMenu()
  }
  
  /// Create a new PSTDropdownbutton instance
  ///
  /// - Parameters:
  ///   - options: Selectable options
  ///   - selectedOption: Optional already selected option
  ///   - placeholder: Placeholder text
  ///   - type: Button type
  /// - Returns: A `PSTDropdownButton` instance
  public convenience init(options: [DropdownOption<T>],
                   selectedOption: DropdownOption<T>? = nil,
                   placeholder: String,
                   type: UIButton.ButtonType = .system
  ) {
      self.init(type: type)
      self._options = options
      self._selectedOption = selectedOption
      self.placeholder = placeholder
      
      configure()
      updateDropdownMenu()
  }
  
  required init?(coder: NSCoder) {
      fatalError("init(coder:) has not been implemented")
  }
  
  // MARK: Overrides
  public override func layoutSubviews() {
      super.layoutSubviews()
      if let imageView = self.imageView {
          
          let buttonWidth = frame.width
          let imageWidth = imageView.frame.width
          
          imageEdgeInsets = UIEdgeInsets(top: 0, left: buttonWidth-imageWidth-(contentEdgeInsets.right*2), bottom: 0, right: -(buttonWidth-imageWidth-(contentEdgeInsets.right*2)))
          titleEdgeInsets = UIEdgeInsets(top: 0, left: -imageWidth, bottom: 0, right: imageWidth)
      }
  }
  
  // MARK: - Configuration
  private func configure() {
      self.setTitle(self._selectedOption?.value.dropdownText ?? self.placeholder, for: .normal)
      self.setImage(UIImage(systemName: "chevron.down")?.withRenderingMode(.alwaysTemplate), for: .normal)
      self.tintColor = .label
      
      self.contentHorizontalAlignment = .left
      self.role = .normal
      self.showsMenuAsPrimaryAction = true
  }
  
  private func updateDropdownMenu() {

      let children: [UIAction] = _options.map { option in
          return .init(title: option.value.dropdownText,
                       image: option.systemImage != nil ? UIImage(systemName: option.systemImage!)?.withTintColor(option.imageColor ?? .label, renderingMode: .alwaysOriginal): nil,
                       state: option == self._selectedOption ? .on : .off,
                       handler: { [weak self] _ in
              self?._selectedOption = option
          })
      }
      
      self.menu = UIMenu(title: self.placeholder, children: children)
  }
  
  // MARK: - Action methods
  
  /// Use this option to directly set a value of the dropdown menu
  ///
  /// - Parameter value: Value to be set as `selected`
  func setSelected(value: DropdownOption<T>?) {
      self._selectedOption = value
      self.setTitle(value?.value.dropdownText ?? placeholder, for: .normal)
  }
}

// MARK: - Combine Binding
public extension DropdownButton where T: Hashable & DropdownDescribable {
  
  func bind(to subject: CurrentValueSubject<DropdownOption<T>?, Never>, storeIn subscriptions: CancelBag) {
      subject.sink { [weak self] value in
          if value != self?._selectedOption {
              self?._selectedOption = value
          }
      }.store(in: subscriptions)
      
      self.selectedOption.sink { value in
          if value != subject.value {
              subject.send(value)
          }
      }.store(in: subscriptions)
  }
  
  func bindOptions(to subject: CurrentValueSubject<[DropdownOption<T>], Never>, storeIn subscriptions: CancelBag) {
      subject.sink { [weak self] value in
          if value != self?._options {
              self?._options = value
          }
      }.store(in: subscriptions)
  }
}

let cancelBag: CancelBag = CancelBag()
let selectedSize: CurrentValueSubject<DropdownOption<Int>?, Never> = .init(nil)

let button = DropdownButton<Int>()
button.bind(to: selectedSize, storeIn: cancelBag)

selectedSize.eraseToAnyPublisher()
  .sink { value in
      if value != nil {
          selectedSize.value = nil
      }
  }
  .store(in: cancelBag)

print("Send 1")
selectedSize.send(.init(value: 1))

print("Send 2")
selectedSize.send(.init(value: 2))

print("Send 3")
selectedSize.send(.init(value: 3))


print(selectedSize.value)

So the final result printed should be nil. It does work some times but when executed several times there is also the case that the code ends up in an endless loop causing the Playground to freeze.

Currently I have no clue of how to solve this kind of issue. Maybe you know some way to solve it ? Any help is much appreciated !

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.