Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
206 changes: 85 additions & 121 deletions Sources/Bond/UIKit/ViewControllerLifecycle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,147 +12,111 @@ import Foundation
import UIKit
import ReactiveKit

public enum LifecycleEvent {
public extension ReactiveExtensions where Base: UIViewController {
var lifecycleEvents: Signal<LifecycleEvent, Never> {
self.base.lifecycleEvents.prefix(untilOutputFrom: base.deallocated)
}

func lifecycleEvent(_ event: LifecycleEvent) -> Signal<Void, Never> {
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<LifecycleEvent, Never> {
self.wrapperViewController.lifecycleEvents
}

public func lifecycleEvent(_ event: LifecycleEvent) -> Signal<Void, Never> {
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<LifecycleEvent, Never>()

public var lifecycleEvents: Signal<LifecycleEvent, Never> {
var signal = _lifecycleEvent.toSignal()
if isViewLoaded {
signal = signal.prepend(.viewDidLoad)
}
return signal
}

public func lifecycleEvent(_ event: LifecycleEvent) -> Signal<Void, Never> {
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<LifecycleEvent, Never> {
if let subject = objc_getAssociatedObject(
self,
&StaticVariables.lifecycleSubjectKey
) as? Subject<LifecycleEvent, Never> {
return subject
} else {
let lifeCycle = ViewControllerLifecycle(viewController: self)
let subject = PassthroughSubject<LifecycleEvent, Never>()
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<LifecycleEvent, Never> {
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<LifecycleEvent> {
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<Void, Never> {
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

156 changes: 156 additions & 0 deletions Tests/BondTests/ViewControllerLifecycleTests.swift
Original file line number Diff line number Diff line change
@@ -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<LifecycleEvent, Never>()
sut.reactive.lifecycleEvents.subscribe(accumulator)
sut.viewDidLoad()
XCTAssertEqual(accumulator.values, [.viewDidLoad])
}

func testViewWillAppear() {
let sut = UIViewController()
let accumulator = Subscribers.Accumulator<LifecycleEvent, Never>()
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<LifecycleEvent, Never>()
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<LifecycleEvent, Never>()
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<LifecycleEvent, Never>()
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<LifecycleEvent, Never>()
sut.reactive.lifecycleEvents.subscribe(accumulator)
sut.viewWillLayoutSubviews()
XCTAssertEqual(
accumulator.values,
[
.viewWillLayoutSubviews
]
)
}

func testViewDidLayoutSubviews() {
let sut = UIViewController()
let accumulator = Subscribers.Accumulator<LifecycleEvent, Never>()
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<LifecycleEvent, Never>()
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