🕵🏽‍♂️ Inspector (检查器)

Inspector 是一个用 Swift 编写的调试库。

Header Demo GIF

目录


要求


为什么使用它?

改善开发体验

通过反向 Zeplin 改善 QA 和设计师的反馈


安装

Swift 包管理器

Swift 包管理器是一种用于自动分发 Swift 代码的工具,并集成到 Swift 编译器中。 它还处于早期开发阶段,但 Inspector 确实支持在支持的平台上使用它。

设置好 Swift 包后,将 Inspector 添加为依赖项就像将其添加到 Package.swift 的 dependencies 值一样简单。

// Add to Package.swift

// For projects with iOS 11+ support
dependencies: [
    .package(url: "https://github.com/ipedro/Inspector.git", .upToNextMajor(from: "2.0.0"))
]

// For projects with iOS 14+ support
dependencies: [
    .package(url: "https://github.com/ipedro/Inspector.git", .upToNextMajor(from: "3.0.0"))
]

设置

成功安装后,您需要做的就是在您的应用程序在SceneDelegate.swiftAppDelegate.swift 中完成启动后启动 Inspector。 您可以选择添加自己的自定义内容,并调整一些配置。

SceneDelegate.swift (场景代理)

// Scene Delegate Example

import UIKit

// Your application will not be rejected if you include the Inspector framework in your final bundle, however it's recommended that you import it only when debugging.

#if DEBUG
import Inspector
#endif

final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let _ = (scene as? UIWindowScene) else { return }

        (...)
        
        #if DEBUG
        Inspector.setConfiguration(...) // Optional. Add link to InspectorConfiguration
        Inspector.setCustomization(...) // Optional. Pass an object that conforms to the `InspectorCustomizationProviding` protocol.
        Inspector.start()
        #endif
    }

    (...)
}

AppDelegate.swift (应用代理)

// App Delegate Example

import UIKit

// Your application will not be rejected if you include the Inspector framework in your final bundle, however it's recommended that you import it only when debugging.

#if DEBUG
import Inspector
#endif

final class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {        
        (...)

        #if DEBUG
        Inspector.setConfiguration(...) // Optional. Add link to InspectorConfiguration
        Inspector.setCustomization(...) // Optional. Pass an object that conforms to the `InspectorCustomizationProviding` protocol.
        Inspector.start()
        #endif

        return true
    }

    (...)
}

SwiftUI (Beta)

请注意,SwiftUI 支持尚处于早期阶段,欢迎提供任何反馈。

// Add to your main view, or another view of your choosing

import Inspector
import SwiftUI

struct ContentView: View {
    @State var text = "Hello, world!"
    @State var date = Date()
    @State var isInspecting = false

    var body: some View {
        NavigationView {
            ScrollView {
                VStack(spacing: 15) {
                    DatePicker("Date", selection: $date)
                        .datePickerStyle(GraphicalDatePickerStyle())

                    TextField("text field", text: $text)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .padding()

                    Button("Inspect") {
                        isInspecting.toggle()
                    }
                    .padding()
                }
                .padding(20)
            }
            .inspect(
                isPresented: $isInspecting,
                viewHierarchyLayers: nil,
                elementColorProvider: nil,
                commandGroups: nil,
                elementLibraries: nil
            )
            .navigationTitle("SwiftUI Inspector")
        }
    }
}

启用键盘命令 *(推荐)*

扩展根视图控制器类以启用 Inspector 键盘命令。

// Add to your root view controller.

#if DEBUG
override var keyCommands: [UIKeyCommand]? {
    return Inspector.keyCommands
}
#endif

从发布版本中删除框架文件 *(推荐)*

在您的应用程序目标中

# Run Script Phase that removes `Inspector` and all its dependecies from release builds.

if [ $CONFIGURATION == "Release" ]; then
    echo "Removing Inspector and dependencies from $TARGET_BUILD_DIR/$FULL_PRODUCT_NAME/"

    find $TARGET_BUILD_DIR/$FULL_PRODUCT_NAME -name "Inspector*" | grep . | xargs rm -rf
    find $TARGET_BUILD_DIR/$FULL_PRODUCT_NAME -name "UIKeyCommandTableView*" | grep . | xargs rm -rf
    find $TARGET_BUILD_DIR/$FULL_PRODUCT_NAME -name "UIKeyboardAnimatable*" | grep . | xargs rm -rf
    find $TARGET_BUILD_DIR/$FULL_PRODUCT_NAME -name "UIKitOptions*" | grep . | xargs rm -rf
    find $TARGET_BUILD_DIR/$FULL_PRODUCT_NAME -name "Coordinator*" | grep . | xargs rm -rf
fi

呈现 Inspector

可以通过调用 presentInspector(animated:_:) 方法从任何视图控制器或窗口实例呈现检查器。您可以通过各种创造性的方式实现这一点,这里有一些建议。

使用内置键盘命令(可在模拟器和 iPad 上使用)

启用键盘命令支持后,使用 Simulator.app 或真正的 iPad,您可以

使用手势

您还可以使用手势(例如摇动设备)来呈现 Inspector。 这样就不需要引入 UI。 一种方便的方法是用以下代码对 UIWindow 进行子类化(或扩展)

// Declare inside a subclass or UIWindow extension.

#if DEBUG
open override func motionBegan(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
    super.motionBegan(motion, with: event)

    guard motion == .motionShake else { return }

    Inspector.present()
}
#endif

添加自定义 UI

在您的应用程序上创建自定义界面(例如浮动按钮或您选择的任何其他控件)后,您可以自己调用 Inspector.present(animated:)

// Add to any view controller if your view inherits from `UIControl`

