First, thanks to everyone that takes the time to answer questions. I have gotten so many quick answers to problems over the years from StackOverflow.
I'm new to Object C and iOS programming, but starting with what I think should be a super simple app. It receives a push notification (which works fine) and redirects to a webpage when it has figured out its appid.
The problem is that the while I can get my UIWebView to loadRequest in the viewDidLoad, the same code will not execute in another function.
Here's the code:
AppDelegate.m
// UFAppDelegate.m
#import "UFAppDelegate.h"
#import "UFViewController.h"
#implementation UFAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Add registration for remote notifications
[[UIApplication sharedApplication] registerForRemoteNotificationTypes:
(UIRemoteNotificationType)(UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound)];
return YES;
}
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)devToken {
// ....
NSString *urlString = #"http://www.google.com";
NSURL *url = [NSURL URLWithString:urlString];
NSLog(#"Navigating to URL: %#", url);
UFViewController *theview = [[UFViewController alloc] init];
[theview handleOpenURL:url];
}
#end
ViewController.h:
// UFViewController.h
#import <UIKit/UIKit.h>
#interface UFViewController : UIViewController
- (void)handleOpenURL:(NSURL *)url;
#end
ViewController.m:
// UFViewController.m
#import "UFViewController.h"
#interface UFViewController ()
#property (weak, nonatomic) IBOutlet UIWebView *UFHWebView;
- (void)handleOpenURL:(NSURL *)url;
#end
#implementation UFViewController
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"UFViewController.viewDidLoad started");
// This block that assigns the url and loads it works perfectly here... but not down below.
NSString *urlString = #"http://search.yahoo.com/";
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *requestObj = [NSURLRequest requestWithURL:url];
[_UFHWebView loadRequest:requestObj];
NSLog(#"UFViewController.viewDidLoad completed");
}
- (void)handleOpenURL:(NSURL *)url
{
NSLog(#"UFViewController.handleOpenURL started");
NSLog(#"url = %#",url);
// The below loadRequest does not load!
NSURLRequest *requestObj = [NSURLRequest requestWithURL:url];
[_UFHWebView loadRequest:requestObj];
NSLog(#"UFViewController.handleOpenURL completed");
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
NSLog(#"Error - %#", error);
}
- (BOOL) webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
return YES;
}
#end
When run as coded here, the yahoo page shows from the loadRequest in the didload, but the handleOpenURL one still does not fire. If I comment out the loadRequest out of viewDidLoad, a blank page shows up and the handleOpenURL still does not fire.
Here's the debug sequence. viewDidLoad fires and completes before receiving the AppID and manually firing handleOpenURL:
2013-12-12 15:28:32.606 UF[5896:60b] UFViewController.viewDidLoad started
2013-12-12 15:28:32.608 UF[5896:60b] UFViewController.viewDidLoad completed
2013-12-12 15:28:32.765 UF[5896:60b] Navigating to URL: http://www.google.com
2013-12-12 15:28:32.769 UF[5896:60b] UFViewController.handleOpenURL started
2013-12-12 15:28:32.773 UF[5896:60b] url = http://www.google.com
2013-12-12 15:28:32.775 UF[5896:60b] UFViewController.handleOpenURL completed
Any help appreciated!
Are you sure you have just one instance of UFViewController here?
-didRegisterForRemoteNotificationsWithDeviceToken initializes a new web view controller in a local variable, but I can't see how it's getting shown to the user - you aren't doing anything with the vc once it's created.
Here's what I think might be happening:
App starts. You have an instance of UFViewController in a storyboard. It loads, its -viewDidLoad is called, the log statements print and the webview loads correctly.
A little later, the -didRegisterForRemoteNotificationsWithDeviceToken: method is called. This instantiates a UFViewController, and calls the -handleOpenUrl method on it. The -handleOpenURL log methods fire. However, this controller has not been loaded into the view hierarchy, so its view is never instantiated, and its -viewDidLoad is never called.
This means that the webview that the -loadRequest: is being called on is nil. In Objective-C, it is valid to send a message to nil- unlike most other languages, no error or exception will be thrown. Instead, absolutely nothing happens, hence no web load.
The UFViewController in -handleOpenUrl is only referred to by a local variable within the method, so when the method completes, the last strong reference to it vanishes and it is released.
To check this is correct:
-NSLog(#"%#",[self description]); in your vc methods - this will log the address of the objects and tell you whether they are the same.
-NSLog the web controller's view, to check whether it's nil in handleOpenURL
To solve your problem, I suggest using an NSNotification posted by the AppDelegate and listened for by the UFViewController that is fully instantiated. So in the vc you'd do:
[[NSNotificationCenter defaultCenter]addObserver:self selector:#selector(handleRegistration:) name:#"HandleRegistrationNotification" object:nil];
and then implement the handleRegistration method:
-(void)handleRegistration:(NSNotification*)notification
{
//your handleOpenUrl code
}
and also
-(void)dealloc
{
[[NSNotificationCenter defaultCenter]removeObserver:self name:#"HandleRegistrationNotification" object:nil];
}
(this bit is important because otherwise you may get a crash when the vc is dealloc'd)
Then, in the -didRegisterForRemoteNotifications: method:
[[NSNotificationCenter defaultCenter]postNotificationName:#"HandleRegistrationNotification" object:nil];
If you need to pass any information through the notification (eg urls) you can use the object parameter - this is accessible in the method called on the vc via the object property of the NSNotification parameter.
Full docs on notifications in Objective-C can be found here.
Some minor points - it's generally a bad idea to call your view controllers things like theView rather than theViewController since other people (or you in 6 months time) will end up thinking they're UIView s. Also the webview property should not start with a capital letter- that's generally only used for class names.
This should be called, "How to call a storyboard viewcontroller method from the appdelegate". And the answer is:
in AppDelegate.m:
#import "UFViewController.h"
...
UFViewController *rootViewController = (UFViewController*)self.window.rootViewController;
[rootViewController handleOpenURL:url];
In my code snippets above, this means replacing these lines
UFViewController *theview = [[UFViewController alloc] init];
[theview handleOpenURL:url];
with the ones above. The difference is my original code instantiates a new UFViewController object into a local variable, while the correct code just gets a handle to the view controller already created in the storyboard and calls its method.
Related
So, it is pretty clear question I think. This question is about monitoring ajax requests. I tried it but didn't work as I wanted.
How can I call a function in my native iOS Obj-C application every time a request is sent, received and the browsing link is changed in WKWebView?
Ok, I found a way.
You can create a new class (let's call it MyURLProtocol) which has NSURLProtocol as subclass. Then add this function to MyURLProtocol:
+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
NSLog(#"URL = %#", request.URL.absoluteString);
return NO;
}
This function will be called each time your webview makes a request. And then you need to register this protocol with the loading system. In your Appdelegate.m file include your class and add/replace didFinishLaunchingWithOptions function with this:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[NSURLProtocol registerClass:[MyURLProtocol class]];
return YES;
}
All set. Now you can edit canInitWithRequest function and do what you want with the request.
I am using NSURLSessionDownloadTask objects on an NSURLSession to allow users to download documents while the app is in the background / device locked. I also want to inform the user that individual downloads have finished through a local notification.
To that end, I am triggering a local notification in the -URLSession:downloadTask:didFinishDownloadingToURL: download task delegate method, however I am wondering if there might be a better place to add the code triggering a notification, since the way Apple explains it, the download task will be passed to the system, and from that I am deriving that those delegates will not be called anymore on the download task's delegate once (or shortly after) the app is backgrounded.
My question: What is the best place to add the code for triggering the local notifications? Has anybody had any previous experience in adding this sort of a functionality to their application?
Answer on your question can be found in Apple documentation URL Loading System Programming Guide:
In iOS, when a background transfer completes or requires credentials,
if your app is no longer running, iOS automatically relaunches your
app in the background and calls the
application:handleEventsForBackgroundURLSession:completionHandler:
method on your app’s UIApplicationDelegate object. This call provides
the identifier of the session that caused your app to be launched.
Your app should store that completion handler, create a background
configuration object with the same identifier, and create a session
with that configuration object. The new session is automatically
reassociated with ongoing background activity. Later, when the session
finishes the last background download task, it sends the session
delegate a URLSessionDidFinishEventsForBackgroundURLSession: message.
Your session delegate should then call the stored completion handler.
If any task completed while your app was suspended, the delegate’s
URLSession:downloadTask:didFinishDownloadingToURL: method is then
called with the task and the URL for the newly downloaded file
associated with it.
As you see it's much more complicated then just set delegate object. By delegate methods you will be notified only if app in foreground mode. In other cases (app in background mode, app is terminated) you need handle AppDelegate methods that are described in above quote.
Also Apple provides example project, that shows how to work with background download/upload tasks. This example will help you to find place where to put "Local Notification" code.
As Visput explained above, this method will be called once the download completes.application:handleEventsForBackgroundURLSession:completionHandler:
This will happen if you use the NSURLSessionConfiguration class with the backgroundSessionConfiguraton. You might be missing that piece.
NSURLSessionConfiguration *sessionConfiguration = [NSURLSessionConfiguration backgroundSessionConfiguration:#"com.BGTransfer"];
sessionConfiguration.HTTPMaximumConnectionsPerHost = 5; // To set the max concurrent connections
It is explained in detail here.
As Suggested by #Gautam Jain u have to use backgroundSessionConfiguration to achieve ur objective.Below i have attached a example ,hope it helps you
DownloadModel.h
#import "AppDelegate.h"
#interface DownloadModel : NSObject<NSURLSessionDelegate,NSURLSessionTaskDelegate,NSURLSessionDownloadDelegate>{
NSString *resp;
}
+(instancetype)shared;
-(NSURLSessionDownloadTask *) downloadTaskWithURL:(NSURL*)url ;
#end
DownloadModel.m
#import "DownloadModel.h"
#interface DownloadModel ()
#property (strong,nonatomic) NSURLSession *downloadSession;
#end
#implementation DownloadModel
+(instancetype)shared{
static dispatch_once_t onceToken;
static DownloadModel *downloader=nil;
dispatch_once(&onceToken, ^{
downloader=[DownloadModel new];
});
return downloader;
}
-(id)init{
self=[super init];
if(self){
NSURLSessionConfiguration *downloadConfig=[NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:#"DownloadDemo"];
// downloadConfig.timeoutIntervalForRequest = 30;
// downloadConfig.timeoutIntervalForResource = 30;
// downloadConfig.HTTPMaximumConnectionsPerHost = 1;
// downloadConfig.sessionSendsLaunchEvents=YES;
downloadConfig.allowsCellularAccess = YES;
downloadConfig.networkServiceType = NSURLNetworkServiceTypeBackground;
// downloadConfig.discretionary = YES;
self.downloadSession=[NSURLSession sessionWithConfiguration:downloadConfig delegate:self delegateQueue:nil];
self.downloadSession.sessionDescription=#"Video Downloader";
}
return self;
}
-(NSURLSessionDownloadTask *) downloadTaskWithURL:(NSURL*)url{
return [self.downloadSession downloadTaskWithURL:url];
}
#pragma mark download delegate
use notification OR Local Notification in this method
- (void)URLSession:(NSURLSession *)session downloadTask(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL(NSURL *)location{
[[NSNotificationCenter defaultCenter] postNotificationName:#"DownloadFinish" object:downloadTask userInfo:nil];
}
For Progress of Download
- (void)URLSession:(NSURLSession *)session downloadTask(NSURLSessionDownloadTask *)downloadTask didWriteData(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{
CGFloat progress=(CGFloat)totalBytesWritten/totalBytesExpectedToWrite;
NSDictionary *userInfo=#{#"progress":#(progress)};
[[NSNotificationCenter defaultCenter] postNotificationName:#"DownloadProgress" object:downloadTask userInfo:userInfo];
}
#pragma mark delegate
-(void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session{
AppDelegate *appdelegate=[[UIApplication sharedApplication] delegate];
if(appdelegate.backgroundSessionCompletionHandler){
appdelegate.backgroundSessionCompletionHandler();
appdelegate.backgroundSessionCompletionHandler=nil;
}
}
#end
AppDelegate.h
#interface AppDelegate : UIResponder <UIApplicationDelegate>
#property (strong, nonatomic) UIWindow *window;
#property (copy ,nonatomic) void(^backgroundSessionCompletionHandler)();
#end
AppDelegate.m
-(void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler{
self.backgroundSessionCompletionHandler=completionHandler;
[DownloadModel shared];
}
ViewController.m Call this Method -(NSURLSessionDownloadTask *) downloadTaskWithURL:(NSURL*)url
- (void)viewDidLoad {
//Add Notification observers to track download progress and call the above method
[DownloadModel shared] downloadTaskWithURL:url];
}
Don't Forget to enable Background Fetch
I have a UIWebView that is handling an OAuth authentication flow in my app. The callback URL that the OAuth service uses points to a custom URL scheme I've set up in my app, let's call it mycustomurlscheme.
So once the user authenticates successfully, the OAuth service redirects them to the URL mycustomurlscheme://oauth/success?oauth_token=xxxxxxxxxxxxxxxxxxxx&oauth_verifier=xxxxxxxxxxxxxxxxxxxx. When the UIWebView requests this URL, the app behaves correctly; namely, the application:openURL:sourceApplication:annotation: method in my app delegate gets called, and I dismiss the UIWebView and continue on with the app.
However, I am logging load failures in the UIWebView (using webView:didFailLoadWithError:), and when the above process happens, I see the following in the console:
Error Domain=WebKitErrorDomain Code=102 "Frame load interrupted" UserInfo=0xa1a4810 {NSErrorFailingURLKey=mycustomurlscheme://oauth/success?oauth_token= xxxxxxxxxxxxxxxxxxxx&oauth_verifier=xxxxxxxxxxxxxxxxxxxx, NSErrorFailingURLStringKey=mycustomurlscheme://oauth/success?oauth_token= xxxxxxxxxxxxxxxxxxxx&oauth_verifier=xxxxxxxxxxxxxxxxxxxx, NSLocalizedDescription=Frame load interrupted}
I tried implementing the following UIWebViewDelegate method, thinking that intercepting the request would avoid the error, but the error still happens:
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request
navigationType:(UIWebViewNavigationType)navigationType
{
if ([request.URL.scheme isEqualToString:#"mycustomurlscheme"]) {
[[UIApplication sharedApplication] openURL:request.URL];
return NO;
} else {
return YES;
}
}
This code then causes the following code in my app delegate to run:
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication
annotation:(id)annotation
{
NSNotification *notification = [NSNotification notificationWithName:kDSApplicationLaunchedWithURLNotification object:nil userInfo:#{kApplicationLaunchOptionsURLKey: url}];
[[NSNotificationCenter defaultCenter] postNotification:notification];
return YES;
}
This notification is picked up by the following block that continues the loading & launching of the app, post-authorization:
self.applicationLaunchNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kDSApplicationLaunchedWithURLNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) {
NSURL *url = [notification.userInfo valueForKey:kApplicationLaunchOptionsURLKey];
currentRequestToken.verifier = [DSAPIParametersFromQueryString([url query]) valueForKey:#"oauth_verifier"];
[[DSAPIManager sharedManager] acquireAccessTokenWithRequestToken:currentRequestToken success:^(DSAPIOAuth1Token * accessToken) {
[self finishLogin];
} failure:^(NSError *error) {
[self removeAsObserver];
[self showAlert:NSLocalizedString(#"Could not authorize the app. Please try again later.", nil)];
}];
}];
The finishLogin and removeAsObserver methods look like this:
- (void)finishLogin
{
[self removeAsObserver];
[self.navigationController dismissViewControllerAnimated:YES completion:^{
[self performSegueWithIdentifier:#"PostLoginSegue" sender:self];
}];
}
- (void)removeAsObserver
{
if (self.applicationLaunchNotificationObserver) {
[[NSNotificationCenter defaultCenter] removeObserver:self.applicationLaunchNotificationObserver];
self.applicationLaunchNotificationObserver = nil;
}
}
Is it safe to ignore this error message? Anyone know why it's happening? My hunch is that the UIWebView thinks the URL has an invalid scheme, but the phone knows it's ok. If that's the case is there any way to tell the UIWebView (or the WebKit engine) that this scheme is valid?
The error here is actually the Frame load interrupted nothing to do with the url scheme. Using custom url scheme is perfectly possible and doesn't create any errors. I have them in many of my apps and have never had a problem with them.
I believe your issue is because of the [webView stoploading]; technically it will stop loading when you return NO so the [webView stopLoading]; is just causing an interruption in the process. Remove this line and let it continue to return NO and see what happens I am 99.9% sure this is the reason.
So your code would look like
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
{
if ([request.URL.scheme isEqualToString:#"mycustomurlscheme"]) {
// Dismiss the view controller, continue loading the app, etc.
return NO;
}
return YES; // No need for the else statement if this is the only thing to happen if it fails the if statement.
}
UPDATE
Based don comments removing [webView stopLoading]; didn't fix your issue. If this is the case and you make it into that if statement I suspect that something else is going on. Have you shared all your code? With this // Dismiss the view controller, continue loading the app, etc. in your code I suspect that you haven't shared everything in which case we can't help.
Check if there is anything for "UserAgent" in"standardUserDefaults". There are chances that UserAgent is getting appended in request URL. Or check for any "UserAgent" sent in url request.
The notification I receive contains an url to open in a UIWebView. But I cannot access the UIWebview from my AppDelegate (this is where is receive the notification).
-(void) application:(UIApplication *)application didReceiveRemoteNotification(NSDictionary* ) userInfo{
// handle notification (this works)
//HERE I want to call my UIWebView and load the url I got.
}
One way to do this is to post a notification that your UIWebView receives. Check out the NSNotificationCenter Class Reference or this example on SO.
In my app I have used the following, this is when the UIWebView is already on the screen:
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
NSString *urlString = [userInfo objectForKey:#"url"];
if( [self.window.rootViewController isKindOfClass:[UINavigationController class]] ){
UINavigationController *currentNavigationController = (UINavigationController*)self.window.rootViewController;
if( [currentNavigationController.visibleViewController isKindOfClass:[NHCallVC class]] ){
SomeViewController *currentViewController = (SomeViewController*)currentNavigationController.visibleViewController;
[currentViewController.webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:urlString]]];
}
}
}
The structure with the UINavigationController seems complex, but this was needed for the way I set up my app. This can be different in your app.
The idea is to get the viewcontroller that is opened and load a URL on the UIWebView. The code is assuming the UIViewController with the UIWebView is currently open. The code should be altered if you want to navigate to the correct UIViewController before opening the url in the UIWebView.
Place an observer to the view where UIWebView resides such as:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(recievedNotification:) name:#"ReceivedNotification" object:nil];
Write the appropriate code within recievedNotification function which changes the UIWebView's target url.
And post a notification in didReceiveRemoteNotification function in APP Delegate such as:
[[NSNotificationCenter defaultCenter] postNotificationName:#"ReceivedNotification" object:nil];
Good luck.
I have some code inside the webViewDidFinishLoad delegate method, and I need some way to tell that this code has finished executing from within another method in a completely different part of my code. Is this possible? Basically what I want to do is something like this:
-(void) myMethod
{
// do stuff that causes webViewDidFinishLoad to be called
if (webViewDidFinishLoad has finished executing)
{
// do stuff with outputString
}
}
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
outputString = [webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:#"myJSFunction('%#')",jsInputString]];
}
I looked at this answer already and it made no sense to me.
I should add that inside myMethod, I can be sure that webViewDidFinishLoad will only be called once.
You can always set a boolean to indicate whether the webview has finished loading.
And the value of the boolean will be changed in the webview delegate method:
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
loadFinished = YES;
}
Then in your own method, check "loadFinished" will do the task.
You have to set <UIWebViewDelegate> for interface to use this delegate method
Try to avoid waiting for delegates and notifications in other methods - do the work that depends on them when they call back to your class. Don't call myMethod until you get the delegate message. If myMethod is called from user input, disable the associated controls until after the web view loads.
You could use UIWebView's iVar loading
You could use your own shared boolean flag as iVar and set it in webViewDidFinishLoad.
However, maybe you could reconsider your design and do in webViewDidFinishLoad what to do after loading is finished.
Why not try GCD?
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// WebView load content here
dispatch_async( dispatch_get_main_queue(), ^{
// load myMethod
});
});
use a Boolean and set its value to YES in the last line of webViewDidFinishLoad.
then check in your myMethod like this:
-(void) myMethod
{
// do stuff that causes webViewDidFinishLoad to be called
if (yourBoolean == YES)
{
// do more stuff
}
}
that should do your work.
In webViewDidFinishLoad, you post a notification to your "myMethod" code part.
Something like this:
in UIWebView:
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
[[NSNotificationCenter defaultCenter] postNotificationName:#"webview_finished" object:self userInfo:nil];
}
in your "myMethod" code part:
- (void)viewDidLoad
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(myMethod) name:#"webview_finished" object:nil];
}