How to Debug Faster Without Rebuilding Your iOS App

Problem

Building an iOS app can be time-consuming, especially for large projects with complex dependencies. Waiting for the app to rebuild every time you make a minor change or need to test with different data can significantly slow down your development process.

Solution

To speed up debugging without rebuilding your app, you can leverage ProcessInfo.processInfo.environment and ProcessInfo.processInfo.arguments. These allow you to inject data and control app behavior at runtime, making it easier to test different scenarios quickly.

Using Environment Variables

Environment variables can be used to pass data into your app during runtime. For example, you can autofill the sign-in screen with test credentials when debugging:

func onAppear() {
    email.text = ProcessInfo.processInfo.environment["SIGNIN_EMAIL"]
    password.text = ProcessInfo.processInfo.environment["SIGNIN_PASSWORD"]
}

To set environment variables in Xcode:

  1. Edit Scheme: Go to your project in Xcode, select your target, and choose Edit Scheme.
  2. Select Run: In the scheme editor, select the Run action from the sidebar.
  3. Navigate to Arguments: Click on the Arguments tab.
  4. Add Environment Variables: Under Environment Variables, click the plus button to add new variables.

environment-variables-screenshot

Creating a Debug Configuration Interface

To manage environment variables more efficiently, you can create a utility that abstracts the access:

#if DEBUG
let environment: [String: String] = ProcessInfo.processInfo.environment
#else
let environment: [String: String] = [:]
#endif

public struct DebugConfiguration {
    public static func string(_ key: String) -> String? {
        return environment[key]
    }
}

Now, your code becomes cleaner:

func onAppear() {
    email.text = DebugConfiguration.string("SIGNIN_EMAIL")
    password.text = DebugConfiguration.string("SIGNIN_PASSWORD")
}

Using Launch Arguments

Launch arguments can be used to toggle features or behaviors without changing code. For instance, you might want to autofill the sign-up screen or automatically run onboarding steps during debugging.

To add launch arguments:

  1. Edit Scheme: Select Edit Scheme for your target.
  2. Select Run: Choose the Run action.
  3. Navigate to Arguments: Go to the Arguments tab.
  4. Add Arguments: Under Arguments Passed On Launch, click the plus button

environment-arguments-screenshot

Handling Debug Flags

Create an interface to check for these debug flags:

#if DEBUG
let arguments: [String] = ProcessInfo.processInfo.arguments
#else
let arguments: [String] = []
#endif

public enum DebugFlag: String {
    case signupAutofill = "SIGNUP_AUTOFILL"
    case onboardingAutorun = "ONBOARDING_AUTORUN"
    
    public static func isEnabled(_ flag: DebugFlag) -> Bool {
        return arguments.contains(flag.rawValue)
    }
}

Applying Debug Flags in Your Code

Use these flags to conditionally execute code during debugging.

Autofill Sign-Up Screen:

func onAppear() {
    if DebugFlag.isEnabled(.signupAutofill) {
        email.text = "newuser@example.com"
        password.text = "SecurePassword!"
    }
}

Auto-Run Onboarding Steps:

func onAppear() {
    if DebugFlag.isEnabled(.onboardingAutorun) {
        onContinueButtonTap()
    }
}

func onContinueButtonTap() {
    // Proceed to the next onboarding step
}

Mocking UserDefaults Values

You can mock UserDefaults to test specific scenarios, such as triggering a feature after a certain number of app launches.

Set Up Mock Value via Launch Arguments:

Add the following to Arguments Passed On Launch:

  • -appLaunchCount
  • 10

environment-userdefaults-screenshot

Modify Your Code to Use the Mock Value:

import os

let logger = Logger(subsystem: "com.example.app", category: "AppLaunch")

func onAppear() {
    let appLaunchCount = UserDefaults.standard.integer(forKey: "appLaunchCount")    
    logger.debug("appLaunchCount = \(appLaunchCount)")
    if appLaunchCount >= 10 {
        logger.debug("Showing special feature on 10th launch")
        // Trigger the feature
    }
}

Sample Output:

[AppLaunch] appLaunchCount = 10
[AppLaunch] Showing special feature on 10th launch

Further Thoughts

By utilizing environment variables and launch arguments, you can:

  • Save Time: Test different data inputs and scenarios without rebuilding or hardcoding values. Expecially to test layout changes, mock API responses, or inject localization strings
  • Enhance Flexibility: Toggle features or behaviors dynamically during runtime.
  • Improve Collaboration: Share custom schemes with your team to ensure consistent testing environments.
  • Expand Testing Capabilities: Simulate edge cases, such as network delays or error states, more efficiently.

By integrating these practices into your development workflow, you can streamline testing and focus more on building great features.

Connect with Me

Have a comment or question? Feel free to reach out to me on Twitter or LinkedIn.