Aleksandar • Vacić

iOS bits and pieces

Masterclass - Notifications

One way to declare and assign the notification name is using global strings in .h file:

1
extern NSString *const XXAPIManagerDidAcquireSessionNotification;

then assigning the values in the .m file:

1
NSString *const XXAPIManagerDidAcquireSessionNotification = @"XXAPIManagerDidAcquireSessionNotification";

However, long strings like this are hard to scan when reviewing the code. Thus I prefer to namespace them using structs. In the .h file:

1
2
3
4
5
6
7
//   notifications
extern const struct XXAPIManagerNotificationStruct {
  __unsafe_unretained NSString *DidAcquireSession;
  __unsafe_unretained NSString *DidResetSession;
  __unsafe_unretained NSString *DidDropSession;
  __unsafe_unretained NSString *AccessForbidden;
} XXAPIManagerNotification;

then setting up properties in the .m file:

1
2
3
4
5
6
const struct XXAPIManagerNotificationStruct XXAPIManagerNotification = {
  .DidAcquireSession            = @"XXAPIManagerDidAcquireSessionNotification",
  .DidResetSession          = @"XXAPIManagerDidResetSessionNotification",
  .DidDropSession               = @"XXAPIManagerDidDropSessionNotification",
  .AccessForbidden          = @"XXAPIManagerAccessForbiddenNotification"
};

Attach in init, detach in dealloc

Always assign yourself as observer in the init method, not in viewDidLoad. That way, you can cover cases where your controller is in memory but its view is not - like when it’s a member of UITabBarController.viewControllers but it’s not yet shown.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (instancetype)init {

  self = [super init];
  if (!self) return nil;

//…

  [NSNotificationCenter.defaultCenter
      addObserver:self
      selector:@selector(handleNotification:)
      name:nil
      object:XXAPIManager.defaultManager];

  return self;
}

Since iOS 9, you don’t need to explicitly call removeObserver in dealloc - for previous version make sure you do it.

1
2
3
4
- (void)dealloc {

  [NSNotificationCenter.defaultCenter removeObserver:self];
}

Make sure to check is the view actually loaded before trying to update anything related to the view hierarchy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)handleNotification:(NSNotification *)notification {

  if ([notification.name isEqualToString:XXAPIManagerNotification.DidAcquireSession]) {
      if ( !self.isViewLoaded ) return;

      // …
      return;
  }

  if ([notification.name isEqualToString:XXTranslationManagerNotification.DidChangeLanguage]) {
      dispatch_async(dispatch_get_main_queue(), ^{
      // update non-view related stuff, like tabBarItem
      });
      if ( !self.isViewLoaded ) return;
      dispatch_async(dispatch_get_main_queue(), ^{
      // …
      });
      return;
  }
}

Notice the form: you pass the notification, when matched in the if block it ends with return. This is mandatory, even if you have only one notification. Sooner or later you will add another and missing return statement may come to hunt you. Don’t trust yourself nor anyone else that it will correctly scan and scrutinize already existing code in handleNotification:

Treat each notification like it’s a world of its own - never write any code outside the if block for particular notification. It will save your sanity a year in the future.

In the same vein, wrap entire UI processing in the notification inside dispatch_async call targeting main thread. It does not matter how absolutely positively 100% sure you are that this notification will be posted on the main thread - still wrap its handler inside such block.

If you override loadView…

Additionally, if you have your own loadView that does something, then you need to track when has that method completed and only then is your self.view really loaded. If you ignore this problem, you may end up with some really confusing crash logs.

For example: if you add UICollectionView as subview in the loadView method and you set .dataSource in that place as well. So, now imagine a notification that fires up CV’s reloadData - with enough customers using your app you’re bound to have at least few customers getting into that line in the nano/microsecond between the [super loadView] and the addSubview call in your override.

To safeguard your code, add local property like this one:

1
@property (nonatomic, getter=isLocalViewLoaded) BOOL localViewLoaded;

set it to NO in the init method and set it to YES as the very last line of your loadView method. Then override isViewLoaded to account for this change:

1
2
3
4
5
- (BOOL)isViewLoaded {
  if ( !super.isViewLoaded ) return NO;

  return self.isLocalViewLoaded;
}

Needless to say - none of the stuff you do in your loadView method should consult self.isViewLoaded.

Or you can take the high road and don’t assign delegates and dataSources in loadView (do it in viewDidLoad). But then again – copy-paste programming has so much allure and developers love shortcuts…