From 08774b58f28cccef8931b03c971d73980d0e50e0 Mon Sep 17 00:00:00 2001 From: Mikhail Maslo Date: Wed, 16 Feb 2022 15:38:37 +0300 Subject: [PATCH] Use objc multicastdelegate --- .gitignore | 7 +- BottomSheetDemo.xcodeproj/project.pbxproj | 185 +++++++++++++++++ .../xcschemes/BottomSheetDemo.xcscheme | 78 ++++++++ .../xcschemes/BottomSheetUtils.xcscheme | 67 +++++++ Package.swift | 10 +- ...vigationController+MulticastDelegate.swift | 161 +-------------- .../UIScrollView+MulticastDelegate.swift | 176 +---------------- .../BottomSheetUtils/JMMulticastDelegate.h | 28 +++ .../BottomSheetUtils/JMMulticastDelegate.m | 186 ++++++++++++++++++ .../include/BottomSheetUtils.h | 19 ++ 10 files changed, 593 insertions(+), 324 deletions(-) create mode 100644 BottomSheetDemo.xcodeproj/xcshareddata/xcschemes/BottomSheetDemo.xcscheme create mode 100644 BottomSheetDemo.xcodeproj/xcshareddata/xcschemes/BottomSheetUtils.xcscheme create mode 100644 Sources/BottomSheetUtils/JMMulticastDelegate.h create mode 100644 Sources/BottomSheetUtils/JMMulticastDelegate.m create mode 100644 Sources/BottomSheetUtils/include/BottomSheetUtils.h diff --git a/.gitignore b/.gitignore index ba1ff08..d73a379 100644 --- a/.gitignore +++ b/.gitignore @@ -44,7 +44,7 @@ playground.xcworkspace # # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata # hence it is not needed unless you have added a package configuration file to your project -# .swiftpm +.swiftpm .build/ @@ -87,4 +87,7 @@ fastlane/test_output # After new code Injection tools there's a generated folder /iOSInjectionProject # https://github.com/johnno1962/injectionforxcode -iOSInjectionProject/ \ No newline at end of file +iOSInjectionProject/ + +# MacOS +.DS_Store \ No newline at end of file diff --git a/BottomSheetDemo.xcodeproj/project.pbxproj b/BottomSheetDemo.xcodeproj/project.pbxproj index 83e25ed..cb955c6 100644 --- a/BottomSheetDemo.xcodeproj/project.pbxproj +++ b/BottomSheetDemo.xcodeproj/project.pbxproj @@ -31,6 +31,12 @@ 7DA6E0E7274F919A009F5C37 /* UIScrollView+MulticastDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DA6E0C8274F915A009F5C37 /* UIScrollView+MulticastDelegate.swift */; }; 7DA6E0E8274F919A009F5C37 /* UINavigationController+MulticastDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DA6E0C7274F915A009F5C37 /* UINavigationController+MulticastDelegate.swift */; }; 7DA9B01A27439FA100284B0F /* UIControl+EventHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DA9B01927439FA100284B0F /* UIControl+EventHandling.swift */; }; + 7DB24C8927BD1813001030C7 /* BottomSheetUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7DB24C8327BD1813001030C7 /* BottomSheetUtils.framework */; }; + 7DB24C8A27BD1813001030C7 /* BottomSheetUtils.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7DB24C8327BD1813001030C7 /* BottomSheetUtils.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 7DB24C9327BD18F1001030C7 /* BottomSheetUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7DB24C8327BD1813001030C7 /* BottomSheetUtils.framework */; }; + 7DB24CEC27BD1FAD001030C7 /* JMMulticastDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7DB24CE927BD1FAD001030C7 /* JMMulticastDelegate.m */; }; + 7DB24CEE27BD1FAD001030C7 /* JMMulticastDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7DB24CEB27BD1FAD001030C7 /* JMMulticastDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7DDA237927BD25C800D006FE /* BottomSheetUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 7DDA237827BD25C800D006FE /* BottomSheetUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -41,6 +47,13 @@ remoteGlobalIDString = 7D05F329274139E100EBDBB1; remoteInfo = BottomSheet; }; + 7DB24C8727BD1813001030C7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7D05F2F92741359800EBDBB1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7DB24C8227BD1813001030C7; + remoteInfo = BottomSheetUtils; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -53,6 +66,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7DB24C8B27BD1813001030C7 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 7DB24C8A27BD1813001030C7 /* BottomSheetUtils.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -81,6 +105,10 @@ 7DA6E0C7274F915A009F5C37 /* UINavigationController+MulticastDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UINavigationController+MulticastDelegate.swift"; sourceTree = ""; }; 7DA6E0C8274F915A009F5C37 /* UIScrollView+MulticastDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIScrollView+MulticastDelegate.swift"; sourceTree = ""; }; 7DA9B01927439FA100284B0F /* UIControl+EventHandling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIControl+EventHandling.swift"; sourceTree = ""; }; + 7DB24C8327BD1813001030C7 /* BottomSheetUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BottomSheetUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7DB24CE927BD1FAD001030C7 /* JMMulticastDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JMMulticastDelegate.m; sourceTree = ""; }; + 7DB24CEB27BD1FAD001030C7 /* JMMulticastDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JMMulticastDelegate.h; sourceTree = ""; }; + 7DDA237827BD25C800D006FE /* BottomSheetUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BottomSheetUtils.h; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -89,11 +117,20 @@ buildActionMask = 2147483647; files = ( 7D05F33227413A1500EBDBB1 /* libBottomSheet.a in Frameworks */, + 7DB24C8927BD1813001030C7 /* BottomSheetUtils.framework in Frameworks */, 7D05F342274140F700EBDBB1 /* SnapKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 7D05F327274139E100EBDBB1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7DB24C9327BD18F1001030C7 /* BottomSheetUtils.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7DB24C8027BD1813001030C7 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( @@ -108,6 +145,7 @@ children = ( 7DA6E0B2274F915A009F5C37 /* BottomSheet */, 7D05F3032741359800EBDBB1 /* BottomSheetDemo */, + 7DB24CE827BD1FAD001030C7 /* BottomSheetUtils */, 7D05F33127413A1500EBDBB1 /* Frameworks */, 7D05F3022741359800EBDBB1 /* Products */, ); @@ -118,6 +156,7 @@ children = ( 7D05F3012741359800EBDBB1 /* BottomSheetDemo.app */, 7D05F32A274139E100EBDBB1 /* libBottomSheet.a */, + 7DB24C8327BD1813001030C7 /* BottomSheetUtils.framework */, ); name = Products; sourceTree = ""; @@ -279,8 +318,39 @@ path = Helpers; sourceTree = ""; }; + 7DB24CE827BD1FAD001030C7 /* BottomSheetUtils */ = { + isa = PBXGroup; + children = ( + 7DDA237727BD25C800D006FE /* include */, + 7DB24CEB27BD1FAD001030C7 /* JMMulticastDelegate.h */, + 7DB24CE927BD1FAD001030C7 /* JMMulticastDelegate.m */, + ); + name = BottomSheetUtils; + path = Sources/BottomSheetUtils; + sourceTree = ""; + }; + 7DDA237727BD25C800D006FE /* include */ = { + isa = PBXGroup; + children = ( + 7DDA237827BD25C800D006FE /* BottomSheetUtils.h */, + ); + path = include; + sourceTree = ""; + }; /* End PBXGroup section */ +/* Begin PBXHeadersBuildPhase section */ + 7DB24C7E27BD1813001030C7 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 7DB24CEE27BD1FAD001030C7 /* JMMulticastDelegate.h in Headers */, + 7DDA237927BD25C800D006FE /* BottomSheetUtils.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + /* Begin PBXNativeTarget section */ 7D05F3002741359800EBDBB1 /* BottomSheetDemo */ = { isa = PBXNativeTarget; @@ -289,11 +359,13 @@ 7D05F2FD2741359800EBDBB1 /* Sources */, 7D05F2FE2741359800EBDBB1 /* Frameworks */, 7D05F2FF2741359800EBDBB1 /* Resources */, + 7DB24C8B27BD1813001030C7 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( 7D05F33427413A1500EBDBB1 /* PBXTargetDependency */, + 7DB24C8827BD1813001030C7 /* PBXTargetDependency */, ); name = BottomSheetDemo; packageProductDependencies = ( @@ -320,6 +392,24 @@ productReference = 7D05F32A274139E100EBDBB1 /* libBottomSheet.a */; productType = "com.apple.product-type.library.static"; }; + 7DB24C8227BD1813001030C7 /* BottomSheetUtils */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7DB24C8E27BD1813001030C7 /* Build configuration list for PBXNativeTarget "BottomSheetUtils" */; + buildPhases = ( + 7DB24C7E27BD1813001030C7 /* Headers */, + 7DB24C7F27BD1813001030C7 /* Sources */, + 7DB24C8027BD1813001030C7 /* Frameworks */, + 7DB24C8127BD1813001030C7 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = BottomSheetUtils; + productName = BottomSheetUtils; + productReference = 7DB24C8327BD1813001030C7 /* BottomSheetUtils.framework */; + productType = "com.apple.product-type.framework"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -337,6 +427,9 @@ CreatedOnToolsVersion = 12.5.1; LastSwiftMigration = 1250; }; + 7DB24C8227BD1813001030C7 = { + CreatedOnToolsVersion = 13.0; + }; }; }; buildConfigurationList = 7D05F2FC2741359800EBDBB1 /* Build configuration list for PBXProject "BottomSheetDemo" */; @@ -357,6 +450,7 @@ targets = ( 7D05F3002741359800EBDBB1 /* BottomSheetDemo */, 7D05F329274139E100EBDBB1 /* BottomSheet */, + 7DB24C8227BD1813001030C7 /* BottomSheetUtils */, ); }; /* End PBXProject section */ @@ -371,6 +465,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7DB24C8127BD1813001030C7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -408,6 +509,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 7DB24C7F27BD1813001030C7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 7DB24CEC27BD1FAD001030C7 /* JMMulticastDelegate.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -416,6 +525,11 @@ target = 7D05F329274139E100EBDBB1 /* BottomSheet */; targetProxy = 7D05F33327413A1500EBDBB1 /* PBXContainerItemProxy */; }; + 7DB24C8827BD1813001030C7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7DB24C8227BD1813001030C7 /* BottomSheetUtils */; + targetProxy = 7DB24C8727BD1813001030C7 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -621,6 +735,68 @@ }; name = Release; }; + 7DB24C8C27BD1813001030C7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Joom. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.joomcode.BottomSheetUtils; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 7DB24C8D27BD1813001030C7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2022 Joom. All rights reserved."; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.joomcode.BottomSheetUtils; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -651,6 +827,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 7DB24C8E27BD1813001030C7 /* Build configuration list for PBXNativeTarget "BottomSheetUtils" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7DB24C8C27BD1813001030C7 /* Debug */, + 7DB24C8D27BD1813001030C7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ diff --git a/BottomSheetDemo.xcodeproj/xcshareddata/xcschemes/BottomSheetDemo.xcscheme b/BottomSheetDemo.xcodeproj/xcshareddata/xcschemes/BottomSheetDemo.xcscheme new file mode 100644 index 0000000..77a2ff5 --- /dev/null +++ b/BottomSheetDemo.xcodeproj/xcshareddata/xcschemes/BottomSheetDemo.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BottomSheetDemo.xcodeproj/xcshareddata/xcschemes/BottomSheetUtils.xcscheme b/BottomSheetDemo.xcodeproj/xcshareddata/xcschemes/BottomSheetUtils.xcscheme new file mode 100644 index 0000000..b7a56c3 --- /dev/null +++ b/BottomSheetDemo.xcodeproj/xcshareddata/xcschemes/BottomSheetUtils.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Package.swift b/Package.swift index 36583d9..3f9d6ff 100644 --- a/Package.swift +++ b/Package.swift @@ -12,14 +12,16 @@ let package = Package( .library( name: "BottomSheet", targets: ["BottomSheet"] - ), - ], - dependencies: [ + ) ], targets: [ .target( - name: "BottomSheet", + name: "BottomSheetUtils", dependencies: [] ), + .target( + name: "BottomSheet", + dependencies: ["BottomSheetUtils"] + ), ] ) diff --git a/Sources/BottomSheet/Helpers/Utils/UINavigationController+MulticastDelegate.swift b/Sources/BottomSheet/Helpers/Utils/UINavigationController+MulticastDelegate.swift index b2449d9..648221f 100644 --- a/Sources/BottomSheet/Helpers/Utils/UINavigationController+MulticastDelegate.swift +++ b/Sources/BottomSheet/Helpers/Utils/UINavigationController+MulticastDelegate.swift @@ -7,163 +7,22 @@ // import UIKit +import BottomSheetUtils extension UINavigationController { private static var transitionKey: UInt8 = 0 - var multicastingDelegate: MulticastingNavigationControllerDelegate { - if let object = objc_getAssociatedObject(self, &Self.transitionKey) as? MulticastingNavigationControllerDelegate { + public var multicastingDelegate: MulticastDelegate { + if let object = objc_getAssociatedObject(self, &Self.transitionKey) as? MulticastDelegate { return object } - - let object = MulticastingNavigationControllerDelegate(target: self) - objc_setAssociatedObject(object, &Self.transitionKey, object, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - return object - } -} - -public final class MulticastingNavigationControllerDelegate: NSObject { - private var delegates: NSHashTable = .weakObjects() - - private var subscription: NSKeyValueObservation? - private let target: UINavigationController - init(target: UINavigationController) { - self.target = target - super.init() - - addInitialDelegate() - ensureMutlicastIsSet() - } - - deinit { - subscription?.invalidate() - } - - private func addInitialDelegate() { - if let delegate = target.delegate, delegate !== self { - target.delegate = self - - addDelegate(delegate) - } - } - - private func ensureMutlicastIsSet() { - subscription = target.observe(\.delegate, options: [.initial, .old, .new]) { [weak self] navigationController, change in - guard let self = self else { return } - - let newValue = change.newValue ?? nil - let oldValue = change.oldValue ?? nil - guard oldValue !== newValue, newValue !== self else { - return - } - - if let newValue = newValue { - self.addDelegate(newValue) - } - navigationController.delegate = self - } - } - - public func addDelegate(_ delegate: UINavigationControllerDelegate) { - if delegates.contains(delegate) { - return - } - - delegates.add(delegate) - ensureUIKitCacheUpdated() - } - - public func removeDelegate(_ delegate: UINavigationControllerDelegate) { - if !delegates.contains(delegate) { - return - } - - delegates.remove(delegate) - ensureUIKitCacheUpdated() - } - - private func ensureUIKitCacheUpdated() { - target.delegate = nil - target.delegate = self - } -} - -extension MulticastingNavigationControllerDelegate: UINavigationControllerDelegate { - public func navigationController( - _ navigationController: UINavigationController, - willShow viewController: UIViewController, - animated: Bool - ) { - for delegate in delegates.allObjects { - delegate.navigationController?(navigationController, willShow: viewController, animated: animated) - } - } - - public func navigationController( - _ navigationController: UINavigationController, - didShow viewController: UIViewController, - animated: Bool - ) { - for delegate in delegates.allObjects { - delegate.navigationController?(navigationController, didShow: viewController, animated: animated) - } - } - - public func navigationControllerSupportedInterfaceOrientations( - _ navigationController: UINavigationController - ) -> UIInterfaceOrientationMask { - for delegate in delegates.allObjects { - if let result = delegate.navigationControllerSupportedInterfaceOrientations?(navigationController) { - return result - } - } - - return .all - } - - public func navigationControllerPreferredInterfaceOrientationForPresentation( - _ navigationController: UINavigationController - ) -> UIInterfaceOrientation { - for delegate in delegates.allObjects { - if let result = delegate.navigationControllerPreferredInterfaceOrientationForPresentation?(navigationController) { - return result - } - } - - return .portrait - } - - public func navigationController( - _ navigationController: UINavigationController, - interactionControllerFor animationController: UIViewControllerAnimatedTransitioning - ) -> UIViewControllerInteractiveTransitioning? { - for delegate in delegates.allObjects { - if let result = delegate.navigationController?(navigationController, interactionControllerFor: animationController) { - return result - } - } - - return nil - } - - public func navigationController( - _ navigationController: UINavigationController, - animationControllerFor operation: UINavigationController.Operation, - from fromVC: UIViewController, - to toVC: UIViewController - ) -> UIViewControllerAnimatedTransitioning? { - for delegate in delegates.allObjects { - if let result = delegate.navigationController?( - navigationController, - animationControllerFor: operation, - from: fromVC, - to: toVC - ) { - return result - } - } - - return nil + let object = MulticastDelegate( + target: self, + delegateGetter: #selector(getter: delegate), + delegateSetter: #selector(setter: delegate) + ) + objc_setAssociatedObject(self, &Self.transitionKey, object, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + return object } } diff --git a/Sources/BottomSheet/Helpers/Utils/UIScrollView+MulticastDelegate.swift b/Sources/BottomSheet/Helpers/Utils/UIScrollView+MulticastDelegate.swift index e4edebb..8b3d463 100644 --- a/Sources/BottomSheet/Helpers/Utils/UIScrollView+MulticastDelegate.swift +++ b/Sources/BottomSheet/Helpers/Utils/UIScrollView+MulticastDelegate.swift @@ -7,180 +7,22 @@ // import UIKit +import BottomSheetUtils extension UIScrollView { private static var transitionKey: UInt8 = 0 - var multicastingDelegate: MulticastingScrollViewDelegate { - if let object = objc_getAssociatedObject(self, &Self.transitionKey) as? MulticastingScrollViewDelegate { + public var multicastingDelegate: MulticastDelegate { + if let object = objc_getAssociatedObject(self, &Self.transitionKey) as? MulticastDelegate { return object } - - let object = MulticastingScrollViewDelegate(target: self) + + let object = MulticastDelegate( + target: self, + delegateGetter: #selector(getter: delegate), + delegateSetter: #selector(setter: delegate) + ) objc_setAssociatedObject(self, &Self.transitionKey, object, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) return object } } - -public final class MulticastingScrollViewDelegate: NSObject { - private var delegates: NSHashTable = .weakObjects() - - private var subscription: NSKeyValueObservation? - private let target: UIScrollView - - init(target: UIScrollView) { - self.target = target - super.init() - - addInitialDelegate() - ensureMutlicastIsSet() - } - - deinit { - subscription?.invalidate() - } - - private func addInitialDelegate() { - if let delegate = target.delegate, delegate !== self { - target.delegate = self - - addDelegate(delegate) - } - } - - private func ensureMutlicastIsSet() { - subscription = target.observe(\.delegate, options: [.initial, .old, .new]) { [weak self] scrollView, change in - guard let self = self else { return } - - let newValue = change.newValue ?? nil - let oldValue = change.oldValue ?? nil - guard oldValue !== newValue, newValue !== self else { - return - } - - if let newValue = newValue { - self.addDelegate(newValue) - } - scrollView.delegate = self - } - } - - public func addDelegate(_ delegate: UIScrollViewDelegate) { - if delegates.contains(delegate) { - return - } - - delegates.add(delegate) - ensureUIKitCacheUpdated() - } - - public func removeDelegate(_ delegate: UIScrollViewDelegate) { - if !delegates.contains(delegate) { - return - } - - delegates.remove(delegate) - ensureUIKitCacheUpdated() - } - - private func ensureUIKitCacheUpdated() { - target.delegate = nil - target.delegate = self - } -} - -extension MulticastingScrollViewDelegate: UIScrollViewDelegate { - public func scrollViewDidScroll(_ scrollView: UIScrollView) { - for delegate in delegates.allObjects { - delegate.scrollViewDidScroll?(scrollView) - } - } - - public func scrollViewDidZoom(_ scrollView: UIScrollView) { - for delegate in delegates.allObjects { - delegate.scrollViewDidZoom?(scrollView) - } - } - - public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - for delegate in delegates.allObjects { - delegate.scrollViewWillBeginDragging?(scrollView) - } - } - - public func scrollViewWillEndDragging( - _ scrollView: UIScrollView, - withVelocity velocity: CGPoint, - targetContentOffset: UnsafeMutablePointer - ) { - for delegate in delegates.allObjects { - delegate.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset) - } - } - - public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - for delegate in delegates.allObjects { - delegate.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate) - } - } - - public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) { - for delegate in delegates.allObjects { - delegate.scrollViewWillBeginDecelerating?(scrollView) - } - } - - public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { - for delegate in delegates.allObjects { - delegate.scrollViewDidEndDecelerating?(scrollView) - } - } - - public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { - for delegate in delegates.allObjects { - delegate.scrollViewDidEndScrollingAnimation?(scrollView) - } - } - - public func viewForZooming(in scrollView: UIScrollView) -> UIView? { - for delegate in delegates.allObjects { - if let view = delegate.viewForZooming?(in: scrollView) { - return view - } - } - - return nil - } - - public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { - for delegate in delegates.allObjects { - delegate.scrollViewWillBeginZooming?(scrollView, with: view) - } - } - - public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { - for delegate in delegates.allObjects { - delegate.scrollViewDidEndZooming?(scrollView, with: view, atScale: scale) - } - } - - public func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { - for delegate in delegates.allObjects where delegate.scrollViewShouldScrollToTop?(scrollView) == true { - return true - } - - return false - } - - public func scrollViewDidScrollToTop(_ scrollView: UIScrollView) { - for delegate in delegates.allObjects { - delegate.scrollViewDidScrollToTop?(scrollView) - } - } - - public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) { - for delegate in delegates.allObjects { - delegate.scrollViewDidChangeAdjustedContentInset?(scrollView) - } - } -} diff --git a/Sources/BottomSheetUtils/JMMulticastDelegate.h b/Sources/BottomSheetUtils/JMMulticastDelegate.h new file mode 100644 index 0000000..f389bd9 --- /dev/null +++ b/Sources/BottomSheetUtils/JMMulticastDelegate.h @@ -0,0 +1,28 @@ +// +// JMMulticastDelegate.h +// BottomSheet +// +// Created by Mikhail Maslo on 16.02.2022. +// Copyright © 2022 Joom. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +NS_SWIFT_NAME(MulticastDelegate) +@interface JMMulticastDelegate : NSObject + +- (instancetype)initWithTarget:(id)target + delegateGetter:(SEL)delegateGetter + delegateSetter:(SEL)delegateSetter NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +- (void)addDelegate:(id)delegate NS_SWIFT_NAME(addDelegate(_:)); + +- (void)removeDelegate:(id)delegate NS_SWIFT_NAME(removeDelegate(_:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/BottomSheetUtils/JMMulticastDelegate.m b/Sources/BottomSheetUtils/JMMulticastDelegate.m new file mode 100644 index 0000000..08df8f7 --- /dev/null +++ b/Sources/BottomSheetUtils/JMMulticastDelegate.m @@ -0,0 +1,186 @@ +// +// JMMulticastDelegate.m +// BottomSheet +// +// Created by Mikhail Maslo on 16.02.2022. +// Copyright © 2022 Joom. All rights reserved. +// + +#define JMSuppressPerformSelectorLeakWarning(Code) \ + do { \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"") \ + Code; \ + _Pragma("clang diagnostic pop") \ + } while (0) + +#import "JMMulticastDelegate.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation JMMulticastDelegate { + __weak id _target; + SEL _delegateGetter; + SEL _delegateSetter; + NSHashTable *_delegates; + NSInteger _enumerationCounter; +} + +- (instancetype)initWithTarget:(id)target + delegateGetter:(SEL)delegateGetter + delegateSetter:(SEL)delegateSetter { + self = [super init]; + if (self) { + _target = target; + _delegateGetter = delegateGetter; + _delegateSetter = delegateSetter; + + _delegates = [NSHashTable weakObjectsHashTable]; + + [self addInitialDelegate]; + [self ensureMulticastingDelegateIsSet]; + } + return self; +} + +#pragma mark - Public + +- (void)enumerateDelegatesUsingBlock:(void(^ NS_NOESCAPE)(id delegate, BOOL *stop))block { + ++_enumerationCounter; + + BOOL stop = NO; + for (id delegate in _delegates) { + block(delegate, &stop); + if (stop) { + break; + } + } + + --_enumerationCounter; +} + +#pragma mark - Private + +- (void)addInitialDelegate { + id delegate; + JMSuppressPerformSelectorLeakWarning( + delegate = [_target performSelector:_delegateGetter]; + ); + + if (delegate) { + [_delegates addObject:delegate]; + } +} + +- (void)ensureMulticastingDelegateIsSet { + [_target addObserver:self + forKeyPath:NSStringFromSelector(_delegateGetter) + options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld + context:nil]; + + JMSuppressPerformSelectorLeakWarning([_target performSelector:_delegateSetter withObject:self]); +} + +- (void)observeValueForKeyPath:(nullable NSString *)keyPath + ofObject:(nullable id)object + change:(nullable NSDictionary *)change + context:(nullable void *)context { + if (!keyPath || !object || !change) { + return; + } + + id newDelegate = change[NSKeyValueChangeNewKey]; + id oldDelegate = change[NSKeyValueChangeOldKey]; + if (newDelegate == oldDelegate || newDelegate == self) { + return; + } + + if (oldDelegate && oldDelegate != NSNull.null) { + [self removeDelegate:oldDelegate]; + } + if (newDelegate && newDelegate != NSNull.null) { + [self addDelegate:newDelegate]; + } + + if (newDelegate && newDelegate != NSNull.null) { + JMSuppressPerformSelectorLeakWarning([self->_target performSelector:self->_delegateSetter withObject:self]); + } +} + +- (void)ensureUIKitCacheUpdated { + id strongTarget = _target; + + JMSuppressPerformSelectorLeakWarning([strongTarget performSelector:_delegateSetter withObject:nil]); + JMSuppressPerformSelectorLeakWarning([strongTarget performSelector:_delegateSetter withObject:self]); +} + +#pragma mark - MulticastingDelegate + +- (void)addDelegate:(id)delegate { + if (![_delegates containsObject:delegate]) { + if (_enumerationCounter > 0) { + _delegates = [_delegates copy]; + } + + [_delegates addObject:delegate]; + [self ensureUIKitCacheUpdated]; + } +} + +- (void)removeDelegate:(id)delegate { + if ([_delegates containsObject:delegate]) { + if (_enumerationCounter > 0) { + _delegates = [_delegates copy]; + } + + [_delegates removeObject:delegate]; + [self ensureUIKitCacheUpdated]; + } +} + +#pragma mark - NSObject + +- (BOOL)conformsToProtocol:(Protocol *)aProtocol { + __block BOOL result = NO; + [self enumerateDelegatesUsingBlock:^(id delegate, BOOL *stop) { + if ([delegate conformsToProtocol:aProtocol]) { + result = YES; + *stop = YES; + } + }]; + return result; +} + +- (BOOL)respondsToSelector:(SEL)aSelector { + __block BOOL result = NO; + [self enumerateDelegatesUsingBlock:^(id delegate, BOOL *stop) { + if ([delegate respondsToSelector:aSelector]) { + result = YES; + *stop = YES; + } + }]; + return result; +} + +- (void)forwardInvocation:(NSInvocation *)invocation { + [self enumerateDelegatesUsingBlock:^(id delegate, BOOL *stop) { + if ([delegate respondsToSelector:invocation.selector]) { + [invocation invokeWithTarget:delegate]; + } + }]; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { + __block NSMethodSignature *signature = nil; + [self enumerateDelegatesUsingBlock:^(id delegate, BOOL *stop) { + if ([delegate respondsToSelector:selector]) { + signature = [delegate methodSignatureForSelector:selector]; + *stop = YES; + } + }]; + return signature ?: [NSMethodSignature signatureWithObjCTypes:"@^v^c"]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/BottomSheetUtils/include/BottomSheetUtils.h b/Sources/BottomSheetUtils/include/BottomSheetUtils.h new file mode 100644 index 0000000..37a1e45 --- /dev/null +++ b/Sources/BottomSheetUtils/include/BottomSheetUtils.h @@ -0,0 +1,19 @@ +// +// BottomSheetUtils.h +// BottomSheetUtils +// +// Created by Mikhail Maslo on 16.02.2022. +// Copyright © 2022 Joom. All rights reserved. +// + +#import + +//! Project version number for BottomSheetUtils. +FOUNDATION_EXPORT double BottomSheetUtilsVersionNumber; + +//! Project version string for BottomSheetUtils. +FOUNDATION_EXPORT const unsigned char BottomSheetUtilsVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + +#import