appsquickly / typhoon

Powerful dependency injection for Objective-C ✨✨ (https://PILGRIM.PH is the pure Swift successor to Typhoon!!)✨✨
https://pilgrim.ph
Apache License 2.0
2.7k stars 269 forks source link

Singleton root view controller instantiated from storyboard #404

Closed HiveHicks closed 7 years ago

HiveHicks commented 9 years ago

I want to make my root view controller a singleton, but seems like it's impossible, and I can see why. If I declare a scope of the definition as singleton, Typhoon creates controller first, but then storyboard instantiates it once again, and Typhoon doesn't store reference to it as it's already holding singleton reference.

I've been thinking about it for a while, and I can't think of a way to inject root view controller to any component. Do you have any ideas?

mogol commented 9 years ago

Hey @HiveHicks , could you create/show example code for this issue?

How do you instantiate root view controller first time? second time?

HiveHicks commented 9 years ago

@mogol, I've created a sample project here: https://bitbucket.org/HiveHicks/typhoonsingletoncontroller

ViewController is defined as TyphoonScopeLazySingleton, however root view controller and view controller which is injected into NavigationManager are different:

2015-09-01 13:49:07.691 TyphoonSingletonController[99950:2732354] Root view controller: <ViewController: 0x7fbc486123f0>
2015-09-01 13:49:07.692 TyphoonSingletonController[99950:2732354] Navigation manager view controller: <ViewController: 0x7fbc48571c10>
mogol commented 9 years ago

@HiveHicks, thanks for sample. We have a bug or a feature in instantiating view controllers using storyboards. TyphoonStoryboards always return new instances of view controllers. WA - try not to use Storyboards with Singleton view controller.

We'll fix soon this.

etolstoy commented 8 years ago

FYI: yesterday we've encountered the same issue in our project.

etolstoy commented 8 years ago

I've investigated the problem - and it's a bit deeper than it seems.

We have a bug:

and a problem:

I see two possible solutions:

alexgarbarev commented 8 years ago

@igrekde Hmmm.. I thought we retaining ViewController instances created by storyboard inside Typhoon.

See method, called on each created ViewController:

- (void)inject:(id)instance withDefinition:(TyphoonDefinition *)definition
{
    @synchronized (self) {
        id<TyphoonComponentsPool> pool = [self poolForDefinition:definition];
        [pool setObject:instance forKey:definition.key];
        TyphoonStackElement *element = [TyphoonStackElement elementWithKey:definition.key args:nil];
        [element takeInstance:instance];
        [_stack push:element];
        [self doInjectionEventsOn:instance withDefinition:definition args:nil];
        [_stack pop];
        if ([_stack isEmpty]) {
            [_objectGraphSharedInstances removeAllObjects];
        }
    }
}

Calling this:

[pool setObject:instance forKey:definition.key];

should put instance into correct objects pool.. Is that you looking for?

etolstoy commented 8 years ago

@alexgarbarev I'm not sure that it's a correct behavior:

That's why I'm thinking of a two-step view controller instantiation:

  1. We look for the singleton instance in component pools, and if there is any, we return it.
  2. If there are no singleton instances, we return the original one.
alexgarbarev commented 8 years ago

I just checked demo project. The problem is following:

1) App launched, we injecting ViewController class instance into AppDelegate, retaining into singletons pool 2) Storyboard instantiate new ViewController instance and replaces ViewController stored in the pool.

I think it's correct behavior since we haven't viewController from storyboard yet, but we need to inject that into AppDelegate.. I can't see any good and consistent way to do that.

If we do two-step viewController instantiation, then it's possible to get different instances for same singleton definition, which looks weird.

alexgarbarev commented 8 years ago

I think we should raise an exception, while creating ViewController from storyboard and if we have instance in any singletons pool. Something like "We can't create ViewController from storyboard, since it was already instantiated - please use another definition/scope for that one"

alexgarbarev commented 8 years ago

Another possible bug in the Demo project:

- (ViewController *)viewController
{
    return [TyphoonDefinition withClass:[ViewController class] configuration:^(TyphoonDefinition *definition) {
        [definition setScope:TyphoonScopeLazySingleton];
    }];
}

Once ViewController wasn't created by storyboard and stored here, Typhoon will create a 'bare' ViewController without xib counterpart, just alloc-init created object - not good. Maybe we can create something special for storyboards. Maybe another scope.. Or special definition for Storyboard-based viewControllers

alexgarbarev commented 8 years ago

Something like

- (UIViewController *)viewController
{
   return [TyphoonDefinition withStoryboardId:@"ViewController" configuration:^(TyphoonDefinition *definition) {
        /// injections
        [definition setScope:TyphoonScopeLazySingleton]; /// <-- this can be default scope for that kind of ViewControllers
    }];
}

