Crash Writing NSUserDefaults in CompletionBlock of dismissViewController

Seeing A LOT of crashes for this code which has been around for quite a while (on iOS 15). After a viewController that user has been using is dismissed we check some values to determine if we should prompt for a rating and write some values into NSUserDefaults and which is when the crash happens.

This code gets called when the user clicks a Done Button to dismiss the viewController:

-(void)dismissChart
{
    [self.navigationController dismissViewControllerAnimated:YES completion:^{
    if([IphemerisUtil okToRequestRatingUsingCount:OK_TO_REQUEST_REVIEW_COUNT])
      [IphemerisUtil requestAppRating];
  }];
}

This is what is called and where the crash occurs:


+(BOOL)okToRequestRatingUsingCount:(int)count
{
    int curVersion = [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"] intValue];
   
    NSNumber *lastVerRated = [userSettings objectForKey:RateLastVersionRated];
    int lastVersionRated = (lastVerRated != nil) ? [lastVerRated intValue] : 0;
   
    NSNumber *ratePromptCnt = [userSettings objectForKey:RatePromptCounter];
    int rateCounter = (ratePromptCnt != nil) ? [ratePromptCnt intValue] : 1;
   
    NSDate *dateLastCounterIncrement = [userSettings objectForKey:RatePromptCounterLastDateIncrement];
    if(!dateLastCounterIncrement) {
        dateLastCounterIncrement = [NSDate date];
        [userSettings setObject:dateLastCounterIncrement forKey:RatePromptCounterLastDateIncrement];
     }
   
    if(curVersion > lastVersionRated) {
        if(rateCounter > count)
            return YES;
        else {
            // Increment the counter only once a day
            if([[NSDate date] timeIntervalSinceDate:dateLastCounterIncrement] > (3600 * 24)) {
                rateCounter++;
                [userSettings setObject:[NSNumber numberWithInt:rateCounter] forKey:RatePromptCounter];
        }
     }
  }
  return NO;
}

One of the two userSettings:setObject is crashing. As indicated by this symbolicated crash log for the above code.

Accepted Answer

Isn't it likely that you're trying to access (set) a value in userSettings after it has been dealloc'd by the closure of the view?

What you've done is said, "Please dismiss this view, and when you've finished dismissing it, set something in a variable you've just got rid of."

I don't know what this code does: [IphemerisUtil requestAppRating] but if it's going to pop up a rating request view, where are you going to display it? You've just dismissed this view so there's nowhere to put an alert box.

I'd suggest you try one of two routes:

Option 1. Check whether you should ask for a rating before the view is dismissed, and do the dismissal from the [IphemerisUtil requestAppRating] method, i.e.:

// In viewDidLoad():
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(closeView) name:@"CloseViewAfterRating" object:nil];

- (void)dismissChart
{
    if([IphemerisUtil okToRequestRatingUsingCount:OK_TO_REQUEST_REVIEW_COUNT]) {
        [IphemerisUtil requestAppRating];

    } else {
        [self closeView];
    }
}

- (void)closeView
{
    [self.navigationController dismissViewControllerAnimated:YES completion:nil];
}

// In IphemerisUtil:
- (void)requestAppRating
{
    // Get rating
    // ...

    // When complete, post a notification to the previous view to close
    [[NSNotificationCenter defaultCenter] postNotificationName:@"CloseViewAfterRating" object:nil];
}

Or, if the ratings method blocks the code until it's complete:

- (void)dismissChart
{
    if([IphemerisUtil okToRequestRatingUsingCount:OK_TO_REQUEST_REVIEW_COUNT]) {
        [IphemerisUtil requestAppRating];
    }
    [self.navigationController dismissViewControllerAnimated:YES completion:nil];
}

Option 2. Alternatively, if you are always going to check for a rating after various views are closed, you could do it in the viewDidAppear method of your main view.

Thanks for the suggested code, but look at the code posted. It crashes (or that is how it looks while trying to write a value it just read from UserDefaults. Look at the code, it reads some counts stored in NSUserDefaults then does some math all in that method and then crashes while updating and attempting to write the updated counts all of which are in variables local to that method. It is only reading and writing values obtained from userDefaults?

I did look at the code posted, and I pointed out that you're trying to access a variable that is in the process of being dealloc'd. All the other code in that method is creating variables, not trying to access existing variables.

Your userSettings variable was created in that view, and you've told the navigation controller to remove the view. It will remove it and that variable will be unavailable.

You need to handle it a better way. Perhaps a better way is to write directly to the user defaults, rather than via your userSettings variable?

You could see what the problem is by simply doing NSLog(@"userSettings = %@", userSettings); at the top of this method. Did you try that?

+(BOOL)okToRequestRatingUsingCount:(int)count
{
    NSLog(@"userSettings = %@", userSettings);

    int curVersion = [[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"] intValue];  // Will work without error
   
    NSNumber *lastVerRated = [userSettings objectForKey:RateLastVersionRated];  // Will work, but might return nil if userSettings has been dealloc'd, but no crash
    int lastVersionRated = (lastVerRated != nil) ? [lastVerRated intValue] : 0;  // If userSettings == nil you'll get 0 but no crash
   
    NSNumber *ratePromptCnt = [userSettings objectForKey:RatePromptCounter];  // Will work, but might return nil if userSettings has been dealloc'd, but no crash
    int rateCounter = (ratePromptCnt != nil) ? [ratePromptCnt intValue] : 1;  // If userSettings == nil you'll get 1 but no crash
   
    NSDate *dateLastCounterIncrement = [userSettings objectForKey:RatePromptCounterLastDateIncrement];  // Will work, but might return nil if userSettings has been dealloc'd, but no crash
    if(!dateLastCounterIncrement) {  // If userSettings has been dealloc'd, this will execute
        dateLastCounterIncrement = [NSDate date];
        [userSettings setObject:dateLastCounterIncrement forKey:RatePromptCounterLastDateIncrement];  // If userSettings is nil, this will crash
     }
   
    if(curVersion > lastVersionRated) {
        if(rateCounter > count)
            return YES;
        else {
            // Increment the counter only once a day
            if([[NSDate date] timeIntervalSinceDate:dateLastCounterIncrement] > (3600 * 24)) {
                rateCounter++;
                [userSettings setObject:[NSNumber numberWithInt:rateCounter] forKey:RatePromptCounter];  // If userSettings is nil, this will crash
        }
     }
  }
  return NO;
}

Ok, I will follow the angle you are suggesting. As for logging it, I have NEVER actually seen this crash on any device or in testing. I have only seen it via Crash Reports in Organizer. Whatever is going on never seems to happen on my devices or where I can catch it in Xcode.

***** THIS WAS THE REAL PROBLEM and SOLUTION *****

Moving stuff around helped me find a reliable way to produce the crash in Xcode and catch it in the debugger. Then I was able to consistently see way down in the crash report well after my code that each time it was mentioning an observer of NSUserDefaults. The class where all the above code was being used was registering itself as an observer of NSUserDefaults and also some Notifications. I was removing self as observer of NSNotifications but NOT NSUserDefaults. So after this class was released and attempted to persist values in NSUserDefaults that triggered a notification to it just as it was being release leading to -> CRASH!

Crash Writing NSUserDefaults in CompletionBlock of dismissViewController
 
 
Q