orta / ARAnalytics

Simplify your iOS/Mac analytics
MIT License
1.84k stars 217 forks source link

DSL with Swift #233

Open scisci opened 8 years ago

scisci commented 8 years ago

Would be great to have support for the aspect-oriented DSL style tracking in Swift.

I can get tracked screens and some events to come through, but for events, some selectors don't seem to work and can't figure out how to properly type the Properties callback.

The following is working for me for now:

let trackedEvents =  [
        [
            ARAnalyticsClass: AccountEditVC.self,
            ARAnalyticsDetails: [
                [
                    ARAnalyticsEventName: "Account Edit Save Button",
                    ARAnalyticsSelectorName: "saveChangesBtnTapped:"
                ],
            ]
        ]
   ]

let trackedScreens = [
     [
        ARAnalyticsClass: FeaturesVC.self,
        ARAnalyticsDetails: [[ARAnalyticsPageName: "Slideshow"]]
      ]
]

Can't figure out how to do properties though. And sometimes selectors that aren't IBOutlets are never called.

orta commented 8 years ago

@ashfurrow - this needs the vars / selectors /classes to be classed as dynamic / @objc in swift right?

ashfurrow commented 8 years ago

Correct; subclasses of NSObject don't need @objc since it is already implied, but properties still must be marked as dynamic.

scisci commented 8 years ago

Thanks for letting me know. So all of my tests have been on UIViewControllers so they do inherit from NSObject. However, regular old methods that aren't IBActions weren't being swizzled. Do I need to declare them like this:

public dynamic func foobar()

Also how would one do the ARAnalyticsDetails to add event properties? I couldn't figure out the closure signature.

Thanks for the help.

ashfurrow commented 8 years ago

That's strange, that should work. Can you double-check that you're following the DSL structure strictly? It's the most common source of problems. The closure is the same as Objective-C:

(controller: MyViewController, parameters: NSArray) -> NSDictionary
timbroder commented 8 years ago

I think I'm close getting Swift to work with the DSL. Screen tracking is working, but I don't think I'm hooking into the event selectors correctly.

Can anyone spot what I'm missing here?

        let trackedEvents =  [
            [
                ARAnalyticsClass: LoadingViewController.self,
                ARAnalyticsDetails: [
                    [
                        ARAnalyticsEventName: "LandingRegisterButtonTapped",
                        ARAnalyticsSelectorName: "tapRegister:sender:"
                    ],
                    [
                        ARAnalyticsEventName: "LandingLoginButtonTapped",
                        ARAnalyticsSelectorName: "tapLogin:sender:"
                    ],
                    [
                        ARAnalyticsEventName: "QuickTest",
                        ARAnalyticsSelectorName: "quicktest:"
                    ],
                ]
            ],
        ]

        let trackedScreens = [
            [
                ARAnalyticsClass: LoadingViewController.self,
                ARAnalyticsDetails: [[ARAnalyticsPageName: "Landing"]]
            ],
            [
                ARAnalyticsClass: RegisterViewController.self,
                ARAnalyticsDetails: [[ARAnalyticsPageName: "Registration"]]
            ],
            [
                ARAnalyticsClass: LoginViewController.self,
                ARAnalyticsDetails: [[ARAnalyticsPageName: "Login"]]
            ]
        ]

        ARAnalytics.setupWithAnalytics(providers, configuration: [
                ARAnalyticsTrackedEvents: trackedEvents,
                ARAnalyticsTrackedScreens: trackedScreens
        ])
class LoadingViewController: UIViewController {
    @IBAction func tapLogin(sender: AnyObject) {
        self.quicktest()
    }

    @IBAction func tapRegister(sender: AnyObject) {
        ARAnalytics.event("manualtapRegister", withProperties: ["property.testing": "123456"])
        self.quicktest()

    }

    dynamic func quicktest() {
        print("hi")
    }

}

Much appreciated!

orta commented 8 years ago

"tapLogin:sender:" implies a function like: func tapLogin(thing:Thing, sender:OtherThing) - you want "tapLogin:"

same for the rest of them, and quickTest: has no arguments, so it's just quickTest

timbroder commented 8 years ago

Thanks. I did have that at first, I should have specified. But then quicktest: should have worked, no?

I've updated, but still issues. I've also tried just "quicktest"

        let trackedEvents =  [
            [
                ARAnalyticsClass: LoadingViewController.self,
                ARAnalyticsDetails: [
                    [
                        ARAnalyticsEventName: "LandingRegisterButtonTapped",
                        ARAnalyticsSelectorName: "tapRegister:"
                    ],
                    [
                        ARAnalyticsEventName: "LandingLoginButtonTapped",
                        ARAnalyticsSelectorName: "tapLogin:"
                    ],
                    [
                        ARAnalyticsEventName: "QuickTest",
                        ARAnalyticsSelectorName: "quicktest:"
                    ],
                ]
            ],
        ]
timbroder commented 8 years ago

Some more info. I don't think it's the selectors.

In addEventAnalyticsHook, the RSSWReplacement block is never called when trying to swizzle alloc on my VC

It does get through to the originalIMP = class_replaceMethod(classToSwizzle, selector, newIMP, methodType); line in static void swizzle(Class classToSwizzle, SEL selector, RSSwizzleImpFactoryBlock factoryBlock) but never seems to fire

So then I thought it might be the way I'm instantiating LoadingViewController in the storyboard, So I added a definition for RegisterViewController which I instantiate in code

