├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .ruby-version ├── DebugActions.podspec ├── LICENSE ├── Makefile ├── Metadata ├── async.png └── logo.png ├── Package.swift ├── README.md └── Sources └── DebugActions ├── DebugActionsInteraction.swift └── UIView.swift /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: macos-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | - name: Build 17 | run: make build 18 | env: 19 | DEVELOPER_DIR: /Applications/Xcode_11.4.app/Contents/Developer 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | .DS_Store 8 | *.xcodeproj 9 | 10 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 11 | *.xcscmblueprint 12 | *.xccheckout 13 | 14 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 15 | build/ 16 | DerivedData/ 17 | *.moved-aside 18 | *.pbxuser 19 | !default.pbxuser 20 | *.mode1v3 21 | !default.mode1v3 22 | *.mode2v3 23 | !default.mode2v3 24 | *.perspectivev3 25 | !default.perspectivev3 26 | 27 | ## Gcc Patch 28 | /*.gcno 29 | .build/ 30 | .swiftpm/ 31 | RouterService.xcodeproj/ 32 | build/ -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.6.5 2 | -------------------------------------------------------------------------------- /DebugActions.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'DebugActions' 3 | s.module_name = 'DebugActions' 4 | s.version = '0.2.0' 5 | s.license = { type: 'MIT', file: 'LICENSE' } 6 | s.summary = 'Enchance UIViews with special debugging menus.' 7 | s.homepage = 'https://github.com/rockbruno/DebugActions' 8 | s.author = { 'Bruno Rocha' => 'brunorochaesilva@gmail.com' } 9 | s.social_media_url = 'https://twitter.com/rockbruno_' 10 | s.swift_version = '5.1' 11 | s.ios.deployment_target = '13.0' 12 | s.source = { :git => 'https://github.com/rockbruno/DebugActions.git', :tag => s.version.to_s } 13 | s.source_files = 'Sources/DebugActions/**/*' 14 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Bruno Rocha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: 3 | swift package generate-xcodeproj 4 | xcodebuild -sdk iphonesimulator -------------------------------------------------------------------------------- /Metadata/async.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rockbruno/DebugActions/c02c64d5a34c4604b6fdf293a730d986a06dc852/Metadata/async.png -------------------------------------------------------------------------------- /Metadata/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rockbruno/DebugActions/c02c64d5a34c4604b6fdf293a730d986a06dc852/Metadata/logo.png -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "DebugActions", 8 | platforms: [ 9 | .iOS(.v13) 10 | ], 11 | products: [ 12 | .library(name: "DebugActions", type: .dynamic, targets: ["DebugActions"]) 13 | ], 14 | targets: [ 15 | .target( 16 | name: "DebugActions") 17 | ] 18 | ) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DebugActions 2 | 3 | ![DebugActions](https://github.com/rockbruno/DebugActions/raw/master/Metadata/logo.png "DebugActions") 4 | 5 | `DebugActions` is a framework that abstracts iOS 13's `UIContextMenuInteraction` APIs in order to enhance your `UIViews` with custom, debug-only actions accessible by force-touches. You can use this framework to super-charge views with useful helper actions that improve your daily debugging routine, like displaying the backend data that rendered a view or just copying something to the pasteboard. 6 | 7 | ## Usage 8 | 9 | `DebugAction`'s APIs are available to all `UIViews`. It begins by defining a `DebugMenuInteractionDelegate` (which can be the view itself) that returns all the available actions for that interaction in the shape of an `UIKit` `UIMenuElement`: 10 | 11 | ```swift 12 | extension LoggedUserView: DebugMenuInteractionDelegate { 13 | func copyUUID() -> UIMenuElement { 14 | return UIAction(title: "Copy UUID") { _ in 15 | // Copy the UUID to pasteboard 16 | } 17 | } 18 | 19 | func printLoginJSON() -> UIMenuElement { 20 | return UIAction(title: "Print the Backend's Login Response") { _ in 21 | // Print it! 22 | } 23 | } 24 | 25 | func debugActions() -> [UIMenuElement] { 26 | #if DEBUG // Make sure to provide them only in developer builds! 27 | return [copyUUID(), printLoginJSON()] 28 | #else 29 | return [] 30 | #endif 31 | } 32 | } 33 | ``` 34 | 35 | Now, all you have to do is enable the debug actions in your view! 36 | 37 | ```swift 38 | override func viewDidLoad() { 39 | super.viewDidLoad() 40 | loggedUserView.enableDebugActionsInteraction() 41 | } 42 | ``` 43 | 44 | ## Asynchronous Actions 45 | 46 | If your app supports **iOS 14**, you can also use the new `UIDeferredMenuElement` to create actions that are resolved asynchronously: 47 | 48 | ```swift 49 | func serverInformation() -> UIMenuElement { 50 | return UIDeferredMenuElement { completion in 51 | // an Async task that fetches some information about a server: 52 | completion([printServerInformationAction(serverInfo)]) 53 | } 54 | } 55 | ``` 56 | 57 | This results in a menu element with a loading indicator that is replaced with the final actions once the completion handler is called. 58 | 59 | ![DebugActions Async](https://github.com/rockbruno/DebugActions/raw/master/Metadata/async.png "DebugActions") 60 | 61 | ## Installation 62 | 63 | ### Swift Package Manager 64 | 65 | `.package(url: "https://github.com/rockbruno/DebugActions", .upToNextMinor(from: "0.2.0"))` 66 | 67 | ### CocoaPods 68 | 69 | `pod 'DebugActions'` 70 | -------------------------------------------------------------------------------- /Sources/DebugActions/DebugActionsInteraction.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | public protocol DebugActionsInteractionDelegate: AnyObject { 5 | func debugActions() -> [UIMenuElement] 6 | } 7 | 8 | public final class DebugActionsInteraction: UIContextMenuInteraction { 9 | 10 | static let identifier: NSString = "__debugInteractionSDK" 11 | private let contextMenuDelegateProxy: _Delegate 12 | 13 | public init(delegate: DebugActionsInteractionDelegate) { 14 | let contextMenuDelegateProxy = _Delegate() 15 | contextMenuDelegateProxy.debugDelegate = delegate 16 | self.contextMenuDelegateProxy = contextMenuDelegateProxy 17 | super.init(delegate: contextMenuDelegateProxy) 18 | } 19 | } 20 | 21 | extension DebugActionsInteraction { 22 | class _Delegate: NSObject, UIContextMenuInteractionDelegate { 23 | weak var debugDelegate: DebugActionsInteractionDelegate? 24 | static let title = "Debug Actions" 25 | 26 | public func contextMenuInteraction( 27 | _ interaction: UIContextMenuInteraction, 28 | configurationForMenuAtLocation location: CGPoint 29 | ) -> UIContextMenuConfiguration? { 30 | let debugActions = debugDelegate?.debugActions() ?? [] 31 | guard debugActions.isEmpty == false else { 32 | return nil 33 | } 34 | return UIContextMenuConfiguration( 35 | identifier: DebugActionsInteraction.identifier, 36 | previewProvider: nil 37 | ) { _ in 38 | return UIMenu(title: Self.title, children: debugActions) 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/DebugActions/UIView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import UIKit 3 | 4 | extension UIView { 5 | public func enableDebugActionsInteraction(delegate: DebugActionsInteractionDelegate) { 6 | disableDebugActionsInteraction(delegate: delegate) 7 | let debugInteraction = DebugActionsInteraction(delegate: delegate) 8 | addInteraction(debugInteraction) 9 | } 10 | 11 | public func disableDebugActionsInteraction(delegate: DebugActionsInteractionDelegate) { 12 | for case let interaction as DebugActionsInteraction in interactions { 13 | removeInteraction(interaction) 14 | } 15 | } 16 | } 17 | 18 | extension DebugActionsInteractionDelegate where Self: UIView { 19 | public func enableDebugActionsInteraction() { 20 | enableDebugActionsInteraction(delegate: self) 21 | } 22 | 23 | public func disableDebugActionsInteraction() { 24 | disableDebugActionsInteraction(delegate: self) 25 | } 26 | } 27 | --------------------------------------------------------------------------------