Debuggable Context,使用 Shake 手势触发调试用的 Action Sheet

是什么

可能很多 iOS 开发者都会遇到过这种类似的场景:

  1. 开发 UI (比如某个新的画面或者弹窗) 到一半的时候想要看一看效果,于是随便找一个 View Controller 写一些生成实例和 present 的代码。因为是测试代码,最好不要提交,需要记着删掉。在之后一段时间想要再看一看时又需要写一遍。
  2. 或者,在某个界面有一个由 Server 返回的 response 所触发的 Notification,程序的其他部分会在收到这个通知后继续接下来的流程。但是由于后端还没有实现,想要测试之后流程的话,你会在 viewDidAppear 里延迟个三四秒然后用代码模拟发送这个通知。
  3. 又或者,用户在当前界面的输入会影响到之前界面的状态,不过因为开发中的原因界面间的连接还没有做完,但是这时候你想要直接确认更改之前画面的某个值是否能按照预期工作,于是你把前一个画面的 View Controller 传递给当前的 View Controller,并对其中的状态进行更改。
  4. 以及其他很多很多那些在开发阶段的,为了调试为目的的 “临时代码”。

除了书写这些临时代码,并且在 commit 之前小心谨慎地将它们删掉,以及在下一次想要使用时再写一遍以外,我们应该要有一些更好的方式来处理这类开发需求。之前我接触了一些 React Native 的东西,觉得在 RN 上使用晃动设备的 shake 手势来触发 Debug Menu,进行热刷新和呼出调试台的功能很赞。我最近用 Swift 写了一个类似的小工具,用来简化调试代码和菜单的创建,可以让你在一定程度上避免上面的那些尴尬的情况。

参考 RN 的方式,相对于全局的 debug menu,在 native app 开发的时候,我们可能更希望这个菜单有下面这些特性:

  1. 方便地调出方式,Shake 手势是一个真机和模拟器都良好支持的不错的选择;
  2. 针对每一个上下文 (比如 某个 View Controller) 弹出自己的调试菜单,并执行任意代码;
  3. 可以自主进行上下文注册和注销,方便地在需要的时候开始对 Shake 手势进行响应;
  4. 对于同时存在多个调试上下文的时候,开发者应该可以自由选择操作某一个上下文;
  5. 这些 Debug 代码不应该被包含在 Release 版本中,我们需要由编译器保证调试代码只在 Debug build 中生效。也正因如此,将它们提交到代码库里也不存在问题 (而且应该鼓励这么做)。
  6. 提供过滤的方法,来屏蔽掉某些不关心的或者多余的 (别人添加的) 上下文。

项目我开源在了这个 GitHub 仓库,理论上可以支持 CocoaPods,不过因为它非常简单,加上 License 和注释也就 200 行,所以你想要使用时也完全可以直接把 DebuggableContext.swift 拖到工程里就行了。

怎么用

先简单说明一下用法,然后在后半部分对代码中几个值得一提的地方进行一些说明。

简单来说,用法是这样的:

  1. 让你的需要接受调试命令的类型遵守 DebuggableContext 协议,并通过实现 debugMenus 返回菜单项:

    #if DEBUG
    extension ViewController: DebuggableContext {
        var debugMenus: [DebuggableContextItem] {
            return [
                .init(name: "Color To Cupid") { [weak self] in
                    self?.view.backgroundColor = UIColor(red:0.94, green:0.73, blue:0.83, alpha:1.00)
                },
                .init(name: "Color To Mint") { [weak self] in
                    self?.view.backgroundColor = UIColor(red:0.71, green:0.96, blue:0.82, alpha:1.00)
                }
            ]
        }
    }
    
    extension AnotherViewController: DebuggableContext {
        var debugMenus: [DebuggableContextItem] {
            return [ .init(name: "Say Hello") { print("Hello World!") } ]
        }
    }
    #endif

    这里我们为 ViewController 添加了两个改变背景色的菜单项,为 AnotherViewController 添加了一个 Hello World。

  2. 在合适的地方调用 DebuggableContextregisterDebug(),将这个 context 注册到调试菜单中,对于 View Controller 来说,viewDidLoad 就是一个不错的地方:

    // In both ViewController and AnotherViewController
    override func viewDidLoad() {
        super.viewDidLoad()
        #if DEBUG
        registerDebug()
        #endif
    }
  3. 为了在全局接收到 Shake 手势,需要将 AppDelegate 中原来的 var window: UIWindow? 替换为自定义的 ShakeDetectingWindow

    #if DEBUG
    var _shakeDetectingWindow: ShakeDetectingWindow?
    var window: UIWindow? {
        get {
            return _shakeDetectingWindow ?? {
                _shakeDetectingWindow = ShakeDetectingWindow(frame: UIScreen.main.bounds)
                return _shakeDetectingWindow
            }()
        }
        set {}
    }
    #else
    var window: UIWindow?
    #endif

没有第四步了!现在你就可以在这两个 View Controller 中通过摇晃设备或者模拟器快捷键 (Ctrl + Cmd + Z) 唤起调试菜单了~效果如下,它在第二个 AnotherViewController 中通过调试菜单将第一个 ViewController 的背景色改为了“丘比特色”:

默认情况下,通过 registerDebug() 注册的还存在于内存中的 context 都会显示出来,随着实现 DebuggableContext 的类型增多,可能会出现难以寻找需要的 context 的问题。这种时候,你可以让你的 View Controller 实现 DebuggableContextFiltering,来将需要显示的 context 过滤出来:

```swift

if DEBUG

extension AnotherViewController: DebuggableContextFiltering {

top Created with Sketch.