From 65d7e0839ef61b28c202a45d54476b8ce570c004 Mon Sep 17 00:00:00 2001 From: Dave Lyon Date: Wed, 28 Sep 2016 12:36:37 -0700 Subject: [PATCH 1/2] Experimental first pass at viewcontroller support --- CheatCodes/Classes/CheatCodeCommand.swift | 1 + CheatCodes/Classes/CheatCodes.swift | 36 +++++++++++++ Example/CheatCodes/Base.lproj/Main.storyboard | 54 +++++++++++++++++-- Example/CheatCodes/ViewController.swift | 29 ++++++++-- 4 files changed, 111 insertions(+), 9 deletions(-) diff --git a/CheatCodes/Classes/CheatCodeCommand.swift b/CheatCodes/Classes/CheatCodeCommand.swift index 5204693..d624c09 100644 --- a/CheatCodes/Classes/CheatCodeCommand.swift +++ b/CheatCodes/Classes/CheatCodeCommand.swift @@ -28,6 +28,7 @@ public struct CheatCodeCommand { } internal extension CheatCodeCommand { + func toKeyCommand() -> UIKeyCommand { if #available(iOS 9.0, *) { return UIKeyCommand(input: input, modifierFlags: modifierFlags, action: action, discoverabilityTitle: discoverabilityTitle) diff --git a/CheatCodes/Classes/CheatCodes.swift b/CheatCodes/Classes/CheatCodes.swift index 98d9b3a..9c61f59 100644 --- a/CheatCodes/Classes/CheatCodes.swift +++ b/CheatCodes/Classes/CheatCodes.swift @@ -58,6 +58,24 @@ extension UIKeyCommand { contents(&formatter) formatter.printContents() } +} + +public protocol CheatCodeResponder: CustomDebugStringConvertible { + var cheatCodes: [CheatCodeCommand] { get } +} + +public extension UIResponder { + + func addCheatCodes() { + if #available(iOS 9.0, *) { + guard let viewController = self as? UIViewController, viewController is CheatCodeResponder else { return } + (self as! CheatCodeResponder).cheatCodes.forEach { code in + viewController.addKeyCommand(code.toKeyCommand()) + } + } else { + // Fallback on earlier versions + } + } } @@ -87,6 +105,24 @@ internal extension UIKeyCommand { formatter.addKey(" \($0.keyCombo)", value: $0.discoverabilityTitle) } } + + guard let window = UIApplication.shared.keyWindow else { return } + + let bottomResponder = window.perform(Selector(("_deepestUnambiguousResponder"))).takeUnretainedValue() as? UIResponder + var next: UIResponder? = bottomResponder?.next + while next != nil { + if let cheater = next as? CheatCodeResponder { + let cheaterType = type(of: cheater) + tableFormatted(title: "\(cheaterType) Cheat Codes") { formatter2 in + cheater.cheatCodes.sorted(by: { (c1, c2) -> Bool in + c1.input <= c2.input + }).forEach { + formatter2.addKey(" \($0.keyCombo)", value: $0.discoverabilityTitle) + } + } + } + next = next!.next + } } /// Toggles the tint adjustment mode between `automatic` and `dimmed` on the key window diff --git a/Example/CheatCodes/Base.lproj/Main.storyboard b/Example/CheatCodes/Base.lproj/Main.storyboard index 52ea29e..7d9f49c 100644 --- a/Example/CheatCodes/Base.lproj/Main.storyboard +++ b/Example/CheatCodes/Base.lproj/Main.storyboard @@ -1,25 +1,69 @@ - + - + + + - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/CheatCodes/ViewController.swift b/Example/CheatCodes/ViewController.swift index c26f6b9..1cdf76e 100644 --- a/Example/CheatCodes/ViewController.swift +++ b/Example/CheatCodes/ViewController.swift @@ -7,18 +7,39 @@ // import UIKit +import CheatCodes class ViewController: UIViewController { + let firstRunKey = "showedFirstRunView" + + var hasSeenFirstTimeView: Bool { + get { return UserDefaults.standard.bool(forKey: firstRunKey) } + set { UserDefaults.standard.set(hasSeenFirstTimeView, forKey: firstRunKey) } + } + override func viewDidLoad() { super.viewDidLoad() - // Do any additional setup after loading the view, typically from a nib. + if !hasSeenFirstTimeView { + showFirstTimeView() + } + + // We need to `opt in` somewhere + addCheatCodes() } - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. + func showFirstTimeView() { + hasSeenFirstTimeView = true + // Add a view over the whole main view + print("Gonna show a view!") } } +extension ViewController: CheatCodeResponder { + var cheatCodes: [CheatCodeCommand] { + return [ + CheatCodeCommand(input: UIKeyInputUpArrow, modifierFlags: [.control,.command], action: #selector(showFirstTimeView), discoverabilityTitle: "Show the 'first time' screen") + ] + } +} From 40a37d7a37202915801ef45e9ba4cb1b650324e2 Mon Sep 17 00:00:00 2001 From: Dave Lyon Date: Thu, 29 Sep 2016 12:07:26 -0700 Subject: [PATCH 2/2] Improve the `UIViewController` interface Update documentation and make 0.3.0 release ready --- CheatCodes.podspec | 9 ++--- CheatCodes/Classes/CheatCodeCommand.swift | 25 ++++++++++++ CheatCodes/Classes/CheatCodeResponder.swift | 40 +++++++++++++++++++ CheatCodes/Classes/CheatCodes.swift | 24 ++--------- .../Classes/FormattedKeyValuePrinter.swift | 10 ++++- CheatCodes/Classes/UIKeyModifierFlags.swift | 2 +- Example/CheatCodes/ViewController.swift | 4 +- Example/Pods/Pods.xcodeproj/project.pbxproj | 6 +++ README.md | 27 ++++++++++++- 9 files changed, 116 insertions(+), 31 deletions(-) create mode 100644 CheatCodes/Classes/CheatCodeResponder.swift diff --git a/CheatCodes.podspec b/CheatCodes.podspec index d48f0b6..35537b7 100644 --- a/CheatCodes.podspec +++ b/CheatCodes.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'CheatCodes' - s.version = '0.2.1' + s.version = '0.3.0' s.summary = 'UIKeyCommand shortcuts for debugging applications in the simulator' s.description = <<-DESC @@ -14,8 +14,6 @@ Available Cheat Codes: ⌘ + ⇧ + ^ + ↓: Trigger restorable state preservation ⇧ + ^ + d: Print documents directory path ⇧ + ^ + e: Re-enable user interaction - ⌘ + ⌥ + f: Reset all first run screens - ⇧ + ^ + g: Log in a default user account ⇧ + ^ + h: Print the list of available commands ⇧ + ^ + i: Print general device info ⇧ + ^ + l: Print autolayout backtrace @@ -36,8 +34,7 @@ DESC s.source_files = 'CheatCodes/Classes/**/*' s.pod_target_xcconfig = { - 'SWIFT_ACTIVE_COMPILATION_CONDITIONS' => 'CHEAT_${CONFIGURATION}', - 'SWIFT_VERSION' => '3.0' - } + 'SWIFT_ACTIVE_COMPILATION_CONDITIONS' => 'CHEATS_${CONFIGURATION}', + } end diff --git a/CheatCodes/Classes/CheatCodeCommand.swift b/CheatCodes/Classes/CheatCodeCommand.swift index d624c09..f5653f3 100644 --- a/CheatCodes/Classes/CheatCodeCommand.swift +++ b/CheatCodes/Classes/CheatCodeCommand.swift @@ -3,9 +3,34 @@ import UIKit /// A descriptor for a CheatCodeCommand that wraps a UIKeyCommand public struct CheatCodeCommand { + /** A single-character string constant that will trigger this command when + pressed along with `modifierFlags` + + - note: Also supports these constants: + - UIKeyInputUpArrow + - UIKeyInputDownArrow + - UIKeyInputLeftArrow + - UIKeyInputRightArrow + - UIKeyInputEscape + */ public let input: String + + /** A set of `UIKeyModifierFlags` that must be help along with `input` to + trigger the `UIKeyCommand` + + - note: The current default is `[.control, .shift]` + - note: When using the iOS simulator, `.option` may cause issues due to + being used as part of the "Pinch" gesture support. Suggested flags + are: `[.control, .shift]`, `[.control, .command]` + and `[.control, .command, .shift]` + */ public let modifierFlags: UIKeyModifierFlags + + /// The `#selector` to call when the key command is activated public let action: Selector + + /// A description used in an on-screen overlay (on iPad) and in the default + /// help output for `CheatCodes` in the debug console public let discoverabilityTitle: String diff --git a/CheatCodes/Classes/CheatCodeResponder.swift b/CheatCodes/Classes/CheatCodeResponder.swift new file mode 100644 index 0000000..e3d3bce --- /dev/null +++ b/CheatCodes/Classes/CheatCodeResponder.swift @@ -0,0 +1,40 @@ +/** + Protocol that indicates a `UIViewController` has opted in to using `CheatCodes` + + - requires: Must call `addCheatCodes()` in `viewDidLoad` method to install + `UIKeyCommands` + - warning: you are strongly encouraged to wrap your extension in an `#if` block + so that it will not be compiled for your app store releases. + + #if CHEATS_ENABLED + extension MyViewController: CheatCodeResponder { ... } + #endif +*/ +public protocol CheatCodeResponder: class { + var cheatCodes: [CheatCodeCommand] { get } +} + +/** Extenstion to trigger installing `UIKeyCommand`s from `CheatCodeCommands` defined + in the `UIViewController`'s `cheatCodes` computed var. + + - seealso: CheatCodeResponder +*/ +public extension UIViewController { + #if CHEATS_Release + /// :nodoc: + @available(iOS 9.0, *) + func addCheatCodes() { /* Don't do anything in Release builds */ } + #else + /** + Add the cheat codes defined by this ViewController's `cheatCodes` computed var + - requires: conformance to `CheatCodeResponder` + */ + @available(iOS 9.0, *) + func addCheatCodes() { + guard let viewController = self as? CheatCodeResponder else { return } + viewController.cheatCodes.forEach { code in + addKeyCommand(code.toKeyCommand()) + } + } + #endif +} diff --git a/CheatCodes/Classes/CheatCodes.swift b/CheatCodes/Classes/CheatCodes.swift index 9c61f59..c0dee9f 100644 --- a/CheatCodes/Classes/CheatCodes.swift +++ b/CheatCodes/Classes/CheatCodes.swift @@ -14,7 +14,10 @@ fileprivate extension Cheat { } // MARK: - Cheat Codes Public Infterface -/// CheatCodes additions +/** + Cheat code extensions for `UIKeyCommand` -- adding them here privately makes them + accessible when installing them at the `AppDelegate` level. + */ extension UIKeyCommand { #if CHEATS_Release @@ -60,25 +63,6 @@ extension UIKeyCommand { } } -public protocol CheatCodeResponder: CustomDebugStringConvertible { - var cheatCodes: [CheatCodeCommand] { get } -} - -public extension UIResponder { - - func addCheatCodes() { - if #available(iOS 9.0, *) { - guard let viewController = self as? UIViewController, viewController is CheatCodeResponder else { return } - (self as! CheatCodeResponder).cheatCodes.forEach { code in - viewController.addKeyCommand(code.toKeyCommand()) - } - } else { - // Fallback on earlier versions - } - } - -} - // MARK: - Cheat Codes Private Interface internal extension UIKeyCommand { #if CHEATS_Release diff --git a/CheatCodes/Classes/FormattedKeyValuePrinter.swift b/CheatCodes/Classes/FormattedKeyValuePrinter.swift index 2c983a4..e5893fd 100644 --- a/CheatCodes/Classes/FormattedKeyValuePrinter.swift +++ b/CheatCodes/Classes/FormattedKeyValuePrinter.swift @@ -5,14 +5,20 @@ import Foundation the title, and the dictionary with the keys right aligned to the longest key. */ public struct FormattedKeyValuePrinter { + /// A title for the generated output public let title: String - var keyValuePairs = [(String,String)]() - var maxKeyLength = 0 + + private var keyValuePairs = [(String,String)]() + private var maxKeyLength = 0 init(title: String) { self.title = title } + /** Add a key and value to the formatted output + - parameter key: a descriptor for the value shown + - parameter value: the relevant piece of data being described + */ public mutating func addKey(_ key: String, value: String?) { maxKeyLength = max(maxKeyLength, key.characters.count) keyValuePairs.append((key,value ?? "(NO VALUE)")) diff --git a/CheatCodes/Classes/UIKeyModifierFlags.swift b/CheatCodes/Classes/UIKeyModifierFlags.swift index 23dd1aa..36b6423 100644 --- a/CheatCodes/Classes/UIKeyModifierFlags.swift +++ b/CheatCodes/Classes/UIKeyModifierFlags.swift @@ -1,6 +1,6 @@ import UIKit -extension UIKeyModifierFlags { +internal extension UIKeyModifierFlags { func printableKeys() -> String { return [ diff --git a/Example/CheatCodes/ViewController.swift b/Example/CheatCodes/ViewController.swift index 1cdf76e..e68a4f9 100644 --- a/Example/CheatCodes/ViewController.swift +++ b/Example/CheatCodes/ViewController.swift @@ -25,7 +25,9 @@ class ViewController: UIViewController { } // We need to `opt in` somewhere - addCheatCodes() + if #available(iOS 9.0, *) { + addCheatCodes() + } } func showFirstTimeView() { diff --git a/Example/Pods/Pods.xcodeproj/project.pbxproj b/Example/Pods/Pods.xcodeproj/project.pbxproj index caa4b1e..6b13972 100644 --- a/Example/Pods/Pods.xcodeproj/project.pbxproj +++ b/Example/Pods/Pods.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 8E3194D934DDEC10B42FC04A3153EEC6 /* Pods-CheatCodes_Tests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BABA845FFEFF5932EEC617A8EFC022A /* Pods-CheatCodes_Tests-dummy.m */; }; C9670584F7BCB0F372E9A50CD38754EB /* Pods-CheatCodes_Example-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = DB8B9C39F83B6BDC20EBBA3020BBCD8D /* Pods-CheatCodes_Example-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; CBE472AC6AEE305A4A3CAE3C12472C95 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEC22C73C1608DFA5D5D78BDCB218219 /* Foundation.framework */; }; + D530FA8D1D9D8E3400A404ED /* CheatCodeResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D530FA8C1D9D8E3400A404ED /* CheatCodeResponder.swift */; }; D543B1131D84D05600103639 /* CheatCodeCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = D543B1121D84D05600103639 /* CheatCodeCommand.swift */; }; D543B1151D84D0CA00103639 /* UIKeyModifierFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = D543B1141D84D0CA00103639 /* UIKeyModifierFlags.swift */; }; D5DD5CFE1D8F572100BFA475 /* FormattedKeyValuePrinter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DD5CFD1D8F572000BFA475 /* FormattedKeyValuePrinter.swift */; }; @@ -61,6 +62,7 @@ CD4690E310F3C3AB85EADDEFD2DFDF69 /* CheatCodes-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "CheatCodes-prefix.pch"; sourceTree = ""; }; CD8941ECAE0440A44385894602C654FC /* Pods-CheatCodes_Tests.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = "sourcecode.module-map"; path = "Pods-CheatCodes_Tests.modulemap"; sourceTree = ""; }; CEC22C73C1608DFA5D5D78BDCB218219 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS9.3.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + D530FA8C1D9D8E3400A404ED /* CheatCodeResponder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheatCodeResponder.swift; sourceTree = ""; }; D543B1121D84D05600103639 /* CheatCodeCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CheatCodeCommand.swift; sourceTree = ""; }; D543B1141D84D0CA00103639 /* UIKeyModifierFlags.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIKeyModifierFlags.swift; sourceTree = ""; }; D5DD5CFD1D8F572000BFA475 /* FormattedKeyValuePrinter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormattedKeyValuePrinter.swift; sourceTree = ""; }; @@ -148,6 +150,7 @@ isa = PBXGroup; children = ( D543B1121D84D05600103639 /* CheatCodeCommand.swift */, + D530FA8C1D9D8E3400A404ED /* CheatCodeResponder.swift */, 099EB141C22C531B316079935A43CD21 /* CheatCodes.swift */, D5DD5CFD1D8F572000BFA475 /* FormattedKeyValuePrinter.swift */, D543B1141D84D0CA00103639 /* UIKeyModifierFlags.swift */, @@ -359,6 +362,7 @@ D543B1131D84D05600103639 /* CheatCodeCommand.swift in Sources */, D543B1151D84D0CA00103639 /* UIKeyModifierFlags.swift in Sources */, 8D5889B0A643C2FBC89BD999196E001C /* CheatCodes.swift in Sources */, + D530FA8D1D9D8E3400A404ED /* CheatCodeResponder.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -543,6 +547,7 @@ PRODUCT_NAME = CheatCodes; SDKROOT = iphoneos; SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "CHEATS_${CONFIGURATION}"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -648,6 +653,7 @@ PRODUCT_NAME = CheatCodes; SDKROOT = iphoneos; SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "CHEATS_${CONFIGURATION}"; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; diff --git a/README.md b/README.md index 316826e..a357ff2 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,32 @@ class AppDelegate { } ``` -### Adding Custom Commands +### Adding Custom Commands (to a view controller) + +```swift +import UIKit +import CheatCodes +class MyViewController: UIViewController { + override func viewDidLoad() { + super.viewDidLoad() + + // When compiled for a release build, this method does nothing + addCheatCodes() + } +} + +#if CHEATS_ENABLED +extension MyViewController: CheatCodeResponder { + var cheatCodes: [CheatCodeCommand] { + return [ + CheatCodeCommand(input: UIKeyInputUpArrow, modifierFlags: [.control,.command], action: #selector(showFirstTimeView), discoverabilityTitle: "Show the 'first time' screen") + ] + } +} +#endif +``` + +### Adding Custom Commands (global) ```swift #if CHEATS_ENABLED