Amin Benarieb
iOS dev
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:
- Edit Scheme: Go to your project in Xcode, select your target, and choose Edit Scheme.
- Select Run: In the scheme editor, select the Run action from the sidebar.
- Navigate to Arguments: Click on the Arguments tab.
- Add Environment Variables: Under Environment Variables, click the plus button to add new variables.
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:
- Edit Scheme: Select Edit Scheme for your target.
- Select Run: Choose the Run action.
- Navigate to Arguments: Go to the Arguments tab.
- Add Arguments: Under Arguments Passed On Launch, click the plus button
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
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.