From f3094d2d1e8d404b9dceb9b41aa91d5ccc910e95 Mon Sep 17 00:00:00 2001 From: Arthur da Paz Date: Wed, 9 Sep 2020 21:21:36 -0300 Subject: [PATCH 01/12] 1. Make this a SPM package 2. Removed Objective-C bridging for a shitty (swifty) approach --- .../BlockBasedSelector/BlockBasedSelector.h | 17 ---------- .../BlockBasedSelector/BlockBasedSelector.m | 19 ----------- .../BlockBasedSelector.swift | 19 ----------- .../{ => SimpleTwoWayBinding}/Bindable.swift | 33 +++++++++++++++++-- .../NSObject+Observable.swift | 0 .../Observable+FunctionalConvenience.swift | 0 .../Observable.swift | 0 .../PausableObservable.swift | 2 +- .../SimpleTwoWayBindings-Bridging-Header.h | 0 .../UIControls+Bindable.swift | 2 +- .../SimpleTwoWayBindingTests.swift | 0 11 files changed, 33 insertions(+), 59 deletions(-) delete mode 100644 Sources/BlockBasedSelector/BlockBasedSelector.h delete mode 100644 Sources/BlockBasedSelector/BlockBasedSelector.m delete mode 100644 Sources/BlockBasedSelector/BlockBasedSelector.swift rename Sources/{ => SimpleTwoWayBinding}/Bindable.swift (70%) rename Sources/{ => SimpleTwoWayBinding}/NSObject+Observable.swift (100%) rename Sources/{ => SimpleTwoWayBinding}/Observable+FunctionalConvenience.swift (100%) rename Sources/{ => SimpleTwoWayBinding}/Observable.swift (100%) rename Sources/{ => SimpleTwoWayBinding}/PausableObservable.swift (99%) rename Sources/{ => SimpleTwoWayBinding}/SimpleTwoWayBindings-Bridging-Header.h (100%) rename Sources/{ => SimpleTwoWayBinding}/UIControls+Bindable.swift (99%) rename Tests/{ => SimpleTwoWayBindingTests}/SimpleTwoWayBindingTests.swift (100%) diff --git a/Sources/BlockBasedSelector/BlockBasedSelector.h b/Sources/BlockBasedSelector/BlockBasedSelector.h deleted file mode 100644 index 479b313..0000000 --- a/Sources/BlockBasedSelector/BlockBasedSelector.h +++ /dev/null @@ -1,17 +0,0 @@ -// BlockBasedSelector.h -// -// Created by Charlton Provatas on 11/2/17. -// Copyright © 2017 CharltonProvatas. All rights reserved. -// - -#import - -@interface BlockBasedSelector : NSObject - -@end - -typedef void (^OBJCBlock)(id foo); - -void class_addMethodWithBlock(Class class, SEL newSelector, OBJCBlock block); - - diff --git a/Sources/BlockBasedSelector/BlockBasedSelector.m b/Sources/BlockBasedSelector/BlockBasedSelector.m deleted file mode 100644 index 24fbf86..0000000 --- a/Sources/BlockBasedSelector/BlockBasedSelector.m +++ /dev/null @@ -1,19 +0,0 @@ -// -// BlockBasedSelector.m -// -// Created by Charlton Provatas on 11/2/17. -// Copyright © 2017 CharltonProvatas. All rights reserved. -// - -#import "BlockBasedSelector.h" -#import - -@implementation BlockBasedSelector -@end - -void class_addMethodWithBlock(Class class, SEL newSelector, OBJCBlock block) -{ - IMP newImplementation = imp_implementationWithBlock(block); - Method method = class_getInstanceMethod(class, newSelector); - class_addMethod(class, newSelector, newImplementation, method_getTypeEncoding(method)); -} diff --git a/Sources/BlockBasedSelector/BlockBasedSelector.swift b/Sources/BlockBasedSelector/BlockBasedSelector.swift deleted file mode 100644 index 3303eb2..0000000 --- a/Sources/BlockBasedSelector/BlockBasedSelector.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// BlockBasedSelector.swift -// -// Created by Charlton Provatas on 11/2/17. -// Copyright © 2017 CharltonProvatas. All rights reserved. - -import Foundation -import UIKit - -func Selector(_ block: @escaping () -> Void) -> Selector { - let selector = NSSelectorFromString("\(CACurrentMediaTime())") - class_addMethodWithBlock(_Selector.self, selector) { (_) in block() } - return selector -} - -let Selector = _Selector.shared -@objc class _Selector: NSObject { - static let shared = _Selector() -} diff --git a/Sources/Bindable.swift b/Sources/SimpleTwoWayBinding/Bindable.swift similarity index 70% rename from Sources/Bindable.swift rename to Sources/SimpleTwoWayBinding/Bindable.swift index 91a2ddd..dfa2717 100644 --- a/Sources/Bindable.swift +++ b/Sources/SimpleTwoWayBinding/Bindable.swift @@ -55,15 +55,44 @@ extension Bindable where Self: NSObject { @discardableResult public func bind(with observable: Observable) -> BindingReceipt { - if let _self = self as? UIControl { - _self.addTarget(Selector, action: Selector{ [weak self] in self?.valueChanged() }, for: [.editingChanged, .valueChanged]) + + if self is UIControl { + //let closure: (() -> Void)! = + let sleeve = ActionClosure{ [weak self] in + self?.valueChanged() + } + + (self as! UIControl).addTarget(sleeve, action: #selector(ActionClosure.invoke), for: [.valueChanged, .editingChanged]) + + objc_setAssociatedObject(self, memoryAddress, sleeve, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + self.binder = observable if let val = observable.value { self.updateValue(with: val) } + return self.observe(for: observable) { (value) in self.updateValue(with: value) } } } + + +extension NSObject { + fileprivate var memoryAddress: String { + String(format: "%p", unsafeBitCast(self, to: UInt.self)) + } + + @objc fileprivate final class ActionClosure: NSObject { + let closure: () -> Void + + init(_ closure: @escaping () -> Void) { + self.closure = closure + } + + @objc func invoke() { + closure() + } + } +} diff --git a/Sources/NSObject+Observable.swift b/Sources/SimpleTwoWayBinding/NSObject+Observable.swift similarity index 100% rename from Sources/NSObject+Observable.swift rename to Sources/SimpleTwoWayBinding/NSObject+Observable.swift diff --git a/Sources/Observable+FunctionalConvenience.swift b/Sources/SimpleTwoWayBinding/Observable+FunctionalConvenience.swift similarity index 100% rename from Sources/Observable+FunctionalConvenience.swift rename to Sources/SimpleTwoWayBinding/Observable+FunctionalConvenience.swift diff --git a/Sources/Observable.swift b/Sources/SimpleTwoWayBinding/Observable.swift similarity index 100% rename from Sources/Observable.swift rename to Sources/SimpleTwoWayBinding/Observable.swift diff --git a/Sources/PausableObservable.swift b/Sources/SimpleTwoWayBinding/PausableObservable.swift similarity index 99% rename from Sources/PausableObservable.swift rename to Sources/SimpleTwoWayBinding/PausableObservable.swift index 2bdf9b8..5ad4570 100644 --- a/Sources/PausableObservable.swift +++ b/Sources/SimpleTwoWayBinding/PausableObservable.swift @@ -5,7 +5,7 @@ // Created by Ryan Forsythe on 6/15/20. // -import Foundation +import UIKit /// A helper class to manage pausable receipts public class ReceiptBag { diff --git a/Sources/SimpleTwoWayBindings-Bridging-Header.h b/Sources/SimpleTwoWayBinding/SimpleTwoWayBindings-Bridging-Header.h similarity index 100% rename from Sources/SimpleTwoWayBindings-Bridging-Header.h rename to Sources/SimpleTwoWayBinding/SimpleTwoWayBindings-Bridging-Header.h diff --git a/Sources/UIControls+Bindable.swift b/Sources/SimpleTwoWayBinding/UIControls+Bindable.swift similarity index 99% rename from Sources/UIControls+Bindable.swift rename to Sources/SimpleTwoWayBinding/UIControls+Bindable.swift index 133578c..4eac52e 100644 --- a/Sources/UIControls+Bindable.swift +++ b/Sources/SimpleTwoWayBinding/UIControls+Bindable.swift @@ -5,7 +5,7 @@ // Created by Manish Katoch on 11/26/17. // -import Foundation +import UIKit extension UITextField : Bindable { public typealias BindingType = String diff --git a/Tests/SimpleTwoWayBindingTests.swift b/Tests/SimpleTwoWayBindingTests/SimpleTwoWayBindingTests.swift similarity index 100% rename from Tests/SimpleTwoWayBindingTests.swift rename to Tests/SimpleTwoWayBindingTests/SimpleTwoWayBindingTests.swift From e00faa0733209fdb0654c76f79bc9eab09da0567 Mon Sep 17 00:00:00 2001 From: Arthur da Paz Date: Wed, 9 Sep 2020 23:23:47 -0300 Subject: [PATCH 02/12] removed uneeded files --- Sources/SimpleTwoWayBinding/Bindable.swift | 4 ++-- .../SimpleTwoWayBindings-Bridging-Header.h | 8 -------- 2 files changed, 2 insertions(+), 10 deletions(-) delete mode 100644 Sources/SimpleTwoWayBinding/SimpleTwoWayBindings-Bridging-Header.h diff --git a/Sources/SimpleTwoWayBinding/Bindable.swift b/Sources/SimpleTwoWayBinding/Bindable.swift index dfa2717..67a37bd 100644 --- a/Sources/SimpleTwoWayBinding/Bindable.swift +++ b/Sources/SimpleTwoWayBinding/Bindable.swift @@ -57,8 +57,8 @@ extension Bindable where Self: NSObject { public func bind(with observable: Observable) -> BindingReceipt { if self is UIControl { - //let closure: (() -> Void)! = - let sleeve = ActionClosure{ [weak self] in + let sleeve = ActionClosure { + [weak self] in self?.valueChanged() } diff --git a/Sources/SimpleTwoWayBinding/SimpleTwoWayBindings-Bridging-Header.h b/Sources/SimpleTwoWayBinding/SimpleTwoWayBindings-Bridging-Header.h deleted file mode 100644 index 10c68b0..0000000 --- a/Sources/SimpleTwoWayBinding/SimpleTwoWayBindings-Bridging-Header.h +++ /dev/null @@ -1,8 +0,0 @@ -// -// SimpleTwoWayBindings-Bridging-Header.h -// Pods -// -// Created by Manich Katoch on 11/26/17. -// - -#import "BlockBasedSelector.h" From 3a3363059198255da4107fea658a8156b214b79f Mon Sep 17 00:00:00 2001 From: Arthur da Paz Date: Wed, 9 Sep 2020 23:31:27 -0300 Subject: [PATCH 03/12] Delete SimpleTwoWayBinding.podspec --- SimpleTwoWayBinding.podspec | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 SimpleTwoWayBinding.podspec diff --git a/SimpleTwoWayBinding.podspec b/SimpleTwoWayBinding.podspec deleted file mode 100644 index 61fdf52..0000000 --- a/SimpleTwoWayBinding.podspec +++ /dev/null @@ -1,19 +0,0 @@ -Pod::Spec.new do |s| - s.name = 'SimpleTwoWayBinding' - s.version = '0.0.7' - s.summary = 'Ultra light weight and simple two way binding for iOS UIControls.' - s.description = <<-DESC -Ultra light weight and simple two way binding for UIControls. -Written with love and hope in Swift 5. - DESC - - s.homepage = 'https://github.com/manishkkatoch/SimpleTwoWayBindingIOS' - s.license = { :type => 'MIT', :file => 'LICENSE' } - s.author = { 'Manish Katoch' => 'manish.katoch@gmail.com' } - s.source = { :git => 'https://github.com/manishkkatoch/SimpleTwoWayBindingIOS.git', :tag => s.version.to_s } - s.ios.deployment_target = '8.0' - - s.source_files = 'Sources/**/*' - s.frameworks = 'UIKit' - s.swift_version = '5.0' -end From 9f839dd6e1f97f3a946de44b4e7095eb0db614a4 Mon Sep 17 00:00:00 2001 From: Arthur da Paz Date: Wed, 9 Sep 2020 23:31:51 -0300 Subject: [PATCH 04/12] Delete .travis.yml --- .travis.yml | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b37768b..0000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -# references: -# * http://www.objc.io/issue-6/travis-ci.html -# * https://github.com/supermarin/xcpretty#usage - -osx_image: xcode11 -language: swift -# cache: cocoapods -# podfile: Example/Podfile -# before_install: -# - gem install cocoapods # Since Travis is not always on latest version -# - pod install --project-directory=Example -script: -- pod lib lint From f02dac524d748672662f9b9743b5b45ce6846a83 Mon Sep 17 00:00:00 2001 From: Arthur da Paz Date: Wed, 9 Sep 2020 23:32:40 -0300 Subject: [PATCH 05/12] melhorias --- .../contents.xcworkspacedata | 7 ++++++ Package.swift | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata create mode 100644 Package.swift diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..8143b1d --- /dev/null +++ b/Package.swift @@ -0,0 +1,23 @@ +// swift-tools-version:5.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "SimpleTwoWayBinding", + platforms: [.iOS(.v10)], + products: [ + .library( + name: "SimpleTwoWayBinding", + targets: ["SimpleTwoWayBinding"]), + ], + dependencies: [ ], + targets: [ + .target( + name: "SimpleTwoWayBinding", + dependencies: []), + .testTarget( + name: "SimpleTwoWayBindingTests", + dependencies: ["SimpleTwoWayBinding"]), + ] +) From 0d8d901270b8b1c204058e0989bc670b69bb54ea Mon Sep 17 00:00:00 2001 From: Arthur da Paz Date: Thu, 10 Sep 2020 11:21:45 -0300 Subject: [PATCH 06/12] Added ObservableTrash --- .../Observable+FunctionalConvenience.swift | 8 ++++++++ Sources/SimpleTwoWayBinding/Observable.swift | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Sources/SimpleTwoWayBinding/Observable+FunctionalConvenience.swift b/Sources/SimpleTwoWayBinding/Observable+FunctionalConvenience.swift index 737310e..866d5a6 100644 --- a/Sources/SimpleTwoWayBinding/Observable+FunctionalConvenience.swift +++ b/Sources/SimpleTwoWayBinding/Observable+FunctionalConvenience.swift @@ -51,6 +51,14 @@ public extension Observable { func bind(replay: Bool = true, on queue: DispatchQueue? = nil, _ target: inout Root, _ path: WritableKeyPath) -> BindingReceipt { bind(replay: replay, on: queue) { [weak target] value in target?[keyPath: path] = value } } + + typealias ObservableTrash = [ObservableThing] + /// Bind to this observable with an object/keypath pair + /// - Parameters: + /// - bag: If there's a value in this observable, after setting up the binding immediately fire the observation function with that value, rather than the default behavior of waiting for a new value to come into the stream. Defaults to true. + func dispose(by bag: inout ObservableTrash) { + bag.append(self) + } /// Create a new observable whose value is mapped from this observable's values /// - Parameters: diff --git a/Sources/SimpleTwoWayBinding/Observable.swift b/Sources/SimpleTwoWayBinding/Observable.swift index c74f378..c58850b 100644 --- a/Sources/SimpleTwoWayBinding/Observable.swift +++ b/Sources/SimpleTwoWayBinding/Observable.swift @@ -8,13 +8,15 @@ import Foundation import UIKit +public protocol ObservableThing { } + public struct BindingReceipt: Hashable, Identifiable { public let id = UUID() public func hash(into hasher: inout Hasher) { hasher.combine(id) } public static func == (lhs: BindingReceipt, rhs: BindingReceipt) -> Bool { lhs.id == rhs.id } } -public class Observable { +public class Observable: ObservableThing { public typealias Observer = (_ observable: Observable, ObservedType) -> Void /// Map of receipt objects to the binding blocks those objects represent; see bind(observer:) and unbind(:) From 60eb8ce69bc556492701fceb1f3192c68ccf5f7f Mon Sep 17 00:00:00 2001 From: Arthur da Paz Date: Thu, 10 Sep 2020 11:56:03 -0300 Subject: [PATCH 07/12] Adicionada uma disparadora --- .../Observable+FunctionalConvenience.swift | 8 ----- Sources/SimpleTwoWayBinding/Observable.swift | 34 +++++++++++++++++-- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/Sources/SimpleTwoWayBinding/Observable+FunctionalConvenience.swift b/Sources/SimpleTwoWayBinding/Observable+FunctionalConvenience.swift index 866d5a6..737310e 100644 --- a/Sources/SimpleTwoWayBinding/Observable+FunctionalConvenience.swift +++ b/Sources/SimpleTwoWayBinding/Observable+FunctionalConvenience.swift @@ -51,14 +51,6 @@ public extension Observable { func bind(replay: Bool = true, on queue: DispatchQueue? = nil, _ target: inout Root, _ path: WritableKeyPath) -> BindingReceipt { bind(replay: replay, on: queue) { [weak target] value in target?[keyPath: path] = value } } - - typealias ObservableTrash = [ObservableThing] - /// Bind to this observable with an object/keypath pair - /// - Parameters: - /// - bag: If there's a value in this observable, after setting up the binding immediately fire the observation function with that value, rather than the default behavior of waiting for a new value to come into the stream. Defaults to true. - func dispose(by bag: inout ObservableTrash) { - bag.append(self) - } /// Create a new observable whose value is mapped from this observable's values /// - Parameters: diff --git a/Sources/SimpleTwoWayBinding/Observable.swift b/Sources/SimpleTwoWayBinding/Observable.swift index c58850b..85f3ed7 100644 --- a/Sources/SimpleTwoWayBinding/Observable.swift +++ b/Sources/SimpleTwoWayBinding/Observable.swift @@ -10,10 +10,30 @@ import UIKit public protocol ObservableThing { } -public struct BindingReceipt: Hashable, Identifiable { +public typealias ReceiptDisposer = [BindingReceipt] + +extension ReceiptDisposer { + public func dispose() { + forEach { disposable in + disposable.dispose() + } + } +} + +public final class BindingReceipt: Hashable, Identifiable { public let id = UUID() public func hash(into hasher: inout Hasher) { hasher.combine(id) } public static func == (lhs: BindingReceipt, rhs: BindingReceipt) -> Bool { lhs.id == rhs.id } + + public var dispose: (() -> Void)! + + deinit { + dispose() + } + + public func add(to disposal: inout ReceiptDisposer) { + disposal.append(self) + } } public class Observable: ObservableThing { @@ -36,14 +56,24 @@ public class Observable: ObservableThing { notifyObservers(value) } } + + fileprivate var _onDispose: () -> Void - public init(_ value: ObservedType? = nil) { + public init(_ value: ObservedType? = nil, onDispose: @escaping () -> Void = {}) { self.value = value + self._onDispose = onDispose } @discardableResult public func bind(observer: @escaping Observer) -> BindingReceipt { let r = BindingReceipt() + + r.dispose = { [weak self] in + self?.observers[r] = nil + self?.bindings[r] = nil + self?._onDispose() + } + observers[r] = observer return r } From c6dbd8ede524e7fafee4d2f17ca3f651213a4864 Mon Sep 17 00:00:00 2001 From: Arthur da Paz Date: Thu, 10 Sep 2020 15:02:32 -0300 Subject: [PATCH 08/12] resolvendo conflitos --- .../NSObject+Observable.swift | 2 +- .../Observable+FunctionalConvenience.swift | 30 ++++----- Sources/SimpleTwoWayBinding/Observable.swift | 61 +++++-------------- 3 files changed, 30 insertions(+), 63 deletions(-) diff --git a/Sources/SimpleTwoWayBinding/NSObject+Observable.swift b/Sources/SimpleTwoWayBinding/NSObject+Observable.swift index 2410965..2897dcb 100644 --- a/Sources/SimpleTwoWayBinding/NSObject+Observable.swift +++ b/Sources/SimpleTwoWayBinding/NSObject+Observable.swift @@ -9,7 +9,7 @@ import Foundation extension NSObject { @discardableResult - public func observe(for observable: Observable, with: @escaping (T) -> ()) -> BindingReceipt { + public func observe(for observable: Observable, with: @escaping (T) -> Void) -> BindingReceipt { observable.bind { observable, value in DispatchQueue.main.async { with(value) diff --git a/Sources/SimpleTwoWayBinding/Observable+FunctionalConvenience.swift b/Sources/SimpleTwoWayBinding/Observable+FunctionalConvenience.swift index 737310e..6df7dd4 100644 --- a/Sources/SimpleTwoWayBinding/Observable+FunctionalConvenience.swift +++ b/Sources/SimpleTwoWayBinding/Observable+FunctionalConvenience.swift @@ -10,7 +10,7 @@ import Foundation public extension Observable { -/// Bind to this observable with a simple value function, optionally replaying the existing value into the stream immediately + /// Bind to this observable with a simple value function, optionally replaying the existing value into the stream immediately /// /// This is a nice alternative to the standard `bind((Observable, ObservedType)->Void)`, since we're 99% of the time uninterested in getting a reference to the Observable itself. /// - Parameters: @@ -40,7 +40,7 @@ public extension Observable { } return r } - + /// Bind to this observable with an object/keypath pair /// - Parameters: /// - replay: If there's a value in this observable, after setting up the binding immediately fire the observation function with that value, rather than the default behavior of waiting for a new value to come into the stream. Defaults to true. @@ -51,7 +51,7 @@ public extension Observable { func bind(replay: Bool = true, on queue: DispatchQueue? = nil, _ target: inout Root, _ path: WritableKeyPath) -> BindingReceipt { bind(replay: replay, on: queue) { [weak target] value in target?[keyPath: path] = value } } - + /// Create a new observable whose value is mapped from this observable's values /// - Parameters: /// - replay: If there's a value in this observable, after setting up the binding immediately fire the observation function with that value, rather than the default behavior of waiting for a new value to come into the stream. Defaults to true. @@ -64,7 +64,7 @@ public extension Observable { child.setObserving({ _ = self }, receipt: r) return child } - + /// Create a new observable of the same type as this observable, whose value is filtered before delivery /// - Parameters: /// - replay: If there's a value in this observable, after setting up the binding immediately fire the observation function with that value, rather than the default behavior of waiting for a new value to come into the stream. Defaults to true. @@ -79,7 +79,7 @@ public extension Observable { child.setObserving({ _ = self }, receipt: r) return child } - + /// Create a new observable whose value is defined by integrating this observable's value with the new observable's value using the `reducer` function /// - Parameters: /// - replay: If there's a value in this observable, after setting up the binding immediately fire the observation function with that value, rather than the default behavior of waiting for a new value to come into the stream. Defaults to true. @@ -94,14 +94,14 @@ public extension Observable { child.setObserving({ _ = self }, receipt: r) return child } - + func debug(_ message: String) -> Observable { map { value in print(message + " (Current value: \(value))") return value } } - + /// Creates a new observable whose value is mapped from this observable's values, unless the mapping function returns nil /// - Parameters: /// - replay: If there's a value in this observable, after setting up the binding immediately fire the observation function with that value, rather than the default behavior of waiting for a new value to come into the stream. Defaults to true. @@ -139,11 +139,11 @@ let ObserverZipThread = DispatchQueue(label: "RWGPS.Observer.Zipping") private class Zip2Observable: Observable<(A?, B?)> { weak var a: Observable? weak var b: Observable? - + init(_ a: Observable, _ b: Observable) { self.a = a self.b = b - + super.init() let ra = a.bind(replay: false) { [weak self] a in self?.value = (a, self?.b?.value) @@ -163,12 +163,12 @@ public func zip(_ a: Observable, _ b: Observable) -> Observable<(A?, private class Zip3Observable: Observable<(A?, B?, C?)> { weak var ab: Zip2Observable? weak var c: Observable? - + init(_ a: Observable, _ b: Observable, _ c: Observable) { let ab = Zip2Observable(a, b) self.ab = ab self.c = c - + super.init() let rab = ab.bind(replay: false) { [weak self] ab in self?.value = (ab.0, ab.1, self?.c?.value) @@ -187,12 +187,12 @@ public func zip(_ a: Observable, _ b: Observable, _ c: Observable private class Zip4Observable: Observable<(A?, B?, C?, D?)> { weak var abc: Zip3Observable? weak var d: Observable? - + init(_ a: Observable, _ b: Observable, _ c: Observable, _ d: Observable) { let abc = Zip3Observable(a, b, c) self.abc = abc self.d = d - + super.init() let rabc = abc.bind(replay: false) { [weak self] abc in self?.value = (abc.0, abc.1, abc.2, self?.d?.value) @@ -211,12 +211,12 @@ public func zip(_ a: Observable, _ b: Observable, _ c: Observa private class Zip5Observable: Observable<(A?, B?, C?, D?, E?)> { weak var abcd: Zip4Observable? weak var e: Observable? - + init(_ a: Observable, _ b: Observable, _ c: Observable, _ d: Observable, _ e: Observable) { let abcd = Zip4Observable(a, b, c, d) self.abcd = abcd self.e = e - + super.init() let rabcd = abcd.bind(replay: false) { [weak self] abcd in self?.value = (abcd.0, abcd.1, abcd.2, abcd.3, self?.e?.value) diff --git a/Sources/SimpleTwoWayBinding/Observable.swift b/Sources/SimpleTwoWayBinding/Observable.swift index 85f3ed7..b3f9b9f 100644 --- a/Sources/SimpleTwoWayBinding/Observable.swift +++ b/Sources/SimpleTwoWayBinding/Observable.swift @@ -8,48 +8,26 @@ import Foundation import UIKit -public protocol ObservableThing { } - -public typealias ReceiptDisposer = [BindingReceipt] - -extension ReceiptDisposer { - public func dispose() { - forEach { disposable in - disposable.dispose() - } - } -} - -public final class BindingReceipt: Hashable, Identifiable { +public struct BindingReceipt: Hashable, Identifiable { public let id = UUID() public func hash(into hasher: inout Hasher) { hasher.combine(id) } public static func == (lhs: BindingReceipt, rhs: BindingReceipt) -> Bool { lhs.id == rhs.id } - - public var dispose: (() -> Void)! - - deinit { - dispose() - } - - public func add(to disposal: inout ReceiptDisposer) { - disposal.append(self) - } } -public class Observable: ObservableThing { +public class Observable { public typealias Observer = (_ observable: Observable, ObservedType) -> Void - + /// Map of receipt objects to the binding blocks those objects represent; see bind(observer:) and unbind(:) private var observers: [BindingReceipt: Observer] = [:] /// Map of other observers we've been bound to; see map(:) & other functional conveniences. This allows us to hold strong references to the anonymous observables generated in a chained series of calls, and break them when needed. private var bindings: [BindingReceipt: () -> Void] = [:] - + internal var paused: Bool = false - + public var value: ObservedType? { didSet { fire() } } - + /// Notify all observers with the current value if non-nil. public func fire() { if let value = value { @@ -57,31 +35,21 @@ public class Observable: ObservableThing { } } - fileprivate var _onDispose: () -> Void - - public init(_ value: ObservedType? = nil, onDispose: @escaping () -> Void = {}) { + public init(_ value: ObservedType? = nil) { self.value = value - self._onDispose = onDispose } - + @discardableResult public func bind(observer: @escaping Observer) -> BindingReceipt { let r = BindingReceipt() - - r.dispose = { [weak self] in - self?.observers[r] = nil - self?.bindings[r] = nil - self?._onDispose() - } - observers[r] = observer return r } - + public func setObserving(_ referenceHolder: @escaping () -> Void, receipt: BindingReceipt) { bindings[receipt] = referenceHolder } - + public func unbind(_ r: BindingReceipt) { guard observers[r] != nil else { print("Warning: attempted to unbind with an invalid receipt") @@ -90,15 +58,14 @@ public class Observable: ObservableThing { observers[r] = nil bindings[r] = nil } - + internal func notifyObservers(_ value: ObservedType) { observers.values.forEach { [unowned self] observer in guard paused == false else { return } observer(self, value) } } - - - -} + + +} From c30c27009e06e0944e4456c2ad091b18c8c564c6 Mon Sep 17 00:00:00 2001 From: Ryan Forsythe Date: Tue, 1 Dec 2020 12:59:12 -0800 Subject: [PATCH 09/12] Allow keypath binding to set values on properties of type Optional --- .../Observable+FunctionalConvenience.swift | 41 ++++++++++++------- .../SimpleTwoWayBindingTests.swift | 20 +++++++++ 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/Sources/SimpleTwoWayBinding/Observable+FunctionalConvenience.swift b/Sources/SimpleTwoWayBinding/Observable+FunctionalConvenience.swift index 6df7dd4..b9fb77e 100644 --- a/Sources/SimpleTwoWayBinding/Observable+FunctionalConvenience.swift +++ b/Sources/SimpleTwoWayBinding/Observable+FunctionalConvenience.swift @@ -10,7 +10,7 @@ import Foundation public extension Observable { - /// Bind to this observable with a simple value function, optionally replaying the existing value into the stream immediately +/// Bind to this observable with a simple value function, optionally replaying the existing value into the stream immediately /// /// This is a nice alternative to the standard `bind((Observable, ObservedType)->Void)`, since we're 99% of the time uninterested in getting a reference to the Observable itself. /// - Parameters: @@ -40,7 +40,7 @@ public extension Observable { } return r } - + /// Bind to this observable with an object/keypath pair /// - Parameters: /// - replay: If there's a value in this observable, after setting up the binding immediately fire the observation function with that value, rather than the default behavior of waiting for a new value to come into the stream. Defaults to true. @@ -51,7 +51,18 @@ public extension Observable { func bind(replay: Bool = true, on queue: DispatchQueue? = nil, _ target: inout Root, _ path: WritableKeyPath) -> BindingReceipt { bind(replay: replay, on: queue) { [weak target] value in target?[keyPath: path] = value } } - + + /// Bind to this observable with an object/keypath pair + /// - Parameters: + /// - replay: If there's a value in this observable, after setting up the binding immediately fire the observation function with that value, rather than the default behavior of waiting for a new value to come into the stream. Defaults to true. + /// - queue: (Optional) Queue to run the binding function on when it fires; nil runs on whatever queue the value was set from. Defaults to nil. + /// - target: object to use writeable keypath on + /// - path: a writeable keypath to a property of the target to set + @discardableResult + func bind(replay: Bool = true, on queue: DispatchQueue? = nil, _ target: inout Root, _ path: WritableKeyPath>) -> BindingReceipt { + bind(replay: replay, on: queue) { [weak target] value in target?[keyPath: path] = value } + } + /// Create a new observable whose value is mapped from this observable's values /// - Parameters: /// - replay: If there's a value in this observable, after setting up the binding immediately fire the observation function with that value, rather than the default behavior of waiting for a new value to come into the stream. Defaults to true. @@ -64,7 +75,7 @@ public extension Observable { child.setObserving({ _ = self }, receipt: r) return child } - + /// Create a new observable of the same type as this observable, whose value is filtered before delivery /// - Parameters: /// - replay: If there's a value in this observable, after setting up the binding immediately fire the observation function with that value, rather than the default behavior of waiting for a new value to come into the stream. Defaults to true. @@ -79,7 +90,7 @@ public extension Observable { child.setObserving({ _ = self }, receipt: r) return child } - + /// Create a new observable whose value is defined by integrating this observable's value with the new observable's value using the `reducer` function /// - Parameters: /// - replay: If there's a value in this observable, after setting up the binding immediately fire the observation function with that value, rather than the default behavior of waiting for a new value to come into the stream. Defaults to true. @@ -94,14 +105,14 @@ public extension Observable { child.setObserving({ _ = self }, receipt: r) return child } - + func debug(_ message: String) -> Observable { map { value in print(message + " (Current value: \(value))") return value } } - + /// Creates a new observable whose value is mapped from this observable's values, unless the mapping function returns nil /// - Parameters: /// - replay: If there's a value in this observable, after setting up the binding immediately fire the observation function with that value, rather than the default behavior of waiting for a new value to come into the stream. Defaults to true. @@ -139,11 +150,11 @@ let ObserverZipThread = DispatchQueue(label: "RWGPS.Observer.Zipping") private class Zip2Observable: Observable<(A?, B?)> { weak var a: Observable? weak var b: Observable? - + init(_ a: Observable, _ b: Observable) { self.a = a self.b = b - + super.init() let ra = a.bind(replay: false) { [weak self] a in self?.value = (a, self?.b?.value) @@ -163,12 +174,12 @@ public func zip(_ a: Observable, _ b: Observable) -> Observable<(A?, private class Zip3Observable: Observable<(A?, B?, C?)> { weak var ab: Zip2Observable? weak var c: Observable? - + init(_ a: Observable, _ b: Observable, _ c: Observable) { let ab = Zip2Observable(a, b) self.ab = ab self.c = c - + super.init() let rab = ab.bind(replay: false) { [weak self] ab in self?.value = (ab.0, ab.1, self?.c?.value) @@ -187,12 +198,12 @@ public func zip(_ a: Observable, _ b: Observable, _ c: Observable private class Zip4Observable: Observable<(A?, B?, C?, D?)> { weak var abc: Zip3Observable? weak var d: Observable? - + init(_ a: Observable, _ b: Observable, _ c: Observable, _ d: Observable) { let abc = Zip3Observable(a, b, c) self.abc = abc self.d = d - + super.init() let rabc = abc.bind(replay: false) { [weak self] abc in self?.value = (abc.0, abc.1, abc.2, self?.d?.value) @@ -211,12 +222,12 @@ public func zip(_ a: Observable, _ b: Observable, _ c: Observa private class Zip5Observable: Observable<(A?, B?, C?, D?, E?)> { weak var abcd: Zip4Observable? weak var e: Observable? - + init(_ a: Observable, _ b: Observable, _ c: Observable, _ d: Observable, _ e: Observable) { let abcd = Zip4Observable(a, b, c, d) self.abcd = abcd self.e = e - + super.init() let rabcd = abcd.bind(replay: false) { [weak self] abcd in self?.value = (abcd.0, abcd.1, abcd.2, abcd.3, self?.e?.value) diff --git a/Tests/SimpleTwoWayBindingTests/SimpleTwoWayBindingTests.swift b/Tests/SimpleTwoWayBindingTests/SimpleTwoWayBindingTests.swift index dc1e663..2d3e7ab 100644 --- a/Tests/SimpleTwoWayBindingTests/SimpleTwoWayBindingTests.swift +++ b/Tests/SimpleTwoWayBindingTests/SimpleTwoWayBindingTests.swift @@ -506,4 +506,24 @@ class SimpleTwoWayBindingTests: XCTestCase { wait(for: [bindFired, asyncMainFinished], timeout: 1) } + + func testKeypathBinding() { + class Foo { + var bar: Int + var qux: Int? + + init(_ i: Int) { + bar = i + } + } + + var f = Foo(0) + + let a: Observable = Observable() + + a.bind(&f, \.bar) + a.bind(&f, \.qux) + a.value = 1 + XCTAssertEqual(f.bar, f.qux ?? Int.min) + } } From ac3062ab269be78f083341e0bac371b9b06f29fb Mon Sep 17 00:00:00 2001 From: Arthur da Paz Date: Thu, 28 Jan 2021 17:13:10 -0300 Subject: [PATCH 10/12] Downgrade swift-tools --- .gitignore | 1 + Package.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a119ed5..c4d34a5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .DS_Store # Xcode +.swiftpm build/ *.pbxuser !default.pbxuser diff --git a/Package.swift b/Package.swift index 8143b1d..d0356c9 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.2 +// swift-tools-version:5.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription From 5219c9e79b9b6aba281d5b56a437b5c7cc15430d Mon Sep 17 00:00:00 2001 From: Arthur da Paz Date: Fri, 29 Jan 2021 21:30:27 -0300 Subject: [PATCH 11/12] Added UISegmentedControl replay --- Sources/SimpleTwoWayBinding/Bindable.swift | 28 +++++++++---------- .../UIControls+Bindable.swift | 15 ++++++++++ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/Sources/SimpleTwoWayBinding/Bindable.swift b/Sources/SimpleTwoWayBinding/Bindable.swift index 67a37bd..cec1e2e 100644 --- a/Sources/SimpleTwoWayBinding/Bindable.swift +++ b/Sources/SimpleTwoWayBinding/Bindable.swift @@ -55,16 +55,19 @@ extension Bindable where Self: NSObject { @discardableResult public func bind(with observable: Observable) -> BindingReceipt { + + if let control = self as? UIControl { - if self is UIControl { - let sleeve = ActionClosure { - [weak self] in + let memoryAddress = String(format: "%p", unsafeBitCast(self, to: UInt.self)) + let sleeve = ActionClosure { [weak self] in self?.valueChanged() } - - (self as! UIControl).addTarget(sleeve, action: #selector(ActionClosure.invoke), for: [.valueChanged, .editingChanged]) - - objc_setAssociatedObject(self, memoryAddress, sleeve, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + + control.addTarget(sleeve, + action: #selector(ActionClosure.invoke), + for: [.valueChanged, .editingChanged]) + + objc_setAssociatedObject(control, memoryAddress, sleeve, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } self.binder = observable @@ -75,17 +78,14 @@ extension Bindable where Self: NSObject { return self.observe(for: observable) { (value) in self.updateValue(with: value) } - } + } } -extension NSObject { - fileprivate var memoryAddress: String { - String(format: "%p", unsafeBitCast(self, to: UInt.self)) - } +private extension NSObject { - @objc fileprivate final class ActionClosure: NSObject { - let closure: () -> Void + @objc final class ActionClosure: NSObject { + private let closure: () -> Void init(_ closure: @escaping () -> Void) { self.closure = closure diff --git a/Sources/SimpleTwoWayBinding/UIControls+Bindable.swift b/Sources/SimpleTwoWayBinding/UIControls+Bindable.swift index 4eac52e..5436e7d 100644 --- a/Sources/SimpleTwoWayBinding/UIControls+Bindable.swift +++ b/Sources/SimpleTwoWayBinding/UIControls+Bindable.swift @@ -67,6 +67,21 @@ extension UISegmentedControl : Bindable { public func updateValue(with value: Int) { self.selectedSegmentIndex = value } + + public func bind(replay: Bool = false, with observable: Observable) -> BindingReceipt { + self.register(for: observable) + let r = self.observe(for: observable) { [weak self] (value) in + self?.updateValue(with: value) + } + if let s = observable.value { + DispatchQueue.main.async { [weak self] in + self?.updateValue(with: s) + } + + } + return r + } + } public class BindableTextView: UITextView, Bindable, UITextViewDelegate { From 889533ca9b6db1374eca14834e66ad3889faebad Mon Sep 17 00:00:00 2001 From: Arthur da Paz Date: Fri, 29 Jan 2021 21:45:44 -0300 Subject: [PATCH 12/12] Rollback --- .../UIControls+Bindable.swift | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/Sources/SimpleTwoWayBinding/UIControls+Bindable.swift b/Sources/SimpleTwoWayBinding/UIControls+Bindable.swift index 5436e7d..105cef7 100644 --- a/Sources/SimpleTwoWayBinding/UIControls+Bindable.swift +++ b/Sources/SimpleTwoWayBinding/UIControls+Bindable.swift @@ -7,7 +7,7 @@ import UIKit -extension UITextField : Bindable { +extension UITextField: Bindable { public typealias BindingType = String public func observingValue() -> String? { @@ -21,7 +21,7 @@ extension UITextField : Bindable { } } -extension UISwitch : Bindable { +extension UISwitch: Bindable { public typealias BindingType = Bool public func observingValue() -> Bool? { @@ -33,7 +33,7 @@ extension UISwitch : Bindable { } } -extension UISlider : Bindable { +extension UISlider: Bindable { public typealias BindingType = Float public func observingValue() -> Float? { @@ -45,7 +45,7 @@ extension UISlider : Bindable { } } -extension UIStepper : Bindable { +extension UIStepper: Bindable { public typealias BindingType = Double public func observingValue() -> Double? { @@ -57,7 +57,7 @@ extension UIStepper : Bindable { } } -extension UISegmentedControl : Bindable { +extension UISegmentedControl: Bindable { public typealias BindingType = Int public func observingValue() -> Int? { @@ -67,21 +67,6 @@ extension UISegmentedControl : Bindable { public func updateValue(with value: Int) { self.selectedSegmentIndex = value } - - public func bind(replay: Bool = false, with observable: Observable) -> BindingReceipt { - self.register(for: observable) - let r = self.observe(for: observable) { [weak self] (value) in - self?.updateValue(with: value) - } - if let s = observable.value { - DispatchQueue.main.async { [weak self] in - self?.updateValue(with: s) - } - - } - return r - } - } public class BindableTextView: UITextView, Bindable, UITextViewDelegate {