var myControl: MyControl

override func viewDidLoad() {
    super.viewDidLoad()

    myControl.addTarget(self, action: #selector(tap), for: .touchUpInside)
}

@objc 
private func tap(_ sender: Any) {
    Inspector.present(animated: true)
}

自定义

Inspector 允许您通过 InspectorCustomizationProviding 协议自定义并在特定于您的代码库的视图上引入新的行为。

InspectorCustomizationProviding 协议


var viewHierarchyLayers: [Inspector.ViewHierarchyLayer]? { get }

ViewHierarchyLayer 是可切换的,并在 Inspector 界面的 Highlight views 部分显示,也可以使用 Ctrl + Shift + 1 - 8 触发。 您可以使用默认的图层之一或创建自己的图层。

默认的视图层级结构图层:

// Example

var viewHierarchyLayers: [Inspector.ViewHierarchyLayer]? {
    [
        .controls,
        .buttons,
        .staticTexts + .images,
        .layer(
            name: "Without accessibility identifiers",
            filter: { element in
                guard let accessibilityIdentifier = element.accessibilityIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines) else {
                    return true
                }
                return accessibilityIdentifier.isEmpty
            }
        )
    ]
}

var elementIconProvider: Inspector.ElementIconProvider? { get }

Return your own icons for custom classes or override exsiting ones. Preferred size is 32 x 32
// Example

var elementIconProvider: Inspector.ElementIconProvider? {
    .init { view in
        switch view {
        case is MyView:
            return UIImage(named: "my-view-icon-32")
            
        default:
            // you can alwayws fallback to default icons
            return nil
        }
    }
}

var elementColorProvider: Inspector.ElementColorProvider? { get }

返回您自己的层级结构标签颜色方案,而不是(或扩展)默认颜色方案。

// Example

var elementColorProvider: Inspector.ElementColorProvider? {
    .init { view in
        switch view {
        case is MyView:
            return .systemPink
            
        default:
            // you can alwayws fallback to default color scheme if needed
            return nil
        }
    }
}

var commandGroups: [Inspector.CommandGroup]? { get }

命令组显示为主 Inspector UI 上的部分,并且可以具有与之关联的键盘命令快捷方式,您可以拥有任意数量的组,以及任意数量的命令。

// Example

var commandGroups: [Inspector.CommandGroup]? {
    guard let window = window else { return [] }
    
    [
        .group(
            commands: [
                .command(
                    title: "Reset",
                    icon: .exampleCommandIcon,
                    keyCommand: .control(.shift(.key("r"))),
                    closure: {
                        // Instantiates a new initial view controller on a Storyboard application.
                        let storyboard = UIStoryboard(name: "Main", bundle: nil)
                        let vc = storyboard.instantiateInitialViewController()

                        // set new instance as the root view controller
                        window.rootViewController = vc
                        
                        // restart inspector
                        Inspector.restart()
                    }
                )
            ]
        )
    ]
}

var elementLibraries: [Inspector.ElementPanelType: [InspectorElementLibraryProtocol]] { get }

元素库是符合 InspectorElementLibraryProtocol 的实体,并且每个都绑定到唯一的类型。 *专业提示:使用枚举。*

// Example

var elementLibraries: [Inspector.ElementPanelType: [InspectorElementLibraryProtocol]] {
    [.attributes: ExampleElementLibrary.allCases]
}
// Element Library Example

import UIKit
import Inspector

enum ExampleAttributesLibrary: InspectorElementLibraryProtocol, CaseIterable {
    case roundedButton

    var targetClass: AnyClass {
        switch self {
        case .roundedButton:
            return RoundedButton.self
        }
    }

    func sections(for object: NSObject) -> InspectorElementSections {
        switch self {
        case .roundedButton:
            return .init(with: RoundedButtonAttributesSectionDataSource(with: object))
        }
    }
}
// Element Section Data Source

#if DEBUG
import UIKit
import Inspector

final class RoundedButtonAttributesSectionDataSource: InspectorElementSectionDataSource {
    var state: InspectorElementSectionState = .collapsed

    var title: String = "Rounded Button"

    let roundedButton: RoundedButton

    init?(with object: NSObject) {
        guard let roundedButton = object as? RoundedButton else {
            return nil
        }
        self.roundedButton = roundedButton
    }

    enum Properties: String, CaseIterable {
        case animateOnTouch = "Animate On Touch"
        case cornerRadius = "Round Corners"
        case backgroundColor = "Background Color"
    }

    var properties: [InspectorElementProperty] {
        Properties.allCases.map { property in
            switch property {
            case .animateOnTouch:
                return .switch(
                    title: property.rawValue,
                    isOn: { self.roundedButton.animateOnTouch }
                ) { animateOnTouch in
                    self.roundedButton.animateOnTouch = animateOnTouch
                }

            case .cornerRadius:
                return .switch(
                    title: property.rawValue,
                    isOn: { self.roundedButton.roundCorners }
                ) { roundCorners in
                    self.roundedButton.roundCorners = roundCorners
                }

            case .backgroundColor:
                return .colorPicker(
                    title: property.rawValue,
                    color: { self.roundedButton.backgroundColor }
                ) { newBackgroundColor in
                    self.roundedButton.backgroundColor = newBackgroundColor
                }
            }
        }
    }
}

#endif

捐赠

您可以通过 PayPal 支持开发。


鸣谢

InspectorPedro Almeida 拥有和维护。 您可以在 Twitter 上关注他 @ipedro 以获取项目更新和发布。


许可证

Inspector 在 MIT 许可证下发布。 请参阅 LICENSE 了解详细信息。