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

Swift : Prevents instrumentation of Typhoon methods by static/vtabling them. #243

Closed twyneed closed 10 years ago

twyneed commented 10 years ago

i'm using swift with typhoon and somehow my components don't get injected through property-injection. For simple types like Strings it is working. I provided a simple example which explains the problem. The Output shows the result where serviceB has a null reference to serviceA. All String properties are set properly and no error is thrown. What am i'm doing wrong here?

XCode: 6-beta5 ,Typhoon: 2.1.0

MYServiceA.swift

@objc(MYServiceA) public class MYServiceA : NSObject {

    public var text : String!

} 

MYServiceB.swift

@objc(MYServiceB) public class MYServiceB : NSObject {

    public var text : String!
    public var serivceA : MYServiceA!

}

MYAssembly.swift

public class MYAssembly : TyphoonAssembly {

    public func serviceA() -> AnyObject {
        var definitionBlock : TyphoonDefinitionBlock = {(definition : TyphoonDefinition!) in
            definition.injectProperty("text", with: "some a text")
            definition.scope = TyphoonScopeSingleton
        }
        return TyphoonDefinition.withClass(NSClassFromString("MYServiceA"), configuration: definitionBlock)
    }

    public func serviceB() -> AnyObject {
        var definitionBlock : TyphoonDefinitionBlock = {(definition : TyphoonDefinition!) in
            definition.injectProperty("text", with: "some b text")
            definition.injectProperty("serivceA", with: self.serviceA())
            definition.scope = TyphoonScopeSingleton
        }
        return TyphoonDefinition.withClass(NSClassFromString("MYServiceB"), configuration: definitionBlock)
    }

}

AppDelegate.swift

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {

        var assembly : MYAssembly = MYAssembly()
        var factory : TyphoonComponentFactory = TyphoonBlockComponentFactory(assembly: assembly) as TyphoonComponentFactory
        factory.makeDefault()

        var serviceA : MYServiceA = TyphoonComponentFactory.defaultFactory().componentForKey("serviceA") as MYServiceA
        println("MYServiceA")
        println("- instance=\(serviceA != nil)")
        println("- text=\(serviceA.text)")            

        var serviceB : MYServiceB = TyphoonComponentFactory.defaultFactory().componentForKey("serviceB") as MYServiceB
        println("MYServiceB")
        println("- instance=\(serviceB != nil)")
        println("- text=\(serviceB.text)")
        println("- serviceA.instance=\(serviceB.serivceA != nil)")            

        return true
    }
..
}

Output

MYServiceA
- instance=true
- text=some a text

MYServiceB
- instance=true
- text=some b text
- serviceA.instance=false
twyneed commented 10 years ago

FYI: I've send a test-project to info@typhoonframework.org which contains the code from above

alexgarbarev commented 10 years ago

Hi @ robbiebubble. Thanks for feedback, very usefull information for us. I investigated the problem, and found that method swizzling is not working properly for swift classes. When you calling swift method in swift - it is not sending message, like objective-c but something like vtable in C++ See the example below:

func funcA() -> String! {
    return "funcA"
}

func funcB() -> String! {
   return "funcB"
}

func application(application: UIApplication!, didFinishLaunchingWithOptions launchOptions: NSDictionary!) -> Bool {

    var methodA: Method = class_getInstanceMethod(object_getClass(self), Selector.convertFromStringLiteral("funcA"))
    var methodB: Method = class_getInstanceMethod(object_getClass(self), Selector.convertFromStringLiteral("funcB"))
    method_exchangeImplementations(methodA, methodB)

    println("funcA = \(self.funcA())")

    let key = "funcA"
    println("funcA = \(self.valueForKey(key))")

    return true
}

The output will be

func = funcA
func = funcB

This example shows that swizzling works, but only when sending message, for example when calling valueForKey(key) And swizzling doesnt works when direct method calling. Seems that it works like C function calls.

This is the reason why Typhoon didn't work properly for swift-based assemblies. We will work to fix that (I already have few ideas). But for now, use valueForKey(key) approach.

twyneed commented 10 years ago

@alexgarbarev Thx for (fast) investigation! Works like a charm :)

MYAssembly.swift

...
 definition.injectProperty("serivceA", with: self.valueForKey("serviceA"))
...

Output

...
- serviceA.instance=true
...
jasperblues commented 10 years ago

In the latest beta of Swift, I think you have to do this:

. . otherwise they're candidates for inline or 'performance enhancing' even if the assembly is marked '@objc'. Thanks to @hsuanyat for this advice.

Add the word 'dynamic' after 'func'

jasperblues commented 10 years ago

http://stackoverflow.com/a/25219216/404201

jasperblues commented 10 years ago

@robbiebubble,

@alexgarbarev and I have just been having a discussion:

We think:

. . . Typhoon assemblies make good use of the ObjC runtime's dyanmic dispatch features, which current versions of Swift will often second guess.

I you're writing a serious app, and have a deadline we recommend this approach. Meanwhile, if you're doing research, then your feedback is incredibly valuable.

twyneed commented 10 years ago

I you're writing a serious app, and have a deadline we recommend this approach

As usual, thats the case. To be honest i didn't expected to get that incredible (fast + working) feedback to my questions regarding typhoon (i know this won't always be case because all of you probably have to do things beside). But because of that and as long as i got a fallback-solution (e.g. moving the assemblies to an objective-c class which isn't a big deal) i will stick on the "swift-way" and try to get as far as possible.

jasperblues commented 10 years ago

:+1: That will be very helpful for us. . It depends how things evolve with Swift, but we think Swift-style assemblies will work somewhat differently to the Objective-C way.

Aleksey did a couple of hours research yesterday and found:

    var anotherAssembly : AnyObject!

    override init() {
        super.init()
        self.anotherAssembly = TyphoonCollaboratingAssemblyProxy()
    }

. . normally Typhoon will detect this and do it for you, but it doesn't happen in Swift!

https://github.com/typhoon-framework/Typhoon/wiki/Types-of-Injections#injection-with-run-time-arguments

jasperblues commented 10 years ago

Using dynamic fixes this, as shown in 'Swift Quick Start'

bradfeehan commented 9 years ago

Using dynamic fixes this, as shown in 'Swift Quick Start'

@jasperblues: does this include the following?

Runtime arguments - one of the best features of Typhoon - don't work at all :(

jasperblues commented 9 years ago

@bradfeehan runtime args should work AFAIK

Example

Given:

    public dynamic func wanderingKnight(homeFort : Fort) -> AnyObject {

        return TyphoonDefinition.withClass(Knight.self) {
            (definition) in

            definition.useInitializer("initWithQuest:") {
                (initializer) in

                initializer.injectParameterWith(self.defaultQuest())
            }
            definition.injectProperty("homeFort", with: homeFort)
        }
    }

Then:

    internal func test_injects_runtime_args() {

        let assembly = SwiftMiddleAgesAssembly()
        TyphoonAssemblyActivator.withAssembly(assembly).activate()

        let fort = Fort()
        let knight = assembly.wanderingKnight(fort) as Knight
        println(knight.description())
        XCTAssertTrue(knight.isKindOfClass(Knight.self))
        XCTAssertNotNil(knight.homeFort)

    }
jasperblues commented 9 years ago

@bradfeehan Also note this issue is a duplicate of #286