Skip to content

Commit 3210dd6

Browse files
Improve lifecycle (#60)
Simulates view controller lifecycle more accurately
1 parent 00b5164 commit 3210dd6

File tree

5 files changed

+161
-42
lines changed

5 files changed

+161
-42
lines changed

HammerTests.podspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Pod::Spec.new do |spec|
22
spec.name = "HammerTests"
3-
spec.version = "0.15.0"
4-
spec.summary = "iOS touch and keyboard syntheis library for unit tests."
3+
spec.version = "0.16.0"
4+
spec.summary = "iOS touch and keyboard synthesis library for unit tests."
55
spec.description = "Hammer is a touch and keyboard synthesis library for emulating user interaction events. It enables new ways of triggering UI actions in unit tests, replicating a real world environment as much as possible."
66
spec.homepage = "https://github.com/lyft/Hammer"
77
spec.screenshots = "https://user-images.githubusercontent.com/585835/116217617-ab410080-a6fe-11eb-9de1-3d42f7dd6037.gif"

Sources/Hammer/EventGenerator/EventGenerator.swift

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,11 @@ public final class EventGenerator {
2222
public let window: UIWindow
2323

2424
/// The view that was used to create the event generator
25-
public private(set) var mainView: UIView
25+
public let mainView: UIView
2626

2727
var activeTouches = TouchStorage()
2828
var debugWindow = DebugVisualizerWindow()
2929
var eventCallbacks = [UInt32: CompletionHandler]()
30-
private var isUsingCustomWindow: Bool = false
3130

3231
/// The default sender id for all events.
3332
///
@@ -42,18 +41,25 @@ public final class EventGenerator {
4241

4342
/// Initialize an event generator for a specified UIWindow.
4443
///
45-
/// - parameter window: The window to receive events.
46-
public init(window: UIWindow) throws {
44+
/// - parameter window: The window to receive events.
45+
/// - parameter mainView: The view that was used to create the event generator
46+
private init(window: UIWindow, mainView: UIView) throws {
4747
self.window = window
48+
self.mainView = mainView
4849
self.window.layoutIfNeeded()
4950
self.debugWindow.frame = self.window.frame
50-
self.mainView = window
5151

5252
UIApplication.swizzle()
5353
UIApplication.registerForHIDEvents(ObjectIdentifier(self)) { [weak self] event in
5454
self?.markerEventReceived(event)
5555
}
56+
}
5657

58+
/// Initialize an event generator for a specified UIWindow.
59+
///
60+
/// - parameter window: The window to receive events.
61+
public convenience init(window: UIWindow) throws {
62+
try self.init(window: window, mainView: window)
5763
try self.waitUntilWindowIsReady()
5864
}
5965

@@ -64,20 +70,15 @@ public final class EventGenerator {
6470
///
6571
/// - parameter viewController: The viewController to receive events.
6672
public convenience init(viewController: UIViewController) throws {
67-
let window = viewController.view.window ?? UIWindow(wrapping: viewController)
68-
69-
if #available(iOS 13.0, *) {
70-
window.backgroundColor = .systemBackground
73+
if let window = viewController.view.window {
74+
try self.init(window: window, mainView: viewController.view)
7175
} else {
72-
window.backgroundColor = .white
76+
let window = HammerWindow()
77+
window.presentContained(viewController)
78+
try self.init(window: window, mainView: viewController.view)
7379
}
7480

75-
window.makeKeyAndVisible()
76-
window.layoutIfNeeded()
77-
78-
try self.init(window: window)
79-
self.isUsingCustomWindow = true
80-
self.mainView = viewController.view
81+
try self.waitUntilWindowIsReady()
8182
}
8283

8384
/// Initialize an event generator for a specified UIView.
@@ -88,25 +89,22 @@ public final class EventGenerator {
8889
/// - parameter alignment: The wrapping alignment to use.
8990
public convenience init(view: UIView, alignment: WrappingAlignment = .center) throws {
9091
if let window = view.window {
91-
try self.init(window: window)
92+
try self.init(window: window, mainView: view)
9293
} else {
93-
try self.init(viewController: UIViewController(wrapping: view.topLevelView, alignment: alignment))
94+
let viewController = UIViewController(wrapping: view.topLevelView, alignment: alignment)
95+
let window = HammerWindow()
96+
window.presentContained(viewController)
97+
try self.init(window: window, mainView: view)
9498
}
9599

96-
self.mainView = view
100+
try self.waitUntilWindowIsReady()
97101
}
98102

99103
deinit {
100104
UIApplication.unregisterForHIDEvents(ObjectIdentifier(self))
101-
if self.isUsingCustomWindow {
102-
self.window.isHidden = true
103-
self.window.rootViewController = nil
104-
self.debugWindow.isHidden = true
105-
self.debugWindow.rootViewController = nil
106-
if #available(iOS 13.0, *) {
107-
self.window.windowScene = nil
108-
self.debugWindow.windowScene = nil
109-
}
105+
self.debugWindow.removeFromScene()
106+
if let window = self.window as? HammerWindow {
107+
window.dismissContained()
110108
}
111109
}
112110

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import UIKit
2+
3+
// Custom Window to have proper simulation of presentation and dismissal lifecycle events
4+
final class HammerWindow: UIWindow {
5+
private let hammerViewController = HammerViewController()
6+
7+
override var safeAreaInsets: UIEdgeInsets {
8+
return .zero
9+
}
10+
11+
init() {
12+
super.init(frame: UIScreen.main.bounds)
13+
self.rootViewController = self.hammerViewController
14+
15+
if #available(iOS 13.0, *) {
16+
self.backgroundColor = .systemBackground
17+
} else {
18+
self.backgroundColor = .white
19+
}
20+
}
21+
22+
@available(*, unavailable)
23+
required init?(coder: NSCoder) {
24+
fatalError("init(coder:) has not been implemented")
25+
}
26+
27+
func presentContained(_ viewController: UIViewController) {
28+
self.makeVisibleAndKey()
29+
self.hammerViewController.presentContained(viewController)
30+
}
31+
32+
func dismissContained() {
33+
self.hammerViewController.dismissContained()
34+
self.removeFromScene(removeViewController: false)
35+
}
36+
}
37+
38+
private final class HammerViewController: UIViewController {
39+
private let containerView = UIView()
40+
41+
override var shouldAutomaticallyForwardAppearanceMethods: Bool { false }
42+
override var prefersStatusBarHidden: Bool { true }
43+
44+
override func viewDidLoad() {
45+
super.viewDidLoad()
46+
self.view.backgroundColor = .clear
47+
self.containerView.translatesAutoresizingMaskIntoConstraints = false
48+
self.view.addSubview(self.containerView)
49+
50+
// We only activate the top and leading constraints to allow the content to size itself.
51+
NSLayoutConstraint.activate([
52+
self.containerView.topAnchor.constraint(equalTo: self.view.topAnchor),
53+
self.containerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
54+
self.containerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
55+
self.containerView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
56+
])
57+
}
58+
59+
func presentContained(_ viewController: UIViewController) {
60+
viewController.beginAppearanceTransition(true, animated: false)
61+
self.addChild(viewController)
62+
63+
viewController.view.translatesAutoresizingMaskIntoConstraints = false
64+
self.containerView.addSubview(viewController.view)
65+
NSLayoutConstraint.activate([
66+
viewController.view.topAnchor.constraint(equalTo: self.containerView.topAnchor),
67+
viewController.view.bottomAnchor.constraint(equalTo: self.containerView.bottomAnchor),
68+
viewController.view.leadingAnchor.constraint(equalTo: self.containerView.leadingAnchor),
69+
viewController.view.trailingAnchor.constraint(equalTo: self.containerView.trailingAnchor),
70+
])
71+
72+
viewController.didMove(toParent: self)
73+
viewController.endAppearanceTransition()
74+
self.view.layoutIfNeeded()
75+
}
76+
77+
func dismissContained() {
78+
for viewController in self.children {
79+
viewController.beginAppearanceTransition(false, animated: false)
80+
viewController.willMove(toParent: nil)
81+
viewController.view.removeFromSuperview()
82+
viewController.removeFromParent()
83+
viewController.endAppearanceTransition()
84+
}
85+
}
86+
}
87+
88+
extension UIWindow {
89+
func makeVisibleAndKey(file: StaticString = #file, line: UInt = #line) {
90+
self.addToMainSceneIfNeeded(file: file, line: line)
91+
self.makeKeyAndVisible()
92+
}
93+
94+
func addToMainSceneIfNeeded(file: StaticString = #file, line: UInt = #line) {
95+
guard #available(iOS 13.0, *) else {
96+
return
97+
}
98+
99+
guard self.windowScene == nil else {
100+
return
101+
}
102+
103+
if let mainScene = UIScene.mainOrFirstConnectedScene {
104+
self.windowScene = mainScene
105+
} else {
106+
assertionFailure("Unable to find main scene", file: file, line: line)
107+
}
108+
}
109+
110+
func removeFromScene(removeViewController: Bool = true) {
111+
self.isHidden = true
112+
113+
if #available(iOS 13.0, *) {
114+
self.windowScene = nil
115+
}
116+
117+
if removeViewController {
118+
self.rootViewController = nil
119+
}
120+
}
121+
}
122+
123+
@available(iOS 13.0, *)
124+
private extension UIScene {
125+
static var mainOrFirstConnectedScene: UIWindowScene? {
126+
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
127+
return scenes.first { $0.screen == UIScreen.main } ?? scenes.first
128+
}
129+
}

Sources/Hammer/Utilties/UIKit+Extensions.swift

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,6 @@ extension UIDevice {
4343
}
4444
}
4545

46-
extension UIWindow {
47-
convenience init(wrapping viewController: UIViewController) {
48-
self.init(frame: UIScreen.main.bounds)
49-
if #available(iOS 13.0, *), self.windowScene == nil {
50-
self.windowScene = UIApplication.shared.connectedScenes
51-
.compactMap { $0 as? UIWindowScene }.first
52-
}
53-
self.rootViewController = viewController
54-
}
55-
}
56-
5746
extension UIViewController {
5847
convenience init(wrapping view: UIView, alignment: EventGenerator.WrappingAlignment) {
5948
self.init(nibName: nil, bundle: nil)

Tests/HammerTests/KeyboardTests.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,11 @@ final class KeyboardTests: XCTestCase {
187187
view.centerXAnchor.constraint(equalTo: viewController.view.centerXAnchor),
188188
])
189189

190-
let window = UIWindow(wrapping: viewController)
190+
let window = UIWindow(frame: UIScreen.main.bounds)
191+
window.rootViewController = viewController
192+
window.addToMainSceneIfNeeded()
191193
window.isHidden = false
194+
defer { window.removeFromScene() }
192195

193196
let eventGenerator = try EventGenerator(window: window)
194197
try eventGenerator.waitUntilHittable(timeout: 1)

0 commit comments

Comments
 (0)