Source code address: GHApplicationMediator
Why isn’t the AppDelegate easy to maintain
The AppDelegate controls the main life cycle of an App, such as building the main view after App initialization is complete, App receiving remote message callbacks, URl-scheme callbacks, third-party SDK initialization, database initialization, and so on.
For this reason, the amount of code in the App delegate gets larger and larger as the App versions iterate. When the amount of code in the AppDelegate reaches a certain point, it’s time to start thinking about modularizing the code in the AppDelegate.
Version 1.0
At the time of considering this solution, our project had just passed the prototyping phase, we weren’t using many SDKS, and the business needs weren’t there yet.
In this context, I’ve chosen to encapsulate the AppDelegate with a Category.
Create an AppDelegate+XXX Category, such as the AppDelegate+CEReachability
#import "AppDelegate.h"
@interface AppDelegate (CEReachability)
- (void)setupReachability;
@end
@implementation AppDelegate (CEReachability)
- (void)setupReachability
{
// Allocate a reachability object
Reachability *reach = [Reachability reachabilityWithHostname:kServerBaseUrl];
// Set the blocks
reach.reachableBlock = ^(Reachability *reach) {
if (reach.currentReachabilityStatus == ReachableViaWWAN) {
BLYLogInfo(@ "ReachabilityStatusChangeBlock -- - > cellular data network");
[CESettingsManager sharedInstance].needNoWifiAlert = YES;
} else if (reach.currentReachabilityStatus == ReachableViaWiFi) {
BLYLogInfo("@" ReachabilityStatusChangeBlock - > WiFi network);
[CESettingsManager sharedInstance].needNoWifiAlert = NO; }}; reach.unreachableBlock = ^(Reachability *reach) { BLYLogInfo(@ "ReachabilityStatusChangeBlock -- - > unknown network status");
};
// Start the notifier, which will cause the reachability object to retain itself!
[reach startNotifier];
}
Copy the code
Then register the module in the AppDelegate
#import "AppDelegate+CEReachability.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[self setupReachability];
return YES;
}
Copy the code
Some of you may be wondering, why don’t you just implement UIApplicationDelegate methods in a Category.
Import multiple categories that all implement the same method (for example: – (void) applicationWillResignActive (UIApplication *) application), Which implementation to choose when calling the method is determined by the compile order of the Category file (specified in Build Phases > Complie Sources), and the method implementation of the last compiled Category file will be used. Regardless of the import order, in fact, when you have two categories that implement the same method, no matter which Category you imPRt, the actual implementation of the method is always the one that compiled the last Category file.
Advantages:
- Initial modular, the registration method of different modules by Category specified.
Disadvantages:
- Categories are mutually exclusive, and the same method cannot be implemented in different categories at the same time.
- You need to maintain the implementation logic for the different function modules in the AppDelegate.
Version 2.0
As business needs increase, third party payments, IM, and various URL-scheme configurations, especially Open URLS and Push Notifications require dependencies, the solution quickly becomes unmet, and strange registration modes become intermixed.
Forced to live, I decided to reconstruct for the second time.
The primary motivation for this refactoring is that because of the mutual exclusion between categories, a dependent process must be written in an AppDelegate. (Such as Open Url, third-party payment is used, browser redirect is also used)
Therefore, I added ApplicationMediator to manage the communication between the AppDelegate and the module and realize the logic of forwarding messages to the module.
ApplicationMediator
ApplicationMediator is a singleton that manages the registration and removal of modules.
@interface CEApplicationMediator : UIResponder<UIApplicationDelegate.UNUserNotificationCenterDelegate>
@property (nonatomic.strong) NSHashTable *applicationModuleDelegates;
+ (instancetype)sharedInstance;
+ (void)registerAppilgationModuleDelegate:(id<UIApplicationDelegate>)moduleDelegate;
+ (void)registerNotificationModuleDelegate:(id<UIApplicationDelegate,UNUserNotificationCenterDelegate>)moduleDelegate;
+ (BOOL)removeModuleDelegateByClass:(Class)moduleClass;
@property (nonatomic.assign) UNNotificationPresentationOptions defaultNotificationPresentationOptions;
@end
Copy the code
Module
Module according to the need to implement UIApplicationDelegate and UNUserNotificationCenterDelegate will be allowed to join the UIApplication life cycle.
@implementation CEAMWindowDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
window.backgroundColor = [UIColor whiteColor];
// We need to assign the Window to an AppDelegate. Sometimes we will use a global AppDelegate to fetch the Window.
[UIApplication sharedApplication].delegate.window = window;
CELaunchPageViewController *launchVC = [[CELaunchPageViewController alloc] init];
window.rootViewController = launchVC;
[window makeKeyAndVisible];
return YES;
}
@end
Copy the code
@implementation CEAMReachabilityDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Allocate a reachability object
Reachability *reach = [Reachability reachabilityWithHostname:kServerBaseUrl];
// Set the blocks
reach.reachableBlock = ^(Reachability *reach) {
if (reach.currentReachabilityStatus == ReachableViaWWAN) {
BLYLogInfo(@ "ReachabilityStatusChangeBlock -- - > cellular data network");
} else if (reach.currentReachabilityStatus == ReachableViaWiFi) {
BLYLogInfo("@" ReachabilityStatusChangeBlock - > WiFi network); }}; reach.unreachableBlock = ^(Reachability *reach) { BLYLogInfo(@ "ReachabilityStatusChangeBlock -- - > unknown network status");
};
[reach startNotifier];
return YES;
}
@end
Copy the code
Module registration
After the module is created, it takes effect after being registered.
@implementation AppDelegate
+ (void)load
{
// CoreData
[CEApplicationMediator registerAppilgationModuleDelegate:[[CEAMCoreDataDelegate alloc] init]];
/ /...
}
@end
Copy the code
There are two ways to register
- Register in the AppDelegate’s + (void)load
- Register in the + (void)load of ApplicationMediator.
Either way is ok, each has its pros and cons
- Registered in AppDelegate, the delegate is coupled with AppDelegate, but ApplicationMediator is decoupended from delegate, and ApplicationMediator can be extracted as a component and used as middleware.
- Registration in ApplicationMediator is the opposite. In this way, module maintenance only needs to be conducted around ApplicationMediator, and the code is more centralized.
I use the method of registering in AppDelegate, mainly preparing to use ApplicationMediator as a component.
forward
As a keyboard guy, I’m pretty fast at typing, and in less than five minutes I’ve written a manual forward of five major LIFECYCLE functions in UIApplicationDelegate, but when I open the UIApplicationDelegate header, I’m blinded, Delegate methods make my scalp tingle.
Well, yes, that’s where the message forwarding mechanism comes in handy.
AppDelegate
All AppDelegate methods are handled by ApplicationMediator. The module forwarding logic is described later.
@implementation AppDelegate
+ (void)load
{
// Register the module
}
- (BOOL)respondsToSelector:(SEL)aSelector
{
return [[CEApplicationMediator sharedInstance] respondsToSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
return [[CEApplicationMediator sharedInstance] methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
[[CEApplicationMediator sharedInstance] forwardInvocation:anInvocation];
}
@end
Copy the code
The AppDelegate only needs to handle the registration module.
ApplicationMediator
#pragma mark- Handle Method
/ * * fail [super respondsToSelector: aSelector] to detect whether the object from the super inherited methods. So call [super respondsToSelector: aSelector], is to call [the self respondsToSelector: aSelector] * * /
- (BOOL)respondsToSelector:(SEL)aSelector
{
BOOL result = [super respondsToSelector:aSelector];
if(! result) { result = [self hasDelegateRespondsToSelector:aSelector];
}
return result;
}
/** This method is also used when NSInvocation is created, for example during messaging. If the current Classf can handle methods that are not directly implemented, then this method must be overridden. * /
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
id delegate = [self delegateRespondsToSelector:aSelector];
if (delegate) {
return [delegate methodSignatureForSelector:aSelector];
}
return [super methodSignatureForSelector:aSelector];
}
/** Unrecognized message processing */
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
__block BOOL isExec = NO;
NSMethodSignature *methodSignature = anInvocation.methodSignature;
const char *returnType = methodSignature.methodReturnType;
// No value is returned, or YES is returned by default
if (0 == strcmp(returnType, @encode(void)) ||
anInvocation.selector == @selector(application:didFinishLaunchingWithOptions:)) {
[self notifySelectorOfAllDelegates:anInvocation.selector nofityHandler:^(id delegate) {
[anInvocation invokeWithTarget:delegate];
isExec = YES;
}];
} else if (0 == strcmp(returnType, @encode(BOOL))) {
// BOOL is returned
[self notifySelectorOfAllDelegateUntilSuccessed:anInvocation.selector defaultReturnValue:NO nofityHandler:^BOOL(id delegate) {
[anInvocation invokeWithTarget:delegate];
// Get the return value
NSUInteger returnValueLenth = anInvocation.methodSignature.methodReturnLength;
BOOL *retValue = (BOOL *)malloc(returnValueLenth);
[anInvocation getReturnValue:retValue];
BOOL result = *retValue;
return result;
}];
} else {
/ / equivalent to [self doesNotRecognizeSelector: anInvocation. The selector];
[superforwardInvocation:anInvocation]; }} - (BOOL)hasDelegateRespondsToSelector:(SEL)selector
{
__block BOOL result = NO;
[self.applicationModuleDelegates enumerateObjectsUsingBlock:^(id _Nonnull delegate, NSUInteger idx, BOOL * _Nonnull stop) {
if ([delegate respondsToSelector:selector]) {
result = YES;
*stop = YES; }}];return result;
}
- (id)delegateRespondsToSelector:(SEL)selector
{
__block id resultDelegate;
[self.applicationModuleDelegates enumerateObjectsUsingBlock:^(id _Nonnull delegate, NSUInteger idx, BOOL * _Nonnull stop) {
if ([delegate respondsToSelector:selector]) {
resultDelegate = delegate;
*stop = YES; }}];return resultDelegate;
}
/** Notify all delegate response methods @param Selector Response method @param nofityHandler delegated to handle the invocation event */
- (void)notifySelectorOfAllDelegates:(SEL)selector nofityHandler:(void(^) (id delegate))nofityHandler
{
if (_applicationModuleDelegates.count == 0) {
return;
}
[self.applicationModuleDelegates enumerateObjectsUsingBlock:^(id _Nonnull delegate, NSUInteger idx, BOOL * _Nonnull stop) {
if ([delegate respondsToSelector:selector]) {
if(nofityHandler) { nofityHandler(delegate); }}}]; }/** Notify all delegates and interrupt the notification when a delegate responds successfully. The @param Selector response method @param defaultReturnValue returns the default value. (When set to YES, it returns YES even if there is no response object.) @param nofityHandler Delegate handles the call event @return delegate handles the result */
- (BOOL)notifySelectorOfAllDelegateUntilSuccessed:(SEL)selector defaultReturnValue:(BOOL)defaultReturnValue nofityHandler:(BOOL(^) (id delegate))nofityHandler
{
__block BOOL success = defaultReturnValue;
if (_applicationModuleDelegates.count == 0) {
return success;
}
[self.applicationModuleDelegates enumerateObjectsUsingBlock:^(id _Nonnull delegate, NSUInteger idx, BOOL * _Nonnull stop) {
if ([delegate respondsToSelector:selector]) {
if (nofityHandler) {
success = nofityHandler(delegate);
if (success) {
*stop = YES; }}}}];return success;
}
Copy the code
Here is a brief description of the message forwarding process.
- (BOOL)respondsToSelector:(SEL)aSelector
Before calling the protocol method, the object is checked to see if it implements the protocol method, and if it responds, the corresponding method is called.- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
When the called method cannot be found, if the method is not implemented, the system calls NSObject’s doesNotRecognizeSelector method, which throws an exception and crashes. When you implement this method, the system will ask you to return the corresponding method implementation of selector, so you can turn on message forwarding here.- (void)forwardInvocation:(NSInvocation *)anInvocation
When the method finishes forwarding Settings, we enter this method, and we control the execution of the method.
In Step 3, the customized forwarding scheme is implemented:
- Delegate methods with no return value, and
application:didFinishLaunchingWithOptions:
This method returns only YES, when forwarding, polling notification. - The delegate method of the BOOL return value is used to enable the polling notification and obtain the result of each execution. When the result is YES, the polling is finished. The thing to note here is that the polling order is related to the order of registration, so you need to pay attention to the order of registration.
- Methods with completionHandler are mainly push message modules. Since competitionHandler can only be called once, and the method has no BOOL return value, such methods can only be implemented in ApplicationMediator, and each method is manually forwarded. See the source code for specific implementation.
Version 3.0 that hasn’t started yet
With version 2.0, it’s easier to add modules, but there’s still a lot of room for improvement.
- For example, registering modules in AppDelegate determines the dependencies between modules based on the order in which the code is written. In the actual use process, there was still a problem of initialization confusion due to the dependency module relationship. In order to reduce class inheritance and protocol inheritance, the existing scheme of the system is used in the design. In the future, this component may be designed more perfect according to the design idea of the chain of responsibility.
- The AppDelegate has a default UIWindow that a lot of third party libraries pass through
[UIApplication sharedApplication].delegate.window.bounds.size
To get the screen size, so remember to assign the Window to the AppDelegate when creating or changing the Window. Only document constraints have been passed so far, and improvements will be made in the future.