UIPrintInteractionController printing to multiple AirPrint printers - ios

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.

Related

How to print a specific iFrame from a WKWebView

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!

iOS: method returning wrong value of a boolean variable set inside a block

I have a method which returns a boolean value. It should return true if at least one image/asset has been found from a URL. The code is as shown below.
Inside the block when i print the count of objects in the array, it prints properly. However, outside the block, the count is zero and it does not enter the if block and the method always returns FALSE. I guess this happens because the function is returning before the block is being executed. How do I tackle this issue? How do I ensure that the method returns true if atleast one URL has been added to self.imageURLs inside the block?
-(BOOL)getPhotos
{
self.imagesFound = FALSE;
//get all image url's from database
//for each row returned do the following:
while (sqlite3_step(statement) == SQLITE_ROW)
{
NSString *URLString = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 0)];
NSURL *imageURL = [NSURL URLWithString:URLString];
ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];
[library assetForURL:imageURL resultBlock:^(ALAsset *asset)
{
if (asset) {
//if an image exists with this URL, add it to self.imageURLs
[self.imageURLs addObject:URLString];
NSLog(#"no. of objects in array: %lu", (unsigned long)self.imageURLs.count);
}
}
failureBlock:^(NSError *error)
{
// error handling
NSLog(#"failure-----");
}];
}
if (self.imageURLs.count > 0) {
NSLog(#"self.imageURLs count = %lu", (unsigned long)self.imageURLs.count);
self.imagesFound = TRUE;
}
else
{
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:#"Sorry!" message:#"No photos found" delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil, nil];
[alert show];
}
return self.imagesFound;
}
This is may be because of the blocks are sometimes asynchronous, that is after executing
[library assetForURL:imageURL resultBlock:^(ALAsset *asset).....
It wont wait for completing the execution of block, it will immediately executes the next statements.
check which log is printing first
NSLog(#"no. of objects in array: %lu", (unsigned long)self.imageURLs.count);
or
a log after the
[library assetForURL:imageURL resultBlock:^(ALAsset *asset).....
A block can therefore maintain a set of state (data) that it can use
to impact behavior when executed.
Write all code after the execution completion of your block and send a notification instead of returning the value.
[library assetForURL:imageURL resultBlock:^(ALAsset *asset)
{
if (asset)
{
//if an image exists with this URL, add it to self.imageURLs
[self.imageURLs addObject:URLString];
self.imagesFound = TRUE;
//send a notificaton from here as TRUE,
}
}failureBlock:^(NSError *error)
{
//self.imagesFound = NO;
//display error alert.
//send a notificaton from here as FALSE,
}];
This kind of thing is pretty much exactly what either delegates or completion handlers on blocks are for. Because your block is asynchronous, it moves on and executes the rest of your code before any chance of your boolean being set.
To do it via delegation, you would:
create a class that when instantiated does what you want it to do on a background thread, and set the caller as a delegate.
When it is done, call back the delegate method, and then carry your script on accordingly from there.
OR simply add a completion handler which will kick off the rest of your code that is dependent on the result that you are currently not getting at all.
In my experience, although the delegation takes longer to write, and is much more difficult to get your head around if you're new, it makes your code more reusable, and better abstracted in order to be able to be reused elsewhere in your application where the same asynchronous count or operation is required.

Copying from "id" variable

I have created a class NetCalculator which I am calling when a button is pressed. The method calculate network it gets 2 NSStrings and returns an id object (either "Network" object or "UIAlertView". Then I am checking which object is and I present the data. When I am using the UIAlertView the app is crashing after showing 2-3 alerts.
Any ides why this happens? On terminal it doesnt show any error just some random hexadecimal.
-(IBAction)calculate:(id)sender {
id result;
Network *network = [[Network alloc]init];
NetCalculator *netCalculated = [[NetCalculator alloc] init];
result = [netCalculated calculateNetworkWithIP:ipLabel.text andSubnet:subnetLabel.text];
if([result isKindOfClass:[Network class]]){
network = result;
NSLog(#"network %#",network.networkIP);
}
else if([result isKindOfClass:[UIAlertView class]]) {
UIAlertView *alert;
alert = result;
[alert show];
}
};
Your code is quite strange to me. Your method calculateNetworkWithIP could return a Network result or a UIAlertView result. I wouldn't follow such an approach.
If the problem relies on memory you should show us hot that method is implemented.
Anyway, I would propose some changes (The following code does not take into account ARC or non ARC code). In particular, I would modify the calculateNetworkWithIP to return a Network result. An error will populated if a problem arises and it is passed as an argument.
- (Network*) calculateNetworkWithIP:(NSString *)ip subnet:(NSString*)subnet error:(NSError**)error
If all is ok, the result would be be a Network and so print it or reused it somewhere. Otherwise an NSError would be returned and based on that create and show an alert view.
So, here pseudo code to do it.
NetCalculator *netCalculated = [[NetCalculator alloc] init];
NSError* error = nil;
Network* networkResult = [netCalculated calculateNetworkWithIP:ipLabel.text subnet:subnetLabel.text error:&error];
if(error != nil) {
// create and show an alert view with the error you received
} else {
// all ok so, for example, save the result in a instance variable
}
To follow a similar approach you can take a look at why is "error:&error" used here (objective-c).

iOS Error: request (0x_) other than the current request(0x0) signalled it was complete on connection 0x_

I'm using Nico Kreipke's FTPManager (click here to go to GiHub) to download some data from an FTP address.
The code works if it's run before the user's first interaction, after that it will usually fail (about 9 out of 10).
When it fails, the following message is written (0x_ are actually valid addresses):
request (0x_) other than the current request(0x0) signalled it was complete on connection 0x_
That message isn't written by neither my code nor by FTPManager, but by Apple's. On its GitHub, I've found some one with the same error, but the source of it could possible be the same as mine. (That person wasn't using ARC.)
If I try to print the objects of those addresses with the pocommand, the console writes that there's no description available.
Also, the memory keeps adding up until the app receives a memory warning, and soon after the OS terminates it.
By pausing the app when that message appears, I can see that the main thread is in a run loop.
CFRunLoopRun();
The Code
self.ftpManager = [[FTPManager alloc] init];
[self downloadFTPFiles:#"192.168.2.1/sda1/1668"];
ftpManageris a strong reference.
The downloadFTPFiles: method:
- (void) downloadFTPFiles:(NSString*) basePath
{
NSLog(#"Reading contents of path: %#", basePath);
FMServer* server = [FMServer serverWithDestination: basePath username:#"test" password:#"test"];
NSArray* serverData = [self.ftpManager contentsOfServer:server];
NSLog(#"Number of items: %d", serverData.count);
for(int i=0; i < serverData.count; i++)
{
NSDictionary * sDataI = serverData[i];
NSString* name = [sDataI objectForKey:(id)kCFFTPResourceName];
NSNumber* type = [sDataI objectForKey:(id)kCFFTPResourceType];
if([type intValue] == 4)
{
NSLog(#"%# is Folder", name);
NSString * nextDestination = [basePath stringByAppendingPathComponent: name];
[self downloadFTPFiles:nextDestination];
}
else
{
NSLog(#"%# is File", name);
[self.ftpManager downloadFile:name toDirectory:[NSURL fileURLWithPath:NSHomeDirectory()] fromServer:server];
}
}
}
What I've Done
I've tried running that code on several places:
The app delegate's application:didFinishLaunchingWithOptions:;
The viewDidLoad, viewWillAppear: and viewDidAppear: of the a view controller loaded just after the app launches and a view controller presented later.
By an action triggered with a button event.
The download of the data is always well performed when executed by the delegate or a view controller loaded with the app (with an exception). But when run after the user's first interaction with the app, it'll most likely fail with the mentioned error.
The exception for view controllers loaded before the user's first interaction is when the call is in either the viewWillAppear: or viewDidAppear: methods. When it's called a second time (for example, a tab of a tab bar controller) it'll also, most likely, fail.
The Question
Does anyone have an idea of what may be happening, or if I'm doing something wrong? Or any alternative solution, maybe?
Any help to solve this problem will be welcomed.
Thanks,
Tiago
I ended up sending the downloadFile:toDirectory:fromServer: message inside a dispatch_async block. I've also created an FTPManage for every file downloaded.
It worked, but I have no idea why.
I'm leaving this answer to whomever crosses with this problem.
If anyone can let me know why this technique worked, please comment bellow so I can update the answer.
Here's the new way I'm downloading each file:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
FTPManager *manager = [[FTPManager alloc] init];
[manager downloadFile:name toDirectory:[NSURL fileURLWithPath:path] fromServer:server];
});
Again, If you know why this worked, let me know.
Thanks.
Full Method
- (void) downloadFTPFiles:(NSString*) basePath
{
NSLog(#"Reading contents of path: %#", basePath);
FMServer *server = [FMServer serverWithDestination:basePath username:#"test" password:#"test"];
NSArray *serverData = [self.ftpManager contentsOfServer:server];
NSLog(#"Number of items: %d", serverData.count);
for(int i=0; i < serverData.count; i++)
{
NSDictionary *sDataI = serverData[i];
NSString *name = [sDataI objectForKey:(id)kCFFTPResourceName];
NSNumber *type = [sDataI objectForKey:(id)kCFFTPResourceType];
if([type intValue] == 4)
{
NSLog(#"%# is Folder", name);
NSString *nextDestination = [basePath stringByAppendingPathComponent:name];
[self downloadFTPFiles:nextDestination];
}
else
{
NSLog(#"%# is File", name);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
FTPManager *manager = [[FTPManager alloc] init];
[manager downloadFile:name toDirectory:[NSURL fileURLWithPath:path] fromServer:server];
});
}
}
}

Start something asynchronously, then wait on it later only if needed

I want to start a task that runs on another thread "just in case it is needed" to minimize the time the user will have to wait on it later. If there is time for it to complete, the user will not have to wait, but if it has not completed then waiting would be necessary.
Something like, opening a database in viewDidLoad: that will be needed when and if the user pushes a button on the screen. If I wait to open the database until the user actually pushes the button there is a lag. So I want to open it early. Since I don't know how long it will take to open and I don't know how long until the user hits the button, I need a way of saying, if that other task has not completed yet then wait, otherwise just go ahead.
For example:
#implementation aViewController
- (void) viewDidLoad {
[self.dbManager openOrCreateDbWithCompletionHandler: ^(NSError *err) {
if( err ) NSLog( #"There was a problem opening the database" );
}];
}
- (IBAction) goButtonTouched: (id) sender {
// Wait here until the database is open and ready to use.
if( ???DatabaseNotAvailableYet??? ) {
[self putSpinnerOnScreen];
???BlockProgressHereUntilDatabaseAvailable???
[self takeSpinnerOffScreen];
}
// Use the database...
NSManagedObjectContext *context = [self theDatabaseContext];
// Build the search request for the attribute desired
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: NSStringFromClass([Destinations class])];
request.predicate = [NSPredicate predicateWithFormat: #"dId == %#", sender.tag];
request.sortDescriptors = nil;
// Perform the search
NSError *error = nil;
NSArray *matches = [context executeFetchRequest: request error: &error];
// Use the search results
if( !matches || matches.count < 1 ) {
NSLog( #"Uh oh, got a nil back from my Destination fetch request!" );
UIAlertView *alert = [[UIAlertView alloc] initWithTitle: #"No Info"
message: #"The database did not have information for this selection"
delegate: nil
cancelButtonTitle: #"OK"
otherButtonTitles: nil];
[alert show];
} else {
MyOtherViewController *movc = [[MyOtherViewContoller alloc] init];
movc.destDetails = [matches lastObject];
[self.navigationController pushViewController: movc animated: YES];
}
}
#end
My hope is that there is never a spinner on the screen and never any delay for the user but, since I don't know how long it will take for the database connection to be established, I have to be prepared for it not being ready when the user pushes the button.
I can't use the call back for when openOrCreateDbWithCompletionHandler: completes since I don't want to do anything then, only when the user pushes the button.
I thought about using a semaphore but it seems like I would only signal it once (in the completion handler of the openOrCreateDbWithCompletionHandler: call) but would wait on it every time a button was pushed. That seems like it would only work for the first button push.
I thought about using dispatch_group_async() for openOrCreateDbWithCompletionHandler: then dispatch_group_wait() in goButtonTouched: but since openOrCreateDbWithCompletionHandler: does its work on another thread and returns immediately, I don't think the wait state would be set.
I can simply set a my own flag, something like before the openOrCreateDbWithCompletionHandler:, self.notOpenYet = YES;, then in its completion handler do self.notOpenYet = NO;, then in goButtonTouched: replace ???DatabaseNotAvailableYet??? with self.notOpenYet, but then how do I block progress on its state? Putting in loops and timers seems kludgy since I don't know if the wait will be nanoseconds or seconds.
This seems like a common enough situation, I am sure that you have all done this sort of thing commonly and it is poor education on my side, but I have searched stackOverflow and the web and have not found a satisfying answer.
I think, blocking execution is a bad habit unless you are building your own event loop, which is rarely necessary. You also don't need to do any GCD stuff in your case. Just get a feeling for async.
The following should work:
#implementation aViewController
- (void) viewDidLoad {
self.waitingForDB = NO;
self.databaseReady = NO;
[self.dbManager openOrCreateDbWithCompletionHandler: ^(NSError *err) {
if( err ){
NSLog( #"There was a problem opening the database" )
}else{
[self performSelectorOnMainThread:#selector(handleDatabaseReady) withObject:nil waitUntilDone:NO];
};
}];
}
- (void)handleDatabaseReady{
self.databaseReady = YES;
if(self.waitingForDB){
[self takeSpinnerOffScreen];
[self go];
}
}
- (IBAction) goButtonTouched: (id) sender {
// Wait here until the database is open and ready to use.
if( !self.databaseReady ) {
self.waitingForDB = YES;
[self putSpinnerOnScreen];
else{
[self go];
}
}
-(void)go{
// Use the database...
NSManagedObjectContext *context = [self theDatabaseContext];
// Build the search request for the attribute desired
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName: NSStringFromClass([Destinations class])];
request.predicate = [NSPredicate predicateWithFormat: #"dId == %#", sender.tag];
request.sortDescriptors = nil;
// Perform the search
NSError *error = nil;
NSArray *matches = [context executeFetchRequest: request error: &error];
// Use the search results
if( !matches || matches.count < 1 ) {
NSLog( #"Uh oh, got a nil back from my Destination fetch request!" );
UIAlertView *alert = [[UIAlertView alloc] initWithTitle: #"No Info"
message: #"The database did not have information for this selection"
delegate: nil
cancelButtonTitle: #"OK"
otherButtonTitles: nil];
[alert show];
} else {
MyOtherViewController *movc = [[MyOtherViewContoller alloc] init];
movc.destDetails = [matches lastObject];
[self.navigationController pushViewController: movc animated: YES];
}
}
#end
Performing the call to handleDatabaseReady on the main thread guarantees that no race conditions in setting/reading your new properties will appear.
I'd go with the flag. You don't want to block the UI, just show the spinner and return from the goButtonTouched. However, you do need to cancel the spinner, if it is active, in openOrCreateDbWithCompletionHandler:.
This is rather a simple scenario. You make a method that does the stuff. Lets call it doStuff. From main thread, you call performSelectorInBackgroundThread:#selector(doStuff). Do not enable the button by default. Enable it at the end of doStuff so that user won't tap on it until you are ready. To make it more appealing, you can place a spinner in the place of the button and then replace it with the button when doStuff completes.
There are a number of classes and APIs you can use to achieve this kind of thing. You can use NSThread with synchronization primitives like semaphores and events to wait for it to finish when the user actually presses the button. You can use an NSOperation subclass (with an NSOperationQueue), and you can use GCD queues.
I would suggest you take a look at some the information in the Concurrency Programming Guide from Apple.
In your case you would probably be best served adding the operation to a GCD background queue using dispatch_async in combination with a semaphore which you can wait on when the user taps the button. You can check out the question "How do I wait for an asynchronously dispatched block to finish?" for an example.

Resources