Masterclass - Notifications
One way to declare and assign the notification name is using global strings in .h file:
extern NSString *const XXAPIManagerDidAcquireSessionNotification;
then assigning the values in the .m file:
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:
// 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:
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.
- (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.
- (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.
- (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:
@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:
- (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…