On OSX, I am using XCode to make a desktop app with a webview in it. The webview loads ok, and I can dynamically load content into it - but when I click on links inside the webview, they are not followed. They change color, but no new page is loaded. If I code my links with javascript like this - then they work.
link there
Is there an Objective-C one liner that allows links to be followed inside web-views?
Is there another issue I am not aware of here?
The links at the top of Gmail open in a new window. To make them work you have to implement at least the WebUIDelegate methods webView:createWebViewWithRequest: and webViewShow:. If you simply want to open all links in the same web view, you could return it from webView:createWebViewWithRequest: instead of creating a new one.
Turns out it was because I had code I copied form the web with some custom function to ignore WebPolicyDecisionListener...
Sorry for asking question without giving all the details - all this objective-c is new to me, I don't know which bits do what yet. I do some pointing and clicking, and then some coding - I don't know exactly how that all links up. With other languages, you have the whole program in one place - it takes a bit of a learning curve to get used to... but I digress.
I fixed by adding some comments - see code below...
- (void)webView:(WebView *)aWebView
decidePolicyForNavigationAction:(NSDictionary *)actionInformation
request:(NSURLRequest *)request
frame:(WebFrame *)frame
decisionListener:(id < WebPolicyDecisionListener >)listener
{
if ([self requestIsLinkClick:actionInformation]) {
if ([#"method" isEqual:[[request URL] scheme]]) {
SEL selector = NSSelectorFromString([[request URL] resourceSpecifier]);
if ([prototypeDelegate respondsToSelector:selector]) {
[prototypeDelegate performSelector:selector];
}
}
// [listener ignore];
} // else {
[listener use];
//}
}
Related
I was under the impression that when using bindings (been following this tutorial despite being outdated. http://cocoadevcentral.com/articles/000085.php - You can use it to see what I'm doing) the Persistent Store would automagically save the changes you make. In fact, though it was hours ago and I wouldn't be surprised if I'm now going mad, I got it working, and when I made a change it would persist on rebuilding the app.
However, the test app I've built following the tutorial no longer saves and despite showing the changes I make within the app, they disappear once I re-run the app. I've been checking the Core Data debug menu and nothing happens when I press the "+" button which is set up to the "Add" method of my NSArrayController. I know it's accessing my data model too as my textField for the Title (again, see the tutorial so you know what I'm referring to) adopts the default text I put in the DataModel section. The only thing missing therefore is the actual saving.
So my real question is, based on the tutorial, what part of the bindings actually makes the managedObjectContext save? Is there a flag or something that isn't checked?
I don't know if it's important or not, but there were differences between the tutorial and my project, mainly that the NSArrayControllers are bound to "App Delegate"with a Model Key Path of "self.managedObjectContext". Also, I removed all the relationships in an attempt to whittle down the issue.
Any help would be greatly appreciated.
Regards,
Mike
UPDATE: Here are some pictures that show the bindings.
How I set up the NSArrayController:
Here is how is how my Data Model Looks:
Lastly, this is how I set up the TextFields to update the NSArrayControllers:
I hope this helps to get a an ideas as to the set up.
Thanks,
Mike
Could you check to make sure you've copied all the Core Data boiler-plate code from the source code of the tutorial you mentioned.
Specifically this part in the App Delegate:
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender {
NSError *error;
NSManagedObjectContext *context;
int reply = NSTerminateNow;
context = [self managedObjectContext];
if (context != nil) {
if ([context commitEditing]) {
if (![context save:&error]) {
// This default error handling implementation should be changed to make sure the error presented includes application specific error recovery. For now, simply display 2 panels.
BOOL errorResult = [[NSApplication sharedApplication] presentError:error];
if (errorResult == YES) { // Then the error was handled
reply = NSTerminateCancel;
} else {
// Error handling wasn't implemented. Fall back to displaying a "quit anyway" panel.
int alertReturn = NSRunAlertPanel(nil, #"Could not save changes while quitting. Quit anyway?" , #"Quit anyway", #"Cancel", nil);
if (alertReturn == NSAlertAlternateReturn) {
reply = NSTerminateCancel;
}
}
}
} else {
reply = NSTerminateCancel;
}
}
return reply;
}
If it's there, changes will be saved when the app is terminated normally. Pressing the 'stop' button in Xcode will terminate the app immediately, without going through the method mentioned above.
My guess is that you are not going mad, but first exited the app properly and have been pressing the 'stop' button later ;).
I'm trying to figure out how to limit my NSDocument based application to one open document at a time. It is quickly becoming a mess.
Has anyone been able to do this in a straightforward & reliable way?
////EDIT////
I would like to be able to prompt the user to save an existing open document and close it before creating/opening a new document.
////EDIT 2
I'm now trying to just return an error with an appropriate message if any documents are opening -- however, the error message is not displaying my NSLocalizedKeyDescription. This is in my NSDocumentController subclass.
-(id)openUntitledDocumentAndDisplay:(BOOL)displayDocument error:(NSError **)outError{
if([self.documents count]){
NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithObject:#"Only one document can be open at a time. Please close your document." forKey:NSLocalizedDescriptionKey];
*outError = [NSError errorWithDomain:#"Error" code:192 userInfo:dict];
return nil;
}
return [super openUntitledDocumentAndDisplay:displayDocument error:outError];
}
It won't be an easy solution, since it's a pretty complex class, but I would suggest that you subclass NSDocumentController and register your own which disables opening beyond a certain number of documents. This will allow you to prevent things like opening files by dropping them on the application's icon in the dock or opening in the finder, both of which bypass the Open menu item.
You will still need to override the GUI/menu activation code to prevent Open... from being available when you have a document open already, but that's just to make sure you don't confuse the user.
Your document controller needs to be created before any other document controllers, but that's easy to do by placing a DocumentController instance in your MainMenu.xib and making sure the class is set to your subclass. (This will cause it to call -sharedDocumentController, which will create an instance of yours.)
In your document controller, then, you will need to override:
- makeDocumentForURL:withContentsOfURL:ofType:error:
- makeUntitledDocumentOfType:error:
- makeDocumentWithContentsOfURL:ofType:error:
to check and see if a document is already open and return nil, setting the error pointer to a newly created error that shows an appropriate message (NSLocalizedDescriptionKey).
That should take care of cases of drag-and-drop, applescript,etc.
EDIT
As for your additional request of the close/save prompt on an opening event, that's a nastier problem. You could:
Save off the information (basically the arguments for the make requests)
Send the -closeAllDocumentsWithDelegate:didCloseAllSelector:contextInfo: with self as a delegate and a newly-created routine as the selector
When you receive the selector, then either clear out the saved arguments, or re-execute the commands with the arguments you saved.
Note that step 2 and 3 might need to be done on delay with performSelector
I haven't tried this myself (the rest I've done before), but it seems like it should work.
Here's the solution I ended up with. All of this is in a NSDocumentController subclass.
- (NSInteger)runModalOpenPanel:(NSOpenPanel *)openPanel forTypes:(NSArray *)extensions{
[openPanel setAllowsMultipleSelection:NO];
return [super runModalOpenPanel:openPanel forTypes:extensions];
}
-(NSUInteger)maximumRecentDocumentCount{
return 0;
}
-(void)newDocument:(id)sender{
if ([self.documents count]) {
[super closeAllDocumentsWithDelegate:self
didCloseAllSelector:#selector(newDocument:didCloseAll:contextInfo:) contextInfo:(void*)sender];
}
else{
[super newDocument:sender];
}
}
- (void)newDocument:(NSDocumentController *)docController didCloseAll: (BOOL)didCloseAll contextInfo:(void *)contextInfo{
if([self.documents count])return;
else [super newDocument:(__bridge id)contextInfo];
}
-(void)openDocument:(id)sender{
if ([self.documents count]) {
[super closeAllDocumentsWithDelegate:self
didCloseAllSelector:#selector(openDocument:didCloseAll:contextInfo:) contextInfo:(void*)sender];
}
else{
[super openDocument:sender];
}
}
- (void)openDocument:(NSDocumentController *)docController didCloseAll: (BOOL)didCloseAll contextInfo:(void *)contextInfo{
if([self.documents count])return;
else [super openDocument:(__bridge id)contextInfo];
}
Also, I unfortunately needed to remove the "Open Recent" option from the Main Menu. I haven't figured out how to get around that situation.
I have been working with xcode 4 for a few months now with no problem, well with some problems but that is another story, and now have a weird problem. I can not add any new methods... What I mean to say is when I add a new method xcode doesn't recognize it. The tag follow doesn't see it and the debugger errors saying there is no method by that name. However, the method just above it with the exact same signature minus the name works famously.
Has anyone ever seen this before? If so could you please send me in the right direction. Any help and I would be eternally grateful.
- (void)showMyCalendar:(TKCalendarMonthView*) calendar1
{
if (calendar1.frame.origin.y == -calendar1.frame.size.height+calendarShadowOffset)
{
[self displayCalendar:calendar1];
}
else
{
[self hideCalendar:calendar1];
}
[self showMyCalendar:calendar1]; // If I put a call here xcode sees it.
}
- (void)anotherMethod:(TKCalendarMonthView*) calendar1
{
if (calendar1.frame.origin.y == -calendar1.frame.size.height+calendarShadowOffset)
{
[self displayCalendar:calendar1];
}
else
{
[self hideCalendar:calendar1];
}
[self showMyCalendar:calendar1]; // If I put a call here xcode DOES NOT see it.
}
Thanks,
Ed
Did you include a method definition in your .h file for the .m file that this method is contained in?
- (void)anotherMethod:(TKCalendarMonthView*) calendar1;
I know when its done loading... (webViewDidFinishLoad), but I want to use
[webView.layer renderInContext:UIGraphicsGetCurrentContext()];
to create an image from the UIWebView. Occasionally I get the image prior to the webView finishing its rendering. I can use performSelector to delay the get of the image, but the amount of wait is arbitrary and brittle.
This may depend upon the kind of graphics context you need the view rendered into, but you can call
- (void)drawRect:(CGRect)area forViewPrintFormatter:(UIViewPrintFormatter *)formatter
which apparently tricks the UIWebView into thinking that it's being printed. This may help if your ultimate goal is to capture the complete page. We've had the problem recently in which even if the page was fully loaded, calling plain old -drawRect: didn't render the entire page if some of it was offscreen.
I had a similar problem in an application where I fully control the content. This won't work if you load arbitrary pages from the web but it may work if you know your high-level DOM structure and it doesn't change much.
What worked for me was the following.
I had to recursively find the first UIWebView's subview of type UIWebOverflowScrollView with a frame of (0, 0, 1024, 768). If I couldn't find one, that was a sure sign the content hasn't been rendered yet.
Having found it, I check this view's layer.sublayers.count. When the rendering finishes, I would always end up with one or three sublayers. If the rendering hasn't finished, however, the content view always had at most one sublayer.
Now, that's specific to my DOM structure—but it may be possible that you make up a similar “test” if you compare sublayer tree before and after rendering. For me, the rule of thumb was “the first recursively found subview of type UIWebOverflowScrollView will have at least three sublayers”, for you it will likely be different.
Anyway, take great care if you decide to use this approach, for even though you won't get rejected for looking into UIWebView's view and layer hierarchy, this kind of behaviour is unreliable and is very likely to change in future versions of iOS. It may very well be inconsistent between iOS 5 and iOS 6 as well.
Finally, code snippets for MonoTouch which should be straightforward to translate to Objective C:
bool IsContentScrollView (UIScrollView scrollView)
{
return scrollView.Frame.Equals (new RectangleF (0, 0, 1024, 768));
}
[DllImport ("/usr/lib/libobjc.dylib")]
private static extern IntPtr object_getClassName (IntPtr obj);
public static string GetClassName (this UIView view) {
return Marshal.PtrToStringAuto (object_getClassName (view.Handle));
}
bool IsWebOverflowScrollView (UIScrollView scrollView)
{
return scrollView.GetClassName () == "UIWebOverflowScrollView";
}
IEnumerable<UIScrollView> ScrollViewsInside (UIView view)
{
foreach (var subview in view.Subviews) {
foreach (var scrollView in ScrollViewsInside (subview).ToList())
yield return scrollView;
if (subview is UIScrollView)
yield return (UIScrollView)subview;
}
}
bool CanMakeThumbnail {
get {
var scrollViews = ScrollViewsInside (this).Where (IsWebOverflowScrollView).ToList ();
var contentView = scrollViews.FirstOrDefault (IsContentScrollView);
// I don't know why, but this seems to be a good enough heuristic.
// When the screen is black, either contentView is null or it has just 1 sublayer.
// This may *break* on any iOS updates or DOM changes--be extra careful!
if (contentView == null)
return false;
var contentLayer = contentView.Layer;
if (contentLayer == null || contentLayer.Sublayers == null || contentLayer.Sublayers.Length < 3)
return false;
return true;
}
}
What about using the window.onload or jQuerys $(document).ready event to trigger the shouldStartLoad callback?
Something like:
$(document).ready(function(){
location.href = "app://do.something"
})
and in your UIWebViewDelegate do something like:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSURL *url = request.URL;
if([url.host isEqualToString:#"app"]){
//maybe check for "do.something"
//at this point you know, when the DOM is finished
}
}
With this method you can forward every possible event from the JS code to your obj-c code.
Hope that helps. The code sample is written in the browser, and therefore not tested! ;-)
- (void)webViewDidFinishLoad:(UIWebView *)webView {
if (webView.isLoading)
return;
else
{
[self hideProgress];
}
}
If you have access to the HTML file and can edit it - then just add some special variable which will tell the code that rendering is done.
Then use method stringByEvaluatingJavaScriptFromString to know should you start some actions or not.
The example below is pretty dirty, but hope it will help and give you the idea:
(void)webViewDidFinishLoad:(UIWebView *)webView
{
BOOL renderingDone = NO;
while (!(renderingDone == [[webView stringByEvaluatingJavaScriptFromString:#"yourjavascriptvariable"] isEqualToString:#"YES"]))
{
// the app will be "freezed" till the moment when your javascript variable will not get "YES" value
}
// do your stuff, rendering is done
}
if (webView.loading == YES)
{
//Load image
}
Something like that might work.
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.