From cca7cea9360996594031669b33ffcc63cb72f891 Mon Sep 17 00:00:00 2001 From: Fabio Felici Date: Tue, 10 May 2022 18:55:35 +0200 Subject: [PATCH 1/3] Changed `ViewControllerLifecycle` implementation using swizzling --- .../Bond/UIKit/ViewControllerLifecycle.swift | 243 +++++++++--------- .../ViewControllerLifecycleTests.swift | 156 +++++++++++ 2 files changed, 273 insertions(+), 126 deletions(-) create mode 100644 Tests/BondTests/ViewControllerLifecycleTests.swift diff --git a/Sources/Bond/UIKit/ViewControllerLifecycle.swift b/Sources/Bond/UIKit/ViewControllerLifecycle.swift index 66812e59..485fdcd2 100644 --- a/Sources/Bond/UIKit/ViewControllerLifecycle.swift +++ b/Sources/Bond/UIKit/ViewControllerLifecycle.swift @@ -12,147 +12,138 @@ import Foundation import UIKit import ReactiveKit -public enum LifecycleEvent { - case viewDidLoad - case viewWillAppear - case viewDidAppear - case viewWillDisappear - case viewDidDisappear - case viewDidLayoutSubviews - case viewWillLayoutSubviews -} - -/// Observe UIViewController life cycle events -public final class ViewControllerLifecycle { - - private let wrapperViewController: WrapperViewController - - public var lifecycleEvents: Signal { - self.wrapperViewController.lifecycleEvents +public extension ReactiveExtensions where Base: UIViewController { + var lifecycleEvents: Signal { + self.base.lifecycleEvents.prefix(untilOutputFrom: base.deallocated) } - - public func lifecycleEvent(_ event: LifecycleEvent) -> Signal { - self.wrapperViewController.lifecycleEvent(event) - } - - public init(viewController: UIViewController) { - self.wrapperViewController = WrapperViewController() - if viewController.isViewLoaded { - self.addAsChildViewController(viewController) - } else { - viewController - .reactive - .keyPath(\.view, startWithCurrentValue: false) - .prefix(maxLength: 1) - .bind(to: viewController) { (viewController, _) in - self.addAsChildViewController(viewController) - } - } - } - - private func addAsChildViewController(_ viewController: UIViewController) { - - viewController.addChild(self.wrapperViewController) - viewController.view.addSubview(self.wrapperViewController.view) - self.wrapperViewController.view.frame = .zero - self.wrapperViewController.view.autoresizingMask = [] - self.wrapperViewController.didMove(toParent: viewController) + + func lifecycleEvent(_ event: LifecycleEvent) -> Signal { + self.lifecycleEvents.filter { $0 == event }.eraseType() } } -private extension ViewControllerLifecycle { - - final class WrapperViewController: UIViewController { - - - deinit { - _lifecycleEvent.send(completion: .finished) - } - - private let _lifecycleEvent = PassthroughSubject() - - public var lifecycleEvents: Signal { - var signal = _lifecycleEvent.toSignal() - if isViewLoaded { - signal = signal.prepend(.viewDidLoad) - } - return signal - } - - public func lifecycleEvent(_ event: LifecycleEvent) -> Signal { - return lifecycleEvents.filter { $0 == event }.eraseType() - } - - //MARK: - Overrides - override func viewDidLoad() { - super.viewDidLoad() - _lifecycleEvent.send(.viewDidLoad) - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - _lifecycleEvent.send(.viewWillAppear) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - _lifecycleEvent.send(.viewDidAppear) - } - - override func viewWillDisappear(_ animated: Bool) { - super.viewWillDisappear(animated) - _lifecycleEvent.send(.viewWillDisappear) - } - - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - _lifecycleEvent.send(.viewDidDisappear) - } - - override func viewWillLayoutSubviews() { - super.viewWillLayoutSubviews() - _lifecycleEvent.send(.viewWillLayoutSubviews) - } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - _lifecycleEvent.send(.viewDidLayoutSubviews) +public enum LifecycleEvent: CaseIterable, Equatable { + public static var allCases: [LifecycleEvent] { + return [ + .viewDidLoad, + .viewWillAppear(false), + .viewDidAppear(false), + .viewWillDisappear(false), + .viewDidDisappear(false), + .viewWillLayoutSubviews, + .viewDidLayoutSubviews + ] + } + + case viewDidLoad + case viewWillAppear(Bool) + case viewDidAppear(Bool) + case viewWillDisappear(Bool) + case viewDidDisappear(Bool) + case viewWillLayoutSubviews + case viewDidLayoutSubviews + + var associatedSelector: Selector { + switch self { + case .viewDidLoad: + return #selector(UIViewController.viewDidLoad) + case .viewWillAppear: + return #selector(UIViewController.viewWillAppear(_:)) + case .viewDidAppear: + return #selector(UIViewController.viewDidAppear(_:)) + case .viewWillDisappear: + return #selector(UIViewController.viewWillDisappear(_:)) + case .viewDidDisappear: + return #selector(UIViewController.viewDidDisappear(_:)) + case .viewWillLayoutSubviews: + return #selector(UIViewController.viewWillLayoutSubviews) + case .viewDidLayoutSubviews: + return #selector(UIViewController.viewDidLayoutSubviews) } } } -public protocol ViewControllerLifecycleProvider: class { - var viewControllerLifecycle: ViewControllerLifecycle { get } -} +extension UIViewController { -extension UIViewController: ViewControllerLifecycleProvider { - - private struct AssociatedKeys { - static var viewControllerLifeCycleKey = "viewControllerLifeCycleKey" + private enum StaticVariables { + static var lifecycleSubjectKey = "lifecycleSubjectKey" + static var swizzled = false } - - public var viewControllerLifecycle: ViewControllerLifecycle { - if let lifeCycle = objc_getAssociatedObject(self, &AssociatedKeys.viewControllerLifeCycleKey) { - return lifeCycle as! ViewControllerLifecycle - } else { - let lifeCycle = ViewControllerLifecycle(viewController: self) - objc_setAssociatedObject( + + private var lifecycleEventsSubject: Subject { + get { + if let subject = objc_getAssociatedObject( self, - &AssociatedKeys.viewControllerLifeCycleKey, - lifeCycle, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) - return lifeCycle + &StaticVariables.lifecycleSubjectKey + ) as? Subject { + return subject + } else { + let subject = PassthroughSubject() + objc_setAssociatedObject( + self, + &StaticVariables.lifecycleSubjectKey, + subject, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + return subject + } } } -} -extension ReactiveExtensions where Base: ViewControllerLifecycleProvider { - public var lifecycleEvents: Signal { - self.base.viewControllerLifecycle.lifecycleEvents - } + private typealias _IMP_BOOL = @convention(c) (UnsafeRawPointer, Selector, Bool) -> Void + private typealias _IMP = @convention(c) (UnsafeRawPointer, Selector) -> Void + + var lifecycleEvents: SafeSignal { + let signal = isViewLoaded ? self.lifecycleEventsSubject.prepend(.viewDidLoad).toSignal() : self.lifecycleEventsSubject.toSignal() + guard !StaticVariables.swizzled else { return signal } + StaticVariables.swizzled = true + for lifecycle in LifecycleEvent.allCases { + let selector = lifecycle.associatedSelector + guard let method = class_getInstanceMethod(UIViewController.self, selector) else { return signal } + let existingImplementation = method_getImplementation(method) + + switch lifecycle { + case .viewDidLoad, + .viewDidLayoutSubviews, + .viewWillLayoutSubviews: + + let newImplementation: @convention(block) (UnsafeRawPointer) -> Void = { me in + let viewController = unsafeBitCast(me, to: UIViewController.self) + viewController.lifecycleEventsSubject.send(lifecycle) + unsafeBitCast(existingImplementation, to: _IMP.self)(me, selector) + } + let swizzled = imp_implementationWithBlock(newImplementation) + method_setImplementation(method, swizzled) + + case .viewWillAppear, + .viewDidAppear, + .viewWillDisappear, + .viewDidDisappear: - public func lifecycleEvent(_ event: LifecycleEvent) -> Signal { - self.base.viewControllerLifecycle.lifecycleEvent(event) + let newImplementation: @convention(block) (UnsafeRawPointer, Bool) -> Void = { me, animated in + let event: LifecycleEvent + switch lifecycle { + case .viewWillAppear: + event = .viewWillAppear(animated) + case .viewDidAppear: + event = .viewDidAppear(animated) + case .viewWillDisappear: + event = .viewWillDisappear(animated) + case .viewDidDisappear: + event = .viewDidDisappear(animated) + default: + fatalError("Received unexpected lifecycle event") + } + let viewController = unsafeBitCast(me, to: UIViewController.self) + viewController.lifecycleEventsSubject.send(event) + unsafeBitCast(existingImplementation, to: _IMP_BOOL.self)(me, selector, animated) + } + let swizzled = imp_implementationWithBlock(newImplementation) + method_setImplementation(method, swizzled) + } + } + return signal } } - #endif + diff --git a/Tests/BondTests/ViewControllerLifecycleTests.swift b/Tests/BondTests/ViewControllerLifecycleTests.swift new file mode 100644 index 00000000..7900fea9 --- /dev/null +++ b/Tests/BondTests/ViewControllerLifecycleTests.swift @@ -0,0 +1,156 @@ +#if os(iOS) || os(tvOS) + +import XCTest +import ReactiveKit +@testable import Bond + +class ViewControllerLifecycleTests: XCTestCase { + + func testViewDidLoad() { + let sut = UIViewController() + let accumulator = Subscribers.Accumulator() + sut.reactive.lifecycleEvents.subscribe(accumulator) + sut.viewDidLoad() + XCTAssertEqual(accumulator.values, [.viewDidLoad]) + } + + func testViewWillAppear() { + let sut = UIViewController() + let accumulator = Subscribers.Accumulator() + sut.reactive.lifecycleEvents.subscribe(accumulator) + sut.viewWillAppear(false) + sut.viewWillAppear(true) + XCTAssertEqual( + accumulator.values, + [ + .viewWillAppear(false), + .viewWillAppear(true) + ] + ) + } + + func testViewDidAppear() { + let sut = UIViewController() + let accumulator = Subscribers.Accumulator() + sut.reactive.lifecycleEvents.subscribe(accumulator) + sut.viewDidAppear(false) + sut.viewDidAppear(true) + XCTAssertEqual( + accumulator.values, + [ + .viewDidAppear(false), + .viewDidAppear(true) + ] + ) + } + + func testViewWillDisappear() { + let sut = UIViewController() + let accumulator = Subscribers.Accumulator() + sut.reactive.lifecycleEvents.subscribe(accumulator) + sut.viewWillDisappear(false) + sut.viewWillDisappear(true) + XCTAssertEqual( + accumulator.values, + [ + .viewWillDisappear(false), + .viewWillDisappear(true) + ] + ) + } + + func testViewDidDisappear() { + let sut = UIViewController() + let accumulator = Subscribers.Accumulator() + sut.reactive.lifecycleEvents.subscribe(accumulator) + sut.viewDidDisappear(false) + sut.viewDidDisappear(true) + XCTAssertEqual( + accumulator.values, + [ + .viewDidDisappear(false), + .viewDidDisappear(true) + ] + ) + } + + func testViewWillLayoutSubviews() { + let sut = UIViewController() + let accumulator = Subscribers.Accumulator() + sut.reactive.lifecycleEvents.subscribe(accumulator) + sut.viewWillLayoutSubviews() + XCTAssertEqual( + accumulator.values, + [ + .viewWillLayoutSubviews + ] + ) + } + + func testViewDidLayoutSubviews() { + let sut = UIViewController() + let accumulator = Subscribers.Accumulator() + sut.reactive.lifecycleEvents.subscribe(accumulator) + sut.viewDidLayoutSubviews() + XCTAssertEqual( + accumulator.values, + [ + .viewDidLayoutSubviews + ] + ) + } + + func testCustomViewController() { + + class TestViewController: UIViewController { + override func viewDidLoad() { + super.viewDidLoad() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + + override func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + } + } + + let sut = TestViewController() + let accumulator = Subscribers.Accumulator() + sut.reactive.lifecycleEvents.subscribe(accumulator) + sut.viewDidLoad() + sut.viewWillAppear(false) + sut.viewWillLayoutSubviews() + sut.viewDidLayoutSubviews() + sut.viewDidAppear(false) + XCTAssertEqual( + accumulator.values, + [ + .viewDidLoad, + .viewWillAppear(false), + .viewWillLayoutSubviews, + .viewDidLayoutSubviews, + .viewDidAppear(false) + ] + ) + } +} + +#endif From 5ee19110163d763670a1c0bda45780d6c6bf921c Mon Sep 17 00:00:00 2001 From: Fabio Felici Date: Wed, 18 May 2022 11:47:59 +0200 Subject: [PATCH 2/3] Removed animated parameter to maintain existing API --- .../Bond/UIKit/ViewControllerLifecycle.swift | 35 +++---------------- .../ViewControllerLifecycleTests.swift | 20 +++++------ 2 files changed, 15 insertions(+), 40 deletions(-) diff --git a/Sources/Bond/UIKit/ViewControllerLifecycle.swift b/Sources/Bond/UIKit/ViewControllerLifecycle.swift index 485fdcd2..5860abc5 100644 --- a/Sources/Bond/UIKit/ViewControllerLifecycle.swift +++ b/Sources/Bond/UIKit/ViewControllerLifecycle.swift @@ -23,23 +23,11 @@ public extension ReactiveExtensions where Base: UIViewController { } public enum LifecycleEvent: CaseIterable, Equatable { - public static var allCases: [LifecycleEvent] { - return [ - .viewDidLoad, - .viewWillAppear(false), - .viewDidAppear(false), - .viewWillDisappear(false), - .viewDidDisappear(false), - .viewWillLayoutSubviews, - .viewDidLayoutSubviews - ] - } - case viewDidLoad - case viewWillAppear(Bool) - case viewDidAppear(Bool) - case viewWillDisappear(Bool) - case viewDidDisappear(Bool) + case viewWillAppear + case viewDidAppear + case viewWillDisappear + case viewDidDisappear case viewWillLayoutSubviews case viewDidLayoutSubviews @@ -121,21 +109,8 @@ extension UIViewController { .viewDidDisappear: let newImplementation: @convention(block) (UnsafeRawPointer, Bool) -> Void = { me, animated in - let event: LifecycleEvent - switch lifecycle { - case .viewWillAppear: - event = .viewWillAppear(animated) - case .viewDidAppear: - event = .viewDidAppear(animated) - case .viewWillDisappear: - event = .viewWillDisappear(animated) - case .viewDidDisappear: - event = .viewDidDisappear(animated) - default: - fatalError("Received unexpected lifecycle event") - } let viewController = unsafeBitCast(me, to: UIViewController.self) - viewController.lifecycleEventsSubject.send(event) + viewController.lifecycleEventsSubject.send(lifecycle) unsafeBitCast(existingImplementation, to: _IMP_BOOL.self)(me, selector, animated) } let swizzled = imp_implementationWithBlock(newImplementation) diff --git a/Tests/BondTests/ViewControllerLifecycleTests.swift b/Tests/BondTests/ViewControllerLifecycleTests.swift index 7900fea9..9452be5b 100644 --- a/Tests/BondTests/ViewControllerLifecycleTests.swift +++ b/Tests/BondTests/ViewControllerLifecycleTests.swift @@ -23,8 +23,8 @@ class ViewControllerLifecycleTests: XCTestCase { XCTAssertEqual( accumulator.values, [ - .viewWillAppear(false), - .viewWillAppear(true) + .viewWillAppear, + .viewWillAppear ] ) } @@ -38,8 +38,8 @@ class ViewControllerLifecycleTests: XCTestCase { XCTAssertEqual( accumulator.values, [ - .viewDidAppear(false), - .viewDidAppear(true) + .viewDidAppear, + .viewDidAppear ] ) } @@ -53,8 +53,8 @@ class ViewControllerLifecycleTests: XCTestCase { XCTAssertEqual( accumulator.values, [ - .viewWillDisappear(false), - .viewWillDisappear(true) + .viewWillDisappear, + .viewWillDisappear ] ) } @@ -68,8 +68,8 @@ class ViewControllerLifecycleTests: XCTestCase { XCTAssertEqual( accumulator.values, [ - .viewDidDisappear(false), - .viewDidDisappear(true) + .viewDidDisappear, + .viewDidDisappear ] ) } @@ -144,10 +144,10 @@ class ViewControllerLifecycleTests: XCTestCase { accumulator.values, [ .viewDidLoad, - .viewWillAppear(false), + .viewWillAppear, .viewWillLayoutSubviews, .viewDidLayoutSubviews, - .viewDidAppear(false) + .viewDidAppear ] ) } From 8de2f2932994ec8321bfdddbe44571ba6cf1a15e Mon Sep 17 00:00:00 2001 From: Fabio Felici Date: Thu, 19 May 2022 17:15:24 +0200 Subject: [PATCH 3/3] No need for `Equatable` and `get` on the lifecycle subject property --- .../Bond/UIKit/ViewControllerLifecycle.swift | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/Sources/Bond/UIKit/ViewControllerLifecycle.swift b/Sources/Bond/UIKit/ViewControllerLifecycle.swift index 5860abc5..35b447e8 100644 --- a/Sources/Bond/UIKit/ViewControllerLifecycle.swift +++ b/Sources/Bond/UIKit/ViewControllerLifecycle.swift @@ -22,7 +22,7 @@ public extension ReactiveExtensions where Base: UIViewController { } } -public enum LifecycleEvent: CaseIterable, Equatable { +public enum LifecycleEvent: CaseIterable { case viewDidLoad case viewWillAppear case viewDidAppear @@ -59,22 +59,20 @@ extension UIViewController { } private var lifecycleEventsSubject: Subject { - get { - if let subject = objc_getAssociatedObject( + if let subject = objc_getAssociatedObject( + self, + &StaticVariables.lifecycleSubjectKey + ) as? Subject { + return subject + } else { + let subject = PassthroughSubject() + objc_setAssociatedObject( self, - &StaticVariables.lifecycleSubjectKey - ) as? Subject { - return subject - } else { - let subject = PassthroughSubject() - objc_setAssociatedObject( - self, - &StaticVariables.lifecycleSubjectKey, - subject, - .OBJC_ASSOCIATION_RETAIN_NONATOMIC - ) - return subject - } + &StaticVariables.lifecycleSubjectKey, + subject, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) + return subject } }