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 or Optimizely.
More specifically, I want to understand the following:
1) How does the client code (iOS) send the view hierarchy to the web-server in such a way that when you choose any UI element on the web dashboard it immediately shown on the iOS client?
I saw FLEX for example, and how it manage to get the view hierarchy, but I don't understand how the iphone client "knows" which view is picked in the web dashboard.
2) Moreover, in Apptimize I can choose any UI element from the web dashboard, change its text or color and it will immediately change in the app. Not only that, without adding any code, just by having the SDK.
The changes I make (text, background color, etc) will remain for all the future sessions of the app. How can this be implemented?
I'm guessing they are using some sort of reflection, but how can they get it to work for all users and for all future sessions? how does the client code find the right UI element? and how does it work on UITableViewCell?
3) Is it possible to detect every time a UIViewController is loaded? i.e. get a callback on each viewDidLoad? if so, how?
See some screenshots below:
My name is Baraa and I'm a Software Engineering Intern working on the mobile team at Optimizely, so I can share some high-level insight into how the Optimizely SDK works on both Android and iOS.
On iOS, the Optimizely SDK uses a technique called swizzling. This allows us to apply visual changes to the application based on whatever experiments are currently active in our data file.
On Android, Optimizely uses reflection to attach the SDK as a listener for interaction and lifecycle events to apply visual changes to the application based on whatever experiments are active in the data file.
For the full list of methods that we swizzle on iOS and listeners that we intercept on Android, please check out this help article: https://help.optimizely.com/hc/en-us/articles/205014107-How-Optimizely-s-SDKs-Work-SDK-Order-of-execution-experiment-activation-and-goals#execute
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> aspectInfo, BOOL animated) {
NSLog(#"View Controller %# will appear animated: %tu", aspectInfo.instance, animated);
} error:NULL];
This method call
+ (id<AspectToken>)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/<r>/<g>/<b>/
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 <Aspects/Aspects.h>
#import <OCFWebServer/OCFWebServer.h>
#import <OCFWebServer/OCFWebServerRequest.h>
#import <OCFWebServer/OCFWebServerResponse.h>
#import <objc/runtime.h>
#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> 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> 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
The company Leanplum offers a Visual Interface Editor for iOS and Android: This requires no coding, and Leanplum will automatically detect the elements and allow you to change them. No engineers or app store resubmissions required.
Regarding your questions:
With installing the iOS or Android SDK in your app, you enable a feature called Visual Editor. While in development mode and with the Website Dashboard open, the SDK sends information about the view hierarchy in real-time to your Browser. The view hierarchy is scanned in a similar way a DOM is built on a regular website.
You can choose any UI element on your app and change the appearance of it in real-time. This works by identifying the exact element in the view tree and sending the changes to the SDK.
This can be achieved by adding custom hooks or a technique called "swizzling". Take a look at this blog post, how it works.
To learn more about the Leanplum Visual Interface Editor, check out leanplum.com. They offer a free 30-day trial.
(Disclaimer: I am an Engineer at Leanplum.)
Related
I am having a hard time as a beginner to Objective-C with learning how and when a function is being called, as I am not seeing it explicitly stated. Below is some code for logging into, and playing a song from the Spotify SDK that I found online.
#import "AppDelegate.h"
#interface AppDelegate ()
#property (nonatomic, strong) SPTAuth *auth;
#property (nonatomic, strong) SPTAudioStreamingController *player;
#property (nonatomic, strong) UIViewController *authViewController;
#end
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.auth = [SPTAuth defaultInstance];
self.player = [SPTAudioStreamingController sharedInstance];
// The client ID you got from the developer site
self.auth.clientID = #"5bd669abf2a14fb59839c2c0570843fe";
// The redirect URL as you entered it at the developer site
self.auth.redirectURL = [NSURL URLWithString:#"spotlightmusic://returnafterlogin"];
// Setting the `sessionUserDefaultsKey` enables SPTAuth to automatically store the session object for future use.
self.auth.sessionUserDefaultsKey = #"current session";
// Set the scopes you need the user to authorize. `SPTAuthStreamingScope` is required for playing audio.
self.auth.requestedScopes = #[SPTAuthStreamingScope];
// Become the streaming controller delegate
self.player.delegate = self;
// Start up the streaming controller.
NSError *audioStreamingInitError;
NSAssert([self.player startWithClientId:self.auth.clientID error:&audioStreamingInitError],
#"There was a problem starting the Spotify SDK: %#", audioStreamingInitError.description);
// Start authenticating when the app is finished launching
dispatch_async(dispatch_get_main_queue(), ^{
[self startAuthenticationFlow];
});
return YES;
}
- (void)startAuthenticationFlow
{
// Check if we could use the access token we already have
if ([self.auth.session isValid]) {
// Use it to log in
[self.player loginWithAccessToken:self.auth.session.accessToken];
} else {
// Get the URL to the Spotify authorization portal
NSURL *authURL = [self.auth spotifyWebAuthenticationURL];
// Present in a SafariViewController
self.authViewController = [[SFSafariViewController alloc] initWithURL:authURL];
[self.window.rootViewController presentViewController:self.authViewController animated:YES completion:nil];
}
}
- (BOOL)application:(UIApplication *)app
openURL:(NSURL *)url
options:(NSDictionary *)options
{
// If the incoming url is what we expect we handle it
if ([self.auth canHandleURL:url]) {
// Close the authentication window
[self.authViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil];
self.authViewController = nil;
// Parse the incoming url to a session object
[self.auth handleAuthCallbackWithTriggeredAuthURL:url callback:^(NSError *error, SPTSession *session) {
if (session) {
// login to the player
[self.player loginWithAccessToken:self.auth.session.accessToken];
}
}];
return YES;
}
return NO;
}
- (void)audioStreamingDidLogin:(SPTAudioStreamingController *)audioStreaming
{
[self.player playSpotifyURI:#"spotify:track:3DWOTqMQGp5q75fnVsWwaN" startingWithIndex:0 startingWithPosition:0 callback:^(NSError *error) {
if (error != nil) {
NSLog(#"*** failed to play: %#", error);
return;
}
}];
}
#end
I am wondering how exactly these functions are being called sequentially, and specifically how the audioStreamingDidLogin one is being run.
Additionally I was wondering how it would look to call that function from the view controller with some sort of input coming from the UI.
Any help with this logic would be greatly appreciated! Thanks.
Your question is closely tied to the Spotify framework being used. It is not a question of when Objective-C is executing something - the language has a standard sequential execution model - but how the framework is doing callbacks, e.g. audioStreamingDidLogin, to your code and utilising threads/GCD to do concurrent execution.
First you should read the Spotify framework documentation.
You can also place a breakpoint at the start of each method and then run under the debugger. When a breakpoint is hit check which thread has stopped and the stack trace. That should give you a good idea of execution flow and the concurrent threads being used.
HTH
UIApplicationDelegate method application:didFinishLaunchingWithOptions: is called first, followed by application:openURL:options:.
That first app delegate method sets self as the delegate for an AudioStreamingController. This is how audioStreamingDidLogin gets called. You're telling the streaming controller, "Tell me (self) when interesting things happen". (See the SPTAudioStreamingControllerDelegate docs for what else it might tell you about).
You probably wouldn't (shouldn't) call this function directly, especially if there's a chance that you might call it before auth is complete. Doing so would likely result in an error on the call to playSpotifyURI. If you're certain that the user is authenticated, then you don't need to call it. Just call what it calls: playSpotifyURI.
I love blocks and it makes me sad when I can't use them. In particular, this happens mostly every time I use delegates (e.g.: with UIKit classes, mostly pre-block functionality).
So I wonder... Is it possible -using the crazy power of ObjC-, to do something like this?
// id _delegate; // Most likely declared as class variable or it will be released
_delegate = [DelegateFactory delegateOfProtocol:#protocol(SomeProtocol)];
_delegate performBlock:^{
// Do something
} onSelector:#selector(someProtocolMethod)]; // would execute the given block when the given selector is called on the dynamic delegate object.
theObject.delegate = (id<SomeProtocol>)_delegate;
// Profit!
performBlock:onSelector:
If YES, how? And is there a reason why we shouldn't be doing this as much as possible?
Edit
Looks like it IS possible. Current answers focus on the first part of the question, which is how. But it'd be nice to have some discussion on the "should we do it" part.
Okay, I finally got around to putting WoolDelegate up on GitHub. Now it should only take me another month to write a proper README (although I guess this is a good start).
The delegate class itself is pretty straightforward. It simply maintains a dictionary mapping SELs to Block. When an instance recieves a message to which it doesn't respond, it ends up in forwardInvocation: and looks in the dictionary for the selector:
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = [anInvocation selector];
GenericBlock handler = [self handlerForSelector:sel];
If it's found, the Block's invocation function pointer is pulled out and passed along to the juicy bits:
IMP handlerIMP = BlockIMP(handler);
[anInvocation Wool_invokeUsingIMP:handlerIMP];
}
(The BlockIMP() function, along with other Block-probing code, is thanks to Mike Ash. Actually, a lot of this project is built on stuff I learned from his Friday Q&A's. If you haven't read those essays, you're missing out.)
I should note that this goes through the full method resolution machinery every time a particular message is sent; there's a speed hit there. The alternative is the path that Erik H. and EMKPantry each took, which is creating a new clas for each delegate object that you need, and using class_addMethod(). Since every instance of WoolDelegate has its own dictionary of handlers, we don't need to do that, but on the other hand there's no way to "cache" the lookup or the invocation. A method can only be added to a class, not to an instance.
I did it this way for two reasons: this was an excercise to see if I could work out the part that's coming next -- the hand-off from NSInvocation to Block invocation -- and the creation of a new class for every needed instance simply seemed inelegant to me. Whether it's less elegant than my solution, I will leave to each reader's judgement.
Moving on, the meat of this procedure is actually in the NSInvocation category that's found in the project. This utilizes libffi to call a function that's unknown until runtime -- the Block's invocation -- with arguments that are also unknown until runtime (which are accessible via the NSInvocation). Normally, this is not possible, for the same reason that a va_list cannot be passed on: the compiler has to know how many arguments there are and how big they are. libffi contains assembler for each platform that knows/is based on those platforms' calling conventions.
There's three steps here: libffi needs a list of the types of the arguments to the function that's being called; it needs the argument values themselves put into a particular format; then the function (the Block's invocation pointer) needs to be invoked via libffi and the return value put back into the NSInvocation.
The real work for the first part is handled largely by a function which is again written by Mike Ash, called from Wool_buildFFIArgTypeList. libffi has internal structs that it uses to describe the types of function arguments. When preparing a call to a function, the library needs a list of pointers to these structures. The NSMethodSignature for the NSInvocation allows access of each argument's encoding string; translating from there to the correct ffi_type is handled by a set of if/else lookups:
arg_types[i] = libffi_type_for_objc_encoding([sig getArgumentTypeAtIndex:actual_arg_idx]);
...
if(str[0] == #encode(type)[0]) \
{ \
if(sizeof(type) == 1) \
return &ffi_type_sint8; \
else if(sizeof(type) == 2) \
return &ffi_type_sint16; \
Next, libffi wants pointers to the argument values themselves. This is done in Wool_buildArgValList: get the size of each argument, again from the NSMethodSignature, and allocate a chunk of memory that size, then return the list:
NSUInteger arg_size;
NSGetSizeAndAlignment([sig getArgumentTypeAtIndex:actual_arg_idx],
&arg_size,
NULL);
/* Get a piece of memory that size and put its address in the list. */
arg_list[i] = [self Wool_allocate:arg_size];
/* Put the value into the allocated spot. */
[self getArgument:arg_list[i] atIndex:actual_arg_idx];
(An aside: there's several notes in the code about skipping over the SEL, which is the (hidden) second passed argument to any method invocation. The Block's invocation pointer doesn't have a slot to hold the SEL; it just has itself as the first argument, and the rest are the "normal" arguments. Since the Block, as written in client code, could never access that argument anyways (it doesn't exist at the time), I decided to ignore it.)
libffi now needs to do some "prep"; as long as that succeeds (and space for the return value can be allocated), the invocation function pointer can now be "called", and the return value can be set:
ffi_call(&inv_cif, (genericfunc)theIMP, ret_val, arg_vals);
if( ret_val ){
[self setReturnValue:ret_val];
free(ret_val);
}
There's some demonstrations of the functionality in main.m in the project.
Finally, as for your question of "should this be done?", I think the answer is "yes, as long as it makes you more productive". WoolDelegate is completely generic, and an instance can act like any fully written-out class. My intention for it, though, was to make simple, one-off delegates -- that only need one or two methods, and don't need to live past their delegators -- less work than writing a whole new class, and more legible/maintainable than sticking some delegate methods into a view controller because it's the easiest place to put them. Taking advantage of the runtime and the language's dynamism like this hopefully can increase your code's readability, in the same way, e.g., Block-based NSNotification handlers do.
I just put together a little project that lets you do just this...
#interface EJHDelegateObject : NSObject
+ (id)delegateObjectForProtocol:(Protocol*) protocol;
#property (nonatomic, strong) Protocol *protocol;
- (void)addImplementation:(id)blockImplementation forSelector:(SEL)selector;
#end
#implementation EJHDelegateObject
static NSInteger counter;
+ (id)delegateObjectForProtocol:(Protocol *)protocol
{
NSString *className = [NSString stringWithFormat:#"%s%#%i",protocol_getName(protocol),#"_EJH_implementation_", counter++];
Class protocolClass = objc_allocateClassPair([EJHDelegateObject class], [className cStringUsingEncoding:NSUTF8StringEncoding], 0);
class_addProtocol(protocolClass, protocol);
objc_registerClassPair(protocolClass);
EJHDelegateObject *object = [[protocolClass alloc] init];
object.protocol = protocol;
return object;
}
- (void)addImplementation:(id)blockImplementation forSelector:(SEL)selector
{
unsigned int outCount;
struct objc_method_description *methodDescriptions = protocol_copyMethodDescriptionList(self.protocol, NO, YES, &outCount);
struct objc_method_description description;
BOOL descriptionFound = NO;
for (int i = 0; i < outCount; i++){
description = methodDescriptions[i];
if (description.name == selector){
descriptionFound = YES;
break;
}
}
if (descriptionFound){
class_addMethod([self class], selector, imp_implementationWithBlock(blockImplementation), description.types);
}
}
#end
And using an EJHDelegateObject:
self.alertViewDelegate = [EJHDelegateObject delegateObjectForProtocol:#protocol(UIAlertViewDelegate)];
[self.alertViewDelegate addImplementation:^(id _self, UIAlertView* alertView, NSInteger buttonIndex){
NSLog(#"%# dismissed with index %i", alertView, buttonIndex);
} forSelector:#selector(alertView:didDismissWithButtonIndex:)];
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:#"Example" message:#"My delegate is an EJHDelegateObject" delegate:self.alertViewDelegate cancelButtonTitle:#"Cancel" otherButtonTitles:#"OK", nil];
[alertView show];
Edit: This is what I've come up after having understood your requirement. This is just a quick hack, an idea to get you started, it's not properly implemented, nor is it tested. It is supposed to work for delegate methods that take the sender as their only argument. It works It is supposed to work with normal and struct-returning delegate methods.
typedef void *(^UBDCallback)(id);
typedef void(^UBDCallbackStret)(void *, id);
void *UBDDelegateMethod(UniversalBlockDelegate *self, SEL _cmd, id sender)
{
UBDCallback cb = [self blockForSelector:_cmd];
return cb(sender);
}
void UBDelegateMethodStret(void *retadrr, UniversalBlockDelegate *self, SEL _cmd, id sender)
{
UBDCallbackStret cb = [self blockForSelector:_cmd];
cb(retaddr, sender);
}
#interface UniversalBlockDelegate: NSObject
- (BOOL)addDelegateSelector:(SEL)sel isStret:(BOOL)stret methodSignature:(const char *)mSig block:(id)block;
#end
#implementation UniversalBlockDelegate {
SEL selectors[128];
id blocks[128];
int count;
}
- (id)blockForSelector:(SEL)sel
{
int idx = -1;
for (int i = 0; i < count; i++) {
if (selectors[i] == sel) {
return blocks[i];
}
}
return nil;
}
- (void)dealloc
{
for (int i = 0; i < count; i++) {
[blocks[i] release];
}
[super dealloc];
}
- (BOOL)addDelegateSelector:(SEL)sel isStret:(BOOL)stret methodSignature:(const char *)mSig block:(id)block
{
if (count >= 128) return NO;
selectors[count] = sel;
blocks[count++] = [block copy];
class_addMethod(self.class, sel, (IMP)(stret ? UBDDelegateMethodStret : UBDDelegateMethod), mSig);
return YES;
}
#end
Usage:
UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectZero];
UniversalBlockDelegate *d = [[UniversalBlockDelegate alloc] init];
webView.delegate = d;
[d addDelegateSelector:#selector(webViewDidFinishLoading:) isStret:NO methodSignature:"v#:#" block:^(id webView) {
NSLog(#"Web View '%#' finished loading!", webView);
}];
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://google.com"]]];
I'm using Reachability in my iPad app and discovered some issues when using modalViewControllers.
In my mainViewController I have a BOOL variable determining weather I'm online or not. Here's my code:
// mainViewController.h
BOOL online;
// mainViewController.m
- (void)reachabilityChanged:(NSNotification *)note
{
if([[note object] isReachable]) {
online = YES;
}
else {
online = NO;
}
}
- (void)getOnline
{
NSLog(#"%d", online);
}
// modalViewController.m
#import "mainViewController.h"
- (IBAction)dismissMe
{
mainViewController *main = [[mainViewController alloc] init];
[main getOnline];
[self dismissModalViewControllerAnimated:YES];
}
When I'm calling [self getOnline] within the mainViewController, it returns 1 ('cause I am online).
But: when I'm calling [main getOnline] within the modalViewController, it returns 0 in the log.
Does anybody know why?!
I also tried to put the online variable as a #property into the modalViewController to handle the if online stuff within the modal. But when I assign a value to it (from the main), and log it within the modal, it always returns (NULL).
Hope, you can help me! With best regards, Julian
Short answer: because they use different instances of the online variable.
Long answer: you should only declare BOOL online in the header, not define it. Defining should happen in the .m file, like this:
In the mainViewController.h:
extern BOOL online; // Declare the variable
In the mainViewController.m:
BOOL online; // Define the variable
// the rest of your code
The way your code is written, a separate BOOL online is created for each .m file that includes mainViewController.h; I am sure this is not what you intended.
I'm writing an iPhone app, and I'm surprised that there seem to be no NSQueue or NSStack classes in Apple's Foundation Framework. I see that it would be quite easy to roll my own, starting with an NSMutableArray, so I'll do that unless I've missed something. Have I missed something?
Here's my Stack class, in case it's useful to those who come after me. As you can see, the pop method involves enough code that you'd want to factor it out.
Stack.h:
#import <Foundation/Foundation.h>
#interface Stack : NSObject {
NSMutableArray *contents;
}
- (void)push:(id)object;
- (id)pop;
#end
Stack.m
#import "Stack.h"
#implementation Stack
// superclass overrides
- (id)init {
if (self = [super init]) {
contents = [[NSMutableArray alloc] init];
}
return self;
}
- (void)dealloc {
[contents release];
[super dealloc];
}
// Stack methods
- (void)push:(id)object {
[contents addObject:object];
}
- (id)pop {
id returnObject = [[contents lastObject] retain];
if (returnObject) {
[contents removeLastObject];
}
return [returnObject autorelease];
}
#end
as far as I know there is no generic class avaialbe. Try using the NSMutableArray, add via addObject and get first/last via objectAtIndex and removeObjectAtIndex.
Another easy way would be to extend NSMutableArray's capabilities by making use of Objective C's categories. You can do that by adding two files to your project:
NSMutableArray+Stack.h
#interface NSMutableArray (StackExtension)
- (void)push:(id)object;
- (id)pop;
#end
NSMutableArray+Stack.m
#import "NSMutableArray+Stack.h"
#implementation NSMutableArray (StackExtension)
- (void)push:(id)object {
[self addObject:object];
}
- (id)pop {
id lastObject = [self lastObject];
[self removeLastObject];
return lastObject;
}
#end
Now you can use a regular NSMutableArray in every other file of your project like a stack and call push or pop on that object. Don't forget to #import NSMutableArray+Stack.h in those files. Here is some sample code how you can use your new NSMutableArray as a stack:
NSMutableArray *myStack = [[NSMutableArray alloc] init]; // stack size = 0
NSString *aString = #"hello world";
[myStack push:myString]; // stack size = 1
NSString *anotherString = #"hello universe";
[myStack push:anotherString]; // stack size = 2
NSString *topMostStackObject;
topMostStackObject = [myStack pop]; // stack size = 1
NSLog("%#",topMostStackObject);
topMostStackObject = [myStack pop]; // stack size = 0
NSLog("%#",topMostStackObject);
The log output will be:
hello universe
hello world
I'm a bit late to this party, but are you aware of CHDataStructures?
http://cocoaheads.byu.edu/code/CHDataStructures
I have put a working iOS Objective C queue object on GitHub. The code was taken from various posts and by no means is owned by me.
https://github.com/esromneb/ios-queue-object/
If you see any problems please fork, and make a pull request!
Yes, an NSMutableArray doubles as a stack or queue. (It would be slightly inefficient as a queue.)
You could also use C++'s stack and queue adapter, but it makes memory management a bit messy if you want to store Objective-C objects with it.
ObjectiveSugar is a very popular CocoaPod that provides, among a bunch of other great stuff, push and pop API calls on NSMutableArray. Sure, it's not in the iOS SDK, but I'm sharing it here because I was looking for the same thing, and this was the solution I went with (and it certainly didn't hurt that we were already using this CocoaPod in our codebase).
No. You missed nothing. That's all. Objective-C is higher level language look like C. Low level control is not required.
Cocoa classes are designed for easier use than efficiency. If you want to deal with performance, you have an option of raw C (or C++) implementation. Otherwise, just use easy way. Of course, early-optimization is evil.
If you want a kind of encapsulation, just make a new class which contains NSMutableArray within it. Hide inner NSMutableArray and just expose what you want. But you'll realize this is unnecessary.
I have noticed that some apps like Safari and Mail show a loading indicator in the status bar (the bar at the very top of the phone) when they are accessing the network. Is there a way to do the same thing in SDK apps, or is this an Apple only thing?
It's in UIApplication:
For Objective C:
Start:
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
End:
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
For swift :
Start
UIApplication.shared.isNetworkActivityIndicatorVisible = true
End
UIApplication.shared.isNetworkActivityIndicatorVisible = false
I've found the following macros pretty useful!
#define ShowNetworkActivityIndicator() [UIApplication sharedApplication].networkActivityIndicatorVisible = YES
#define HideNetworkActivityIndicator() [UIApplication sharedApplication].networkActivityIndicatorVisible = NO
So you can just call ShowNetworkActivityIndicator(); or HideNetworkActivityIndicator(); from within your app (as long as the header is included of course!).
I wrote a singleton that solves the problem of multiple connections by keeping a counter of what is happening (to avoid removing the status when a connection returns but another one is still active):
The header file:
#import <Foundation/Foundation.h>
#interface RMActivityIndicator : NSObject
-(void)increaseActivity;
-(void)decreaseActivity;
-(void)noActivity;
+(RMActivityIndicator *)sharedManager;
#end
and implementation:
#import "RMActivityIndicator.h"
#interface RMActivityIndicator ()
#property(nonatomic,assign) unsigned int activityCounter;
#end
#implementation RMActivityIndicator
- (id)init
{
self = [super init];
if (self) {
self.activityCounter = 0;
}
return self;
}
-(void)increaseActivity{
#synchronized(self) {
self.activityCounter++;
}
[self updateActivity];
}
-(void)decreaseActivity{
#synchronized(self) {
if (self.activityCounter>0) self.activityCounter--;
}
[self updateActivity];
}
-(void)noActivity{
self.activityCounter = 0;
[self updateActivity];
}
-(void)updateActivity{
UIApplication* app = [UIApplication sharedApplication];
app.networkActivityIndicatorVisible = (self.activityCounter>0);
}
#pragma mark -
#pragma mark Singleton instance
+(RMActivityIndicator *)sharedManager {
static dispatch_once_t pred;
static RMActivityIndicator *shared = nil;
dispatch_once(&pred, ^{
shared = [[RMActivityIndicator alloc] init];
});
return shared;
}
#end
Example:
[[RMActivityIndicator sharedManager]increaseActivity];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:self.networkReceiveProcessQueue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
[[RMActivityIndicator sharedManager]decreaseActivity];
}
A single line code to do that:
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
The status bar network activity indicator was deprecated in iOS 13.
Using UIApplication.shared.isNetworkActivityIndicatorVisible = true will not work anymore.
The deprecation message says:
Provide a custom network activity UI in your app if desired.
You need to take care of hiding the activity indicator also once your network call is done.
If you use AFNetworking, then you don't need to do much.
Do following changes in AppDelegate Class:
Import AFNetworking/AFNetworkActivityIndicatorManager.h
Put this in didFinishLaunchingWithOptions:
[[AFNetworkActivityIndicatorManager sharedManager] setEnabled:YES]
It might also be helpful to make sure you are running it on the main thread as it is UI related.
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES];
});
As many have said, there is no network activity indicator for the iPhone X and probably for the other new iPhones with the notch.
I came across this incredible library written by Ortwin Gentz, FutureTap:
https://github.com/futuretap/FTLinearActivityIndicator
It puts the indicator right back where it was when the iPhone X was initially released, many would remember the Knight Rider type of indicator.
This library is available for Swift 4.2, so you will need to change the Swift Language settings, as described here:
Type 'NSAttributedStringKey' (aka 'NSString') has no member 'font'