So I have a WebView inside a custom NSObject subclass called GoogleLinkFetcher and what I do is load a request from the webview and in webView:didFinishLoadForFrame: I call self to call a method on it. If I don't call that method everything works fine, if I do an EXC_BAD_ACCESS error points to that line. I read something about EXC_BAD_ACCESS regarding deallocating and memory allocations but I'm in an ARC environment so I would expect not to have problems with that... Here is the code:
-(void)searchLinks
{
NSLog(#"searching links at googlelinksearcher url: %#", googleUrl);
NSURLRequest *request = [NSURLRequest requestWithURL:googleUrl];
[[webView mainFrame] loadRequest:request];
[webView setFrameLoadDelegate:self];
}
-(void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame
{
if(frame == sender.mainFrame)
{
NSLog(#"main frame");
[self getLinks];
}
}
The error points right to [self getLinks].
I hope somebody could help, thanks in advance!
The problem might be that you start the loading process but don't hold a strong reference to your GoogleLinkFetcher instance and it is released before the web view finishes loading (actually right after it starts).
Put a breakpoint in webView:didFinishLoadForFrame: method and check if self is still a valid instance of GoogleLinkFetcher. Or NSLog self before you call getLinks.
So I am attempting to throw together a simple test to verify that I am receiving frequency values from my audioController correctly.
In my view I am making a call like this to setup up a block callback:
- (void) registerVolumeCallback {
NSNumberBlock frequencyCallback = ^(NSNumber *frequency) {
self.currentFrequency = frequency;
};
self.audioController.frequencyCallback = frequencyCallback;
}
In my audio controller the frequency callback block is called with an nsnumber containing the frequency.
In my tests file I have the following:
- (void) testFrequencyAudioServiceCallbackActive {
OCMockObject *mockEqualizer = [OCMockObject partialMockForObject:self.testEqualizer];
[[[mockEqualizer stub] andCall:#selector(mockDidUpdateFrequency:)
onObject:self] setCurrentFrequency:[OCMArg any]];
[self.testEqualizer startAnimating];
[ mockEqualizer verify];
}
And:
- (void) mockDidUpdateFrequency: (NSNumber *) frequency {
GHAssertTrue((frequency!= nil), #"Audio Service is messing up");
}
Where test equalizer is an an instance of the aforementioned view. So Im trying to do some swizzling here. Problem is, mockDidUpdateFrequency is never called. I tried putting:
self.currentFrequency = frequency;
outside of the block, and the swizzling does happen and I do get a call to mockDidUpdateFrequency. I also tried:
- (void) registerVolumeCallback {
__block UIEqualizer *blockSafeSelf = self;
NSNumberBlock frequencyCallback = ^(NSNumber *frequency) {
blockSafeSelf.currentFrequency = frequency;
};
self.audioController.frequency = frequencyCallback;
}
No luck. Some weird instance stuff is going on here in the block context that I am not aware of. Anyone know whats happening?
You'll need to provide some more details for a definitive answer. For example, how is registerVolumeCallback invoked? And is frequencyCallback your own code, or a third-party API?
With what you've provided, I suspect that frequencyCallback is an asynchronous call. So even though startAnimating might create the condition where it will eventually be invoked, you immediately verify the mock before the callback has had a chance to be invoked. To get your test to do what you want as written, you need to understand what queue that block is executed on, and you need to give it a chance to execute.
If it's invoked asynchronously on the main queue, you can let the main run loop spin before calling verify:
[self.testEqualizer startAnimating];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:.1]];
[mockEqualizer verify];
If it's invoked on a different queue, you have some different options, but it would help to have a clearer picture how your code is structured first.
I have a UIWebView that is successfully being created dynamically from an included class file (I think... there are no errors being spit out). My function that creates this webview in the Foo.m class file is:
+(void) openWebView {
dispatch_async(dispatch_get_main_queue(), ^{
UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectMake(0, 0, 16, 16)];
[webView setDelegate:self];
[webView setHidden:YES];
NSURL *url = [NSURL URLWithString:address];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
[webView loadRequest:urlRequest];
NSLog(#"Successful web open: ", url);
});
}
I do get a warning error message on the setDelegate line (it doesn't crash the app though) that reads:
Incompatible pointer types sending 'const Class' to parameter of type 'id:<UIWebViewDelegate>'
The problem is that while the NSLog does log the correct URL it is trying to open, the page actually never gets called (I can tell this because I have a PHP counter on that page that increments each time it is opened).
So my questions are:
Am I missing something in the .h file? I've added nothing there in
respect to this new WebView
I feel like I am missing the object that self references to. It was also made mention here in my precursor question to this one.
How can I check the response of the loadRequest
Sorry everyone, as you probably can figure out from my questions, Obj-C is a little new to me, but I am really struggling here and would appreciate the help! Thanks.
You are implementing a class method (indicated by the + sign). self in a class method refers to the class itself, not an instance. The delegate of a UIWebView (or anything really) has to be an object though.
Your openWebView method is a class method because of the + at the beginning. For this reason, you don't have an object. self in this case returns the class (hence the warning). Perhaps you want openWebView to be an instance method, so change the + to - and then self will point to your instantiated object.
To check if the loadRequest worked, you implement the UIWebViewDelegate delegate methods webViewDidStartLoad and webViewDidFinishLoad in your delegate (ie Foo.m) and see if they get called.
This is my first app, and actually isn't even fully mine but rather involves re-working an existing app to add functionality.
It involves a JSON feed which I'm successfully reading in and then trying to pass the value of a URL to a view. Here's the code from my app delegate that is successfully fired once the feed is read in:
- (void)JSONFetch:(MYJSONFetch *)fetch gotTheCollection:(id)collection
{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
self.testViewController.feedURL = [NSURL URLWithString:[collection objectForKey:#"Listings"]];
[JSONFetch release];
JSONFetch = nil;
}
Then in my testViewController I have this viewDidLoad method:
- (void)viewDidLoad {
[super viewDidLoad];
if(self.feedURL)
{
[self startDownload];
}
}
Eventhough, when I debug, the gotTheCollection method passes a value to the feedURL of the view, it then fails on the if(self feedURL) check within the view and thus the view never gets populated.
As I'm so new to this code I've no idea if the sequence is wrong, or maybe it's how I'm passing the variable.
I know the description is relatively vague but even on a basic level I don't know if this functionality works in objective C, it doesn't cause any errors though, just sits there not loading because it can't get the data.
UPDATE: Definition of FeedURL follows, in the H file:
NSURL *feedURL;
then
#property (nonatomic, retain) NSURL *feedURL;
then in the M file:
#synthesize feedURL;
Thanks for the help guys, I finally decided to just restart the entire upgrade as the project had become a mess of reworked code and I couldn't be sure what worked and what didn't. As a result there's no clear answer to this but I imagine Franks was probably the closest so I'll mark that as the answer.
The NSURL is being autoreleased, you will need to retain it yourself
Assign the NSURL to feedURL, like so
self.testViewController.feedURL = [[NSURL URLWithString:[collection objectForKey:#"Listings"]] retain];
This will also mean you will have to release it yourself.
I am building a really basic Cocoa application using WebKit, to display a Flash/Silverlight application within it. Very basic, no intentions for it to be a browser itself.
So far I have been able to get it to open basic html links (<a href="..." />) in a new instance of Safari using
[[NSWorkspace sharedWorkspace] openURL:[request URL]];
Now my difficulty is opening a link in a new instance of Safari when window.open() is used in JavaScript. I "think" (and by this, I have been hacking away at the code and am unsure if i actually did or not) I got this kind of working by setting the WebView's policyDelegate and implementing its
-webView:decidePolicyForNavigationAction:request:frame:decisionListener:
delegate method. However this led to some erratic behavior.
So the simple question, what do I need to do so that when window.open() is called, the link is opened in a new instance of Safari.
Thanks
Big point, I am normally a .NET developer, and have only been working with Cocoa/WebKit for a few days.
I made from progress last night and pinned down part of my problem.
I am already using webView:decidePolicyForNewWindowAction:request:newFrameName:decisionListener: and I have gotten it to work with anchor tags, however the method never seems to get called when JavaScript is invoked.
However when window.open() is called webView:createWebViewWithRequest:request is called, I have tried to force the window to open in Safari here, however request is always null. So I can never read the URL out.
I have done some searching around, and this seems to be a known "misfeature" however I have not been able to find a way to work around it.
From what I understand createWebViewWithRequest gives you the ability to create the new webview, the the requested url is then sent to the new webView to be loaded. This is the best explanation I have been able to find so far.
So while many people have pointed out this problem, I have yet to see any solution which fits my needs. I will try to delve a little deeper into the decidePolicyForNewWindowAction again.
Thanks!
Well, I'm handling it by creating a dummy webView, setting it's frameLoad delegate to a custom class that handles
- (void)webView:decidePolicyForNavigationAction:actionInformation :request:frame:decisionListener:
and opens a new window there.
code :
- (WebView *)webView:(WebView *)sender createWebViewWithRequest:(NSURLRequest *)request {
//this is a hack because request URL is null here due to a bug in webkit
return [newWindowHandler webView];
}
and NewWindowHandler :
#implementation NewWindowHandler
-(NewWindowHandler*)initWithWebView:(WebView*)newWebView {
webView = newWebView;
[webView setUIDelegate:self];
[webView setPolicyDelegate:self];
[webView setResourceLoadDelegate:self];
return self;
}
- (void)webView:(WebView *)sender decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener {
[[NSWorkspace sharedWorkspace] openURL:[actionInformation objectForKey:WebActionOriginalURLKey]];
}
-(WebView*)webView {
return webView;
}
There seems to be a bug with webView:decidePolicyForNewWindowAction:request:newFrameName:decisionListener: in that the request is always nil, but there is a robust solution that works with both normal target="_blank" links as well as javascript ones.
Basically I use another ephemeral WebView to handle the new page load in. Similar to Yoni Shalom but with a little more syntactic sugar.
To use it first set a delegate object for your WebView, in this case I'm setting myself as the delegate:
webView.UIDelegate = self;
Then just implement the webView:createWebViewWithRequest: delegate method and use my block based API to do something when a new page is loaded, in this case I'm opening the page in an external browser:
-(WebView *)webView:(WebView *)sender createWebViewWithRequest:(NSURLRequest *)request {
return [GBWebViewExternalLinkHandler riggedWebViewWithLoadHandler:^(NSURL *url) {
[[NSWorkspace sharedWorkspace] openURL:url];
}];
}
That's pretty much it. Here's the code for my class. Header:
// GBWebViewExternalLinkHandler.h
// TabApp2
//
// Created by Luka Mirosevic on 13/03/2013.
// Copyright (c) 2013 Goonbee. All rights reserved.
//
#import <Foundation/Foundation.h>
#class WebView;
typedef void(^NewWindowCallback)(NSURL *url);
#interface GBWebViewExternalLinkHandler : NSObject
+(WebView *)riggedWebViewWithLoadHandler:(NewWindowCallback)handler;
#end
Implemetation:
// GBWebViewExternalLinkHandler.m
// TabApp2
//
// Created by Luka Mirosevic on 13/03/2013.
// Copyright (c) 2013 Goonbee. All rights reserved.
//
#import "GBWebViewExternalLinkHandler.h"
#import <WebKit/WebKit.h>
#interface GBWebViewExternalLinkHandler ()
#property (strong, nonatomic) WebView *attachedWebView;
#property (strong, nonatomic) GBWebViewExternalLinkHandler *retainedSelf;
#property (copy, nonatomic) NewWindowCallback handler;
#end
#implementation GBWebViewExternalLinkHandler
-(id)init {
if (self = [super init]) {
//create a new webview with self as the policyDelegate, and keep a ref to it
self.attachedWebView = [WebView new];
self.attachedWebView.policyDelegate = self;
}
return self;
}
-(void)webView:(WebView *)sender decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener {
//execute handler
if (self.handler) {
self.handler(actionInformation[WebActionOriginalURLKey]);
}
//our job is done so safe to unretain yourself
self.retainedSelf = nil;
}
+(WebView *)riggedWebViewWithLoadHandler:(NewWindowCallback)handler {
//create a new handler
GBWebViewExternalLinkHandler *newWindowHandler = [GBWebViewExternalLinkHandler new];
//store the block
newWindowHandler.handler = handler;
//retain yourself so that we persist until the webView:decidePolicyForNavigationAction:request:frame:decisionListener: method has been called
newWindowHandler.retainedSelf = newWindowHandler;
//return the attached webview
return newWindowHandler.attachedWebView;
}
#end
Licensed as Apache 2.
You don't mention what kind of erratic behaviour you are seeing. A quick possibility, is that when implementing the delegate method you forgot to tell the webview you are ignoring the click by calling the ignore method of the WebPolicyDecisionListener that was passed to your delegate, which may have put things into a weird state.
If that is not the issue, then how much control do you have over the content you are displaying? The policy delegate gives you easy mechanisms to filter all resource loads (as you have discovered), and all new window opens via webView:decidePolicyForNewWindowAction:request:newFrameName:decisionListener:. All window.open calls should funnel through that, as will anything else that triggers a new window.
If there are other window opens you want to keep inside your app, you will to do a little more work. One of the arguments passed into the delegate is a dictionary containing information about the event. Insie that dictionary the WebActionElementKey will have a dictionary containing a number of details, including the original dom content of the link. If you want to poke around in there you can grab the actual DOM element, and check the text of the href to see if it starts with window.open. That is a bit heavy weight, but if you want fine grained control it will give it to you.
By reading all posts, i have come up with my simple solution, all funcs are in same class,here it is, opens a link with browser.
- (WebView *)webView:(WebView *)sender createWebViewWithRequest:(NSURLRequest *)request {
return [self externalWebView:sender];
}
- (void)webView:(WebView *)sender decidePolicyForNavigationAction:(NSDictionary *)actionInformation request:(NSURLRequest *)request frame:(WebFrame *)frame decisionListener:(id<WebPolicyDecisionListener>)listener
{
[[NSWorkspace sharedWorkspace] openURL:[actionInformation objectForKey:WebActionOriginalURLKey]];
}
-(WebView*)externalWebView:(WebView*)newWebView
{
WebView *webView = newWebView;
[webView setUIDelegate:self];
[webView setPolicyDelegate:self];
[webView setResourceLoadDelegate:self];
return webView;
}
Explanation:
Windows created from JavaScript via window.open go through createWebViewWithRequest.
All window.open calls result in a createWebViewWithRequest: with a null request, then later a location change on that WebView.
For further information, see this old post on the WebKit mailing list.
An alternative to returning a new WebView and waiting for its loadRequest: method to be called, I ended up overwriting the window.open function in the WebView's JSContext:
First, I set my controller to be the WebFrameLoadDelegate of the WebView:
myWebView.frameLoadDelegate = self;
Then, in the delegate method, I overwrote the window.open function, and I can process the URL there instead.
- (void)webView:(WebView *)webView didCreateJavaScriptContext:(JSContext *)context forFrame:(WebFrame *)frame{
context[#"window"][#"open"] = ^(id url){
NSLog(#"url to load: %#", url);
};
}
This let me handle the request however I needed to without the awkward need to create additional WebViews.