Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forward delegate #2

Merged
merged 5 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions Example/ExampleTests/UICollectionViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,33 @@ class UICollectionViewTests: XCTestCase {
XCTAssertEqual(didScroll, true)
XCTAssertEqual(didSelect, true)
}

func test_setDelegate() {
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout())
let delegate = StubCollectionViewDelegate()

var resultIndexPath: IndexPath? = nil

collectionView.didSelectItemPublisher
.sink(receiveValue: { resultIndexPath = $0 })
.store(in: &subscriptions)

collectionView.setDelegate(delegate)
.store(in: &subscriptions)

let givenIndexPath = IndexPath(row: 1, section: 0)
collectionView.delegate!.collectionView!(collectionView, didSelectItemAt: givenIndexPath)
let offset = collectionView.delegate!.collectionView!(collectionView, targetContentOffsetForProposedContentOffset: CGPoint(x: 0, y: 0))

XCTAssertEqual(resultIndexPath, givenIndexPath)
XCTAssertEqual(offset, StubCollectionViewDelegate.offset)
}
}

private class StubCollectionViewDelegate: NSObject, UICollectionViewDelegate {
static let offset = CGPoint(x: 1, y: 2)

func collectionView( _ collectionView: UICollectionView, targetContentOffsetForProposedContentOffset proposedContentOffset: CGPoint ) -> CGPoint {
Self.offset
}
}
28 changes: 28 additions & 0 deletions Example/ExampleTests/UIScrollViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,32 @@ class UIScrollViewTests: XCTestCase {
XCTAssertEqual(resultView, givenView)
XCTAssertEqual(resultScale, givenScale)
}

func test_setDelegate() {
let scrollView = UIScrollView()
let delegate = StubScrollViewDelegate()

var didScroll = false

scrollView.didScrollPublisher
.sink(receiveValue: { didScroll = true })
.store(in: &subscriptions)

scrollView.setDelegate(delegate)
.store(in: &subscriptions)

scrollView.delegate!.scrollViewDidScroll!(scrollView)
let viewForZooming = scrollView.delegate!.viewForZooming!(in: scrollView)

XCTAssertEqual(didScroll, true)
XCTAssertEqual(viewForZooming, StubScrollViewDelegate.view)
}
}

private class StubScrollViewDelegate: NSObject, UIScrollViewDelegate {
static let view = UIView()

func viewForZooming(in scrollView: UIScrollView) -> UIView? {
Self.view
}
}
30 changes: 30 additions & 0 deletions Example/ExampleTests/UISearchBarTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,34 @@ class UISearchBarTests: XCTestCase {
XCTAssertEqual(clicked, true)
subscription.cancel()
}

func test_setDelegate() {
var subscriptions = Set<AnyCancellable>()
let searchbar = UISearchBar()
let delegate = StubSearchBarDelegate()

var resultSearchText = ""

searchbar.textDidChangePublisher
.sink(receiveValue: { resultSearchText = $0 })
.store(in: &subscriptions)

searchbar.setDelegate(delegate)
.store(in: &subscriptions)

let givenSearchText = "Hello world"
searchbar.delegate!.searchBar!(searchbar, textDidChange: givenSearchText)
let shouldBeginEditing = searchbar.delegate!.searchBarShouldBeginEditing!(searchbar)

XCTAssertEqual(resultSearchText, givenSearchText)
XCTAssertEqual(shouldBeginEditing, StubSearchBarDelegate.shouldBeginEditing)
}
}

private class StubSearchBarDelegate: NSObject, UISearchBarDelegate {
static let shouldBeginEditing = true

func searchBarShouldBeginEditing(_ searchBar: UISearchBar) -> Bool {
Self.shouldBeginEditing
}
}
31 changes: 31 additions & 0 deletions Example/ExampleTests/UITableViewTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,35 @@ class UITableViewTests: XCTestCase {
XCTAssertEqual(firstResultIndexPaths, [givenIndexPath])
XCTAssertEqual(firstResultIndexPaths, secondResultIndexPaths)
}

