AppDelegate created with Typhoon Assembly (plist method) is created twice and property injection doesn't work

517 views Asked by At

I am trying to bootstrap Typhoon using the PList integration method but my ApplicationDelegate is being created twice. The first time it is created, it is obviously being created by Typhoon. That time, it uses the special initializer initWithAssembly: and Typhoon feeds it the assembly.

The second time, the time that matters, it is created using init. It never gets a reference to the assembly.

Just in case, I also injected the assembly via the property method. No go.

Here is the code:

Assembly

- (UIApplication *)sharedApplication {
    return [TyphoonDefinition withClass:[UIApplication class] configuration:^(TyphoonDefinition *definition) {
        [definition useInitializer:@selector(sharedApplication)];
    }];
}

- (CTISApplicationDelegate *)appDelegate {
    return [TyphoonDefinition withClass:[CTISApplicationDelegate class]
                          configuration:^(TyphoonDefinition *definition) {
                              [definition useInitializer:@selector(initWithAssembly:) parameters:^(TyphoonMethod *initializer) {
                                  [initializer injectParameterWith:@(3)];
                              }];

                              definition.scope = TyphoonScopeSingleton;
                          }];
}

AppDelegate

@property (nonatomic, strong, readwrite) ApplicationAssembly *assembly;

@property (nonatomic, strong, readwrite) UIWindow *window;

- (instancetype)initWithAssembly:(ApplicationAssembly *)assembly;

...

// This gets called once, the first time, and assembly is NOT nil.
- (instancetype)initWithAssembly:(ApplicationAssembly *)assembly {
    self = [super init];

    if (self) {
        self.assembly = assembly;
    }

    return self;
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame]];

// This gets ca

lled once (after second init) and self.assembly is nil.

AcceptDisclaimerAppInfoModule *disclaimer = [[self.assembly applicationInformationModuleAssembly] acceptDisclaimerModule];

[disclaimer launchModuleFromWindow:self.window];

[self.window makeKeyAndVisible];
return YES;

}

1

There are 1 answers

0
tacos_tacos_tacos On

After looking online and going a bit crazy over-thinking this problem, I came to a number of conclusions.

The root of the problem is that Typhoon and my main.m entry point were not in sync in any form. So, main.m calls UIApplicationMain() and one of the arguments is a string that specifies the kind of id<UIApplicationDelegate> you want. I've never seen any deviation from this pattern so I was not willing to change that up.

Therefore, it is a given that the id<UIApplicationDelegate> is not going to be constructed via Typhoon in a way that is "built-into" the framework. And while you could do one of the following, I don't recommend any: they all seem wrong.

  • Instantiate an instance of your root TyphoonAssembly directly from your app delegate
  • Create a singleton IContainer object that can hug the TyphoonAssembly instance created on startup
  • Use categories with associated objects in an evil manner

The problem is... at some point, you're going to need to do one of these evil things no matter what if you don't get it right.

The reason is... Typhoon is clearly designed to work in the context of "object graphs," so the entire TyphoonAssembly and any connected assemblies can be thought of as webs of graphs. Once you get in the web, you're fine - you can take it from there. You just need to get in...

So, I decided to do as follows:

  1. Create interfaces for each "object graph" of related objects I call IContainer, even if they spanned multiple assemblies or if were smaller than an assembly. This disconnected the idea of Typhoon from the IContainer and makes it possible to debug without Typhoon by substituting a mock IContainer in place.
  2. Use constructor injection EXCLUSIVELY except in one very notable case - the one I had just mentioned, the app delegate. There, use property injection to inject just one property - the IContainer in question.
  3. Whenever you use property injection, you might as well just inject a single property, the IContainer, because you've already broken encapsulation and you might as well make it easy on yourself.
  4. Implement something fun to prove to yourself that Typhoon's default scope works they way you think it does. I implemented a few "alerts" whenever I detected multiple calls to any constructor in the same object graph.
  5. Use id<nonatomic, weak> for delegate types, not id<nonatomic, assign> as I had done for the past year. Something about the way Typhoon works under the hood must make it constantly be letting go of delegates.
  6. Use PList injection and Assembly Composition. An example:

In your Info.plist, add a key called TyphoonInitialAssemblies with Array type and values that are the class names of your assemblies. But...

Don't forget to do the other half, which is to make sure you have a "root" assembly like RootAssembly and then some ModuleAssemblys that are stored by the RootAssembly:

@protocol IAppLaunchContainer

- (UIWindow *)launchWindow;
- (UIViewController *)launchRootViewController;
- (UIImageView *)launchImageView;

@end

@protocol IDefaultUIComponentsContainer

- (UIView *)uiDefaultView;
- (UILabel *)uiDefaultLabelWithName:(NSString *)name;
- (UIButton *)uiDefaultButtonWithTitle:(NSString *)title;

@end

@interface RootAssembly : TyphoonAssembly<IAppLaunchContainer, IDefaultUIComponentsContainer>

@property (nonatomic, strong) SubAssemblyA *thisModuleAssembly;
@property (nonatomic, strong) SubAssemblyB *thatModuleAssembly;

@end

In this case, your Info.plist would have:

  1. TyphoonInitialAssemblies (Array)
    • SubAssemblyA
    • SubAssemblyB