CBAssistiveTouch is an iOS Swift package for building a draggable floating tool button (AssistiveTouch-style) with optional expandable content.
This repository currently exposes two Swift Package products:
CBAssistiveTouch: core floating button and content presentation APIs.CBLoggerWindow: a ready-to-use floating logger console built on top ofCBAssistiveTouch.
- iOS 15.0+
- Swift 5.10+
Add the package in Xcode (File -> Add Package Dependencies...) or in Package.swift:
dependencies: [
.package(url: "https://github.com/cardinalblue/CBAssistiveTouch.git", from: "2.0.0")
]Then add one or both products to your target:
.product(name: "CBAssistiveTouch", package: "CBAssistiveTouch")
.product(name: "CBLoggerWindow", package: "CBAssistiveTouch")import CBAssistiveTouch
import UIKit
@MainActor
final class DebugTools {
private var assistiveTouch: AssistiveTouch?
func install(on window: UIWindow) {
let contentViewController = UIViewController()
contentViewController.view.backgroundColor = .systemBackground
contentViewController.preferredContentSize = CGSize(width: 320, height: 360)
let layout = DefaultAssistiveTouchLayout(applicationWindow: window)
layout.margin = 16
assistiveTouch = AssistiveTouch(
applicationWindow: window,
layout: layout,
contentViewController: contentViewController
)
assistiveTouch?.show()
}
}Main controls:
show(),hide(),toggle()showContent(),hideContent(),toggleContent()
let layout = DefaultAssistiveTouchLayout(applicationWindow: window)
let iconView = UIImageView(image: UIImage(systemName: "hammer.fill"))
iconView.tintColor = .white
iconView.backgroundColor = .systemBlue
iconView.contentMode = .center
iconView.frame = CGRect(x: 0, y: 0, width: 44, height: 44)
iconView.layer.cornerRadius = 22
iconView.layer.masksToBounds = true
layout.customView = iconView
layout.assistiveTouchSize = iconView.bounds.size
layout.margin = 16You can also provide your own AssistiveTouchLayout implementation if the default layout is not enough.
If your presented view controller should allow touches to pass through specific areas, conform to ATPassthroughable:
import CBAssistiveTouch
extension DebugPanelViewController: ATPassthroughable {
func shouldPassthroughTouch(at point: CGPoint) -> Bool {
// Return true for areas that should pass touches to underlying views.
return false
}
}import CBLoggerWindow
import SwiftUI
import UIKit
@main
struct DemoApp: App {
@StateObject private var loggerController = LoggerDemoController()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(loggerController)
.onAppear { loggerController.configureIfNeeded() }
}
}
}
@MainActor
final class LoggerDemoController: ObservableObject {
private var loggerWindow: CBLoggerWindow?
func configureIfNeeded() {
guard loggerWindow == nil else { return }
guard let window = UIApplication.shared.cbatKeyWindow else { return }
let loggerWindow = CBLoggerWindow(applicationWindow: window, margin: 16)
loggerWindow.show()
loggerWindow.log(event: "SwiftUI demo ready")
self.loggerWindow = loggerWindow
}
func log(_ event: String) {
loggerWindow?.log(event: event)
}
func toggleConsole() {
loggerWindow?.toggleContent()
}
}
private extension UIApplication {
var cbatKeyWindow: UIWindow? {
connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap(\.windows)
.first(where: \.isKeyWindow)
}
}import CBLoggerWindow
import UIKit
final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
private var loggerWindow: CBLoggerWindow?
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
guard let windowScene = scene as? UIWindowScene else { return }
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIViewController()
window.makeKeyAndVisible()
self.window = window
let loggerWindow = CBLoggerWindow(applicationWindow: window, margin: 16)
loggerWindow.show()
loggerWindow.log(event: "UIKit demo ready")
self.loggerWindow = loggerWindow
}
}You can add custom action buttons to the logger console toolbar:
let loggerWindow = CBLoggerWindow(
applicationWindow: window,
margin: 16,
actions: [
CBLoggerWindow.Action(title: "RESET") {
// restore defaults
},
CBLoggerWindow.Action(title: "EXPORT") {
// export logs
}
]
)Custom actions appear in the toolbar alongside the built-in close and clear buttons.
- A demo app is included in
Demo/. - Main demo controller:
Demo/Demo/AssistiveTouchDemoController.swift.
Install local development dependencies:
npm install
mise installGenerate the Demo project with Tuist:
cd Demo
mise exec -- tuist install
mise exec -- tuist generate- Lint rules are in
.swiftlint.yml. - Tool versions are pinned in
.mise.toml(SwiftLint). - Git pre-push hook (
.husky/pre-push) runs SwiftLint in strict mode (changed files on feature branches, all files on base branch).
MIT. See LICENSE.