In that example, I do see getting into NSObjectRACSignalForSelector but failing if (targetMethod == NULL) and getting to class_replaceMethod with the right selector string

Also, on this class though, I never get into the RSSWReplacement block

I was able to verify my selector strings if I can figure this part out

        let loadingVC = LoadingViewController()
        let canRegister = loadingVC.respondsToSelector(Selector("tapRegister:"))
        let canLogin = loadingVC.respondsToSelector(Selector("tapLogin:"))
        let canTest = loadingVC.respondsToSelector(Selector("quicktest"))

        print("canRegister \(canRegister)")
        print("canRegister \(canLogin)")
        print("canRegister \(canTest)")

        let registerVC = RegisterViewController()
        let canPhoto = registerVC.respondsToSelector(Selector("tapAddPicture:"))

        print("canPhoto \(canPhoto)")
shwetsolanki commented 8 years ago

unable to figure out a proper way to add ARAnalyticsProperties callback

let someClassEvents: NSDictionary = [
            ARAnalyticsClass : NSClassFromString("ViewController")!,
            ARAnalyticsDetails : [
                ARAnalyticsEventName : "SomeEvent",
                ARAnalyticsSelectorName : "viewDidLoad",
                ARAnalyticsProperties : {(controller: UIViewController, parameters: NSArray) -> NSDictionary in
                    return ["somekey" : "somevalue"]
                }

            ]
        ]

I am getting the following error on the closure. Any suggestions?

Contextual type 'AnyObject' cannot be used with dictionary literal

orta commented 8 years ago

Remove the : NSDictionary - the examples above don't have them.

shwetsolanki commented 8 years ago

After removing : NSDictionary, I am getting error on the return statement

'(dictionaryLiteral: (NSCopying, AnyObject))' is not convertible to '(dictionaryLiteral: (NSString, NSString)...)'

shwetsolanki commented 8 years ago

Could you help us with a Swift Example, if possible?

ashfurrow commented 8 years ago

"viewDidLoad" should be "viewDidLoad:" because it has a parameter. Can you try that?

ashfurrow commented 8 years ago

Oh wait, no it doesn't >.<

shwetsolanki commented 8 years ago

@ashfurrow the error is on compile time. @orta I have updated to the following, now the error changes

let someClassEvents = [
            ARAnalyticsClass : NSClassFromString("ViewController")!,
            ARAnalyticsDetails : [
                ARAnalyticsEventName : "SomeEvent",
                ARAnalyticsSelectorName : "viewDidLoad",
                ARAnalyticsProperties : {(controller: AnyObject!, parameters:[AnyObject]!) -> NSDictionary in

                    var someDict = NSMutableDictionary()
                    someDict["someKey"] = "someValue"
                    return someDict
                }
            ] as NSDictionary
        ]

Error is while inserting the block to the dictionary.

Value of type '(AnyObject!, [AnyObject]!) -> NSDictionary' does not conform to expected dictionary value type 'AnyObject'

shwetsolanki commented 8 years ago

I got it working by adding another ObjC class called AREvents in my project.

@implementation AREvents
+(NSDictionary *)eventWithName:(NSString *)name selector:(NSString *)selector properties:(NSDictionary *(^)(id, NSArray *))propertiesBlock
{
    return @{
             @"event" : name,
             @"selector" : selector,
             @"properties" : propertiesBlock
             };
}
@end

I tried importing ARDSL.h to use the externs, but was getting some error, will resolve that later. This serves my needs as of now.

ashfurrow commented 8 years ago

We had a similar issue with putting Swift closures in NSDictionaries. Our solution involves an unsafeBitCast from the closure to AnyObject, which makes the compiler happy but you need to be careful. We wrapped the whole thing up into this abstraction, so we would call toBlock() on the closure when it had to be turned into an AnyObject for NSDictionary use. I think that solution would work here.

Ideally we'd provide some mechanism in ARAnalytics to do this for you, but that would mean introducing the first Swift code into it, which has implications around which versions of iOS we can support, etc. Maybe we could put something in another library? Open to suggestions on that. In the meantime @icios, try that out and see if it works :cake:

orta commented 8 years ago

OK, this caught me, after some debugging I've figured out that the alloc function isn't called when you initialise view controllers from swift, so:

    Sale *sale = [Sale modelWithJSON:@{}];
    SaleViewModel *model = [[SaleViewModel alloc] initWithSale:sale saleArtworks:@[]];
    id viewController = [[AuctionInformationViewController alloc] initWithSaleViewModel:model];
    [self pushViewController:viewController animated:YES];

sets the VC up for analytics

    func userDidPressInfo(titleView: AuctionTitleView) {
        let auctionInforVC = AuctionInformationViewController(saleViewModel: saleViewModel)
        auctionInforVC.titleViewDelegate = self
        [...]

Does not.

ashfurrow commented 8 years ago

Whoa.

GabrielCartier commented 8 years ago

Any news for an update of the DSL for Swift?

orta commented 8 years ago

No, we've been adding them inline in swift ATM due to time constraints, you're welcome to have a think / PR about it though

GabrielCartier commented 8 years ago

Yea, I'll check if I can come up with something. Is it even possible to do it in Swift or would it only work with classes inheriting from NSObject?

ashfurrow commented 8 years ago

Only NSObject is possible I'm afraid.

GabrielCartier commented 8 years ago

Ok, thanks!