I am using a technique similar to the question linked below to capture window.print() and trigger printing of a WKWebView. This works fine for the main page.
Capturing window.print() from a WKWebView
What I am hoping to do is print an iFrame in the page, not the page itself. In JavaScript, I focus the iFrame and call window.print(), which works in Safari on iOS (which uses WKWebView?), and various desktop browsers.
Does anybody know how to use the WKWebView printFormatter to print an iFrame of HTML content?
i've been struggling with the exact same issue for the last 3 days, and was desperately looking across the web for a solution, which I didn't find.
But I finally got it running, so here is my solution for this:
Add the following user script to WKWebViewconfig.userContentController:
WKUserScript *printScript = [[WKUserScript alloc] initWithSource:#"window.print = function() {window.webkit.messageHandlers.print.postMessage('print')}"
injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO];
[webViewConfig.userContentController addUserScript:printScript];
[webViewConfig.userContentController addScriptMessageHandler:self name:#"print"];
=> this is the same as in Capturing window.print() from a WKWebview, with one crucial difference: forMainFrameOnly is set to NO, because we want to inject this in our iframe, and not only in the top window
Define a message handler in the view controller holding your WKWebview:
In the header:
#interface MyViewcontroller : UIViewController <UIGestureRecognizerDelegate, WKNavigationDelegate, WKScriptMessageHandler>
Implementation:
-(void) userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
Use the message handler to retrieve iframe's source (which is your pdf), and use-it as printing item for iOS's PrintController:
-(void) userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
if([message.name isEqualToString:#"print"])
{
[self.webView evaluateJavaScript:#"window.document.querySelector(\"iframe\").src" completionHandler:^(NSString *result, NSError *error)
{
UIPrintInteractionController *printController = UIPrintInteractionController.sharedPrintController;
printController.printingItem = [NSURL URLWithString:result];
UIPrintInteractionCompletionHandler completionHandler = ^(UIPrintInteractionController *printController, BOOL completed, NSError *error)
{
if(!completed)
{
if(error != nil) NSLog(#"Print failed: \nDomain: %#\nError code:%ld", error.domain, (long) error.code);
else NSLog(#"Print canceled");
}
NSLog(#"Print completed!\nSelected Printer ID: %#", printController.printInfo.printerID);
};
if(printController != nil)
[printController presentAnimated:YES completionHandler:completionHandler];
}];
}
else
NSLog(#"Unrecognized message received from web page"); }
NOTE: i use window.document.querySelector(\"iframe\") because our iframe does not have an id, otherwise I would have used window.getComponentByID. This important in case you have multiple iframes, which was not my case
I hope this can help you and others as well!
Related
One really frustrating thing i've came across, is the inability to print to multiple AirPrint printers at once. Don't get this confused with printing out multiple ITEMS to 1 printer.
Example:
We have 3 AirPrint printers.
Each printer needs something printing out.. one after the other.
If you've already got your UIPrinter object, you would call:
- (BOOL)printToPrinter:(UIPrinter *)printer
completionHandler:(UIPrintInteractionCompletionHandler)completion;
In the completion method, the normal process would be to fire the next job in the completion method like so:
- (void)printAllJobs
// let's use an example printer url:
UIPrinter *printer = [[UIPrinter alloc] initWithURL:[NSURL URLWithString:#"ipps://mycomputer.:863/printers/label"]];
// get the printer interaction controller:
UIPrintInteractionController *controller = [UIPrintInteractionController sharedPrintController];
// now print:
[controller printToPrinter:printer completionHandler:^(UIPrintInteractionController * _Nonnull printInteractionController, BOOL completed, NSError * _Nullable error) {
// here.. you would check if the job is complete:
if (completed) {
// print to the next printer:
// THIS METHOD GETS FIRED BUT DOESN'T ACTUALLY PRINT
[self printToNextPrinter];
}
}
}
- (void)printToNextPrinter {
// create next printer:
UIPrinter *nextPrinter = [[UIPrinter alloc] initWithURL:[NSURL URLWithString:#"ipps://mycomputer.:863/printers/roller"]];
// get controller:
UIPrintInteractionController *controller = [UIPrintInteractionController sharedPrintController];
// print:
[controller printToPrinter:printer completionHandler:^(UIPrintInteractionController * _Nonnull printInteractionController, BOOL completed, NSError * _Nullable error) {
// this never gets executed.
}
}
What i've found out:
There's one noticeable thing taking place when this happens; there is a UIAlertController presented on screen that says "Printing to.. xxx" - which shows the printing progress.
Solution:
The solution is to check every 2 seconds to see if there's an alert visible on screen.. if so, repeat the same method, if not then fire the next printing job. It seems like Apple doesn't allow background printing with no alerts, so this is the only way.. other than diving into CUPS which would be a mess.
Example project: http://cl.ly/360k3M3a2y05
I'm playing with the Reddit API for a school project, and came across this library for using it in Objective-C/Swift.
The professor wants us to get our toes wet with Swift, which I'm happy to do, and the goal of the project is to add an extra function onto an existing website's API. (I chose Reddit obviously.)
The mentioned library doesn't have a way to get all the subscriptions for a particular user (only to get one page at a time with the option to paginate), so I want to add the option to get them all in one clean call.
I'm leveraging the method in the aforementioned library that allows you to paginate, the method looks like this:
- (NSURLSessionDataTask *)subscribedSubredditsInCategory:(RKSubscribedSubredditCategory)category pagination:(RKPagination *)pagination completion:(RKListingCompletionBlock)completion {
NSMutableDictionary *taskParameters = [NSMutableDictionary dictionary];
[taskParameters addEntriesFromDictionary:[pagination dictionaryValue]];
NSString *path = [NSString stringWithFormat:#"subreddits/mine/%#.json", RKStringFromSubscribedSubredditCategory(category)];
return [self getPath:path parameters:taskParameters completion:^(NSHTTPURLResponse *response, id responseObject, NSError *error) {
if (!completion) return;
if (responseObject)
{
// A crude check to see if we have been redirected to the login page:
NSString *path = [[response URL] path];
NSRange range = [path rangeOfString:#"login"];
if (range.location != NSNotFound)
{
completion(nil, nil, [RKClient authenticationRequiredError]);
return;
}
// Parse the response:
NSArray *subredditsJSON = responseObject[#"data"][#"children"];
NSMutableArray *subredditObjects = [[NSMutableArray alloc] initWithCapacity:[subredditsJSON count]];
for (NSDictionary *subredditJSON in subredditsJSON)
{
NSError *mantleError = nil;
RKSubreddit *subreddit = [MTLJSONAdapter modelOfClass:[RKSubreddit class] fromJSONDictionary:subredditJSON error:&mantleError];
if (!mantleError)
{
[subredditObjects addObject:subreddit];
}
}
RKPagination *pagination = [RKPagination paginationFromListingResponse:responseObject];
completion([subredditObjects copy], pagination, nil);
}
else
{
completion(nil, nil, error);
}
}];
}
My addition is rather simple, I just call this above method recursively and save the pagination after each successful request, until there's no pages left, and then return the result:
- (void)allSubscribedSubredditsInCategory:(RKSubscribedSubredditCategory)category completion:(void (^)(NSArray *subreddits, NSError *error))completion {
RKPagination *pagination = [RKPagination paginationWithLimit:100];
[self recursiveSubscribedSubredditsWithPagination:pagination subredditsSoFar:[NSArray array] completion:completion];
}
- (void)recursiveSubscribedSubredditsWithPagination:(RKPagination *)pagination subredditsSoFar:(NSArray *)subredditsSoFar completion:(void (^)(NSArray *subreddits, NSError *error))completion {
[self subscribedSubredditsInCategory:RKSubscribedSubredditCategorySubscriber pagination:pagination completion:^(NSArray *newSubreddits, RKPagination *newPagination, NSError *newError) {
// If pagination is nil, we cannot go any further and have reached the end
if (newPagination == nil) {
NSArray *newSubredditsSoFar = [subredditsSoFar arrayByAddingObjectsFromArray:newSubreddits];
NSArray *subredditsWithoutDuplicates = [[NSSet setWithArray:newSubredditsSoFar] allObjects];
completion(subredditsWithoutDuplicates, newError);
} else {
NSArray *newSubredditsSoFar = [subredditsSoFar arrayByAddingObjectsFromArray:newSubreddits];
[self recursiveSubscribedSubredditsWithPagination:newPagination subredditsSoFar:newSubredditsSoFar completion:completion];
}
}];
}
So it looks like this in my viewDidLoad of my view controller:
RKClient.sharedClient().signInWithUsername("includedinproject", password: "includedinproject") { (error) -> Void in
RKClient.sharedClient().allSubscribedSubredditsInCategory(.Subscriber, completion: { (subreddits, error) -> Void in
print(subreddits)
}) <-- error occurs here?
}
However, whenever I call it, I get an EXC_BAD_ACCESS runtime error that doesn't really provide anything other than a memory address, and it appears to be caused at the end of the method in viewDidLoad, as labeled above.
The weird thing that occurs, however, is that this only occurs seemingly on the iPhone 4s simulator. If I build it to run on say, the newest 6s, it works fine. I'm puzzled (it has to work on all simulators for full points).
I went to my professor about it and he has no idea. We emulated the project in Objective-C (rebuilt the project as an Objective-C one) and the call seems to work fine.
My professor even did something with Instruments (not much experience myself) looking at "Zombies" and enabled it in the project as well, and nothing seemed to give him information either, we're both pretty confused.
What is going on here that's causing it to work great in Objective-C, and in Swift if the device isn't a 4s? Example project is at the top.
I'm developing an iOS app using phonegap and HTML5.
After launchImage in app, I open InAppBrowser, which opens promptly.
But, tapping on links in it sometimes does not open the desired page and event "falls through" to main webView.
i.e., InAppBrowser is closed on tapping the link.
Please do help me out as I'm stuck with this for the past two days.
Try this one
Put this code in your MainViewController.m before #end tag
- (BOOL) webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
{
NSURL *url = [request URL];
// Intercept the external http requests and forward to Safari.app
// Otherwise forward to the PhoneGap WebView
if ([[url scheme] isEqualToString:#"http"] || [[url scheme] isEqualToString:#"https"]){
[[UIApplication sharedApplication] openURL:url];
return NO;
}
else {
return [super webView:theWebView shouldStartLoadWithRequest:request navigationType:navigationType];
}
}
Resolved this issue by bypassing the error code. It is a bit of dirty workaround but couldn't help it. In "CDVInAppBrowser.m", modified as below and InAppBrowser loads my url successfully.
- (void)webView:(UIWebView*)theWebView didFailLoadWithError:(NSError*)error
{
//By passing error code
if(error.code == -999)
return;
// log fail message, stop spinner, update back/forward
NSLog(#"webView:didFailLoadWithError - %i: %#", error.code, [error localizedDescription]);
self.backButton.enabled = theWebView.canGoBack;
self.forwardButton.enabled = theWebView.canGoForward;
[self.spinner stopAnimating];
self.addressLabel.text = NSLocalizedString(#"Load Error", nil);
[self.navigationDelegate webView:theWebView didFailLoadWithError:error];
}
I have a issue that seems to have not been answered yet. I am new to cordova so I am not sure if this is a call in the framework. I have a a plugin which uses the Zbar libaray to scan barcodes. The resulting of the scan is managed by a delegate and is returning in the method imagePickerController:didFinishPickingMediaWithInfo: . My plugin calls the scan method but returns after my scan method ends. I need it to return the date to my website which requested it. I need to know how I can get get my scan method to wait for my zbar delegate to complete before sending a responds to my webview. Thank you in advance if you can solve this for me. and no the call [super writeJavascript:jsCallback] doesn't work, I am using cordova not phonegap.
#import "Camera.h"
#import <Cordova/CDV.h>
#import "AppDelegate.h"
#implementation Camera
#synthesize resultStr, command, hasPendingOperation;
//Override
- (void)pluginInitialize
{
NSLog(#"%#", #"init Camera");
}
- (void)scan:(CDVInvokedUrlCommand*)mycommand{
NSLog(#"%#", #"Camera.scan");
self.command = mycommand;
ZBarReaderViewController *reader = [ZBarReaderViewController new];
reader.readerDelegate = self;
reader.supportedOrientationsMask = ZBarOrientationMaskAll;
ZBarImageScanner *scanner = reader.scanner;
// TODO: (optional) additional reader configuration here
// EXAMPLE: disable rarely used I2/5 to improve performance
[scanner setSymbology: ZBAR_I25
config: ZBAR_CFG_ENABLE
to: 0];
// present and release the controller
[self.viewController presentViewController:reader animated:YES completion:nil];
NSLog(#"%#", #"finsihed plugin");
}
- (void) imagePickerController: (UIImagePickerController*) reader
didFinishPickingMediaWithInfo: (NSDictionary*) info
{
// ADD: get the decode results
id<NSFastEnumeration> results =
[info objectForKey: ZBarReaderControllerResults];
ZBarSymbol *symbol = nil;
for(symbol in results)
// EXAMPLE: just grab the first barcode
break;
self.resultStr = symbol.data;
// ADD: dismiss the controller (NB dismiss from the *reader*!)
[reader dismissViewControllerAnimated:YES completion:nil];
CDVPluginResult* pluginResult = nil;
if (self.resultStr != nil && [self.resultStr length] > 0) {
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:self.resultStr];
} else {
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:#"Failed to scan barcode!"];
}
NSLog(#"%#", self.resultStr); //<----- this is the date I need to return to my //webview this issue is scan: has already completed and returned
[self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
}
#end
When you call the plugin from javascript, you provide a callback for success and failure. This callback is the only way for your plugin to communicate values back to your web layer.
Specifically, your javascript will look something like this.
cordova.exec(
function (result) {
// do stuff with plugin result! Hurray!
},
function (error) {
self.alert("Things went downhill, sorry... :\r\r" + error);
},
"PluginName", "MethodName", [parameters]
);
As the plugin result is already communicated through a callback, the fact that it is provided from a delegate should be irrelevant.
Update
Your question inquired about waiting on the Objective-C side. Do not wait. That's not how Cordova is designed, you'll even see warnings pop up in the console if you do not return immediately from the call.
Cordova plugin calls are designed to call asynchronous callbacks, and you have to design your web interface around those.
A common approach is to display a spinner or placeholder text while performing the call
Display spinner or placeholder text ("retrieving data")
Call plugin
In callback:
Remove spinner / placeholder.
Display result.
I discovered the issue, it has to do with the scope of the UIImagePickerController. This object is the only thing that is still within scope when the delegate method runs. I was mistakenly saving the callback ID as a class property, I thought I could retrieve it when the imagePickerController:didFinishPickingMediaWithInfo: method was called.
The fix was simply to extend the ZBAR reader class and add a property so I can store the call back ID. All Cordova plugins need the correct callback ID to return to the JS-side property.
Is it possible to download and add a passbook from within a webview without modifying the app to support the new MIME type or unknown MIME types like Sparrow did?
I have a news ios app with a webview. In the webview I display the news items and a banner. When you click the banner I want to open a url to a .pkpass file and add it to my passbook. Instead I get a FrameLoadInterrupted error and nothing visible happens.
If I open the url from safari this works fine, chrome, since earlier this week (version 23) also opens the url like intended.
Is this some weird strategy from Apple maybe? not allowing this MIME type to properly open from a webview?
My best bet is that the UIWebView is just not capable of handling the Passbook passes. You could however try and catch the downloading in the UIWebViewDelegate method -(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType.
What I mean to say is that you have to handle this part your self, since the http://passkit.com/samples/ I used does not return an url which ends pkpass thus it is totally depended on how you request the passbook files.
If you do in include the .pkpass extension you can check for the extension in the request.
If you know what kind of URL the passbook file is at you write your own download code here and pass it to passbook via the passbook api.
There does not seem to be any great on fix for this, you could load the failed ULR in safari:
- (void) webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
NSLog(#"Webview: %#", error);
if ([error.domain isEqualToString:#"WebKitErrorDomain"] && error.code == 102) {
NSString *failedURL = [error.userInfo objectForKey:NSURLErrorFailingURLStringErrorKey];
if (failedURL == nil) {
return;
}
NSURL *url = [NSURL URLWithString:failedURL];
[[UIApplication sharedApplication] openURL:url];
}
}
But this is just really bad coding.
Okay, talked to the engineers at WWDC and this is a know bug in UIWebView but Apple probably won't fix it because they're encouraging people to adopt the new SFSafariViewController. We did come up with a hack to fix it should you need to support iOS 6-8:
Add the PassKit framework to the project if it isn't already.
#import <PassKit/PassKit.h>
Set up a delegate for the UIWebView (for example the view controller launching the UIWebView)
<UIWebViewDelegate>
Add a class variable to cache the UIWebView requests
NSURLRequest *_lastRequest;
Set the delegate
self.webView.delegate = self;
Add the callback to grab all requests and cache in case of failure
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
_lastRequest = request;
return YES;
}
Add the failure callback and re-fetch the URL to see if it is a pass and if so, present the pass to the user
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
// try to get headers in case of passbook pass
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:_lastRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
// check for PKPass
if ([response.MIMEType isEqualToString:#"application/vnd.apple.pkpass"]) {
NSError *error;
PKPass *pass = [[PKPass alloc] initWithData:data error:&error];
if (error) {
NSLog(#"Error: %#", error);
} else {
PKAddPassesViewController *apvc = [[PKAddPassesViewController alloc] initWithPass:pass];
[self presentViewController:apvc animated:YES completion:nil];
}
}
}];
}
It's a horrible hack for what should be supported, but it works regardless of the extension and should support re-directs. If you want to pile on the bug train, you can reference radar://21314226