From 698f0edc156deaca898c9e30d5d99508d532625d Mon Sep 17 00:00:00 2001 From: Gabriel Lanata Date: Sat, 21 Sep 2024 15:10:07 -0700 Subject: [PATCH 1/4] Changes --- .../EventGenerator/EventGenerator.swift | 54 ++++---- Sources/Hammer/Utilties/HammerWindow.swift | 130 ++++++++++++++++++ 2 files changed, 156 insertions(+), 28 deletions(-) create mode 100644 Sources/Hammer/Utilties/HammerWindow.swift diff --git a/Sources/Hammer/EventGenerator/EventGenerator.swift b/Sources/Hammer/EventGenerator/EventGenerator.swift index 39767d2..61895d9 100644 --- a/Sources/Hammer/EventGenerator/EventGenerator.swift +++ b/Sources/Hammer/EventGenerator/EventGenerator.swift @@ -22,12 +22,11 @@ public final class EventGenerator { public let window: UIWindow /// The view that was used to create the event generator - public private(set) var mainView: UIView + public let mainView: UIView var activeTouches = TouchStorage() var debugWindow = DebugVisualizerWindow() var eventCallbacks = [UInt32: CompletionHandler]() - private var isUsingCustomWindow: Bool = false /// The default sender id for all events. /// @@ -42,18 +41,25 @@ public final class EventGenerator { /// Initialize an event generator for a specified UIWindow. /// - /// - parameter window: The window to receive events. - public init(window: UIWindow) throws { + /// - parameter window: The window to receive events. + /// - parameter mainView: The view that was used to create the event generator + private init(window: UIWindow, mainView: UIView) throws { self.window = window + self.mainView = mainView self.window.layoutIfNeeded() self.debugWindow.frame = self.window.frame - self.mainView = window UIApplication.swizzle() UIApplication.registerForHIDEvents(ObjectIdentifier(self)) { [weak self] event in self?.markerEventReceived(event) } + } + /// Initialize an event generator for a specified UIWindow. + /// + /// - parameter window: The window to receive events. + public convenience init(window: UIWindow) throws { + try self.init(window: window, mainView: window) try self.waitUntilWindowIsReady() } @@ -64,20 +70,15 @@ public final class EventGenerator { /// /// - parameter viewController: The viewController to receive events. public convenience init(viewController: UIViewController) throws { - let window = viewController.view.window ?? UIWindow(wrapping: viewController) - - if #available(iOS 13.0, *) { - window.backgroundColor = .systemBackground + if let window = viewController.view.window { + try self.init(window: window, mainView: viewController.view) } else { - window.backgroundColor = .white + let window = HammerWindow() + window.presentContained(viewController) + try self.init(window: window, mainView: viewController.view) } - window.makeKeyAndVisible() - window.layoutIfNeeded() - - try self.init(window: window) - self.isUsingCustomWindow = true - self.mainView = viewController.view + try self.waitUntilWindowIsReady() } /// Initialize an event generator for a specified UIView. @@ -88,25 +89,22 @@ public final class EventGenerator { /// - parameter alignment: The wrapping alignment to use. public convenience init(view: UIView, alignment: WrappingAlignment = .center) throws { if let window = view.window { - try self.init(window: window) + try self.init(window: window, mainView: view) } else { - try self.init(viewController: UIViewController(wrapping: view.topLevelView, alignment: alignment)) + let viewController = UIViewController(wrapping: view.topLevelView, alignment: alignment) + let window = HammerWindow() + window.presentContained(viewController) + try self.init(window: window, mainView: view) } - self.mainView = view + try self.waitUntilWindowIsReady() } deinit { UIApplication.unregisterForHIDEvents(ObjectIdentifier(self)) - if self.isUsingCustomWindow { - self.window.isHidden = true - self.window.rootViewController = nil - self.debugWindow.isHidden = true - self.debugWindow.rootViewController = nil - if #available(iOS 13.0, *) { - self.window.windowScene = nil - self.debugWindow.windowScene = nil - } + self.debugWindow.removeFromScene() + if let window = self.window as? HammerWindow { + window.dismissContained() } } diff --git a/Sources/Hammer/Utilties/HammerWindow.swift b/Sources/Hammer/Utilties/HammerWindow.swift new file mode 100644 index 0000000..b3404dc --- /dev/null +++ b/Sources/Hammer/Utilties/HammerWindow.swift @@ -0,0 +1,130 @@ +import UIKit + +// Custom Window to have proper simulation of presentation and dismissal lifecycle events +final class HammerWindow: UIWindow { + private let hammerViewController = HammerViewController() + + override var safeAreaInsets: UIEdgeInsets { + return .zero + } + + init() { + super.init(frame: UIScreen.main.bounds) + self.rootViewController = self.hammerViewController + + if #available(iOS 13.0, *) { + self.backgroundColor = .systemBackground + } else { + self.backgroundColor = .white + } + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func presentContained(_ viewController: UIViewController) { + self.makeVisibleAndKey() + self.hammerViewController.presentContained(viewController) + } + + func dismissContained() { + self.hammerViewController.dismissContained() + self.removeFromScene(removeViewController: false) + } +} + +private final class HammerViewController: UIViewController { + private let containerView = UIView() + + override var shouldAutomaticallyForwardAppearanceMethods: Bool { false } + override var prefersStatusBarHidden: Bool { true } + + override func viewDidLoad() { + super.viewDidLoad() + self.view.backgroundColor = .clear + self.containerView.translatesAutoresizingMaskIntoConstraints = false + self.view.addSubview(self.containerView) + + // We only activate the top and leading constraints to allow the content to size itself. + NSLayoutConstraint.activate([ + self.containerView.topAnchor.constraint(equalTo: self.view.topAnchor), + self.containerView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor), + self.containerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), + self.containerView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), + ]) + } + + func presentContained(_ viewController: UIViewController) { + viewController.beginAppearanceTransition(true, animated: false) + self.addChild(viewController) + + viewController.view.translatesAutoresizingMaskIntoConstraints = false + self.containerView.addSubview(viewController.view) + NSLayoutConstraint.activate([ + viewController.view.topAnchor.constraint(equalTo: self.containerView.topAnchor), + viewController.view.bottomAnchor.constraint(equalTo: self.containerView.bottomAnchor), + viewController.view.leadingAnchor.constraint(equalTo: self.containerView.leadingAnchor), + viewController.view.trailingAnchor.constraint(equalTo: self.containerView.trailingAnchor), + ]) + + viewController.didMove(toParent: self) + viewController.endAppearanceTransition() + self.view.layoutIfNeeded() + } + + func dismissContained() { + for viewController in self.children { + viewController.beginAppearanceTransition(false, animated: false) + viewController.willMove(toParent: nil) + viewController.view.removeFromSuperview() + viewController.removeFromParent() + viewController.endAppearanceTransition() + } + } +} + +extension UIWindow { + func makeVisibleAndKey(file: StaticString = #file, line: UInt = #line) { + self.addToMainSceneIfNeeded(file: file, line: line) + self.makeKeyAndVisible() + } + + func addToMainSceneIfNeeded(file: StaticString = #file, line: UInt = #line) { + guard #available(iOS 13.0, *) else { + return + } + + guard self.windowScene == nil else { + return + } + + if let mainScene = UIScene.mainOrFirstConnectedScene { + self.windowScene = mainScene + } else { + assertionFailure("Unable to find main scene", file: file, line: line) + } + } + + func removeFromScene(removeViewController: Bool = true) { + self.isHidden = true + + if #available(iOS 13.0, *) { + self.windowScene = nil + } + + if removeViewController { + self.rootViewController = nil + } + } +} + + +@available(iOS 13.0, *) +private extension UIScene { + static var mainOrFirstConnectedScene: UIWindowScene? { + let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } + return scenes.first { $0.screen == UIScreen.main } ?? scenes.first + } +} From cf7abdb9cdf35cc50c94dc2a5ca938db183d5bc7 Mon Sep 17 00:00:00 2001 From: Gabriel Lanata Date: Sat, 21 Sep 2024 15:11:17 -0700 Subject: [PATCH 2/4] Bump version --- HammerTests.podspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/HammerTests.podspec b/HammerTests.podspec index 79a0951..fd8137c 100644 --- a/HammerTests.podspec +++ b/HammerTests.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |spec| spec.name = "HammerTests" - spec.version = "0.15.0" - spec.summary = "iOS touch and keyboard syntheis library for unit tests." + spec.version = "0.16.0" + spec.summary = "iOS touch and keyboard synthesis library for unit tests." 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." spec.homepage = "https://github.com/lyft/Hammer" spec.screenshots = "https://user-images.githubusercontent.com/585835/116217617-ab410080-a6fe-11eb-9de1-3d42f7dd6037.gif" From f2468b9184e5cfe6a542a31c536c30fdaf071a56 Mon Sep 17 00:00:00 2001 From: Gabriel Lanata Date: Sat, 21 Sep 2024 15:14:10 -0700 Subject: [PATCH 3/4] Cleanup --- Sources/Hammer/Utilties/UIKit+Extensions.swift | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/Sources/Hammer/Utilties/UIKit+Extensions.swift b/Sources/Hammer/Utilties/UIKit+Extensions.swift index 933b251..c9b10d3 100644 --- a/Sources/Hammer/Utilties/UIKit+Extensions.swift +++ b/Sources/Hammer/Utilties/UIKit+Extensions.swift @@ -43,17 +43,6 @@ extension UIDevice { } } -extension UIWindow { - convenience init(wrapping viewController: UIViewController) { - self.init(frame: UIScreen.main.bounds) - if #available(iOS 13.0, *), self.windowScene == nil { - self.windowScene = UIApplication.shared.connectedScenes - .compactMap { $0 as? UIWindowScene }.first - } - self.rootViewController = viewController - } -} - extension UIViewController { convenience init(wrapping view: UIView, alignment: EventGenerator.WrappingAlignment) { self.init(nibName: nil, bundle: nil) From 6d4d9eecdee4605ee5e6e3ad8b6938808c8dfddc Mon Sep 17 00:00:00 2001 From: Gabriel Lanata Date: Sat, 21 Sep 2024 15:19:52 -0700 Subject: [PATCH 4/4] Fix --- Sources/Hammer/Utilties/HammerWindow.swift | 1 - Tests/HammerTests/KeyboardTests.swift | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/Hammer/Utilties/HammerWindow.swift b/Sources/Hammer/Utilties/HammerWindow.swift index b3404dc..62c1266 100644 --- a/Sources/Hammer/Utilties/HammerWindow.swift +++ b/Sources/Hammer/Utilties/HammerWindow.swift @@ -120,7 +120,6 @@ extension UIWindow { } } - @available(iOS 13.0, *) private extension UIScene { static var mainOrFirstConnectedScene: UIWindowScene? { diff --git a/Tests/HammerTests/KeyboardTests.swift b/Tests/HammerTests/KeyboardTests.swift index 19f62f7..3fd23de 100644 --- a/Tests/HammerTests/KeyboardTests.swift +++ b/Tests/HammerTests/KeyboardTests.swift @@ -187,8 +187,11 @@ final class KeyboardTests: XCTestCase { view.centerXAnchor.constraint(equalTo: viewController.view.centerXAnchor), ]) - let window = UIWindow(wrapping: viewController) + let window = UIWindow(frame: UIScreen.main.bounds) + window.rootViewController = viewController + window.addToMainSceneIfNeeded() window.isHidden = false + defer { window.removeFromScene() } let eventGenerator = try EventGenerator(window: window) try eventGenerator.waitUntilHittable(timeout: 1)