I'm setting up a simple WKNavigationDelegate in my dynamic framework to get WKWebView's default user agent string:
#interface MyDelegate: NSObject <WKNavigationDelegate>
#end
static NSString *_defaultUserAgent;
static WKWebView *_defaultWebView;
static MyDelegate *_myDelegate;
#implementation MyDelegate
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation {
DispatchHelper.runOnMain = ^{
[_defaultWebView evaluateJavaScript:#"navigator.userAgent" completionHandler:^(id __nullable userAgent, NSError * __nullable error) {
_defaultUserAgent = userAgent;
_defaultWebView = nil;
_myDelegate = nil;
}];
};
}
#end
#implementation WKWebView (Util)
+ (void)load {
_myDelegate = MyDelegate.new;
WKWebView *wkWebView = WKWebView.new;
wkWebView.navigationDelegate = _myDelegate;
[wkWebView loadHTMLString:#"<HTML><BODY>TEST</BODY></HTML>" baseURL:nil];
_defaultWebView = wkWebView;
}
#end
Is this safe, or is +load too early to try something like this? In my testing I haven't noticed any issues with it, but after reading this Mike Ash blog, he says using +load is dangerous/tricky.
Specifically from the blog:
Keep in mind that there's no autorelease pool present at loading time
(usually) so you'll need to wrap your code in one if you're calling
into Objective-C stuff.
Am I at risk here by not using #autoreleasepool? I'm confused on how adding
+ (void)load {
#autoreleasepool {
_myDelegate = MyDelegate.new;
WKWebView *wkWebView = WKWebView.new;
wkWebView.navigationDelegate = _myDelegate;
[wkWebView loadHTMLString:#"<HTML><BODY>TEST</BODY></HTML>" baseURL:nil];
_defaultWebView = wkWebView;
}
}
helps me here.
+load is rife with danger and fragility. I have had to track down many a fun bug due to +load surprises over the decades. In general, it should be avoided. And when used, it should touch a minimal amount of the rest of the system specifically because you'll be changing the order within which things are initialized at runtime.
I would recommend that you have some kind of initialization hook in your framework that your framework's clients are expected to call in the app, typically during the app delegate's didFinishLaunching:.... method.
You can put in assert()s along other code paths that can warn or raise if the framework was not properly initialized.
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 want to create a float window above the main window, so code like blow.
I use a fake singleton pattern to manage a UIWindow. Because I don't want other object retain the window. The code:
#class testWindow;
static testWindow *singletonSelf;
static dispatch_once_t once;
#implementation testWindow
+ (testWindow *)sharedWindow {
dispatch_once(&(once), ^{
singletonSelf = [[testWindow alloc] initWithFrame:CGRectMake(200,200,200,200)];
});
return singletonSelf;
}
+ (void)dismiss {
singletonSelf = nil;
once = 0;
}
...
- (void)closeButtonClicked {
self.hidden = YES;
[testWindow dismiss];
}
There is a button on the window. The method '-closeButtonClicked' will called when the button clicked. When -closeButtonClicked called, I will set the Window hidden property to YES and set singletonSelf,once to nil for the future to create a new window.
But when I did this, the window did not dealloc immediately. The windows property of the [UIApplication sharedApplication] even contain the window I want to release.
Until I tap the screen(any where),the windows property of the [UIApplication sharedApplication] remove the window,than the window will released.
I want to remove the window from windows immediately when I call the +dismiss method.
THX for help.
This is my first question on StackOverFlow~~~
Ignore my poor english~
I know the fake singleton is now suggest, but I this is a good choice on this situation. If u have other choice please tell share with me~
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.)
I have a problem with my network activity indicator in that sometimes it will continue to be displayed when it should not be.
I wrote my own manager for it and swapped it out for one that uses an NSAssert statement like this...
- (void)setNetworkActivityIndicatorVisible:(BOOL)setVisible {
static NSInteger NumberOfCallsToSetVisible = 0;
if (setVisible)
NumberOfCallsToSetVisible++;
else
NumberOfCallsToSetVisible--;
// The assertion helps to find programmer errors in activity indicator management.
// Since a negative NumberOfCallsToSetVisible is not a fatal error,
// it should probably be removed from production code.
NSAssert(NumberOfCallsToSetVisible >= 0, #"Network Activity Indicator was asked to hide more often than shown");
// Display the indicator as long as our static counter is > 0.
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:(NumberOfCallsToSetVisible > 0)];
}
I found it on SO and it has immediately pointed out that something is going wrong with my use of this function.
All of my network activity is exclusively run through a single NSOperationQueue which is managed by a singleton class. Every operation is a subclass of NSOperation (actually a subclass of a TemplateOperation which is a subclass of NSOperation).
Anyway, all the downloads and uploads are working fine and I'm doing them all like this...
- (void)sendRequest:(NSURLRequest *)request
{
NSError *error = nil;
NSURLResponse *response = nil;
[[NetworkManager sharedInstance] setNetworkActivityIndicatorVisible:YES];
NSData *data = [NSURLConnection sendSynchronousRequest:request
returningResponse:&response
error:&error];
[[NetworkManager sharedInstance] setNetworkActivityIndicatorVisible:NO];
// other stuff...
[self processData:data];
}
The important lines are immediately before and after I send the NSURLConnection synchronously.
Immediately before I send the request I set the network activity indicator to visible (using my manager class) and then immediately after I set it back to invisible.
Except the NSAssert has pointed out that somewhere this is not happening properly.
Could it be that running this function from multiple threads could be causing an issue? How could I solve this?
Integer increment or decrement is not thread-safe (as far as I know), so if two threads call your method "simultaneously", the count might not get updated properly.
One solution would be to add some synchronization directive (such as #synchronized)
to your method. Or you use the atomic increment/decrement functions:
#include <libkern/OSAtomic.h>
- (void)setNetworkActivityIndicatorVisible:(BOOL)setVisible {
static volatile int32_t NumberOfCallsToSetVisible = 0;
int32_t newValue = OSAtomicAdd32((setVisible ? +1 : -1), &NumberOfCallsToSetVisible);
NSAssert(newValue >= 0, #"Network Activity Indicator was asked to hide more often than shown");
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:(newValue > 0)];
}