Hello there!
I have been trying to implement the Apple Search Attribution API as a plugin in Unity.
I researched and found several projects which already have implemented such a feature, so I was quite surprised when it didn’t work on my side.
Here is my AppleSearchAd.mm posting it in its entirety to perhaps help someone else in the future:
#import "AppleSearchAd.h"
#include <cstring>
@interface AppleSearchAd()
+ (void)requestSearchAdAttributionDetailsWithTimeBetweenRetries:(NSTimeInterval)timeBetweenRetries numberOfRetries:(NSInteger)numberOfRetries successBlock:(NSString*(^)(NSDictionary *))successBlock failureBlock:(NSString*(^)(void))failureBlock;
@end
typedef NSString*(^AppleSearchAdTimerHandlerSuccessBlock)(NSDictionary *);
typedef NSString*(^AppleSearchAdTimerHandlerFailureBlock)(void);
@interface AppleSearchAdTimerHandler : NSObject
@property (nonatomic,assign) NSTimeInterval timeBetweenRetries;
@property (nonatomic,assign) NSInteger numberOfRetries;
@property (nonatomic,strong) AppleSearchAdTimerHandlerSuccessBlock successBlock;
@property (nonatomic,strong) AppleSearchAdTimerHandlerFailureBlock failureBlock;
@end
@implementation AppleSearchAdTimerHandler
-(void)retryTimer:(NSTimer *)timer{
NSLog(@"Retrying to request for Search Ad attribution details.");
[AppleSearchAd requestSearchAdAttributionDetailsWithTimeBetweenRetries:self.timeBetweenRetries numberOfRetries:self.numberOfRetries successBlock:self.successBlock failureBlock:self.failureBlock];
self.successBlock = nil;
self.failureBlock = nil;//so that obj does not retain success and failure blocks
}
@end
@implementation AppleSearchAd
NSString *const HasReceivedSearchAdAttributionKey = @"HasReceivedAppleSearchAdAttibution";
+ (NSString*) retrieveAppleSearchAd {
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
// If we haven't already registered HasReceivedSearchAdAttributionKey bool value to NSUserDefaults.
if ([userDefaults objectForKey:HasReceivedSearchAdAttributionKey] == nil) {
NSLog(@"AppleSearchAd-> retrieveAppleSearchAd");
[AppleSearchAd requestSearchAdAttributionDetailsWithTimeBetweenRetries:5 numberOfRetries:3 successBlock:^(NSDictionary *attributionDetails) {
NSLog(@"Successfully got attribution dictionary for search ads.");
NSDictionary *actualData = attributionDetails[@"Version3.1"];
int line = __LINE__;
NSMutableDictionary *d = nil;
NSError * err;
if(actualData) {
d = [NSMutableDictionary dictionaryWithDictionary:actualData];
d[@"Attribution Dictionary Version"] = @"3.1";
}
else {
NSLog(@"*****************************************");
NSLog(@"IMPORTANT:");
NSLog(@"APPLE CHANGED THE VERSION INFO DICTIONARY FOR SEARCH ADS");
NSLog(@"PLEASE TAKE A LOOK AT THIS CODE AND UPDATE IT");
NSLog(@"file:%s line:%d",__FILE__,line);
NSLog(@"*****************************************");
if(attributionDetails.count) {
NSString *firstKey = [[attributionDetails allKeys]firstObject];
if([actualData = [attributionDetails objectForKey:firstKey] isKindOfClass:[NSDictionary class]]){
d = [NSMutableDictionary dictionaryWithDictionary:actualData];
d[@"Attribution Dictionary Version"] = firstKey;
d[@"iad-message"] = @"iad Success";
}
else {
d = [NSMutableDictionary dictionary];
d[@"Attribution Dictionary Version"] = @"Unknown";
d[@"iad-message"] = @"Incorrect iad Version info {3.1} - Update it!";
for(NSString *key in attributionDetails) {
[d setObject:[attributionDetails[key]description] forKey:key];
}
}
}
}
d[@"App Id"] = [[NSBundle mainBundle]bundleIdentifier];
// Register HasReceivedSearchAdAttributionKey bool value so that we don't make more than one Search Ad attribution request per lifetime of app install.
[userDefaults setValue:[NSNumber numberWithBool:YES] forKey:HasReceivedSearchAdAttributionKey];
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:d options:0 error:&err];
return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
} failureBlock:^{
NSMutableDictionary *d = nil;
NSError * err;
d = [NSMutableDictionary dictionary];
d[@"Attribution Dictionary Version"] = @"Unknown";
d[@"App Id"] = [[NSBundle mainBundle]bundleIdentifier];
d[@"iad-message"] = @"Failed to get attribution dictionary for search ads [Maybe asked too many times?]";
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:d options:0 error:&err];
NSLog(@"Failed to get attribution dictionary for search ads [Maybe asked too many times?]");
return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}];
NSMutableDictionary *d = nil;
NSError * err;
d = [NSMutableDictionary dictionary];
d[@"Attribution Dictionary Version"] = @"Unknown";
d[@"App Id"] = [[NSBundle mainBundle]bundleIdentifier];
d[@"iad-message"] = @"ADClient not available - Search ads logic skipped";
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:d options:0 error:&err];
NSLog(@"ADClient not available - Search ads logic skipped");
return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
} else {
NSMutableDictionary *d = nil;
NSError * err;
d = [NSMutableDictionary dictionary];
d[@"Attribution Dictionary Version"] = @"Unknown";
d[@"App Id"] = [[NSBundle mainBundle]bundleIdentifier];
d[@"iad-message"] = @"Search ads already requested";
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:d options:0 error:&err];
NSLog(@"Search ads already requested");
return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}
}
+ (void)requestSearchAdAttributionDetailsWithTimeBetweenRetries:(NSTimeInterval)timeBetweenRetries numberOfRetries:(NSInteger)numberOfRetries successBlock:(NSString*(^)(NSDictionary *))successBlock failureBlock:(NSString*(^)(void))failureBlock {
NSLog(@"AppleSearchAd-> requestSearchAdAttributionDetailsWithTimeBetweenRetries");
Class adClient = NSClassFromString(@"ADClient");
if(adClient != nil) {
if ([[ADClient sharedClient] respondsToSelector:@selector(requestAttributionDetailsWithBlock:)]) {
[[ADClient sharedClient] requestAttributionDetailsWithBlock:^(NSDictionary *attributionDetails, NSError *error) {
if (error) {
switch(error.code) {
case ADClientErrorUnknown:
{
dispatch_async(dispatch_get_main_queue(), ^{
if(numberOfRetries > 0) {
AppleSearchAdTimerHandler *obj = [[AppleSearchAdTimerHandler alloc]init];
obj.numberOfRetries = numberOfRetries - 1;
obj.timeBetweenRetries = timeBetweenRetries;
obj.failureBlock = failureBlock;
obj.successBlock = successBlock;
[NSTimer scheduledTimerWithTimeInterval:timeBetweenRetries target:obj selector:@selector(retryTimer:) userInfo:nil repeats:NO];
obj = nil;//so that the block does not retain obj
}
else {
dispatch_async(dispatch_get_main_queue(), ^{
failureBlock();
});
}
});
}
break;
case ADClientErrorLimitAdTracking:
dispatch_async(dispatch_get_main_queue(), ^{
failureBlock();
});
break;
}
}
else {
dispatch_async(dispatch_get_main_queue(), ^{
successBlock(attributionDetails);
});
}
}];
}
else {
NSLog(@"iOS 10 ADClient call does not exist");
}
}
else {
NSLog(@"ADClient Not Available");
}
}
@end
@implementation AppleSearchAdRetriever
extern "C" {
char* _RetrieveAppleSearchAd() {
NSString* appleSearchAdOutput = [AppleSearchAd retrieveAppleSearchAd];
char* res = (char*)malloc(strlen([appleSearchAdOutput UTF8String]) + 1);
strcpy(res, [appleSearchAdOutput UTF8String]);
return res;
}
}
@end
All I get is always: “ADClient not available - Search ads logic skipped”
I also added the AdSupport.framework and Foundation.framework both in Unity (selecting the files in the Plugins folder) and as a PostBuildProcessor (together with iAd.framework):
var target = proj.TargetGuidByName("Unity-iPhone");
proj.AddBuildProperty(target, "GCC_ENABLE_OBJC_EXCEPTIONS", "YES");
proj.AddBuildProperty(target, "CLANG_ENABLE_MODULES", "YES");
proj.AddFrameworkToProject(target, "Metal.framework", false);
proj.AddFrameworkToProject(target, "CoreImage.framework", false);
Debug.Log("[AdTrace]: Adding AdSupport.framework to Xcode project.");
proj.AddFrameworkToProject(target, "AdSupport.framework", true);
Debug.Log("[AdTrace]: AdSupport.framework added successfully.");
// adding iAd framework for Apple Source Attribution API
Debug.Log("[AdTrace]: Adding iAd.framework to Xcode project.");
proj.AddFrameworkToProject(target, "iAd.framework", true);
Debug.Log("[AdTrace]: iAd.framework added successfully.");
Debug.Log("[AdTrace]: Adding Foundation.framework to Xcode project.");
proj.AddFrameworkToProject(target, "Foundation.framework", false);
Debug.Log("[AdTrace]: Foundation.framework added successfully.");
Conclusions:
I am capable of executing the code and read out the created dictionary, which means that the plugin itself works.
No crash is recorded (it did crash before when I had an actual syntax error in it) and everything keeps working normally.
The only thing that happens is that I am never able to actually see a success or failure result.
Only skipped.
Why would that be?
Thanks in advance!
