Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Injection in Swift works only with subclasses of NSObject #267

Closed
sebastianwr opened this issue Oct 25, 2014 · 8 comments
Closed

Injection in Swift works only with subclasses of NSObject #267

sebastianwr opened this issue Oct 25, 2014 · 8 comments

Comments

@sebastianwr
Copy link

So I just spent a day trying to implement the Typhoon lib in my existing app, but I just couldn't get it to work. Whenever I activated a TyphoonDefinition for one class, the app never got to didFinishLaunchingWithOptions method.

I made a minimal app example with nothing more than an empty class with one function that has an empty body. And then it came to me ... what if it has to be a subclass of NSObject.

And voilà: It finally worked. So plain Swift objects cannot be injected (yet), is that correct?

@jasperblues
Copy link
Member

Hi Sebastian,

Yes that's right. The nature of Swift (version 1.0) is:

If a class extends NSObject, then all of Objective-C's introspection and dynamism works. This includes:

  • The ability to ask a class about its methods and properties, and to invoke methods or set properties.
  • The ability to exchange method implementations. (add functionality to all instances).
  • The ability to generate and assign a new sub-class on the fly. (add functionality to a given instance)

One shortcoming of this functionality is support for Swift optional value types. For example Int properties can be enumerated and modified but Int? properties cannot. Optional types can be enumerated partially using reflect/MirrorType, but still not modified.

If a class does not extend NSObject, then only the new, very limited (and in progress?) reflection works (see reflect/MirrorType), which adds limited ability to ask a instance about its class and properties, but none of the additional features above.

When not extending NSObject, or using the '@objc' directive, Swift defaults to static- and vtable-based dispatch. This is faster, however, in the absence of a virtual machine does not allow runtime method interception. This interception is a fundamental part of Cocoa and is required for the following types of features:

  • Cocoa's elegant property observers. (Property observers are baked right in to the Swift language).
  • Non-invasively applying cross-cutting concerns like logging, transaction management (i.e Aspect Oriented Programming).
  • Proxies, message forwarding, etc.

Therefore its recommended that clases in Cocoa/CocoaTouch applications implemented with Swift:

  • Extend from NSObject. The new class dialog in Xcode steers in this direction.
  • Where the overhead of of a dynamic dispatch leads to performance issues, then static dispatch can be used tight loops with calls to methods with very small bodies.

Summary:

  • Swift can behave like C++, with fast static/vtable dispatch and limited reflection. This makes it suitable for lower level or performance intensive applications, but without the complexity, learning curve or risk of error associated with C++
  • While Swift is a compiled language, the messaging style of method invocation adds the introspection and dynamism found in modern languages like Ruby and Python, just like Objective-C, but without Objective-C's legacy syntax.

Reference data: Execution overhead for method invocations:

  • static : < 1.1ns
  • vtable : ~ 1.1ns
  • dynamic : ~4.9ns

(actual performance depends on hardware, but the ratios will remain similar).

Also, the dynamic attribute allows us to explicitly instruct Swift that a method should use dynamic dispatch, and will therefore support interception.

public dynamic func foobar() -> AnyObject {
}

So this means:

  • Until there is bi-directional reflection in Swift there will not be any automated dependency injection.
  • Until there is method interception, there will not be any Typhoon-style dependency injection.

Fortunately for Cocoa applications the majority of classes are UIViewController, etc. By the time this is not the case, Swift must surely support interception and reflection as these are the foundation of Cocoa's power and without these, a successor would be anemic in comparison. . . not that the focus is shifting though from runtime to the compiler (ala ARC).

@sebastianwr
Copy link
Author

Ok, so for a class to be injectable in Swift, it has to subclass NSObject or something below and if it is supposed to be injected for a protocol property (as in var cityDao: CityDao), the CityDao protocol has to be an Objective-C protocol as well!?

One thing I just found out:

I had the following initializer in a class that got injected:

override init() {
 super.init()
 initCoreData()
 ...
}

By the time the initializer was called, all properties that should have been injected were nil. Calling the setup methods above in the override func typhoonDidInject() however worked fine, but that adds a direct dependency to the framework.

The User Guide says you CAN use the default initializer if you can modify the class. But the default initializer in Swift seems to be called much earlier than the injection?

@jasperblues
Copy link
Member

initializers get called before property injection. This is one of the advantages of using intializer injection (called constructor injection in other languages) which you can do with Typhoon as follows:

definition.useInitializer("initWithMainContentViewController:assembly:") {
  (initializer) in
    initializer.injectParameterWith(self.weatherReportController())
    initializer.injectParameterWith(self)
}

So its possible to ensure the instance is in a required state before proceeding. We can get most of the way there with property injection, using either typhoonWillInject and typhoonDidInjection or by specifying a custom method on the definition:

  definition.beforeInjections = "someMethod"

@jasperblues jasperblues removed the bug label Oct 26, 2014
@jasperblues
Copy link
Member

As for protocols, unfortunately they need to have the '@objc' directive added, as in https://github.com/typhoon-framework/Typhoon-Swift-Example/blob/master/PocketForecast/Client/WeatherClient.swift

@sebastianwr
Copy link
Author

Alright, thank you very much, I think I get it. Would you mind if I extend the Swift Guide a little to point out the Objective-C "dependency"?

@jasperblues
Copy link
Member

That would excellent. Its worth noting that the same rules apply as those in Apple's guide on using KVO with Swift.

Unfortunately for "dependency injection on rails" that's as far as we're going to get until Swift has native reflection and introspection. Other alternatives:

  • Manual dependency injection
  • Compile time pre-processing / instrumentation

@sebastianwr
Copy link
Author

Ok, I extended the Swift Quick Start a little with everything I learned and every pitfall I stepped into while using Typhoon the first time. Hope that helps other people getting started.

@alexgarbarev
Copy link
Contributor

Looks great! 👍
Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants