How to do simple AB testing in iOS - ios

I am looking to split up my user base to 10 group and show 10 different UI and see how they feel about it.
so each user group will have single type of UI always.
i.e Let's say I have 10k users and when I roll out my next release when user install I will be showing for 1000 user 1 UI and for another 1000 user 1 UI like all 10K users.
I know this can be done with the help of AB testing framework.
Basically I want to call one API at the launch of app and it has to return value between 1 to 10 then I can store it in my keychain and next time when app is launched I will see if it's already there in keychain and I will not call the API.
So basically the API will know how many requests has come and it'll divide and send right values back
so based on value in keychain I will show different , different UI and here AB testing framework's job would be giving me value 1 to 10 the API part.
There are so many AB testing framework available online.But I couldn't find any framework that suits my needs.
any help is appreciated !

The best approach would be splitting the users into groups in data base and let login API or some other API return some flag to indicate what group each user belongs to and you can show UI accordingly.
But if that's not possible
Then the simplest approach would be generating a random number between 1-10 and keeping it in keychain and showing a particular UI for it so that next time when you launch the app you can look out for the value in Keychain and if its not there then you can create a new random value and store it in the keychain.This way you will show the same UI for that user always.
This splitting approach is not 100% accurate but its close enough I would say
arc4random_uniform
- (NSInteger)randomNumberBetween:(NSInteger)min maxNumber:(NSInteger)max
{
return min + arc4random_uniform((uint32_t)(max - min + 1));
}
if you take sample of these random numbers 10000 times, you can see each number coming 900-1000 times which is 9-10% and its close enough
for(int i=0;i<10000;i++){
NSLog(#"random:%ld",[self randomNumberBetween:1 maxNumber:10]);
}
Seconds of Current time
you can take the seconds of current date and time and if the second is between 1-6 then you can save value 1 in keychain and for 7-12 you can save value 2 in keychain etc..54-60 you can keep value 10 in keychain.
Others
you can consider splitting the users based on Geography or country or timezone and doing this also has its own pit falls.
Like this you can devise your own strategy to split the user
but if none of the suggestions above fits your criteria then the best approach would be to look for third party AB testing frameworks but If it's going to be implemented in enterprise scale they might charge some money for it.
If I come across any such framework that provides this particular functionality alone as you asked I would update it here.

I would like to attribute the credit of this answer to this post as he has pointed out FireBase Remote Config and A/B testing.
As questioner asked I will explain the steps involved in that to achieve it.
Configuration on server
Visit https://console.firebase.google.com/ and sign in with your
google account.
Choose Create project and Click iOS
Key in app id and nick name and click register app
It'll show a link to GoogleService-Info.plist download then drag & drop it in the project
Choose Next
It'll show you Run your app to verify installation you can choose skip this step
Choose remote config from the landing page
Choose Add variable and enter a variable name of your choice but I enter ABTestVariationType and leave value empty and choose Publish changes
Choose A/B testing from the side bar then click Create Experiment then Choose Remote config
In the upcoming pop up Enter the name of your choice I enter as A/B test POC enter some description about it and that's optional anyway
In the the target users choose your app id and in the percentage of target users choose 100% and click Next then it'll show the variants section
In the variants section there will be a general category named Control group with 50% loaded by default and a variant box with 50% filled in and empty box and you can enter any name in that but I would enter variant 2.Now click add a parameter 8 times now you can see each variant has 10% and name all the variants and I would name variant 3,variant 4 to variant 10.
In the same variants section click Add Parameter from Remote config
Now you can see the a box appearing besides each variation parameter.You can enter unique value to identify each flavour.I would enter value 1 for the first variant and 2 for the second variant like that I will finish up with value 10 for the last variant and click Next
Then goal section appears you can choose one of it but I would choose Retention(15+) days and click Review and click start experiment and in prompt that's appearing choose start again
Integrating in the app
Add the following pods in your project
pod 'Firebase/Core'
pod 'Firebase/RemoteConfig'
Drag and drop the GoogleService-Info.plist that was downloaded during the server configuration
Initiate the firebase with following boiler-plate code
#import Firebase;
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions(NSDictionary *)launchOptions
{
[FIRApp configure];
return YES;
}
4.Have the class RcValues which is another boiler-plate code in your project
#import "RcValues.h"
#import Firebase;
#implementation RcValues
+(RcValues *)sharedInstance
{
static RcValues *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[RcValues alloc] init];
});
return sharedInstance;
}
-(id)init{
self=[super init];
if(self)
{
[self AcivateDebugMode];
[self LoadDefaultValues];
[self FetchCloudValues];
}
return self;
}
-(void)LoadDefaultValues
{
[FIRRemoteConfig.remoteConfigsetDefaults:
#{#"appPrimaryColor":#"#FBB03B"}];
}
-(void)FetchCloudValues
{
NSTimeInterval fetchInterval=0;
[FIRRemoteConfig.remoteConfigfetchWithExpirationDuration:
fetchInterval completionHandler:^(FIRRemoteConfigFetchStatus
status, NSError *_Nullable error)
{
NSLog(#"error:%#",error);
[FIRRemoteConfig.remoteConfig activateFetched];
}];
}
-(void)AcivateDebugMode{ //
FIRRemoteConfig.remoteConfig.configSettings=debugSettings;
FIRRemoteConfigSettings *config = [[FIRRemoteConfigSettings alloc] initWithDeveloperModeEnabled:YES];
FIRRemoteConfig.remoteConfig.configSettings=config;
}
#end
5.Invoke the class in appdelegate didFinishinglaunchoptions
RcValues *Obj=[RcValues sharedInstance];
This will download the keyvalue for ABtesting
6.Use the below code to get the AB testing key from firebase to your app
self.flavourNumber.text=[FIRRemoteConfig.remoteConfig
configValueForKey:#"ABTestVariationType"].stringValue;
Based on the key value you can show different UI as you wish.
Firebase will take care of sending the right value and you don't have to worry about divide the users into groups by yourself.
P.S
Please follow the below tutorials for more detailed info this is just a summary and I will try to summarise or add more pictures when I have free time to make it for easier understand if possible I will try to add sample project in github and link it here
firebase-tutorial-ios-ab-testing
firebase-remote-config-tutorial-for-ios

Imagine changing fonts, colour or some values in your iOS app without submitting a new build. It's pretty easy using Remote config. This tutorial will teach you A/B testing, but before A/B testing I would recommend you to look around Remote Config.

Related

CallKit doesn't block numbers from array

I create numbers array from CNContact in singleton. But when I reload my CallKit extensions CallKit doesn't block number that I blocked before. Number length is 11 characters. Array isn't null. After reload CallKit Extension there is no error.
static let shared = BlockNumbersManager()
private init() { }
open var blockNumbers: [CXCallDirectoryPhoneNumber] = []
open func getIntegerBlockNumbers() -> [CXCallDirectoryPhoneNumber] {
return blockNumbers
}
private func addBlockingPhoneNumbers(to context: CXCallDirectoryExtensionContext) throws {
let phoneNumbers: [CXCallDirectoryPhoneNumber] = BlockNumbersManager.shared.getIntegerBlockNumbers() // TODO: add numbers for block dynamically.
for phoneNumber in phoneNumbers {
context.addBlockingEntry(withNextSequentialPhoneNumber: phoneNumber)
}
}
What have I missed?
While #Stuart 's answer makes quite good points, I'd like to add some tips from my side, after I spent over 1 hour to figure out why my Call Directory extension did not work...
The phone number format: check if you've added the country code. In my case I tested with Australian mobile number like 0412345678, so it should be either 61412345678, or +61412345678. Yes both worked for me.
Mind the iOS cache! What you've written in your Call Directory extension does not get executed/called each time you restart or run your app. i.e. You think you've appended some new phone numbers in the blocking array but iOS actually may not load it as you expected. The trick is to toggle your app's option in Settings -> Phone -> Call Blocking & Identification.
You may want to call CXCallDirectoryManager.reloadExtension in your main app when a reload is needed to invalidate the iOS cache.
Currently in iOS 10.x, every time your Call Directory extension runs it must provide a complete set of phone numbers to either block or identify with a label. In other words, if it runs once and adds blocked phone numbers [A, B, C], and you wish to also block phone number D, then the next time the extension is run it must provide the complete list [A, B, C, D].
In iOS 11, some new APIs have been added which allow the extension to provide data incrementally whenever possible. For instance, if the extension has already run once and added blocked phone numbers [A, B, C], and you wish to also block phone number D, then the next time the extension is run it may first check if the system allows incremental loading, and if it does then it may add blocked numbers [D] and the full list of blocked numbers will become [A, B, C, D].
Note that when using the new iOS 11 incremental loading APIs, your extension must always check whether incremental loading is currently allowed. The system may occasionally need to reload the complete list of phone numbers and your extension must remain able to provide the complete list.
See the documentation for these new APIs at https://developer.apple.com/documentation/callkit/cxcalldirectoryextensioncontext
In addition, you may view the 'Call Directory Extension' iOS extension template in Xcode 9 which has been updated to demonstrate the new incremental loading APIs:
Check each phone number in your contact list, if any number without country code are listed then it will not added to blocking list, you must have to provide Country code + Number

Dynamically changing app logo

This question is regarding the iOS 10.3’s new feature of giving user the ability to customize the app logo they see on the homescreen. (Check out MLB at Bat app for reference where they let the user pick which icon will be app logo: http://m.mlb.com/apps/atbat)
According to my research, we need to submit for apple review all the possible logo options. Then user can customize the logo using any of these options. Now in my specific use case, I might not always want all the logo options to be available to all the users. I need help figuring out how to control which logos are shown to all users?
eg. if we have 10 images, for user A we may want to only show Image 1 and 2 to pick from; and for user B we may want to show only Image 3 and Image 4 to pick from as their app logo. Is this possible? Thanks so much in advance!
You can control which icon is set using the setAlternateIconName(_:completionHandler:) method on UIApplication.
Example usage:
UIApplication.shared.setAlternateIconName("myImage", completionHandler: { error in
print("completed")
})
You get to call this whenever it is appropriate in your application. So if you only want to only show a few options, you can do so with your own views, and only call this method when needed.
More information in the documentation here: https://developer.apple.com/reference/uikit/uiapplication/2806818-setalternateiconname
Here is another SO answer with helpful code and images: https://stackoverflow.com/a/41951096/6658553
Add all icons to some folder in your application, with the help of below method you should manage which user should see which icons.
if([user isEqualToString:#"user1"]){
[UIApplication.sharedApplication setAlternateIconName:#"icon1" completionHandler:^(NSError * _Nullable error) {
if (error) {
NSLog(#"---> error - %#",error.description);
}
else{
NSLog(#"---> icon1 ");
}
}]
}
If you want to give user an option to choose from the icons, then also you can write the code accordingly so then he/she can only choose from the filtered ones.

Parse analytics custom event not registering

Both of these commands register as analytics events but I can't see a breakdown of the custom event names.
PFAnalytics.trackEventInBackground("ML Feeling Bad", block: nil)
--
var dictionary = ["test" : "val"]
PFAnalytics.trackEventInBackground("ML Feeling Good", dimensions: dictionary, block: nil)
I'm looking at Analytics > Events > Analytics Requests. Custom Breakdown doesn't seem to have any helpful options.
I've just found! Remove the spaces in events names.
For example change "ML Feeling Good" to "MLFeelingGood"
Seems that there is a bug inside Parse Analytics, because events with spaces are displayed in Explorer, but not in Analytics tab. And events with no spaces are displayed correctly.
It takes some time to show Custom event we have registered as pointed out in this link also.
Does it take 24 hours for custom events to be recorded in Parse Analytics
If u want to see your custom event then u can select Analytics > Events > Analytics Requests > Custom Breakdown and then in Custom BreakDown section you have to select read option from the drop down to see your event which u have registered.
Once you have selected read option you must be able to see your event which was Test in my case it is Category and dayType.
Just select your event and turn it on.
I am using
PFAnalytics.trackEvent("video", dimensions:["title": self.title])
with success (and with the mandatory PFAnalytics.trackAppOpenedWithLaunchOptions(launchOptions) in the implementation of AppDelegate)
Anyway, in the Parse doc about trackEventInBackground:dimensions:block: they say:
block: The block to execute on server response. It should have the
following argument signature: ^(BOOL succeeded, NSError *error)
So maybe you cannot set the block to nil.
I hope this helps!

Google Analytics - Can anyone explain Dimensions and Metrics?

I've implemented the new Google Analytics library (2.0) for iOS for app tracking. Tracking views etc is very simple, but I'm having trouble understanding how to use Dimensions and Metrics.
I've read the documentation multiple times but I'm having trouble wrapping my head around it.
Basically, I want to check how many of my users have a specific setting enabled when using the app.
In semipseudo-code, this is what I'd like to do:
- (void)applicationLaunched
{
id<GAITracker> tracker = [[GAI sharedInstance] trackerWithTrackingId:#"My ID"];
if (_mySettingIsEnabled) {
[tracker setUserValue:#"Enabled" forKey:#"My Setting"];
} else {
[tracker setUserValue:#"Disabled" forKey:#"My Setting"];
}
}
Can anyone explain to me how I would do this per user with Dimensions and Metrics?
Check the official documentation: https://developers.google.com/analytics/devguides/collection/ios/v2/customdimsmets
Make sure you have a new property (GA is organized in Account/Property/Profiles, so click on the "home", than an account, than add a new app property) - I had an older property/profile and nothing was working with the IOS GA library 2.0.
Register on the GA web, open a Property, there will be a tab for Custom Definitions, where you can register custom dimensions and metrics - Lets say "AppMode", which is the custom definition with index 1.
Set the custom dimension/metric value in your code:
NSString *appMode = #"Demo";
[_gaiTracker setCustom:1 dimension:appMode];
Basically, you might go with custom variables.
Having a specific setting enabled might either be on user or on session level, so you should set the scope to either 1 or 2. The setting you want to track is value of your custom var or the "dimension", and depending on the scope you get the metrics # of users or # of visits automatically.
in JS that looks like
_gaq.push(['_setCustomVar',
1, // This custom var is set to slot #1. Required parameter.
'My Setting', // The name of the custom variable. Required parameter.
'Disabled', // The value of the custom variable. Required parameter.
// (possible values might be Free, Bronze, Gold, and Platinum)
1 // Sets the scope to visitor-level. Optional parameter.
]);

iOS : Google Analytics User Timing report is not updated in my Google Analytics Account

I am trying to track my app speed using Google analytics but i could not see anything under app speed in my google analytics account. I have tracked the other parameters like events,crashes and exceptions. For those parameter am able to see the reports generated in my google analytic account. Following is the code what i am using to send event timing.
self.endDate=[NSDate date];
double timeDiff=[_startDate
timeIntervalSinceDate:_endDate];
NSLog(#"timeDiff----%f",timeDiff);
if([[[GAI sharedInstance]defaultTracker] sendTimingWithCategory:category
withValue:timeDiff
withName:#"LoadTime"
withLabel:category]) {
NSLog(#"Succesfully sent load time to GA");
}
Following is the message printed in the console.
GoogleAnalytics 2.0b4 -[GAIDispatcher
dispatchComplete:withStartTime:withRetryNumber:withResponse:withData:withError:]
(GAIDispatcher.m:415) DEBUG: Successfully dispatched hit /GAIHit/p479
(0 retries).
Provide me any sample code if u have.
Please help me in that. Thanks in advance.
I found that the interval had to be a whole number. It's expecting milliseconds but NSTimeInterval is seconds so it tries to send it as "3.1234" but if you convert it to whole milliseconds it will send it as 3123 and you should see results. To convert I used (GA V3)
[tracker send:[[GAIDictionaryBuilder createTimingWithCategory:category interval:#((int)(interval * 1000)) name:name label:label] build]]
Did you happen to see the real time data tab in GA dashboard??? They show the last 30 minutes usage data. Later it updates in your Google analytics dashboard. I have worked with flurry, Google analytics, I find GA is better and faster. Keep trying!!!.
Your implementation seems fine. I don't think it's the problem (and as you get the basic events, it's probably not a problem of initialization). I have the same way to log the timing events (you can find my code below if you want to compare).
What I can tell you is:
1/ It's a "beta" version (yes ok, all things #google are in beta xD), pretty unstable and events take time to be displayed in the administration (I don't see any events for the 18 february yet, for example). At least, more than for a website with similar stats.
2/ I can't display the time events on more than 2 days, or it displays me some errors ^^ (probably too many datas asked for a large timezone)
3/ If there is no label, don't put the category, just set nil. Same for the name. I think they are both optional parameters. And it'll slow down the display of your analytics when you have more stats.
4/ For big set of datas, time events are calculated on part of your visits. But it shouldn't be your problem right now ^^
http://support.google.com/analytics/bin/answer.py?hl=en&answer=1042498
Wait 2 days. If you still don't see anything, try to contact a google analytics representative. Or take the "risk“ to submit it like it is.
My implementation
(in case it could help)
+ (void)trackGoogleTimingInCategory:(NSString *)category withTimeInterval:(NSTimeInterval)time withName:(NSString *)name withLabel:(NSString *)label {
//
if (![ category isKindOfClass:[ NSString class ] ])
return;
NSLog(#"[%#] %# time=%f (%#)", category, name, time, label);
//
if (![ name isKindOfClass:[ NSString class ] ])
name = nil;
if (![ label isKindOfClass:[ NSString class ] ])
label = nil;
//
[ [ [ GAI sharedInstance ] defaultTracker ] sendTimingWithCategory:category withValue:time withName:name withLabel:label ];
}
For the time calculation, I do the same way:
NSTimeInterval timeInterval = [ [ NSDate date ] timeIntervalSinceDate:timeStart ];
You must wait at least 24 hours for data to become available in your Google Analytics dashboard, unless you have a GA Premium account:
http://www.google.com/analytics/premium/features.html
I had the same issue with Google Analytics component for Xamarin on iOS, but got it working using NSNumber.FromInt32:
var start = DateTime.Now;
// do operation
var finish = DateTime.Now;
var timeElapsed = finish.Subtract (start);
GAI.SharedInstance.DefaultTracker
.Send(GAIDictionaryBuilder.CreateTiming(
"Storage",
NSNumber.FromInt32((int)timeElapsed.TotalMilliseconds),
"LoadItems", null)
.Build());

Resources