diff --git a/Sources/Bond/UIKit/ViewControllerLifecycle.swift b/Sources/Bond/UIKit/ViewControllerLifecycle.swift index 66812e59..35b447e8 100644 --- a/Sources/Bond/UIKit/ViewControllerLifecycle.swift +++ b/Sources/Bond/UIKit/ViewControllerLifecycle.swift @@ -12,147 +12,111 @@ import Foundation import UIKit import ReactiveKit -public enum LifecycleEvent { +public extension ReactiveExtensions where Base: UIViewController { + var lifecycleEvents: Signal { + self.base.lifecycleEvents.prefix(untilOutputFrom: base.deallocated) + } + + func lifecycleEvent(_ event: LifecycleEvent) -> Signal { + self.lifecycleEvents.filter { $0 == event }.eraseType() + } +} + +public enum LifecycleEvent: CaseIterable { case viewDidLoad case viewWillAppear case viewDidAppear case viewWillDisappear case viewDidDisappear - case viewDidLayoutSubviews case viewWillLayoutSubviews -} + case viewDidLayoutSubviews -/// Observe UIViewController life cycle events -public final class ViewControllerLifecycle { - - private let wrapperViewController: WrapperViewController - - public var lifecycleEvents: Signal { - self.wrapperViewController.lifecycleEvents - } - - 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) - } + 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) } } - - 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) - } } -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 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 + + private var lifecycleEventsSubject: Subject { + if let subject = objc_getAssociatedObject( + self, + &StaticVariables.lifecycleSubjectKey + ) as? Subject { + return subject } else { - let lifeCycle = ViewControllerLifecycle(viewController: self) + let subject = PassthroughSubject() objc_setAssociatedObject( self, - &AssociatedKeys.viewControllerLifeCycleKey, - lifeCycle, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) - return lifeCycle + &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 viewController = unsafeBitCast(me, to: UIViewController.self) + viewController.lifecycleEventsSubject.send(lifecycle) + 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..9452be5b --- /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, + .viewWillAppear + ] + ) + } + + func testViewDidAppear() { + let sut = UIViewController() + let accumulator = Subscribers.Accumulator() + sut.reactive.lifecycleEvents.subscribe(accumulator) + sut.viewDidAppear(false) + sut.viewDidAppear(true) + XCTAssertEqual( + accumulator.values, + [ + .viewDidAppear, + .viewDidAppear + ] + ) + } + + func testViewWillDisappear() { + let sut = UIViewController() + let accumulator = Subscribers.Accumulator() + sut.reactive.lifecycleEvents.subscribe(accumulator) + sut.viewWillDisappear(false) + sut.viewWillDisappear(true) + XCTAssertEqual( + accumulator.values, + [ + .viewWillDisappear, + .viewWillDisappear + ] + ) + } + + func testViewDidDisappear() { + let sut = UIViewController() + let accumulator = Subscribers.Accumulator() + sut.reactive.lifecycleEvents.subscribe(accumulator) + sut.viewDidDisappear(false) + sut.viewDidDisappear(true) + XCTAssertEqual( + accumulator.values, + [ + .viewDidDisappear, + .viewDidDisappear + ] + ) + } + + 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, + .viewWillLayoutSubviews, + .viewDidLayoutSubviews, + .viewDidAppear + ] + ) + } +} + +#endif