Then initialization would be from Storyboard by specified identifier. ViewController would be created by first call and then cached into Singletons (weak or laze) pool.

If ViewController already exists in pool, we can return it from

- (id)instantiateViewControllerWithIdentifier:(NSString *)identifier

method of TyphoonStoryboard

@igrekde What do you think? Make any sense ?

etolstoy commented 8 years ago

@alexgarbarev that looks great, thanks. Returning the pre-cached instance from - (id)instantiateViewControllerWithIdentifier:(NSString *)identifier is exactly what I meant under two-step instantiation :)

Such definition should use not only view controller identifier, but also storyboard identifier:

- (UIViewController *)viewController
{
   return [TyphoonDefinition withStoryboard:[self myStoryboard] storyboardId:@"ViewController" configuration:^(TyphoonDefinition *definition) {
        /// injections
        [definition setScope:TyphoonScopeLazySingleton]; /// <-- this can be default scope for that kind of ViewControllers
    }];
}

or

- (UIViewController *)viewController
{
   return [TyphoonDefinition withStoryboardName:@"StoryboardName" storyboardId:@"ViewController" configuration:^(TyphoonDefinition *definition) {
        /// injections
        [definition setScope:TyphoonScopeLazySingleton]; /// <-- this can be default scope for that kind of ViewControllers
    }];
}

BTW, currently we use a very same approach, but more verbose:

- (FeedViewController *)viewControllerFeedModule {
    return [TyphoonDefinition withFactory:[self.storyboardAssembly feedStoryboard]
                                 selector:@selector(instantiateViewControllerWithIdentifier:)
                               parameters:^(TyphoonMethod *factoryMethod) {
                                   NSString *identifier = NSStringFromClass([FeedViewController class]);
                                   [factoryMethod injectParameterWith:identifier];
                                } configuration:^(TyphoonFactoryDefinition *definition) {
                                    [definition injectProperty:@selector(output)
                                                          with:[self presenterFeedModule]];
                                }];
}
alexgarbarev commented 8 years ago

@igrekde Yes, I see now. The other reason of another kind of definition is: it's more safe, since it guaranteed Storyboard-based ViewController and has identifier for that. Also we could check inside instantiateViewControllerWithIdentifier, that returning instance was created using Storyboard definition.

That's why I'm thinking of a two-step view controller instantiation:

  1. We look for the singleton instance in component pools, and if there is any, we return it.
  2. If there are no singleton instances, we return the original one.

We should put into singletons pool in case 2 (if we have definition with lazy singleton scope).

etolstoy commented 8 years ago

@alexgarbarev You're right, I forgot to mention this step.

I'll take time to implement this feature in the a couple of days.

alexgarbarev commented 8 years ago

:+1: Awesome!

uranpro commented 8 years ago

Have singleton bug too

// db manager code

- (id)initWithDbName:(NSString *)dbName {
    self = [super init];
    if (self) {
        NSLog(@"INIT WITH %@", dbName);
        [self setupDatabase:dbName];
        NSLog(@"Db %@", self.db);
    }
    return self;
}

// assembly

- (TQDatabaseManager *)dbManager {
    return [TyphoonDefinition withClass:[TQDatabaseManager class] configuration:^(TyphoonDefinition *definition) {
        [definition useInitializer:@selector(initWithDbName:) parameters:^(TyphoonMethod *initializer) {
            [initializer injectParameterWith:TyphoonConfig(@"db.filename")];
        }];
        definition.scope = TyphoonScopeSingleton; // not working =,(

    }];
}

- (TQOfflineViewController *)offlineViewController {
    return [TyphoonDefinition withClass:[TQOfflineViewController class] configuration:^(TyphoonDefinition *definition) {
        [definition injectProperty:@selector(dbManager) with:[_coreComponents dbManager]];
    }];
}

// main
// before this code DBManager already initialized, see log

- (void)offlineTap {
    TQOfflineViewController *controller = [[[TQApplicationAssembly assembly] activate] offlineViewController];

    [self presentViewController:controller animated:YES completion:^{
    }];
}

// log

2015-11-30 17:11:47.968 TQ[1364:800053] INIT WITH db.sqlite
2015-11-30 17:11:48.179 TQ[1364:800053] Db <YapDatabase: 0x145d080c0>
2015-11-30 17:11:55.920 TQ[1364:800053] INIT WITH db.sqlite
2015-11-30 17:11:55.924 TQ[1364:800053] Db (null)
etolstoy commented 7 years ago

Fixed by #451.