TADebugTools is a lightweight, modular in-app debugging toolkit for SwiftUI applications. It allows developers to view, edit, and trigger debugging entries directly from within the app.
- 🔐 Flexible Password Protection: Multiple authentication strategies for different environments
- 🧩 Custom Debug Entries: Easily add toggles, buttons, text fields, constants, and more
- 🗂️ Sectioned UI: Organize your debugging entries by category
- 🧪 Property Wrapper Support: Quickly bind debug state to your app's logic
- 🔄 Bidirectional State Sync: Keep debug entries in sync with your app's state
- 🎯 Production-Ready: Environment-specific configurations and hidden access patterns
To include TADebugTools in your project, add it to your Package.swift file:
let package = Package(
name: "YourProject",
platforms: [
.iOS(.v14)
],
dependencies: [
.package(
url: "git@github.com:TechArtists/ios-debug-tools.git",
from: "0.9.0"
)
],
targets: [
.target(
name: "YourTarget",
dependencies: [
.product(name: "TADebugTools", package: "TADebugTools")
]
)
]
)Or add it via Xcode:
- Go to File > Add Packages
- Enter the repo URL:
git@github.com:YourRepo/TADebugTools.git - Add it to your target
You can either sync entries with an external source of truth (like @AppStorage) or manage the source of truth from the configuration itself.
import SwiftUI
import TADebugTools
public class MyDebugToolConfiguration: TADebugToolConfiguration {
let isPremiumEntry: DebugEntryBool = .init(
title: "Is Premium",
wrappedValue: UserDefaults.standard.bool(forKey: "isPremium")
)
override public init(passwordType: TADebugToolConfiguration.PasswordType = .static(password: "")) {
super.init(passwordType: passwordType)
addEntriesToSections()
}
}struct PaywallWithDebugEntryView: View {
@EnvironmentObject var debugToolConfiguration: MyDebugToolConfiguration
@AppStorage("isPremium") var isPremium: Bool = UserDefaults.standard.bool(forKey: "isPremium")
var body: some View {
VStack {
Toggle(isOn: $isPremium) {
Text("Is Premium")
}
.onAppear {
debugToolConfiguration.isPremiumEntry.onUpdateFromDebugTool = { newValue in
if self.isPremium != newValue {
self.isPremium = newValue
}
}
}
.onChange(of: isPremium, perform: debugToolConfiguration.isPremiumEntry.onUpdateFromApp)
}
.padding()
}
}import SwiftUI
import TADebugTools
enum ServiceEnvironment: String, CaseIterable {
case production
case staging
}
public class MyDebugToolConfiguration2: TADebugToolConfiguration {
@Debuggable(key: "isDebuggableWorking")
var isDebuggableWorking = false
@Debuggable(title: "Test Action") var actionPrint = {
print("Action works")
}
@Debuggable(title: "Async Action") var asyncActionPrint = {
Task {
try await Task.sleep(for: .seconds(1))
print("Async Action works")
}
}
@Debuggable(key: "testConstant", section: .defaults)
var testConstant: String = "Hello World"
@Debuggable(key: "testTextField", textType: .textField)
var testTextField: String = "Hello World"
@Debuggable(key: "environment")
var environment: ServiceEnvironment = .staging
}struct PaywalSingleSourceTruth: View {
@StateObject var debugToolConfiguration: MyDebugToolConfiguration2 = .init()
@State var presentDevToolView: Bool = false
var body: some View {
VStack {
Toggle(isOn: debugToolConfiguration.$isDebuggableWorking) {
Text("Is Debugable Working")
}
Button("Present Dev Tool") {
presentDevToolView = true
}
.popover(isPresented: $presentDevToolView) {
TADebugToolView(configuration: debugToolConfiguration)
}
}
.padding()
}
}import SwiftUI
struct PresentDebugView: View {
@EnvironmentObject var debugToolConfiguration: MyDebugToolConfiguration
@State var presentDevToolView: Bool = false
var body: some View {
Button("Present Dev Tool") {
presentDevToolView = true
}
.popover(isPresented: $presentDevToolView) {
TADebugToolView(configuration: debugToolConfiguration)
}
}
}For production applications, you'll want a more sophisticated setup that handles different environments, secure access, and comprehensive debugging features:
import SwiftUI
import TADebugTools
import Defaults // Using Defaults library for type-safe UserDefaults
public class AppDebugToolConfiguration: TADebugToolConfiguration {
// Environment-aware password strategy
static var passwordStrategy: PasswordStrategy {
#if DEBUG
return DebugPasswordStrategy()
#elseif TESTFLIGHT
return TestflightPasswordStrategy()
#else
return ProductionPasswordStrategy()
#endif
}
// App state entries with bidirectional sync
let isPremiumEntry: DebugEntryBool = .init(
title: "Is Premium",
wrappedValue: Defaults[.isPremium]
)
let hasCompletedOnboardingEntry: DebugEntryBool = .init(
title: "Has Completed Onboarding",
wrappedValue: Defaults[.onboardingCompleted]
)
public override init(passwordType: TADebugToolConfiguration.PasswordType) {
super.init(passwordType: passwordType)
setupEntries()
setupStateSync()
}
private func setupEntries() {
// App state section
addEntry(isPremiumEntry, to: .app)
addEntry(hasCompletedOnboardingEntry, to: .app)
}
private func setupStateSync() {
// Bidirectional sync for premium status
isPremiumEntry.onUpdateFromDebugTool = { newValue in
Defaults[.isPremium] = newValue
}
Task {
for await newIsPremium in Defaults.updates(.isPremium) {
await MainActor.run {
self.isPremiumEntry.onUpdateFromApp(newIsPremium)
}
}
}
// Bidirectional sync for onboarding status
hasCompletedOnboardingEntry.onUpdateFromDebugTool = { newValue in
Defaults[.onboardingCompleted] = newValue
}
Task {
for await newOnboardingStatus in Defaults.updates(.onboardingCompleted) {
await MainActor.run {
self.hasCompletedOnboardingEntry.onUpdateFromApp(newOnboardingStatus)
}
}
}
}
}@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
// Initialize debug configuration early
let debugConfig = AppDebugToolConfiguration(
passwordType: .dynamic(strategy: AppDebugToolConfiguration.passwordStrategy)
)
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// ... other setup code ...
// Configure additional debug entries after services are initialized
setupDebugEntries()
return true
}
private func setupDebugEntries() {
// Add entries that require initialized services
debugConfig.addFirebaseEntries()
debugConfig.addOnboardingEntries()
}
}final class SettingsViewController: UIViewController {
private let debugConfig: AppDebugToolConfiguration
init(debugConfig: AppDebugToolConfiguration) {
self.debugConfig = debugConfig
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}TADebugTools provides several password strategies for different security needs:
// No password required (development)
.none
// Static password
.static(password: "mypassword")
// Dynamic password using custom strategy
.dynamic(strategy: MyCustomPasswordStrategy())Create custom password strategies for advanced security:
import TADebugTools
import UIKit
/// Always allows access (for debug/TestFlight builds)
struct AlwaysTruePasswordStrategy: PasswordStrategy {
func isPasswordValid(_ input: String) -> Bool {
return true
}
var keyboardType: UIKeyboardType {
return .numberPad
}
}For apps with existing state management systems, TADebugTools provides seamless synchronization:
isPremiumEntry.onUpdateFromDebugTool = { newValue in
Defaults[.isPremium] = newValue
}
Task {
for await newIsPremium in Defaults.updates(.isPremium) {
await MainActor.run {
self.isPremiumEntry.onUpdateFromApp(newIsPremium)
}
}
}Best for apps with established state management systems.
// Debug tool syncs with your existing state system
let premiumEntry: DebugEntryBool = .init(
title: "Is Premium",
wrappedValue: MyStateManager.shared.isPremium
)
// Setup bidirectional sync
premiumEntry.onUpdateFromDebugTool = { newValue in
MyStateManager.shared.setPremium(newValue)
}
MyStateManager.shared.onPremiumChanged = { newValue in
premiumEntry.onUpdateFromApp(newValue)
}Best for simple debugging scenarios.
@Debuggable(key: "isPremium")
var isPremium = false
// Use directly in your views
Toggle(isOn: debugConfig.$isPremium) {
Text("Premium Status")
}Organize your debug entries using these predefined sections:
.app: General app controls and state.appSettings: System-level app settings.onboarding: Onboarding-specific controls.logs: Logs and debugging output.defaults: UserDefaults display and manipulation.others: Miscellaneous tools and entries
- DebugEntryBool: Toggle switches for boolean values
- DebugEntryButton: Action buttons that execute code
- DebugEntryConstant: Read-only display of values
- DebugEntryTextField: Text input fields
- DebugEntryTextFieldAlertButton: Buttons that show text input dialogs
- DebugEntryOptions: Dropdown selection for enum values
// Boolean toggle with custom behavior
let customToggle = DebugEntryBool(
title: "Custom Feature",
wrappedValue: false
) { newValue in
// Custom logic when value changes
FeatureManager.shared.setCustomFeature(enabled: newValue)
}
// Button with navigation
let navigationButton = DebugEntryButton(
title: "Advanced Settings",
wrappedValue: {},
onTapShowDestinationView: {
AnyView(AdvancedDebugSettingsView())
}
)- Use environment-specific password strategies to ensure production security
- Hide debug access behind subtle UI patterns (tap counters, gestures)
- Disable debug tools in production builds when not needed
- Use secure password strategies that change over time
- Initialize debug configuration early in app lifecycle
- Use lazy initialization for expensive debug entries
- Avoid heavy operations in debug entry callbacks
- Clean up resources when debug tools are dismissed
- Organize entries logically using sections
- Use clear, descriptive titles for all entries
- Provide immediate feedback for debug actions
- Test debug flows regularly during development
Debug tool not appearing:
- Check if password strategy is correctly configured
- Verify initialization order in AppDelegate
- Ensure debug configuration is passed to presentation layer
State sync not working:
- Verify bidirectional sync callbacks are set up
- Check if state updates are happening on main thread
- Ensure proper memory management (avoid retain cycles)
Performance issues:
- Move expensive operations out of entry initialization
- Use lazy loading for debug entries that access files/network
- Implement proper cleanup in deinitializers
This project is licensed under the MIT License. See the LICENSE file for details.