diff --git a/RxCocoa/Common/CLLocationManager+Rx.swift b/RxCocoa/Common/CLLocationManager+Rx.swift index 18318c7c..e22e3e90 100644 --- a/RxCocoa/Common/CLLocationManager+Rx.swift +++ b/RxCocoa/Common/CLLocationManager+Rx.swift @@ -20,7 +20,7 @@ extension CLLocationManager { For more information take a look at `DelegateProxyType` protocol documentation. */ public var rx_delegate: DelegateProxy { - return proxyForObject(RxCLLocationManagerDelegateProxy.self, self) + return RxCLLocationManagerDelegateProxy.proxyForObject(self) } // MARK: Responding to Location Events diff --git a/RxCocoa/Common/DelegateProxyType.swift b/RxCocoa/Common/DelegateProxyType.swift index 14e6d509..ea38f8e9 100644 --- a/RxCocoa/Common/DelegateProxyType.swift +++ b/RxCocoa/Common/DelegateProxyType.swift @@ -147,82 +147,100 @@ public protocol DelegateProxyType : AnyObject { func setForwardToDelegate(forwardToDelegate: AnyObject?, retainDelegate: Bool) } -/** -Returns existing proxy for object or installs new instance of delegate proxy. - -- parameter object: Target object on which to install delegate proxy. -- returns: Installed instance of delegate proxy. - - - extension UISearchBar { - - public var rx_delegate: DelegateProxy { - return proxyForObject(RxSearchBarDelegateProxy.self, self) - } - - public var rx_text: ControlProperty { - let source: Observable = self.rx_delegate.observe("searchBar:textDidChange:") - ... - } - } -*/ +@available(*, deprecated=2.5, renamed="DelegateProxyType.proxyForObject", message="You can just use normal static protocol extension. E.g. `RxScrollViewDelegateProxy.proxyForObject`") public func proxyForObject(type: P.Type, _ object: AnyObject) -> P { - MainScheduler.ensureExecutingOnScheduler() - - let maybeProxy = P.assignedProxyFor(object) as? P - - let proxy: P - if maybeProxy == nil { - proxy = P.createProxyForObject(object) as! P - P.assignProxy(proxy, toObject: object) - assert(P.assignedProxyFor(object) === proxy) - } - else { - proxy = maybeProxy! - } - - let currentDelegate: AnyObject? = P.currentDelegateFor(object) - - if currentDelegate !== proxy { - proxy.setForwardToDelegate(currentDelegate, retainDelegate: false) - P.setCurrentDelegate(proxy, toObject: object) - assert(P.currentDelegateFor(object) === proxy) - assert(proxy.forwardToDelegate() === currentDelegate) - } - - return proxy + return P.proxyForObject(object) } -func installDelegate(proxy: P, delegate: AnyObject, retainDelegate: Bool, onProxyForObject object: AnyObject) -> Disposable { - weak var weakDelegate: AnyObject? = delegate - - assert(proxy.forwardToDelegate() === nil, "There is already a delegate set -> `\(proxy.forwardToDelegate()!)` for object -> `\(object)`.\nMaybe delegate was already set in `xib` or `storyboard` and now it's being overwritten in code.") - - proxy.setForwardToDelegate(delegate, retainDelegate: retainDelegate) - - // refresh properties after delegate is set - // some views like UITableView cache `respondsToSelector` - P.setCurrentDelegate(nil, toObject: object) - P.setCurrentDelegate(proxy, toObject: object) - - assert(proxy.forwardToDelegate() === delegate, "Setting of delegate failed") - - return AnonymousDisposable { +extension DelegateProxyType { + /** + Returns existing proxy for object or installs new instance of delegate proxy. + + - parameter object: Target object on which to install delegate proxy. + - returns: Installed instance of delegate proxy. + + + extension UISearchBar { + + public var rx_delegate: DelegateProxy { + return proxyForObject(RxSearchBarDelegateProxy.self, self) + } + + public var rx_text: ControlProperty { + let source: Observable = self.rx_delegate.observe("searchBar:textDidChange:") + ... + } + } + */ + public static func proxyForObject(object: AnyObject) -> Self { MainScheduler.ensureExecutingOnScheduler() + + let maybeProxy = Self.assignedProxyFor(object) as? Self + + let proxy: Self + if maybeProxy == nil { + proxy = Self.createProxyForObject(object) as! Self + Self.assignProxy(proxy, toObject: object) + assert(Self.assignedProxyFor(object) === proxy) + } + else { + proxy = maybeProxy! + } + + let currentDelegate: AnyObject? = Self.currentDelegateFor(object) + + if currentDelegate !== proxy { + proxy.setForwardToDelegate(currentDelegate, retainDelegate: false) + Self.setCurrentDelegate(proxy, toObject: object) + assert(Self.currentDelegateFor(object) === proxy) + assert(proxy.forwardToDelegate() === currentDelegate) + } - let delegate: AnyObject? = weakDelegate + return proxy + } + + /** + Sets forward delegate for `DelegateProxyType` associated with a specific object and return disposable that can be used to unset the forward to delegate. + Using this method will also make sure that potential original object cached selectors are cleared and will report any accidental forward delegate mutations. + + - parameter forwardDelegate: Delegate object to set. + - parameter retainDelegate: Retain `forwardDelegate` while it's being set. + - parameter onProxyForObject: Object that has `delegate` property. + - returns: Disposable object that can be used to clear forward delegate. + */ + public static func installForwardDelegate(forwardDelegate: AnyObject, retainDelegate: Bool, onProxyForObject object: AnyObject) -> Disposable { + weak var weakForwardDelegate: AnyObject? = forwardDelegate + + let proxy = Self.proxyForObject(object) - assert(delegate == nil || proxy.forwardToDelegate() === delegate, "Delegate was changed from time it was first set. Current \(proxy.forwardToDelegate()), and it should have been \(proxy)") + assert(proxy.forwardToDelegate() === nil, "There is already a delegate set -> `\(proxy.forwardToDelegate()!)` for object -> `\(object)`.\nMaybe delegate was already set in `xib` or `storyboard` and now it's being overwritten in code.") - proxy.setForwardToDelegate(nil, retainDelegate: retainDelegate) + proxy.setForwardToDelegate(forwardDelegate, retainDelegate: retainDelegate) + + // refresh properties after delegate is set + // some views like UITableView cache `respondsToSelector` + Self.setCurrentDelegate(nil, toObject: object) + Self.setCurrentDelegate(proxy, toObject: object) + + assert(proxy.forwardToDelegate() === forwardDelegate, "Setting of delegate failed") + + return AnonymousDisposable { + MainScheduler.ensureExecutingOnScheduler() + + let delegate: AnyObject? = weakForwardDelegate + + assert(delegate == nil || proxy.forwardToDelegate() === delegate, "Delegate was changed from time it was first set. Current \(proxy.forwardToDelegate()), and it should have been \(proxy)") + + proxy.setForwardToDelegate(nil, retainDelegate: retainDelegate) + } } } extension ObservableType { func subscribeProxyDataSourceForObject(object: AnyObject, dataSource: AnyObject, retainDataSource: Bool, binding: (P, Event) -> Void) -> Disposable { - let proxy = proxyForObject(P.self, object) - let disposable = installDelegate(proxy, delegate: dataSource, retainDelegate: retainDataSource, onProxyForObject: object) + let proxy = P.proxyForObject(object) + let disposable = P.installForwardDelegate(dataSource, retainDelegate: retainDataSource, onProxyForObject: object) let subscription = self.asObservable() // source can't ever end, otherwise it will release the subscriber diff --git a/RxCocoa/OSX/NSTextField+Rx.swift b/RxCocoa/OSX/NSTextField+Rx.swift index 568cf4de..cac60d3f 100644 --- a/RxCocoa/OSX/NSTextField+Rx.swift +++ b/RxCocoa/OSX/NSTextField+Rx.swift @@ -93,14 +93,14 @@ extension NSTextField { For more information take a look at `DelegateProxyType` protocol documentation. */ public var rx_delegate: DelegateProxy { - return proxyForObject(RxTextFieldDelegateProxy.self, self) + return RxTextFieldDelegateProxy.proxyForObject(self) } /** Reactive wrapper for `text` property. */ public var rx_text: ControlProperty { - let delegate = proxyForObject(RxTextFieldDelegateProxy.self, self) + let delegate = RxTextFieldDelegateProxy.proxyForObject(self) let source = Observable.deferred { [weak self] in delegate.textSubject.startWith(self?.stringValue ?? "") diff --git a/RxCocoa/iOS/NSTextStorage+Rx.swift b/RxCocoa/iOS/NSTextStorage+Rx.swift index b2c933b5..262fc01d 100644 --- a/RxCocoa/iOS/NSTextStorage+Rx.swift +++ b/RxCocoa/iOS/NSTextStorage+Rx.swift @@ -22,7 +22,7 @@ extension NSTextStorage { For more information take a look at `DelegateProxyType` protocol documentation. */ public var rx_delegate:DelegateProxy { - return proxyForObject(RxTextStorageDelegateProxy.self, self) + return RxTextStorageDelegateProxy.proxyForObject(self) } /** diff --git a/RxCocoa/iOS/UICollectionView+Rx.swift b/RxCocoa/iOS/UICollectionView+Rx.swift index 1f52db3c..66c99cfe 100644 --- a/RxCocoa/iOS/UICollectionView+Rx.swift +++ b/RxCocoa/iOS/UICollectionView+Rx.swift @@ -112,7 +112,7 @@ extension UICollectionView { For more information take a look at `DelegateProxyType` protocol documentation. */ public var rx_dataSource: DelegateProxy { - return proxyForObject(RxCollectionViewDataSourceProxy.self, self) + return RxCollectionViewDataSourceProxy.proxyForObject(self) } /** @@ -125,8 +125,7 @@ extension UICollectionView { */ public func rx_setDataSource(dataSource: UICollectionViewDataSource) -> Disposable { - let proxy = proxyForObject(RxCollectionViewDataSourceProxy.self, self) - return installDelegate(proxy, delegate: dataSource, retainDelegate: false, onProxyForObject: self) + return RxCollectionViewDataSourceProxy.installForwardDelegate(dataSource, retainDelegate: false, onProxyForObject: self) } /** diff --git a/RxCocoa/iOS/UIImagePickerController+Rx.swift b/RxCocoa/iOS/UIImagePickerController+Rx.swift index 8e239359..63e9cf15 100644 --- a/RxCocoa/iOS/UIImagePickerController+Rx.swift +++ b/RxCocoa/iOS/UIImagePickerController+Rx.swift @@ -24,7 +24,7 @@ import Foundation For more information take a look at `DelegateProxyType` protocol documentation. */ public var rx_delegate: DelegateProxy { - return proxyForObject(RxImagePickerDelegateProxy.self, self) + return RxImagePickerDelegateProxy.proxyForObject(self) } /** diff --git a/RxCocoa/iOS/UIScrollView+Rx.swift b/RxCocoa/iOS/UIScrollView+Rx.swift index 2aaf8e4d..e0f5e7a8 100644 --- a/RxCocoa/iOS/UIScrollView+Rx.swift +++ b/RxCocoa/iOS/UIScrollView+Rx.swift @@ -31,14 +31,14 @@ extension UIScrollView { For more information take a look at `DelegateProxyType` protocol documentation. */ public var rx_delegate: DelegateProxy { - return proxyForObject(RxScrollViewDelegateProxy.self, self) + return RxScrollViewDelegateProxy.proxyForObject(self) } /** Reactive wrapper for `contentOffset`. */ public var rx_contentOffset: ControlProperty { - let proxy = proxyForObject(RxScrollViewDelegateProxy.self, self) + let proxy = RxScrollViewDelegateProxy.proxyForObject(self) let bindingObserver = UIBindingObserver(UIElement: self) { scrollView, contentOffset in scrollView.contentOffset = contentOffset @@ -57,8 +57,7 @@ extension UIScrollView { */ public func rx_setDelegate(delegate: UIScrollViewDelegate) -> Disposable { - let proxy = proxyForObject(RxScrollViewDelegateProxy.self, self) - return installDelegate(proxy, delegate: delegate, retainDelegate: false, onProxyForObject: self) + return RxScrollViewDelegateProxy.installForwardDelegate(delegate, retainDelegate: false, onProxyForObject: self) } } diff --git a/RxCocoa/iOS/UISearchBar+Rx.swift b/RxCocoa/iOS/UISearchBar+Rx.swift index 73413dc0..358baa73 100644 --- a/RxCocoa/iOS/UISearchBar+Rx.swift +++ b/RxCocoa/iOS/UISearchBar+Rx.swift @@ -35,7 +35,7 @@ extension UISearchBar { For more information take a look at `DelegateProxyType` protocol documentation. */ public var rx_delegate: DelegateProxy { - return proxyForObject(RxSearchBarDelegateProxy.self, self) + return RxSearchBarDelegateProxy.proxyForObject(self) } /** diff --git a/RxCocoa/iOS/UISearchController+Rx.swift b/RxCocoa/iOS/UISearchController+Rx.swift index af47a86d..97e55fad 100644 --- a/RxCocoa/iOS/UISearchController+Rx.swift +++ b/RxCocoa/iOS/UISearchController+Rx.swift @@ -22,7 +22,7 @@ extension UISearchController { For more information take a look at `DelegateProxyType` protocol documentation. */ public var rx_delegate: DelegateProxy { - return proxyForObject(RxSearchControllerDelegateProxy.self, self) + return RxSearchControllerDelegateProxy.proxyForObject(self) } /** Reactive wrapper for `delegate` message. diff --git a/RxCocoa/iOS/UITableView+Rx.swift b/RxCocoa/iOS/UITableView+Rx.swift index a8137199..1245aff5 100644 --- a/RxCocoa/iOS/UITableView+Rx.swift +++ b/RxCocoa/iOS/UITableView+Rx.swift @@ -111,7 +111,7 @@ extension UITableView { For more information take a look at `DelegateProxyType` protocol documentation. */ public var rx_dataSource: DelegateProxy { - return proxyForObject(RxTableViewDataSourceProxy.self, self) + return RxTableViewDataSourceProxy.proxyForObject(self) } /** @@ -124,9 +124,7 @@ extension UITableView { */ public func rx_setDataSource(dataSource: UITableViewDataSource) -> Disposable { - let proxy = proxyForObject(RxTableViewDataSourceProxy.self, self) - - return installDelegate(proxy, delegate: dataSource, retainDelegate: false, onProxyForObject: self) + return RxTableViewDataSourceProxy.installForwardDelegate(dataSource, retainDelegate: false, onProxyForObject: self) } // events diff --git a/Tests/RxCocoaTests/DelegateProxyTest+UIKit.swift b/Tests/RxCocoaTests/DelegateProxyTest+UIKit.swift index ea9a9eb5..1fd5c578 100644 --- a/Tests/RxCocoaTests/DelegateProxyTest+UIKit.swift +++ b/Tests/RxCocoaTests/DelegateProxyTest+UIKit.swift @@ -154,6 +154,10 @@ class UITableViewSubclass1 .observe(#selector(TestDelegateProtocol.testEventHappened(_:))) .map { a in (a[0] as! NSNumber).integerValue } } + + func setMineForwardDelegate(testDelegate: TestDelegateProtocol) -> Disposable { + return RxScrollViewDelegateProxy.installForwardDelegate(testDelegate, retainDelegate: false, onProxyForObject: self) + } } class ExtendTableViewDataSourceProxy @@ -183,6 +187,10 @@ class UITableViewSubclass2 .observe(#selector(TestDelegateProtocol.testEventHappened(_:))) .map { a in (a[0] as! NSNumber).integerValue } } + + func setMineForwardDelegate(testDelegate: TestDelegateProtocol) -> Disposable { + return RxTableViewDataSourceProxy.installForwardDelegate(testDelegate, retainDelegate: false, onProxyForObject: self) + } } class ExtendCollectionViewDelegateProxy @@ -212,6 +220,10 @@ class UICollectionViewSubclass1 .observe(#selector(TestDelegateProtocol.testEventHappened(_:))) .map { a in (a[0] as! NSNumber).integerValue } } + + func setMineForwardDelegate(testDelegate: TestDelegateProtocol) -> Disposable { + return RxScrollViewDelegateProxy.installForwardDelegate(testDelegate, retainDelegate: false, onProxyForObject: self) + } } class ExtendCollectionViewDataSourceProxy @@ -241,6 +253,10 @@ class UICollectionViewSubclass2 .observe(#selector(TestDelegateProtocol.testEventHappened(_:))) .map { a in (a[0] as! NSNumber).integerValue } } + + func setMineForwardDelegate(testDelegate: TestDelegateProtocol) -> Disposable { + return RxCollectionViewDataSourceProxy.installForwardDelegate(testDelegate, retainDelegate: false, onProxyForObject: self) + } } class ExtendScrollViewDelegateProxy @@ -270,6 +286,10 @@ class UIScrollViewSubclass .observe(#selector(TestDelegateProtocol.testEventHappened(_:))) .map { a in (a[0] as! NSNumber).integerValue } } + + func setMineForwardDelegate(testDelegate: TestDelegateProtocol) -> Disposable { + return RxScrollViewDelegateProxy.installForwardDelegate(testDelegate, retainDelegate: false, onProxyForObject: self) + } } #if os(iOS) @@ -301,6 +321,10 @@ class UISearchBarSubclass .observe(#selector(TestDelegateProtocol.testEventHappened(_:))) .map { a in (a[0] as! NSNumber).integerValue } } + + func setMineForwardDelegate(testDelegate: TestDelegateProtocol) -> Disposable { + return RxSearchBarDelegateProxy.installForwardDelegate(testDelegate, retainDelegate: false, onProxyForObject: self) + } } #endif @@ -331,6 +355,10 @@ class UITextViewSubclass .observe(#selector(TestDelegateProtocol.testEventHappened(_:))) .map { a in (a[0] as! NSNumber).integerValue } } + + func setMineForwardDelegate(testDelegate: TestDelegateProtocol) -> Disposable { + return RxScrollViewDelegateProxy.installForwardDelegate(testDelegate, retainDelegate: false, onProxyForObject: self) + } } #if os(iOS) class UISearchControllerSubclass @@ -346,5 +374,9 @@ class UISearchControllerSubclass .observe(#selector(TestDelegateProtocol.testEventHappened(_:))) .map { a in (a[0] as! NSNumber).integerValue } } + + func setMineForwardDelegate(testDelegate: TestDelegateProtocol) -> Disposable { + return RxSearchControllerDelegateProxy.installForwardDelegate(testDelegate, retainDelegate: false, onProxyForObject: self) + } } #endif diff --git a/Tests/RxCocoaTests/DelegateProxyTest.swift b/Tests/RxCocoaTests/DelegateProxyTest.swift index aabcdf44..19d690c2 100644 --- a/Tests/RxCocoaTests/DelegateProxyTest.swift +++ b/Tests/RxCocoaTests/DelegateProxyTest.swift @@ -20,10 +20,42 @@ import UIKit optional func testEventHappened(value: Int) } +@objc class MockTestDelegateProtocol: + NSObject, + TestDelegateProtocol, + UICollectionViewDataSource, + UIScrollViewDelegate, + UITableViewDataSource, + UITableViewDelegate { + var numbers = [Int]() + + func testEventHappened(value: Int) { + numbers.append(value) + } + + func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + fatalError() + } + + func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { + fatalError() + } + + func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + fatalError() + } + + func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { + fatalError() + } +} + protocol TestDelegateControl: NSObjectProtocol { func doThatTest(value: Int) var test: Observable { get } + + func setMineForwardDelegate(testDelegate: TestDelegateProtocol) -> Disposable } // MARK: Tests @@ -208,6 +240,18 @@ extension DelegateProxyTest { } XCTAssertEqual(receivedValue, 382763) + autoreleasepool { + let mine = MockTestDelegateProtocol() + let disposable = control.setMineForwardDelegate(mine) + + XCTAssertEqual(mine.numbers, []) + control.doThatTest(2) + XCTAssertEqual(mine.numbers, [2]) + disposable.dispose() + control.doThatTest(3) + XCTAssertEqual(mine.numbers, [2]) + } + XCTAssertFalse(deallocated) XCTAssertFalse(completed) autoreleasepool { @@ -280,7 +324,7 @@ class ThreeDSectionedViewDelegateProxy : DelegateProxy extension ThreeDSectionedView { var rx_proxy: DelegateProxy { - return proxyForObject(ThreeDSectionedViewDelegateProxy.self, self) + return ThreeDSectionedViewDelegateProxy.proxyForObject(self) } }