ARAnalytics icon indicating copy to clipboard operation
ARAnalytics copied to clipboard

DSL with Swift

Open scisci opened this issue 9 years ago • 24 comments

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.

scisci avatar Dec 02 '15 21:12 scisci

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

orta avatar Dec 18 '15 14:12 orta

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

ashfurrow avatar Dec 18 '15 14:12 ashfurrow

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.

scisci avatar Dec 18 '15 16:12 scisci

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

ashfurrow avatar Dec 18 '15 20:12 ashfurrow

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!

timbroder avatar Feb 12 '16 12:02 timbroder

"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

orta avatar Feb 12 '16 12:02 orta

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 avatar Feb 12 '16 12:02 timbroder

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)")

timbroder avatar Feb 12 '16 17:02 timbroder

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

shwetsolanki avatar Mar 01 '16 13:03 shwetsolanki

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

orta avatar Mar 01 '16 14:03 orta

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

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

shwetsolanki avatar Mar 01 '16 15:03 shwetsolanki

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

shwetsolanki avatar Mar 01 '16 16:03 shwetsolanki

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

ashfurrow avatar Mar 02 '16 00:03 ashfurrow

Oh wait, no it doesn't >.<

ashfurrow avatar Mar 02 '16 00:03 ashfurrow

@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 avatar Mar 02 '16 06:03 shwetsolanki

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.

shwetsolanki avatar Mar 02 '16 08:03 shwetsolanki

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:

ashfurrow avatar Mar 02 '16 11:03 ashfurrow

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.

orta avatar Mar 22 '16 19:03 orta

Whoa.

ashfurrow avatar Mar 22 '16 19:03 ashfurrow

Any news for an update of the DSL for Swift?

GabrielCartier avatar Jun 17 '16 05:06 GabrielCartier

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

orta avatar Jun 17 '16 14:06 orta

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?

GabrielCartier avatar Jun 17 '16 15:06 GabrielCartier

Only NSObject is possible I'm afraid.

ashfurrow avatar Jun 17 '16 15:06 ashfurrow

Ok, thanks!

GabrielCartier avatar Jun 17 '16 15:06 GabrielCartier