I agree that sometimes it seems like the AppDelegate
is the logical place to put things that you only want implemented once but may need from several places. Creating a singleton for each one is fine, if those things are complicated, but it does create a lot of additional files and confusion to the project. I also agree with the majority of answers here, that building dependencies on the AppDelegate
is a really poor design.
I think the best solution is to create a Protocol! Then put an IBOutlet
to a property to do what you need to have done in each of the controllers that need the function. Protocols are the standard objective-C way to uncouple classes.
So, as an example, maybe I have a database URL that I may need from a bunch of places. Probably the best way would be to set a property with it each step along the way. But in some situations that may be cumbersome because of using a stock controller and not wanting to subclass it. Here is my solution:
Create file: MainDatabasePovider.h
#import <Foundation/Foundation.h>
@protocol MainDatabaseProvider <NSObject>
@required
@property (nonatomic, readonly) NSURL *applicationDocumentsDirectory;
@property (nonatomic, weak) NSURL *mainDatabase;
@end
Now "anyone" (i.e., any class) that says it implements the MainDatabaseProvder
protocol is guaranteed to provide the two methods above. This could be the AppDelegate
or ANY object.
Now if I want my AppDelegate
to provide the information I change the AppDelegate.h
file to have:
#import "MainDatabaseProvider.h"
@interface AppDelegate : UIResponder <UIApplicationDelegate, MainDatabaseProvider>
@property (strong, nonatomic) UIWindow *window;
@end
(The only change is to add the MainDatabaseProvider protocol to the @inteface
line, again this could be done to ANY class that you want to provide the function).
In my AppDelegate.m
file I have to write the two methods...
@implementation AppDelegate
...
@synthesize mainDatabase = _mainDatabase;
...
- (NSURL *) applicationDocumentsDirectory {
return [[[NSFileManager defaultManager] URLsForDirectory: NSDocumentDirectory inDomains: NSUserDomainMask] lastObject];
}
- (void) setMainDatabase: (NSURL *) mainDatabase {
if( _mainDatabase != mainDatabase ) {
_mainDatabase = mainDatabase;
}
}
- (NSURL *) mainDatabase {
if( !_mainDatabase ) {
NSURL *docURL = self.applicationDocumentsDirectory;
self.mainDatabase = [docURL URLByAppendingPathComponent: @"My Great Database"];
}
return _mainDatabase;
}
...
@end
Now in my controllers or other classes that have need of getting the MainDatabase I add the following:
In their .h
files:
#import "MainDatabaseProvider.h"
...
@interface myGreatViewController: UIViewController
@property (nonatomic, weak) IBOutlet id <MainDatabaseProvider> mainDatabaseProvider;
...
@end
This property can be set in what was formerly known as InterfaceBuilder
by control-dragging or can be set in code in prepareForSegue
or I like to provide a custom getter that default it to the AppDelegate
in case I am lazy or forgetful and don't do either of the above. In their .m
file is would look like:
@implementation myGreatViewController
@synthesize mainDatabaseProvider = _mainDatabaseProvider;
...
- (id <MainDatabaseProvider>) mainDatabaseProvider {
id appDelegate = [[UIApplication sharedApplication] delegate];
if( !_mainDatabaseProvider && [appDelegate conformsToProtocol: @protocol(MainDatabaseProvider)] )
return appDelegate;
return _mainDatabaseProvider;
}
// To get the database URL you would just do something like...
- (void) viewWillAppear: (BOOL) animated {
NSLog( @"In %s the mainDatabaseProvider says the main database is \"%@\"", __func__, self.mainDatabaseProvider.mainDatabase.path );
}
Now the mainDatabaseProvider
can be ANY object. I have the ability to set it in InterfaceBuilder
or my StoryBoard
(although I don't really think of this is a user-interface item so I typically wouldn't but it is pretty typical to do it that way), I can set it in code outside my controller before it gets loaded in prepareForSegue:sender:
or tableView:didSelectRowAtIndexPath:
. And if I don't set it at all it will default to the AppDelegate
if I have configured it properly.
I can even put some safe guards in for when I forget to do things in my old age by changing the getter
listed above to help me out with something like:
- (id <MainDatabaseProvider>) mainDatabaseProvider {
if( !_mainDatabaseProvider ) {
id appDelegate = [[UIApplication sharedApplication] delegate];
if( ![appDelegate conformsToProtocol: @protocol(MainDatabaseProvider)] ) {
NSLog( @"Hey!! The mainDatabaseProvider is not set and the AppDelegate does not conform to the MainDatabaseProvider protocol. How do you expect me to figure out where the database is!" );
} else {
return appDelegate;
}
return _mainDatabaseProvider;
}