iOS UserDefaults Intermittently Returns Nil After getPreferences() Call - Race Condition?

iOS Intermittent Bug: UserDefaults Preferences Loading Issue

Problem Summary

We're experiencing an intermittent issue where UserPreferences.shared.preferences returns inconsistent values even after calling getPreferences(). The behavior is unpredictable and affects critical functionality.

Environment

  • iOS Version: 15+
  • Language: Objective-C with Swift interop
  • Storage: UserDefaults with App Group (group.com.jci.tyco.glss)
  • Architecture: Singleton pattern for UserPreferences (Swift class)

The Issue

When a push notification arrives and triggers the showEvent: method, user preferences are sometimes loaded correctly and sometimes return nil or default values:

Scenario A (Works - ~60% of time):

Scenario B (Fails - ~40% of time):


Observed Pattern

From extensive logging over multiple test runs:

Key Observation:

  • At app launch: Preferences often load successfully
  • Seconds later when push arrives: Same preferences become unavailable
  • User navigates to another screen and back: Preferences suddenly work again

Code Structure

Objective-C AppDelegate.m

- (void)showEvent:(FMEvent *)event eventReadSource:(FMEventReadSourceType)eventSource {
    BOOL isUserRoleEndUser = [[FMUserRolesRepository instance] userRoleAvailable:FMUserRoleEndUser];
    
    UserPreferences *userPrefs = [UserPreferences shared];
    [userPrefs getPreferences];  // ← Called here
    
    // Immediately after, sometimes nil!
    bool hasFireAudio = [userPrefs.preferences.fireAudio.lowercaseString isEqual:@"true"];
    
    NSLog(@"isUserRoleEndUser: %d | Fire Audio: %d", isUserRoleEndUser, hasFireAudio);
    
    // Decision logic based on preferences
    if ([FMConstants getUserRegion] == UserRegionUK && 
        isUserRoleEndUser &&
        userPrefs.preferences &&  // ← Sometimes nil here
        hasFireAudio) {
        // Show Screen A
    } else {
        // Show Screen B
    }
}

```class UserPreferences: NSObject {
    static let shared = UserPreferences()
    
    var preferences: FMUserPreferencesInfo?  // ← This becomes nil intermittently
    
    func getPreferences() {
        let userDefaults = UserDefaults(suiteName: "group.com.jci.tyco.glss")
        
        if let data = userDefaults?.data(forKey: "UserPreferences") {
            // Decode and set preferences
            self.preferences = // decoded object
        } else {
            // Create default preferences with fireAudio = "false"
            self.preferences = FMUserPreferencesInfo()
        }
    }
}
Answered by DTS Engineer in 871335022

Looking at the code you posted, the only way [1] that preferences can be nil at line 15 is if your decoded object code fails. You didn’t show that code, so it’s hard to tell what’s going on there.

It sounds like you can reproduce this reasonably reliably. Given that, you can debug this using logging. I have general advice on that topic in Testing and Debugging Code Running in the Background.

IMPORTANT It’s time to stop using NSLog habitually. Rather, use print(…) for transient debugging and Logger for debugging that you want to keep in your codebase (or for cases like this, where you need to debug without Xcode being attached).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Well, not the only way. You could be dealing with an issue that’s outside of the scope of the Objective-C / Swift execution models, like a memory corruption problem. However, let’s rule out the easy things first (-:

Looking at the code you posted, the only way [1] that preferences can be nil at line 15 is if your decoded object code fails. You didn’t show that code, so it’s hard to tell what’s going on there.

It sounds like you can reproduce this reasonably reliably. Given that, you can debug this using logging. I have general advice on that topic in Testing and Debugging Code Running in the Background.

IMPORTANT It’s time to stop using NSLog habitually. Rather, use print(…) for transient debugging and Logger for debugging that you want to keep in your codebase (or for cases like this, where you need to debug without Xcode being attached).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Well, not the only way. You could be dealing with an issue that’s outside of the scope of the Objective-C / Swift execution models, like a memory corruption problem. However, let’s rule out the easy things first (-:

iOS UserDefaults Intermittently Returns Nil After getPreferences() Call - Race Condition?
 
 
Q