func test_setDelegate() {
let tableView = UITableView()
let delegate = StubTableViewDelegate()

var resultIndexPath: IndexPath? = nil

tableView.didSelectRowPublisher
.sink(receiveValue: { resultIndexPath = $0 })
.store(in: &subscriptions)

tableView.setDelegate(delegate)
.store(in: &subscriptions)

let givenIndexPath = IndexPath(row: 1, section: 0)
tableView.delegate!.tableView!(tableView, didSelectRowAt: givenIndexPath)
let selector = #selector(UITableViewDelegate.tableView(_:heightForRowAt:))
let height = tableView.delegate!.tableView!(tableView, heightForRowAt: givenIndexPath)

XCTAssertTrue(tableView.delegate!.responds(to: selector))
XCTAssertEqual(resultIndexPath, givenIndexPath)
XCTAssertEqual(height, StubTableViewDelegate.height)
}
}

private class StubTableViewDelegate: NSObject, UITableViewDelegate {
static let height = 10.0

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
Self.height
}
}
2 changes: 2 additions & 0 deletions Sources/CombineCocoa/Controls/NSTextStorage+Combine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public extension NSTextStorage {

@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
private class NSTextStorageDelegateProxy: DelegateProxy, NSTextStorageDelegate, DelegateProxyType {
typealias Delegate = NSTextStorageDelegate

func setDelegate(to object: NSTextStorage) {
object.delegate = self
}
Expand Down
6 changes: 6 additions & 0 deletions Sources/CombineCocoa/Controls/UICollectionView+Combine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,16 @@ public extension UICollectionView {
override var delegateProxy: DelegateProxy {
CollectionViewDelegateProxy.createDelegateProxy(for: self)
}

func setDelegate(_ delegate: UICollectionViewDelegate) -> Cancellable {
CollectionViewDelegateProxy.installForwardDelegate(delegate, for: self)
}
}

@available(iOS 13.0, *)
private class CollectionViewDelegateProxy: DelegateProxy, UICollectionViewDelegate, DelegateProxyType {
typealias Delegate = UICollectionViewDelegate

func setDelegate(to object: UICollectionView) {
object.delegate = self
}
Expand Down
7 changes: 6 additions & 1 deletion Sources/CombineCocoa/Controls/UIScrollView+Combine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,14 +135,19 @@ public extension UIScrollView {
@objc var delegateProxy: DelegateProxy {
ScrollViewDelegateProxy.createDelegateProxy(for: self)
}

func setDelegate(_ delegate: UIScrollViewDelegate) -> Cancellable {
ScrollViewDelegateProxy.installForwardDelegate(delegate, for: self)
}
}

@available(iOS 13.0, *)
private class ScrollViewDelegateProxy: DelegateProxy, UIScrollViewDelegate, DelegateProxyType {
typealias Delegate = UIScrollViewDelegate

func setDelegate(to object: UIScrollView) {
object.delegate = self
}
}
#endif
// swiftlint:enable force_cast

6 changes: 6 additions & 0 deletions Sources/CombineCocoa/Controls/UISearchBar+Combine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,16 @@ public extension UISearchBar {
private var delegateProxy: UISearchBarDelegateProxy {
.createDelegateProxy(for: self)
}

func setDelegate(_ delegate: UISearchBarDelegate) -> Cancellable {
UISearchBarDelegateProxy.installForwardDelegate(delegate, for: self)
}
}

@available(iOS 13.0, *)
private class UISearchBarDelegateProxy: DelegateProxy, UISearchBarDelegate, DelegateProxyType {
typealias Delegate = UISearchBarDelegate

func setDelegate(to object: UISearchBar) {
object.delegate = self
}
Expand Down
6 changes: 6 additions & 0 deletions Sources/CombineCocoa/Controls/UITableView+Combine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,16 @@ public extension UITableView {
override var delegateProxy: DelegateProxy {
TableViewDelegateProxy.createDelegateProxy(for: self)
}

func setDelegate(_ delegate: UITableViewDelegate) -> Cancellable {
TableViewDelegateProxy.installForwardDelegate(delegate, for: self)
}
}

@available(iOS 13.0, *)
private class TableViewDelegateProxy: DelegateProxy, UITableViewDelegate, DelegateProxyType {
typealias Delegate = UITableViewDelegate

func setDelegate(to object: UITableView) {
object.delegate = self
}
Expand Down
33 changes: 31 additions & 2 deletions Sources/CombineCocoa/DelegateProxy/DelegateProxyType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,42 @@

#if !(os(iOS) && (arch(i386) || arch(arm)))
import Foundation
import Combine

private var associatedKey = "delegateProxy"

public protocol DelegateProxyType {
associatedtype Object
associatedtype Delegate

func setDelegate(to object: Object)
}

@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public extension DelegateProxyType where Self: DelegateProxy {
static func createDelegateProxy(for object: Object) -> Self {
let delegateProxy = proxy(for: object)

delegateProxy.setDelegate(to: object)

return delegateProxy
}

/// Sets forward delegate for `DelegateProxyType` associated with a specific object and return cancellable that can be used to unset the forward to delegate.
///
/// - parameter forwardDelegate: Delegate object to set.
/// - parameter object: Object that has `delegate` property.
/// - returns: Cancellable object that can be used to clear forward delegate.
static func installForwardDelegate(_ forwardDelegate: Delegate, for object: Object) -> Cancellable {
let delegateProxy = proxy(for: object)
delegateProxy.setForwardToDelegate(forwardDelegate)

return AnyCancellable {
delegateProxy.setForwardToDelegate(nil)
}
}

private static func proxy(for object: Object) -> Self {
objc_sync_enter(self)
defer { objc_sync_exit(self) }

Expand All @@ -32,9 +56,14 @@ public extension DelegateProxyType where Self: DelegateProxy {
objc_setAssociatedObject(object, &associatedKey, delegateProxy, .OBJC_ASSOCIATION_RETAIN)
}

delegateProxy.setDelegate(to: object)

return delegateProxy
}

/// Sets reference of normal delegate that receives all forwarded messages through `self`.
///
/// - parameter delegate: Reference of delegate that receives all messages through `self`.
func setForwardToDelegate(_ delegate: Delegate?) {
self._setForwardToDelegate(delegate)
}
}
#endif
17 changes: 17 additions & 0 deletions Sources/Runtime/ObjcDelegateProxy.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@

static NSMutableDictionary<NSValue *, NSSet<NSValue *> *> *allSelectors;

@interface ObjcDelegateProxy () {
id __weak forwardToDelegate;
}
@end

@implementation ObjcDelegateProxy

- (NSSet *)selectors {
Expand Down Expand Up @@ -41,16 +46,28 @@ - (BOOL)canRespondToSelector:(SEL _Nonnull)selector {
return true;
}
}

if (forwardToDelegate && [forwardToDelegate respondsToSelector:selector]) {
return true;
}

return false;
}

- (void)interceptedSelector:(SEL _Nonnull)selector arguments:(NSArray * _Nonnull)arguments {}

-(void)_setForwardToDelegate:(id __nullable)forwardToDelegate {
self->forwardToDelegate = forwardToDelegate;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSArray * _Nonnull arguments = unpackInvocation(anInvocation);
[self interceptedSelector:anInvocation.selector arguments:arguments];

if (forwardToDelegate && [forwardToDelegate respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:forwardToDelegate];
}
}

NSArray * _Nonnull unpackInvocation(NSInvocation * _Nonnull invocation) {
Expand Down
1 change: 1 addition & 0 deletions Sources/Runtime/include/ObjcDelegateProxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
- (void)interceptedSelector:(SEL _Nonnull)selector arguments:(NSArray * _Nonnull)arguments;
- (BOOL)respondsToSelector:(SEL _Nonnull)aSelector;
- (BOOL)canRespondToSelector:(SEL _Nonnull)selector;
- (void)_setForwardToDelegate:(id __nullable)forwardToDelegate NS_SWIFT_NAME(_setForwardToDelegate(_:));

@end