From 6f36838608670c69ffa077a040a42bcae1b067ee Mon Sep 17 00:00:00 2001 From: SumiaFish <39965559+SumiaFish@users.noreply.github.com> Date: Thu, 1 Sep 2022 14:35:51 +0800 Subject: [PATCH 01/11] Update UICollectionView+Combine.swift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加 CollectionView 对 fllowlayout delegate 的支持 --- .../Controls/UICollectionView+Combine.swift | 131 +++++++++++++++++- 1 file changed, 128 insertions(+), 3 deletions(-) diff --git a/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift b/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift index 920fafc..e5a4557 100644 --- a/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift +++ b/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift @@ -16,6 +16,11 @@ import Combine public extension UICollectionView { /// Combine wrapper for `collectionView(_:didSelectItemAt:)` var didSelectItemPublisher: AnyPublisher { + guard let delegateProxy = innerDelegateWrap?.proxy else { + return Empty(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UICollectionViewDelegate.collectionView(_:didSelectItemAt:)) return delegateProxy.interceptSelectorPublisher(selector) .map { $0[1] as! IndexPath } @@ -24,6 +29,11 @@ public extension UICollectionView { /// Combine wrapper for `collectionView(_:didDeselectItemAt:)` var didDeselectItemPublisher: AnyPublisher { + guard let delegateProxy = innerDelegateWrap?.proxy else { + return Empty(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UICollectionViewDelegate.collectionView(_:didDeselectItemAt:)) return delegateProxy.interceptSelectorPublisher(selector) .map { $0[1] as! IndexPath } @@ -32,6 +42,11 @@ public extension UICollectionView { /// Combine wrapper for `collectionView(_:didHighlightItemAt:)` var didHighlightItemPublisher: AnyPublisher { + guard let delegateProxy = innerDelegateWrap?.proxy else { + return Empty(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UICollectionViewDelegate.collectionView(_:didHighlightItemAt:)) return delegateProxy.interceptSelectorPublisher(selector) .map { $0[1] as! IndexPath } @@ -40,6 +55,11 @@ public extension UICollectionView { /// Combine wrapper for `collectionView(_:didUnhighlightItemAt:)` var didUnhighlightRowPublisher: AnyPublisher { + guard let delegateProxy = innerDelegateWrap?.proxy else { + return Empty(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UICollectionViewDelegate.collectionView(_:didUnhighlightItemAt:)) return delegateProxy.interceptSelectorPublisher(selector) .map { $0[1] as! IndexPath } @@ -48,6 +68,11 @@ public extension UICollectionView { /// Combine wrapper for `collectionView(_:willDisplay:forItemAt:)` var willDisplayCellPublisher: AnyPublisher<(cell: UICollectionViewCell, indexPath: IndexPath), Never> { + guard let delegateProxy = innerDelegateWrap?.proxy else { + return Empty<(cell: UICollectionViewCell, indexPath: IndexPath), Never>(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UICollectionViewDelegate.collectionView(_:willDisplay:forItemAt:)) return delegateProxy.interceptSelectorPublisher(selector) .map { ($0[1] as! UICollectionViewCell, $0[2] as! IndexPath) } @@ -56,6 +81,11 @@ public extension UICollectionView { /// Combine wrapper for `collectionView(_:willDisplaySupplementaryView:forElementKind:at:)` var willDisplaySupplementaryViewPublisher: AnyPublisher<(supplementaryView: UICollectionReusableView, elementKind: String, indexPath: IndexPath), Never> { + guard let delegateProxy = innerDelegateWrap?.proxy else { + return Empty<(supplementaryView: UICollectionReusableView, elementKind: String, indexPath: IndexPath), Never>(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UICollectionViewDelegate.collectionView(_:willDisplaySupplementaryView:forElementKind:at:)) return delegateProxy.interceptSelectorPublisher(selector) .map { ($0[1] as! UICollectionReusableView, $0[2] as! String, $0[3] as! IndexPath) } @@ -64,6 +94,11 @@ public extension UICollectionView { /// Combine wrapper for `collectionView(_:didEndDisplaying:forItemAt:)` var didEndDisplayingCellPublisher: AnyPublisher<(cell: UICollectionViewCell, indexPath: IndexPath), Never> { + guard let delegateProxy = innerDelegateWrap?.proxy else { + return Empty<(cell: UICollectionViewCell, indexPath: IndexPath), Never>(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UICollectionViewDelegate.collectionView(_:didEndDisplaying:forItemAt:)) return delegateProxy.interceptSelectorPublisher(selector) .map { ($0[1] as! UICollectionViewCell, $0[2] as! IndexPath) } @@ -72,22 +107,112 @@ public extension UICollectionView { /// Combine wrapper for `collectionView(_:didEndDisplayingSupplementaryView:forElementKind:at:)` var didEndDisplaySupplementaryViewPublisher: AnyPublisher<(supplementaryView: UICollectionReusableView, elementKind: String, indexPath: IndexPath), Never> { + guard let delegateProxy = innerDelegateWrap?.proxy else { + return Empty<(supplementaryView: UICollectionReusableView, elementKind: String, indexPath: IndexPath), Never>(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UICollectionViewDelegate.collectionView(_:didEndDisplayingSupplementaryView:forElementOfKind:at:)) return delegateProxy.interceptSelectorPublisher(selector) .map { ($0[1] as! UICollectionReusableView, $0[2] as! String, $0[3] as! IndexPath) } .eraseToAnyPublisher() } + + @discardableResult + func setCellSize(_ callback: @escaping (_ indexPath: IndexPath) -> CGSize) -> Self { + innerDelegateWrap?.proxy?.cellSizeCallback = callback + return self + } + + @discardableResult + func setInset(_ callback: @escaping (_ section: Int) -> UIEdgeInsets) -> Self { + innerDelegateWrap?.proxy?.insetCallback = callback + return self + } + + @discardableResult + func setMinimumLineSpacing(_ callback: @escaping (_ section: Int) -> CGFloat) -> Self { + innerDelegateWrap?.proxy?.minimumLineSpacingCallback = callback + return self + } + + @discardableResult + func setMinimumInteritemSpacing(_ callback: @escaping (_ section: Int) -> CGFloat) -> Self { + innerDelegateWrap?.proxy?.minimumInteritemSpacingCallback = callback + return self + } + + @discardableResult + func setHeaderSize(_ callback: @escaping (_ section: Int) -> CGSize) -> Self { + innerDelegateWrap?.proxy?.headerSizeCallback = callback + return self + } + + @discardableResult + func setFooterSize(_ callback: @escaping (_ section: Int) -> CGSize) -> Self { + innerDelegateWrap?.proxy?.footerSizeCallback = callback + return self + } - override var delegateProxy: DelegateProxy { - CollectionViewDelegateProxy.createDelegateProxy(for: self) + func useInnerDelegate() { + innerDelegateWrap = .init() + innerDelegateWrap?.proxy = CollectionViewDelegateProxy.createDelegateProxy(for: self) } + + func removeInnerDelegate() { + innerDelegateWrap = nil + } + + static var innerDelegateWrapKey: Void? + private var innerDelegateWrap: CollectionViewDelegateWrap? { + get { objc_getAssociatedObject(self, &UICollectionView.innerDelegateWrapKey) as? CollectionViewDelegateWrap } + set { objc_setAssociatedObject(self, &UICollectionView.innerDelegateWrapKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } + } + +} + +@available(iOS 13.0, *) +fileprivate class CollectionViewDelegateWrap { + + weak var proxy: CollectionViewDelegateProxy? + } @available(iOS 13.0, *) -private class CollectionViewDelegateProxy: DelegateProxy, UICollectionViewDelegate, DelegateProxyType { +private class CollectionViewDelegateProxy: DelegateProxy, UICollectionViewDelegateFlowLayout, DelegateProxyType { func setDelegate(to object: UICollectionView) { object.delegate = self } + + var cellSizeCallback: (_ indexPath: IndexPath) -> CGSize = { _ in CGSize(width: 0.1, height: 0.1) } + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + cellSizeCallback(indexPath) + } + + var insetCallback: (_ section: Int) -> UIEdgeInsets = { _ in .zero } + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + insetCallback(section) + } + + var minimumLineSpacingCallback: (_ section: Int) -> CGFloat = { _ in 0 } + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + minimumLineSpacingCallback(section) + } + + var minimumInteritemSpacingCallback: (_ section: Int) -> CGFloat = { _ in 0 } + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + minimumInteritemSpacingCallback(section) + } + + var headerSizeCallback: (_ section: Int) -> CGSize = { _ in CGSize(width: 0.1, height: 0.1) } + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { + headerSizeCallback(section) + } + + var footerSizeCallback: (_ section: Int) -> CGSize = { _ in CGSize(width: 0.1, height: 0.1) } + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { + footerSizeCallback(section) + } } #endif // swiftlint:enable force_cast From 07b396c2f341e6e7c10ba490fdabb01ec0db2061 Mon Sep 17 00:00:00 2001 From: SumiaFish <39965559+SumiaFish@users.noreply.github.com> Date: Thu, 1 Sep 2022 15:01:41 +0800 Subject: [PATCH 02/11] Update UICollectionView+Combine.swift MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增加对UICollectionViewFllowlayout的支持 --- .../Controls/UICollectionView+Combine.swift | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift b/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift index e5a4557..dd2aeaa 100644 --- a/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift +++ b/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift @@ -157,6 +157,8 @@ public extension UICollectionView { func useInnerDelegate() { innerDelegateWrap = .init() innerDelegateWrap?.proxy = CollectionViewDelegateProxy.createDelegateProxy(for: self) + innerDelegateWrap?.proxy?.wrap = innerDelegateWrap + innerDelegateWrap?.collectionView = self } func removeInnerDelegate() { @@ -169,49 +171,56 @@ public extension UICollectionView { set { objc_setAssociatedObject(self, &UICollectionView.innerDelegateWrapKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } + var flowLayout: UICollectionViewFlowLayout? { + collectionViewLayout as? UICollectionViewFlowLayout + } + } @available(iOS 13.0, *) fileprivate class CollectionViewDelegateWrap { weak var proxy: CollectionViewDelegateProxy? + weak var collectionView: UICollectionView? } @available(iOS 13.0, *) private class CollectionViewDelegateProxy: DelegateProxy, UICollectionViewDelegateFlowLayout, DelegateProxyType { + weak var wrap: CollectionViewDelegateWrap? + func setDelegate(to object: UICollectionView) { object.delegate = self } - var cellSizeCallback: (_ indexPath: IndexPath) -> CGSize = { _ in CGSize(width: 0.1, height: 0.1) } + var cellSizeCallback: ((_ indexPath: IndexPath) -> CGSize)? func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - cellSizeCallback(indexPath) + cellSizeCallback?(indexPath) ?? wrap?.collectionView?.flowLayout?.itemSize ?? CGSize(width: 0.1, height: 0.1) } - var insetCallback: (_ section: Int) -> UIEdgeInsets = { _ in .zero } + var insetCallback: ((_ section: Int) -> UIEdgeInsets)? func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { - insetCallback(section) + insetCallback?(section) ?? wrap?.collectionView?.flowLayout?.sectionInset ?? .zero } - var minimumLineSpacingCallback: (_ section: Int) -> CGFloat = { _ in 0 } + var minimumLineSpacingCallback: ((_ section: Int) -> CGFloat)? func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { - minimumLineSpacingCallback(section) + minimumLineSpacingCallback?(section) ?? wrap?.collectionView?.flowLayout?.minimumLineSpacing ?? 0 } - var minimumInteritemSpacingCallback: (_ section: Int) -> CGFloat = { _ in 0 } + var minimumInteritemSpacingCallback: ((_ section: Int) -> CGFloat)? func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { - minimumInteritemSpacingCallback(section) + minimumInteritemSpacingCallback?(section) ?? wrap?.collectionView?.flowLayout?.minimumInteritemSpacing ?? 0 } - var headerSizeCallback: (_ section: Int) -> CGSize = { _ in CGSize(width: 0.1, height: 0.1) } + var headerSizeCallback: ((_ section: Int) -> CGSize)? func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { - headerSizeCallback(section) + headerSizeCallback?(section) ?? wrap?.collectionView?.flowLayout?.headerReferenceSize ?? .zero } - var footerSizeCallback: (_ section: Int) -> CGSize = { _ in CGSize(width: 0.1, height: 0.1) } + var footerSizeCallback: ((_ section: Int) -> CGSize)? func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { - footerSizeCallback(section) + footerSizeCallback?(section) ?? wrap?.collectionView?.flowLayout?.footerReferenceSize ?? .zero } } #endif From 61ebed6ca3601a8cd13f126e3da8ee4f747982e8 Mon Sep 17 00:00:00 2001 From: SumiaFish <39965559+SumiaFish@users.noreply.github.com> Date: Mon, 5 Sep 2022 18:40:48 +0800 Subject: [PATCH 03/11] Update UIScrollView+Combine.swift --- .../Controls/UIScrollView+Combine.swift | 206 +++++++++++------- 1 file changed, 128 insertions(+), 78 deletions(-) diff --git a/Sources/CombineCocoa/Controls/UIScrollView+Combine.swift b/Sources/CombineCocoa/Controls/UIScrollView+Combine.swift index 0c922a7..56a40f8 100644 --- a/Sources/CombineCocoa/Controls/UIScrollView+Combine.swift +++ b/Sources/CombineCocoa/Controls/UIScrollView+Combine.swift @@ -1,148 +1,198 @@ // -// UIScrollView+Combine.swift +// UITableView+Combine.swift // CombineCocoa // -// Created by Joan Disho on 09/08/2019. +// Created by Joan Disho on 19/01/20. // Copyright © 2020 Combine Community. All rights reserved. // -#if !(os(iOS) && (arch(i386) || arch(arm))) +#if canImport(UIKit) && !(os(iOS) && (arch(i386) || arch(arm))) +import Foundation import UIKit import Combine // swiftlint:disable force_cast @available(iOS 13.0, *) -public extension UIScrollView { - /// A publisher emitting content offset changes from this UIScrollView. - var contentOffsetPublisher: AnyPublisher { - publisher(for: \.contentOffset) +public extension UITableView { + /// Combine wrapper for `tableView(_:willDisplay:forRowAt:)` + var willDisplayCellPublisher: AnyPublisher<(cell: UITableViewCell, indexPath: IndexPath), Never> { + guard let delegateProxy = delegateProxy else { + return Empty<(cell: UITableViewCell, indexPath: IndexPath), Never>(completeImmediately: false) + .eraseToAnyPublisher() + } + + let selector = #selector(UITableViewDelegate.tableView(_:willDisplay:forRowAt:)) + return delegateProxy.interceptSelectorPublisher(selector) + .map { ($0[1] as! UITableViewCell, $0[2] as! IndexPath) } .eraseToAnyPublisher() } - /// A publisher emitting if the bottom of the UIScrollView is reached. - /// - /// - parameter offset: A threshold indicating how close to the bottom of the UIScrollView this publisher should emit. - /// Defaults to 0 - /// - returns: A publisher that emits when the bottom of the UIScrollView is reached within the provided threshold. - func reachedBottomPublisher(offset: CGFloat = 0) -> AnyPublisher { - contentOffsetPublisher - .map { [weak self] contentOffset -> Bool in - guard let self = self else { return false } - let visibleHeight = self.frame.height - self.contentInset.top - self.contentInset.bottom - let yDelta = contentOffset.y + self.contentInset.top - let threshold = max(offset, self.contentSize.height - visibleHeight) - return yDelta > threshold - } - .removeDuplicates() - .filter { $0 } - .map { _ in () } + /// Combine wrapper for `tableView(_:willDisplayHeaderView:forSection:)` + var willDisplayHeaderViewPublisher: AnyPublisher<(headerView: UIView, section: Int), Never> { + guard let delegateProxy = delegateProxy else { + return Empty<(headerView: UIView, section: Int), Never>(completeImmediately: false) + .eraseToAnyPublisher() + } + + let selector = #selector(UITableViewDelegate.tableView(_:willDisplayHeaderView:forSection:)) + return delegateProxy.interceptSelectorPublisher(selector) + .map { ($0[1] as! UIView, $0[2] as! Int) } .eraseToAnyPublisher() } - /// Combine wrapper for `scrollViewDidScroll(_:)` - var didScrollPublisher: AnyPublisher { - let selector = #selector(UIScrollViewDelegate.scrollViewDidScroll(_:)) + /// Combine wrapper for `tableView(_:willDisplayFooterView:forSection:)` + var willDisplayFooterViewPublisher: AnyPublisher<(footerView: UIView, section: Int), Never> { + guard let delegateProxy = delegateProxy else { + return Empty<(footerView: UIView, section: Int), Never>(completeImmediately: false) + .eraseToAnyPublisher() + } + + let selector = #selector(UITableViewDelegate.tableView(_:willDisplayFooterView:forSection:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { _ in () } + .map { ($0[1] as! UIView, $0[2] as! Int) } .eraseToAnyPublisher() } - /// Combine wrapper for `scrollViewWillBeginDecelerating(_:)` - var willBeginDeceleratingPublisher: AnyPublisher { - let selector = #selector(UIScrollViewDelegate.scrollViewWillBeginDecelerating(_:)) + /// Combine wrapper for `tableView(_:didEndDisplaying:forRowAt:)` + var didEndDisplayingCellPublisher: AnyPublisher<(cell: UITableViewCell, indexPath: IndexPath), Never> { + guard let delegateProxy = delegateProxy else { + return Empty<(cell: UITableViewCell, indexPath: IndexPath), Never>(completeImmediately: false) + .eraseToAnyPublisher() + } + + let selector = #selector(UITableViewDelegate.tableView(_:didEndDisplaying:forRowAt:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { _ in () } + .map { ($0[1] as! UITableViewCell, $0[2] as! IndexPath) } .eraseToAnyPublisher() } - /// Combine wrapper for `scrollViewDidEndDecelerating(_:)` - var didEndDeceleratingPublisher: AnyPublisher { - let selector = #selector(UIScrollViewDelegate.scrollViewDidEndDecelerating(_:)) + /// Combine wrapper for `tableView(_:didEndDisplayingHeaderView:forSection:)` + var didEndDisplayingHeaderViewPublisher: AnyPublisher<(headerView: UIView, section: Int), Never> { + guard let delegateProxy = delegateProxy else { + return Empty<(headerView: UIView, section: Int), Never>(completeImmediately: false) + .eraseToAnyPublisher() + } + + let selector = #selector(UITableViewDelegate.tableView(_:didEndDisplayingHeaderView:forSection:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { _ in () } + .map { ($0[1] as! UIView, $0[2] as! Int) } .eraseToAnyPublisher() } - /// Combine wrapper for `scrollViewWillBeginDragging(_:)` - var willBeginDraggingPublisher: AnyPublisher { - let selector = #selector(UIScrollViewDelegate.scrollViewWillBeginDragging(_:)) + /// Combine wrapper for `tableView(_:didEndDisplayingFooterView:forSection:)` + var didEndDisplayingFooterView: AnyPublisher<(headerView: UIView, section: Int), Never> { + guard let delegateProxy = delegateProxy else { + return Empty<(headerView: UIView, section: Int), Never>(completeImmediately: false) + .eraseToAnyPublisher() + } + + let selector = #selector(UITableViewDelegate.tableView(_:didEndDisplayingFooterView:forSection:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { _ in () } + .map { ($0[1] as! UIView, $0[2] as! Int) } .eraseToAnyPublisher() } - /// Combine wrapper for `scrollViewWillEndDragging(_:withVelocity:targetContentOffset:)` - var willEndDraggingPublisher: AnyPublisher<(velocity: CGPoint, targetContentOffset: UnsafeMutablePointer), Never> { - let selector = #selector(UIScrollViewDelegate.scrollViewWillEndDragging(_:withVelocity:targetContentOffset:)) + /// Combine wrapper for `tableView(_:accessoryButtonTappedForRowWith:)` + var itemAccessoryButtonTappedPublisher: AnyPublisher { + guard let delegateProxy = delegateProxy else { + return Empty(completeImmediately: false) + .eraseToAnyPublisher() + } + + let selector = #selector(UITableViewDelegate.tableView(_:accessoryButtonTappedForRowWith:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { values in - let targetContentOffsetValue = values[2] as! NSValue - let rawPointer = targetContentOffsetValue.pointerValue! - - return (values[1] as! CGPoint, rawPointer.bindMemory(to: CGPoint.self, capacity: MemoryLayout.size)) - } + .map { $0[1] as! IndexPath } .eraseToAnyPublisher() } - /// Combine wrapper for `scrollViewDidEndDragging(_:willDecelerate:)` - var didEndDraggingPublisher: AnyPublisher { - let selector = #selector(UIScrollViewDelegate.scrollViewDidEndDragging(_:willDecelerate:)) + /// Combine wrapper for `tableView(_:didHighlightRowAt:)` + var didHighlightRowPublisher: AnyPublisher { + guard let delegateProxy = delegateProxy else { + return Empty(completeImmediately: false) + .eraseToAnyPublisher() + } + + let selector = #selector(UITableViewDelegate.tableView(_:didHighlightRowAt:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { $0[1] as! Bool } + .map { $0[1] as! IndexPath } .eraseToAnyPublisher() } - /// Combine wrapper for `scrollViewDidZoom(_:)` - var didZoomPublisher: AnyPublisher { - let selector = #selector(UIScrollViewDelegate.scrollViewDidZoom(_:)) + /// Combine wrapper for `tableView(_:didUnHighlightRowAt:)` + var didUnhighlightRowPublisher: AnyPublisher { + guard let delegateProxy = delegateProxy else { + return Empty(completeImmediately: false) + .eraseToAnyPublisher() + } + + let selector = #selector(UITableViewDelegate.tableView(_:didUnhighlightRowAt:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { _ in () } + .map { $0[1] as! IndexPath } .eraseToAnyPublisher() } - /// Combine wrapper for `scrollViewDidScrollToTop(_:)` - var didScrollToTopPublisher: AnyPublisher { - let selector = #selector(UIScrollViewDelegate.scrollViewDidScrollToTop(_:)) + /// Combine wrapper for `tableView(_:didSelectRowAt:)` + var didSelectRowPublisher: AnyPublisher { + guard let delegateProxy = delegateProxy else { + return Empty(completeImmediately: false) + .eraseToAnyPublisher() + } + + let selector = #selector(UITableViewDelegate.tableView(_:didSelectRowAt:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { _ in () } + .map { $0[1] as! IndexPath } .eraseToAnyPublisher() } - /// Combine wrapper for `scrollViewDidEndScrollingAnimation(_:)` - var didEndScrollingAnimationPublisher: AnyPublisher { - let selector = #selector(UIScrollViewDelegate.scrollViewDidEndScrollingAnimation(_:)) + /// Combine wrapper for `tableView(_:didDeselectRowAt:)` + var didDeselectRowPublisher: AnyPublisher { + guard let delegateProxy = delegateProxy else { + return Empty(completeImmediately: false) + .eraseToAnyPublisher() + } + + let selector = #selector(UITableViewDelegate.tableView(_:didDeselectRowAt:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { _ in () } + .map { $0[1] as! IndexPath } .eraseToAnyPublisher() } - /// Combine wrapper for `scrollViewWillBeginZooming(_:with:)` - var willBeginZoomingPublisher: AnyPublisher { - let selector = #selector(UIScrollViewDelegate.scrollViewWillBeginZooming(_:with:)) + /// Combine wrapper for `tableView(_:willBeginEditingRowAt:)` + var willBeginEditingRowPublisher: AnyPublisher { + guard let delegateProxy = delegateProxy else { + return Empty(completeImmediately: false) + .eraseToAnyPublisher() + } + + let selector = #selector(UITableViewDelegate.tableView(_:willBeginEditingRowAt:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { $0[1] as! UIView? } + .map { $0[1] as! IndexPath } .eraseToAnyPublisher() } - /// Combine wrapper for `scrollViewDidEndZooming(_:with:atScale:)` - var didEndZooming: AnyPublisher<(view: UIView?, scale: CGFloat), Never> { - let selector = #selector(UIScrollViewDelegate.scrollViewDidEndZooming(_:with:atScale:)) + /// Combine wrapper for `tableView(_:didEndEditingRowAt:)` + var didEndEditingRowPublisher: AnyPublisher { + guard let delegateProxy = delegateProxy else { + return Empty(completeImmediately: false) + .eraseToAnyPublisher() + } + + let selector = #selector(UITableViewDelegate.tableView(_:didEndEditingRowAt:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { ($0[1] as! UIView?, $0[2] as! CGFloat) } + .map { $0[1] as! IndexPath } .eraseToAnyPublisher() } - @objc var delegateProxy: DelegateProxy { - ScrollViewDelegateProxy.createDelegateProxy(for: self) + override public var delegateProxy: DelegateProxy? { + TableViewDelegateProxy.createDelegateProxy(for: self) } } @available(iOS 13.0, *) -private class ScrollViewDelegateProxy: DelegateProxy, UIScrollViewDelegate, DelegateProxyType { - func setDelegate(to object: UIScrollView) { +private class TableViewDelegateProxy: DelegateProxy, UITableViewDelegate, DelegateProxyType { + func setDelegate(to object: UITableView) { object.delegate = self } } #endif // swiftlint:enable force_cast - From a881644a692346d4408fb9eb82c369aaefc5eb39 Mon Sep 17 00:00:00 2001 From: SumiaFish <39965559+SumiaFish@users.noreply.github.com> Date: Mon, 5 Sep 2022 18:41:29 +0800 Subject: [PATCH 04/11] Update UICollectionView+Combine.swift --- Sources/CombineCocoa/Controls/UICollectionView+Combine.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift b/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift index dd2aeaa..de2c99d 100644 --- a/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift +++ b/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift @@ -175,6 +175,10 @@ public extension UICollectionView { collectionViewLayout as? UICollectionViewFlowLayout } + override public var delegateProxy: DelegateProxy? { + innerDelegateWrap?.proxy + } + } @available(iOS 13.0, *) From e8550d9fc83aeda4e3d5b688c9919a4be3628bab Mon Sep 17 00:00:00 2001 From: SumiaFish <39965559+SumiaFish@users.noreply.github.com> Date: Mon, 5 Sep 2022 18:41:56 +0800 Subject: [PATCH 05/11] Update UITableView+Combine.swift --- .../Controls/UITableView+Combine.swift | 67 ++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/Sources/CombineCocoa/Controls/UITableView+Combine.swift b/Sources/CombineCocoa/Controls/UITableView+Combine.swift index 829fea0..56a40f8 100644 --- a/Sources/CombineCocoa/Controls/UITableView+Combine.swift +++ b/Sources/CombineCocoa/Controls/UITableView+Combine.swift @@ -16,6 +16,11 @@ import Combine public extension UITableView { /// Combine wrapper for `tableView(_:willDisplay:forRowAt:)` var willDisplayCellPublisher: AnyPublisher<(cell: UITableViewCell, indexPath: IndexPath), Never> { + guard let delegateProxy = delegateProxy else { + return Empty<(cell: UITableViewCell, indexPath: IndexPath), Never>(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UITableViewDelegate.tableView(_:willDisplay:forRowAt:)) return delegateProxy.interceptSelectorPublisher(selector) .map { ($0[1] as! UITableViewCell, $0[2] as! IndexPath) } @@ -24,6 +29,11 @@ public extension UITableView { /// Combine wrapper for `tableView(_:willDisplayHeaderView:forSection:)` var willDisplayHeaderViewPublisher: AnyPublisher<(headerView: UIView, section: Int), Never> { + guard let delegateProxy = delegateProxy else { + return Empty<(headerView: UIView, section: Int), Never>(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UITableViewDelegate.tableView(_:willDisplayHeaderView:forSection:)) return delegateProxy.interceptSelectorPublisher(selector) .map { ($0[1] as! UIView, $0[2] as! Int) } @@ -32,6 +42,11 @@ public extension UITableView { /// Combine wrapper for `tableView(_:willDisplayFooterView:forSection:)` var willDisplayFooterViewPublisher: AnyPublisher<(footerView: UIView, section: Int), Never> { + guard let delegateProxy = delegateProxy else { + return Empty<(footerView: UIView, section: Int), Never>(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UITableViewDelegate.tableView(_:willDisplayFooterView:forSection:)) return delegateProxy.interceptSelectorPublisher(selector) .map { ($0[1] as! UIView, $0[2] as! Int) } @@ -40,6 +55,11 @@ public extension UITableView { /// Combine wrapper for `tableView(_:didEndDisplaying:forRowAt:)` var didEndDisplayingCellPublisher: AnyPublisher<(cell: UITableViewCell, indexPath: IndexPath), Never> { + guard let delegateProxy = delegateProxy else { + return Empty<(cell: UITableViewCell, indexPath: IndexPath), Never>(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UITableViewDelegate.tableView(_:didEndDisplaying:forRowAt:)) return delegateProxy.interceptSelectorPublisher(selector) .map { ($0[1] as! UITableViewCell, $0[2] as! IndexPath) } @@ -48,6 +68,11 @@ public extension UITableView { /// Combine wrapper for `tableView(_:didEndDisplayingHeaderView:forSection:)` var didEndDisplayingHeaderViewPublisher: AnyPublisher<(headerView: UIView, section: Int), Never> { + guard let delegateProxy = delegateProxy else { + return Empty<(headerView: UIView, section: Int), Never>(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UITableViewDelegate.tableView(_:didEndDisplayingHeaderView:forSection:)) return delegateProxy.interceptSelectorPublisher(selector) .map { ($0[1] as! UIView, $0[2] as! Int) } @@ -56,6 +81,11 @@ public extension UITableView { /// Combine wrapper for `tableView(_:didEndDisplayingFooterView:forSection:)` var didEndDisplayingFooterView: AnyPublisher<(headerView: UIView, section: Int), Never> { + guard let delegateProxy = delegateProxy else { + return Empty<(headerView: UIView, section: Int), Never>(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UITableViewDelegate.tableView(_:didEndDisplayingFooterView:forSection:)) return delegateProxy.interceptSelectorPublisher(selector) .map { ($0[1] as! UIView, $0[2] as! Int) } @@ -64,6 +94,11 @@ public extension UITableView { /// Combine wrapper for `tableView(_:accessoryButtonTappedForRowWith:)` var itemAccessoryButtonTappedPublisher: AnyPublisher { + guard let delegateProxy = delegateProxy else { + return Empty(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UITableViewDelegate.tableView(_:accessoryButtonTappedForRowWith:)) return delegateProxy.interceptSelectorPublisher(selector) .map { $0[1] as! IndexPath } @@ -72,6 +107,11 @@ public extension UITableView { /// Combine wrapper for `tableView(_:didHighlightRowAt:)` var didHighlightRowPublisher: AnyPublisher { + guard let delegateProxy = delegateProxy else { + return Empty(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UITableViewDelegate.tableView(_:didHighlightRowAt:)) return delegateProxy.interceptSelectorPublisher(selector) .map { $0[1] as! IndexPath } @@ -80,6 +120,11 @@ public extension UITableView { /// Combine wrapper for `tableView(_:didUnHighlightRowAt:)` var didUnhighlightRowPublisher: AnyPublisher { + guard let delegateProxy = delegateProxy else { + return Empty(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UITableViewDelegate.tableView(_:didUnhighlightRowAt:)) return delegateProxy.interceptSelectorPublisher(selector) .map { $0[1] as! IndexPath } @@ -88,6 +133,11 @@ public extension UITableView { /// Combine wrapper for `tableView(_:didSelectRowAt:)` var didSelectRowPublisher: AnyPublisher { + guard let delegateProxy = delegateProxy else { + return Empty(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UITableViewDelegate.tableView(_:didSelectRowAt:)) return delegateProxy.interceptSelectorPublisher(selector) .map { $0[1] as! IndexPath } @@ -96,6 +146,11 @@ public extension UITableView { /// Combine wrapper for `tableView(_:didDeselectRowAt:)` var didDeselectRowPublisher: AnyPublisher { + guard let delegateProxy = delegateProxy else { + return Empty(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UITableViewDelegate.tableView(_:didDeselectRowAt:)) return delegateProxy.interceptSelectorPublisher(selector) .map { $0[1] as! IndexPath } @@ -104,6 +159,11 @@ public extension UITableView { /// Combine wrapper for `tableView(_:willBeginEditingRowAt:)` var willBeginEditingRowPublisher: AnyPublisher { + guard let delegateProxy = delegateProxy else { + return Empty(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UITableViewDelegate.tableView(_:willBeginEditingRowAt:)) return delegateProxy.interceptSelectorPublisher(selector) .map { $0[1] as! IndexPath } @@ -112,13 +172,18 @@ public extension UITableView { /// Combine wrapper for `tableView(_:didEndEditingRowAt:)` var didEndEditingRowPublisher: AnyPublisher { + guard let delegateProxy = delegateProxy else { + return Empty(completeImmediately: false) + .eraseToAnyPublisher() + } + let selector = #selector(UITableViewDelegate.tableView(_:didEndEditingRowAt:)) return delegateProxy.interceptSelectorPublisher(selector) .map { $0[1] as! IndexPath } .eraseToAnyPublisher() } - override var delegateProxy: DelegateProxy { + override public var delegateProxy: DelegateProxy? { TableViewDelegateProxy.createDelegateProxy(for: self) } } From 6237cd3c104a9014cfbb3996d4b2d642d1232722 Mon Sep 17 00:00:00 2001 From: SumiaFish <39965559+SumiaFish@users.noreply.github.com> Date: Mon, 5 Sep 2022 18:53:39 +0800 Subject: [PATCH 06/11] Update UIScrollView+Combine.swift --- .../Controls/UIScrollView+Combine.swift | 174 +++++++++--------- 1 file changed, 89 insertions(+), 85 deletions(-) diff --git a/Sources/CombineCocoa/Controls/UIScrollView+Combine.swift b/Sources/CombineCocoa/Controls/UIScrollView+Combine.swift index 56a40f8..f070d0d 100644 --- a/Sources/CombineCocoa/Controls/UIScrollView+Combine.swift +++ b/Sources/CombineCocoa/Controls/UIScrollView+Combine.swift @@ -1,198 +1,202 @@ // -// UITableView+Combine.swift +// UIScrollView+Combine.swift // CombineCocoa // -// Created by Joan Disho on 19/01/20. +// Created by Joan Disho on 09/08/2019. // Copyright © 2020 Combine Community. All rights reserved. // - -#if canImport(UIKit) && !(os(iOS) && (arch(i386) || arch(arm))) -import Foundation +#if !(os(iOS) && (arch(i386) || arch(arm))) import UIKit import Combine // swiftlint:disable force_cast @available(iOS 13.0, *) -public extension UITableView { - /// Combine wrapper for `tableView(_:willDisplay:forRowAt:)` - var willDisplayCellPublisher: AnyPublisher<(cell: UITableViewCell, indexPath: IndexPath), Never> { - guard let delegateProxy = delegateProxy else { - return Empty<(cell: UITableViewCell, indexPath: IndexPath), Never>(completeImmediately: false) - .eraseToAnyPublisher() - } - - let selector = #selector(UITableViewDelegate.tableView(_:willDisplay:forRowAt:)) - return delegateProxy.interceptSelectorPublisher(selector) - .map { ($0[1] as! UITableViewCell, $0[2] as! IndexPath) } +public extension UIScrollView { + /// A publisher emitting content offset changes from this UIScrollView. + var contentOffsetPublisher: AnyPublisher { + publisher(for: \.contentOffset) .eraseToAnyPublisher() } - /// Combine wrapper for `tableView(_:willDisplayHeaderView:forSection:)` - var willDisplayHeaderViewPublisher: AnyPublisher<(headerView: UIView, section: Int), Never> { - guard let delegateProxy = delegateProxy else { - return Empty<(headerView: UIView, section: Int), Never>(completeImmediately: false) - .eraseToAnyPublisher() - } - - let selector = #selector(UITableViewDelegate.tableView(_:willDisplayHeaderView:forSection:)) - return delegateProxy.interceptSelectorPublisher(selector) - .map { ($0[1] as! UIView, $0[2] as! Int) } + /// A publisher emitting if the bottom of the UIScrollView is reached. + /// + /// - parameter offset: A threshold indicating how close to the bottom of the UIScrollView this publisher should emit. + /// Defaults to 0 + /// - returns: A publisher that emits when the bottom of the UIScrollView is reached within the provided threshold. + func reachedBottomPublisher(offset: CGFloat = 0) -> AnyPublisher { + contentOffsetPublisher + .map { [weak self] contentOffset -> Bool in + guard let self = self else { return false } + let visibleHeight = self.frame.height - self.contentInset.top - self.contentInset.bottom + let yDelta = contentOffset.y + self.contentInset.top + let threshold = max(offset, self.contentSize.height - visibleHeight) + return yDelta > threshold + } + .removeDuplicates() + .filter { $0 } + .map { _ in () } .eraseToAnyPublisher() } - /// Combine wrapper for `tableView(_:willDisplayFooterView:forSection:)` - var willDisplayFooterViewPublisher: AnyPublisher<(footerView: UIView, section: Int), Never> { + /// Combine wrapper for `scrollViewDidScroll(_:)` + var didScrollPublisher: AnyPublisher { guard let delegateProxy = delegateProxy else { - return Empty<(footerView: UIView, section: Int), Never>(completeImmediately: false) + return Empty(completeImmediately: false) .eraseToAnyPublisher() } - let selector = #selector(UITableViewDelegate.tableView(_:willDisplayFooterView:forSection:)) + let selector = #selector(UIScrollViewDelegate.scrollViewDidScroll(_:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { ($0[1] as! UIView, $0[2] as! Int) } + .map { _ in () } .eraseToAnyPublisher() } - /// Combine wrapper for `tableView(_:didEndDisplaying:forRowAt:)` - var didEndDisplayingCellPublisher: AnyPublisher<(cell: UITableViewCell, indexPath: IndexPath), Never> { + /// Combine wrapper for `scrollViewWillBeginDecelerating(_:)` + var willBeginDeceleratingPublisher: AnyPublisher { guard let delegateProxy = delegateProxy else { - return Empty<(cell: UITableViewCell, indexPath: IndexPath), Never>(completeImmediately: false) + return Empty(completeImmediately: false) .eraseToAnyPublisher() } - let selector = #selector(UITableViewDelegate.tableView(_:didEndDisplaying:forRowAt:)) + let selector = #selector(UIScrollViewDelegate.scrollViewWillBeginDecelerating(_:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { ($0[1] as! UITableViewCell, $0[2] as! IndexPath) } + .map { _ in () } .eraseToAnyPublisher() } - /// Combine wrapper for `tableView(_:didEndDisplayingHeaderView:forSection:)` - var didEndDisplayingHeaderViewPublisher: AnyPublisher<(headerView: UIView, section: Int), Never> { + /// Combine wrapper for `scrollViewDidEndDecelerating(_:)` + var didEndDeceleratingPublisher: AnyPublisher { guard let delegateProxy = delegateProxy else { - return Empty<(headerView: UIView, section: Int), Never>(completeImmediately: false) + return Empty(completeImmediately: false) .eraseToAnyPublisher() } - let selector = #selector(UITableViewDelegate.tableView(_:didEndDisplayingHeaderView:forSection:)) + let selector = #selector(UIScrollViewDelegate.scrollViewDidEndDecelerating(_:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { ($0[1] as! UIView, $0[2] as! Int) } + .map { _ in () } .eraseToAnyPublisher() } - /// Combine wrapper for `tableView(_:didEndDisplayingFooterView:forSection:)` - var didEndDisplayingFooterView: AnyPublisher<(headerView: UIView, section: Int), Never> { + /// Combine wrapper for `scrollViewWillBeginDragging(_:)` + var willBeginDraggingPublisher: AnyPublisher { guard let delegateProxy = delegateProxy else { - return Empty<(headerView: UIView, section: Int), Never>(completeImmediately: false) + return Empty(completeImmediately: false) .eraseToAnyPublisher() } - let selector = #selector(UITableViewDelegate.tableView(_:didEndDisplayingFooterView:forSection:)) + let selector = #selector(UIScrollViewDelegate.scrollViewWillBeginDragging(_:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { ($0[1] as! UIView, $0[2] as! Int) } + .map { _ in () } .eraseToAnyPublisher() } - /// Combine wrapper for `tableView(_:accessoryButtonTappedForRowWith:)` - var itemAccessoryButtonTappedPublisher: AnyPublisher { + /// Combine wrapper for `scrollViewWillEndDragging(_:withVelocity:targetContentOffset:)` + var willEndDraggingPublisher: AnyPublisher<(velocity: CGPoint, targetContentOffset: UnsafeMutablePointer), Never> { guard let delegateProxy = delegateProxy else { - return Empty(completeImmediately: false) + return Empty<(velocity: CGPoint, targetContentOffset: UnsafeMutablePointer), Never>(completeImmediately: false) .eraseToAnyPublisher() } - let selector = #selector(UITableViewDelegate.tableView(_:accessoryButtonTappedForRowWith:)) + let selector = #selector(UIScrollViewDelegate.scrollViewWillEndDragging(_:withVelocity:targetContentOffset:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { $0[1] as! IndexPath } + .map { values in + let targetContentOffsetValue = values[2] as! NSValue + let rawPointer = targetContentOffsetValue.pointerValue! + + return (values[1] as! CGPoint, rawPointer.bindMemory(to: CGPoint.self, capacity: MemoryLayout.size)) + } .eraseToAnyPublisher() } - /// Combine wrapper for `tableView(_:didHighlightRowAt:)` - var didHighlightRowPublisher: AnyPublisher { + /// Combine wrapper for `scrollViewDidEndDragging(_:willDecelerate:)` + var didEndDraggingPublisher: AnyPublisher { guard let delegateProxy = delegateProxy else { - return Empty(completeImmediately: false) + return Empty(completeImmediately: false) .eraseToAnyPublisher() } - let selector = #selector(UITableViewDelegate.tableView(_:didHighlightRowAt:)) + let selector = #selector(UIScrollViewDelegate.scrollViewDidEndDragging(_:willDecelerate:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { $0[1] as! IndexPath } + .map { $0[1] as! Bool } .eraseToAnyPublisher() } - /// Combine wrapper for `tableView(_:didUnHighlightRowAt:)` - var didUnhighlightRowPublisher: AnyPublisher { + /// Combine wrapper for `scrollViewDidZoom(_:)` + var didZoomPublisher: AnyPublisher { guard let delegateProxy = delegateProxy else { - return Empty(completeImmediately: false) + return Empty(completeImmediately: false) .eraseToAnyPublisher() } - let selector = #selector(UITableViewDelegate.tableView(_:didUnhighlightRowAt:)) + let selector = #selector(UIScrollViewDelegate.scrollViewDidZoom(_:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { $0[1] as! IndexPath } + .map { _ in () } .eraseToAnyPublisher() } - /// Combine wrapper for `tableView(_:didSelectRowAt:)` - var didSelectRowPublisher: AnyPublisher { + /// Combine wrapper for `scrollViewDidScrollToTop(_:)` + var didScrollToTopPublisher: AnyPublisher { guard let delegateProxy = delegateProxy else { - return Empty(completeImmediately: false) + return Empty(completeImmediately: false) .eraseToAnyPublisher() } - let selector = #selector(UITableViewDelegate.tableView(_:didSelectRowAt:)) + let selector = #selector(UIScrollViewDelegate.scrollViewDidScrollToTop(_:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { $0[1] as! IndexPath } + .map { _ in () } .eraseToAnyPublisher() } - /// Combine wrapper for `tableView(_:didDeselectRowAt:)` - var didDeselectRowPublisher: AnyPublisher { + /// Combine wrapper for `scrollViewDidEndScrollingAnimation(_:)` + var didEndScrollingAnimationPublisher: AnyPublisher { guard let delegateProxy = delegateProxy else { - return Empty(completeImmediately: false) + return Empty(completeImmediately: false) .eraseToAnyPublisher() } - let selector = #selector(UITableViewDelegate.tableView(_:didDeselectRowAt:)) + let selector = #selector(UIScrollViewDelegate.scrollViewDidEndScrollingAnimation(_:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { $0[1] as! IndexPath } + .map { _ in () } .eraseToAnyPublisher() } - /// Combine wrapper for `tableView(_:willBeginEditingRowAt:)` - var willBeginEditingRowPublisher: AnyPublisher { + /// Combine wrapper for `scrollViewWillBeginZooming(_:with:)` + var willBeginZoomingPublisher: AnyPublisher { guard let delegateProxy = delegateProxy else { - return Empty(completeImmediately: false) + return Empty(completeImmediately: false) .eraseToAnyPublisher() } - let selector = #selector(UITableViewDelegate.tableView(_:willBeginEditingRowAt:)) + let selector = #selector(UIScrollViewDelegate.scrollViewWillBeginZooming(_:with:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { $0[1] as! IndexPath } + .map { $0[1] as! UIView? } .eraseToAnyPublisher() } - /// Combine wrapper for `tableView(_:didEndEditingRowAt:)` - var didEndEditingRowPublisher: AnyPublisher { + /// Combine wrapper for `scrollViewDidEndZooming(_:with:atScale:)` + var didEndZooming: AnyPublisher<(view: UIView?, scale: CGFloat), Never> { guard let delegateProxy = delegateProxy else { - return Empty(completeImmediately: false) + return Empty<(view: UIView?, scale: CGFloat), Never>(completeImmediately: false) .eraseToAnyPublisher() } - let selector = #selector(UITableViewDelegate.tableView(_:didEndEditingRowAt:)) + let selector = #selector(UIScrollViewDelegate.scrollViewDidEndZooming(_:with:atScale:)) return delegateProxy.interceptSelectorPublisher(selector) - .map { $0[1] as! IndexPath } + .map { ($0[1] as! UIView?, $0[2] as! CGFloat) } .eraseToAnyPublisher() } - override public var delegateProxy: DelegateProxy? { - TableViewDelegateProxy.createDelegateProxy(for: self) + @objc var delegateProxy: DelegateProxy? { + ScrollViewDelegateProxy.createDelegateProxy(for: self) } } @available(iOS 13.0, *) -private class TableViewDelegateProxy: DelegateProxy, UITableViewDelegate, DelegateProxyType { - func setDelegate(to object: UITableView) { +private class ScrollViewDelegateProxy: DelegateProxy, UIScrollViewDelegate, DelegateProxyType { + func setDelegate(to object: UIScrollView) { object.delegate = self } } #endif // swiftlint:enable force_cast + From 8c0e79e28b402530d65ababeb55d736ced13eb8e Mon Sep 17 00:00:00 2001 From: SumiaFish <39965559+SumiaFish@users.noreply.github.com> Date: Fri, 9 Sep 2022 17:17:48 +0800 Subject: [PATCH 07/11] Update UICollectionView+Combine.swift --- .../CombineCocoa/Controls/UICollectionView+Combine.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift b/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift index de2c99d..92bd90e 100644 --- a/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift +++ b/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift @@ -204,17 +204,20 @@ private class CollectionViewDelegateProxy: DelegateProxy, UICollectionViewDelega var insetCallback: ((_ section: Int) -> UIEdgeInsets)? func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { - insetCallback?(section) ?? wrap?.collectionView?.flowLayout?.sectionInset ?? .zero + let result = insetCallback?(section) ?? wrap?.collectionView?.flowLayout?.sectionInset ?? .zero + return result } var minimumLineSpacingCallback: ((_ section: Int) -> CGFloat)? func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { - minimumLineSpacingCallback?(section) ?? wrap?.collectionView?.flowLayout?.minimumLineSpacing ?? 0 + let result = minimumLineSpacingCallback?(section) ?? wrap?.collectionView?.flowLayout?.minimumLineSpacing ?? 0 + return max(result - 0.5, 0) } var minimumInteritemSpacingCallback: ((_ section: Int) -> CGFloat)? func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { - minimumInteritemSpacingCallback?(section) ?? wrap?.collectionView?.flowLayout?.minimumInteritemSpacing ?? 0 + let result = minimumInteritemSpacingCallback?(section) ?? wrap?.collectionView?.flowLayout?.minimumInteritemSpacing ?? 0 + return max(result - 0.5, 0) } var headerSizeCallback: ((_ section: Int) -> CGSize)? From 778a061c91f9a059c5ab154b98dff8f113f455f5 Mon Sep 17 00:00:00 2001 From: SumiaFish <39965559+SumiaFish@users.noreply.github.com> Date: Mon, 12 Sep 2022 21:45:01 +0800 Subject: [PATCH 08/11] Update UICollectionView+Combine.swift --- .../Controls/UICollectionView+Combine.swift | 74 +------------------ 1 file changed, 1 insertion(+), 73 deletions(-) diff --git a/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift b/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift index 92bd90e..b1f2156 100644 --- a/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift +++ b/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift @@ -117,42 +117,6 @@ public extension UICollectionView { .map { ($0[1] as! UICollectionReusableView, $0[2] as! String, $0[3] as! IndexPath) } .eraseToAnyPublisher() } - - @discardableResult - func setCellSize(_ callback: @escaping (_ indexPath: IndexPath) -> CGSize) -> Self { - innerDelegateWrap?.proxy?.cellSizeCallback = callback - return self - } - - @discardableResult - func setInset(_ callback: @escaping (_ section: Int) -> UIEdgeInsets) -> Self { - innerDelegateWrap?.proxy?.insetCallback = callback - return self - } - - @discardableResult - func setMinimumLineSpacing(_ callback: @escaping (_ section: Int) -> CGFloat) -> Self { - innerDelegateWrap?.proxy?.minimumLineSpacingCallback = callback - return self - } - - @discardableResult - func setMinimumInteritemSpacing(_ callback: @escaping (_ section: Int) -> CGFloat) -> Self { - innerDelegateWrap?.proxy?.minimumInteritemSpacingCallback = callback - return self - } - - @discardableResult - func setHeaderSize(_ callback: @escaping (_ section: Int) -> CGSize) -> Self { - innerDelegateWrap?.proxy?.headerSizeCallback = callback - return self - } - - @discardableResult - func setFooterSize(_ callback: @escaping (_ section: Int) -> CGSize) -> Self { - innerDelegateWrap?.proxy?.footerSizeCallback = callback - return self - } func useInnerDelegate() { innerDelegateWrap = .init() @@ -171,10 +135,6 @@ public extension UICollectionView { set { objc_setAssociatedObject(self, &UICollectionView.innerDelegateWrapKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } - var flowLayout: UICollectionViewFlowLayout? { - collectionViewLayout as? UICollectionViewFlowLayout - } - override public var delegateProxy: DelegateProxy? { innerDelegateWrap?.proxy } @@ -196,39 +156,7 @@ private class CollectionViewDelegateProxy: DelegateProxy, UICollectionViewDelega func setDelegate(to object: UICollectionView) { object.delegate = self } - - var cellSizeCallback: ((_ indexPath: IndexPath) -> CGSize)? - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - cellSizeCallback?(indexPath) ?? wrap?.collectionView?.flowLayout?.itemSize ?? CGSize(width: 0.1, height: 0.1) - } - - var insetCallback: ((_ section: Int) -> UIEdgeInsets)? - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { - let result = insetCallback?(section) ?? wrap?.collectionView?.flowLayout?.sectionInset ?? .zero - return result - } - - var minimumLineSpacingCallback: ((_ section: Int) -> CGFloat)? - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { - let result = minimumLineSpacingCallback?(section) ?? wrap?.collectionView?.flowLayout?.minimumLineSpacing ?? 0 - return max(result - 0.5, 0) - } - - var minimumInteritemSpacingCallback: ((_ section: Int) -> CGFloat)? - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { - let result = minimumInteritemSpacingCallback?(section) ?? wrap?.collectionView?.flowLayout?.minimumInteritemSpacing ?? 0 - return max(result - 0.5, 0) - } - - var headerSizeCallback: ((_ section: Int) -> CGSize)? - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { - headerSizeCallback?(section) ?? wrap?.collectionView?.flowLayout?.headerReferenceSize ?? .zero - } - - var footerSizeCallback: ((_ section: Int) -> CGSize)? - func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { - footerSizeCallback?(section) ?? wrap?.collectionView?.flowLayout?.footerReferenceSize ?? .zero - } + } #endif // swiftlint:enable force_cast From 000ef1a895a48827e867185e4a512f5f64dd97ea Mon Sep 17 00:00:00 2001 From: SumiaFish <39965559+SumiaFish@users.noreply.github.com> Date: Mon, 12 Sep 2022 22:32:14 +0800 Subject: [PATCH 09/11] Update UICollectionView+Combine.swift --- .../Controls/UICollectionView+Combine.swift | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift b/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift index b1f2156..875edc7 100644 --- a/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift +++ b/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift @@ -117,6 +117,42 @@ public extension UICollectionView { .map { ($0[1] as! UICollectionReusableView, $0[2] as! String, $0[3] as! IndexPath) } .eraseToAnyPublisher() } + + @discardableResult + func setCellSize(_ callback: @escaping (_ indexPath: IndexPath) -> CGSize) -> Self { + innerDelegateWrap?.proxy?.cellSizeCallback = callback + return self + } + + @discardableResult + func setInset(_ callback: @escaping (_ section: Int) -> UIEdgeInsets) -> Self { + innerDelegateWrap?.proxy?.insetCallback = callback + return self + } + + @discardableResult + func setMinimumLineSpacing(_ callback: @escaping (_ section: Int) -> CGFloat) -> Self { + innerDelegateWrap?.proxy?.minimumLineSpacingCallback = callback + return self + } + + @discardableResult + func setMinimumInteritemSpacing(_ callback: @escaping (_ section: Int) -> CGFloat) -> Self { + innerDelegateWrap?.proxy?.minimumInteritemSpacingCallback = callback + return self + } + + @discardableResult + func setHeaderSize(_ callback: @escaping (_ section: Int) -> CGSize) -> Self { + innerDelegateWrap?.proxy?.headerSizeCallback = callback + return self + } + + @discardableResult + func setFooterSize(_ callback: @escaping (_ section: Int) -> CGSize) -> Self { + innerDelegateWrap?.proxy?.footerSizeCallback = callback + return self + } func useInnerDelegate() { innerDelegateWrap = .init() @@ -156,7 +192,39 @@ private class CollectionViewDelegateProxy: DelegateProxy, UICollectionViewDelega func setDelegate(to object: UICollectionView) { object.delegate = self } - + + var cellSizeCallback: ((_ indexPath: IndexPath) -> CGSize)? + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + cellSizeCallback?(indexPath) ?? wrap?.collectionView?.flowLayout?.itemSize ?? CGSize(width: 0.1, height: 0.1) + } + + var insetCallback: ((_ section: Int) -> UIEdgeInsets)? + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { + let result = insetCallback?(section) ?? wrap?.collectionView?.flowLayout?.sectionInset ?? .zero + return result + } + + var minimumLineSpacingCallback: ((_ section: Int) -> CGFloat)? + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { + let result = minimumLineSpacingCallback?(section) ?? wrap?.collectionView?.flowLayout?.minimumLineSpacing ?? 0 + return max(result - 0.5, 0) + } + + var minimumInteritemSpacingCallback: ((_ section: Int) -> CGFloat)? + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { + let result = minimumInteritemSpacingCallback?(section) ?? wrap?.collectionView?.flowLayout?.minimumInteritemSpacing ?? 0 + return max(result - 0.5, 0) + } + + var headerSizeCallback: ((_ section: Int) -> CGSize)? + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { + headerSizeCallback?(section) ?? wrap?.collectionView?.flowLayout?.headerReferenceSize ?? .zero + } + + var footerSizeCallback: ((_ section: Int) -> CGSize)? + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { + footerSizeCallback?(section) ?? wrap?.collectionView?.flowLayout?.footerReferenceSize ?? .zero + } } #endif // swiftlint:enable force_cast From f98edf6f48498ee7aa7b171f6d37dacc9ba6cfb3 Mon Sep 17 00:00:00 2001 From: SumiaFish <39965559+SumiaFish@users.noreply.github.com> Date: Mon, 12 Sep 2022 22:37:27 +0800 Subject: [PATCH 10/11] Update UICollectionView+Combine.swift --- .../Controls/UICollectionView+Combine.swift | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift b/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift index 875edc7..c1899f5 100644 --- a/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift +++ b/Sources/CombineCocoa/Controls/UICollectionView+Combine.swift @@ -165,6 +165,10 @@ public extension UICollectionView { innerDelegateWrap = nil } + fileprivate var innerFlowLayout: UICollectionViewFlowLayout? { + collectionViewLayout as? UICollectionViewFlowLayout + } + static var innerDelegateWrapKey: Void? private var innerDelegateWrap: CollectionViewDelegateWrap? { get { objc_getAssociatedObject(self, &UICollectionView.innerDelegateWrapKey) as? CollectionViewDelegateWrap } @@ -195,35 +199,35 @@ private class CollectionViewDelegateProxy: DelegateProxy, UICollectionViewDelega var cellSizeCallback: ((_ indexPath: IndexPath) -> CGSize)? func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { - cellSizeCallback?(indexPath) ?? wrap?.collectionView?.flowLayout?.itemSize ?? CGSize(width: 0.1, height: 0.1) + cellSizeCallback?(indexPath) ?? wrap?.collectionView?.innerFlowLayout?.itemSize ?? CGSize(width: 0.1, height: 0.1) } var insetCallback: ((_ section: Int) -> UIEdgeInsets)? func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { - let result = insetCallback?(section) ?? wrap?.collectionView?.flowLayout?.sectionInset ?? .zero + let result = insetCallback?(section) ?? wrap?.collectionView?.innerFlowLayout?.sectionInset ?? .zero return result } var minimumLineSpacingCallback: ((_ section: Int) -> CGFloat)? func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat { - let result = minimumLineSpacingCallback?(section) ?? wrap?.collectionView?.flowLayout?.minimumLineSpacing ?? 0 + let result = minimumLineSpacingCallback?(section) ?? wrap?.collectionView?.innerFlowLayout?.minimumLineSpacing ?? 0 return max(result - 0.5, 0) } var minimumInteritemSpacingCallback: ((_ section: Int) -> CGFloat)? func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { - let result = minimumInteritemSpacingCallback?(section) ?? wrap?.collectionView?.flowLayout?.minimumInteritemSpacing ?? 0 + let result = minimumInteritemSpacingCallback?(section) ?? wrap?.collectionView?.innerFlowLayout?.minimumInteritemSpacing ?? 0 return max(result - 0.5, 0) } var headerSizeCallback: ((_ section: Int) -> CGSize)? func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { - headerSizeCallback?(section) ?? wrap?.collectionView?.flowLayout?.headerReferenceSize ?? .zero + headerSizeCallback?(section) ?? wrap?.collectionView?.innerFlowLayout?.headerReferenceSize ?? .zero } var footerSizeCallback: ((_ section: Int) -> CGSize)? func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize { - footerSizeCallback?(section) ?? wrap?.collectionView?.flowLayout?.footerReferenceSize ?? .zero + footerSizeCallback?(section) ?? wrap?.collectionView?.innerFlowLayout?.footerReferenceSize ?? .zero } } #endif From 9f5665257dd25a72b2599a49e6d723a8031d0ff6 Mon Sep 17 00:00:00 2001 From: SumiaFish <39965559+SumiaFish@users.noreply.github.com> Date: Fri, 23 Sep 2022 17:02:36 +0800 Subject: [PATCH 11/11] UIButton.tapPublisher => UIButton.clickPublisher --- Sources/CombineCocoa/Controls/UIButton+Combine.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CombineCocoa/Controls/UIButton+Combine.swift b/Sources/CombineCocoa/Controls/UIButton+Combine.swift index 3375ee2..74004e3 100644 --- a/Sources/CombineCocoa/Controls/UIButton+Combine.swift +++ b/Sources/CombineCocoa/Controls/UIButton+Combine.swift @@ -13,7 +13,7 @@ import UIKit @available(iOS 13.0, *) public extension UIButton { /// A publisher emitting tap events from this button. - var tapPublisher: AnyPublisher { + var clickPublisher: AnyPublisher { controlEventPublisher(for: .touchUpInside) } }