I\'m trying to figure out a few things about the implementation going on \"behind the scene\" for manipulating UI elements on the fly, straight from the web console on Apptimize
I wonder the same and couldn't find a definite answer, so here is my (hopefully) educated guess:
Thanks to the runtime environment it is actually not that hard to use Aspect-Orientated-Programming (AOP) in Cocoa(-Touch), in which rules are written to hook in in other classes method calls.
If you google for AOP
and Objective-C
, several libraries pop up that wrap the runtime code nicely.
For example steinpete's Aspect library:
[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id aspectInfo, BOOL animated) {
NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated);
} error:NULL];
This method call
+ (id)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error {
return aspect_add((id)self, selector, options, block, error);
}
calls aspect_add()
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
NSCParameterAssert(self);
NSCParameterAssert(selector);
NSCParameterAssert(block);
__block AspectIdentifier *identifier = nil;
aspect_performLocked(^{
if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
if (identifier) {
[aspectContainer addAspect:identifier withOptions:options];
// Modify the class to allow message interception.
aspect_prepareClassAndHookSelector(self, selector, error);
}
}
});
return identifier;
}
which again calls several other quite frightening looking functions that do the heavy lifting in the runtime
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
NSCParameterAssert(selector);
Class klass = aspect_hookClass(self, error);
Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);
if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
// Make a method alias for the existing method implementation, it not already copied.
const char *typeEncoding = method_getTypeEncoding(targetMethod);
SEL aliasSelector = aspect_aliasForSelector(selector);
if (![klass instancesRespondToSelector:aliasSelector]) {
__unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass);
}
// We use forwardInvocation to hook in.
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
}
}
including method-swizzling.
It is easy to see that here we have a tool that will allow us to send the current state of an app to re-build this in a web-page but also to manipulate objects in an existing code.
Of course this is just a starting point. You will need a web service that assembles the app and sends it to the users.
Personally I never used AOP for such a complex task, but I used it for teaching all view controllers tracking capabilities
- (void)setupViewControllerTracking
{
NSError *error;
@weakify(self);
[UIViewController aspect_hookSelector:@selector(viewDidAppear:)
withOptions:AspectPositionAfter
usingBlock:^(id < AspectInfo > aspectInfo) {
@strongify(self);
UIViewController *viewController = [aspectInfo instance];
NSArray *breadCrumbs = [self breadCrumbsForViewController:viewController];
if (breadCrumbs.count) {
NSString *pageName = [NSString stringWithFormat:@"/%@", [breadCrumbs componentsJoinedByString:@"/"]];
[ARAnalytics pageView:pageName];
}
} error:&error];
}
update
I played a bit and was able to create a prototype. If added to a project, it will changes all view controllers background color to blue and after 5 seconds all living view controllers background to orange, by using AOP and dynamic method adding.
source code: https://gist.github.com/vikingosegundo/0e4b30901b9498ae4b7b
The 5 seconds are triggered by a notification, but it is obvious that this could be a network event.
update 2
I taught my prototype to open a network interface and accept rgb values for the background.
Running in simulator this would be
http://127.0.0.1:8080/color////
http://127.0.0.1:8080/color/50/120/220/
I use OCFWebServer for that
//
// ABController.m
// ABTestPrototype
//
// Created by Manuel Meyer on 12.05.15.
// Copyright (c) 2015 Manuel Meyer. All rights reserved.
//
#import "ABController.h"
#import
#import
#import
#import
#import
#import "UIViewController+Updating.h"
#import "UIView+ABTesting.h"
@import UIKit;
@interface ABController ()
@property (nonatomic, strong) OCFWebServer *webserver;
@end
@implementation ABController
void _ab_register_ab_notificaction(id self, SEL _cmd)
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:NSSelectorFromString(@"ab_notifaction:") name:@"ABTestUpdate" object:nil];
}
void _ab_notificaction(id self, SEL _cmd, id userObj)
{
NSLog(@"UPDATE %@", self);
}
+(instancetype)sharedABController{
static dispatch_once_t onceToken;
static ABController *abController;
dispatch_once(&onceToken, ^{
OCFWebServer *server = [OCFWebServer new];
[server addDefaultHandlerForMethod:@"GET"
requestClass:[OCFWebServerRequest class]
processBlock:^void(OCFWebServerRequest *request) {
OCFWebServerResponse *response = [OCFWebServerDataResponse responseWithText:[[[UIApplication sharedApplication] keyWindow] listOfSubviews]];
[request respondWith:response];
}];
[server addHandlerForMethod:@"GET"
pathRegex:@"/color/[0-9]{1,3}/[0-9]{1,3}/[0-9]{1,3}/"
requestClass:[OCFWebServerRequest class]
processBlock:^(OCFWebServerRequest *request) {
NSArray *comps = request.URL.pathComponents;
UIColor *c = [UIColor colorWithRed:^{ NSString *r = comps[2]; return [r integerValue] / 255.0;}()
green:^{ NSString *g = comps[3]; return [g integerValue] / 255.0;}()
blue:^{ NSString *b = comps[4]; return [b integerValue] / 255.0;}()
alpha:1.0];
[[NSNotificationCenter defaultCenter] postNotificationName:@"ABTestUpdate" object:c];
OCFWebServerResponse *response = [OCFWebServerDataResponse responseWithText:[[[UIApplication sharedApplication] keyWindow] listOfSubviews]];
[request respondWith:response];
}];
dispatch_async(dispatch_queue_create(".", 0), ^{
[server runWithPort:8080];
});
abController = [[ABController alloc] initWithWebServer:server];
});
return abController;
}
-(instancetype)initWithWebServer:(OCFWebServer *)webserver
{
self = [super init];
if (self) {
self.webserver = webserver;
}
return self;
}
+(void)load
{
class_addMethod([UIViewController class], NSSelectorFromString(@"ab_notifaction:"), (IMP)_ab_notificaction, "v@:@");
class_addMethod([UIViewController class], NSSelectorFromString(@"ab_register_ab_notificaction"), (IMP)_ab_register_ab_notificaction, "v@:");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.00001 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self sharedABController];
});
[UIViewController aspect_hookSelector:@selector(viewDidLoad)
withOptions:AspectPositionAfter
usingBlock:^(id aspectInfo) {
dispatch_async(dispatch_get_main_queue(),
^{
UIViewController *vc = aspectInfo.instance;
SEL selector = NSSelectorFromString(@"ab_register_ab_notificaction");
IMP imp = [vc methodForSelector:selector];
void (*func)(id, SEL) = (void *)imp;func(vc, selector);
});
} error:NULL];
[UIViewController aspect_hookSelector:NSSelectorFromString(@"ab_notifaction:")
withOptions:AspectPositionAfter
usingBlock:^(id aspectInfo, NSNotification *noti) {
dispatch_async(dispatch_get_main_queue(),
^{
UIViewController *vc = aspectInfo.instance;
[vc updateViewWithAttributes:@{@"backgroundColor": noti.object}];
});
} error:NULL];
}
@end
//
// UIViewController+Updating.m
// ABTestPrototype
//
// Created by Manuel Meyer on 12.05.15.
// Copyright (c) 2015 Manuel Meyer. All rights reserved.
//
#import "UIViewController+Updating.h"
@implementation UIViewController (Updating)
-(void)updateViewWithAttributes:(NSDictionary *)attributes
{
[[attributes allKeys] enumerateObjectsUsingBlock:^(NSString *obj, NSUInteger idx, BOOL *stop) {
if ([obj isEqualToString:@"backgroundColor"]) {
[self.view setBackgroundColor:attributes[obj]];
}
}];
}
@end
the full code: https://github.com/vikingosegundo/ABTestPrototype