Trying to learn by digging through some code.
Have an accessory button, when its pressed it loads up a menu so the user can pick either a picture from photo library or video (works perfect) have also a sticker menu that pops up similarly to the photo library. The sticker pops up accordingly, only have one if that matters. However when I select it nothing happens. The view is dismissed and it should be sent however it is not.v THE NSLog prints my check statement, I have even added the JSQ sound to play and it does it as well. Working with Firebase/Parse.
It seems to call everything just doesn't attach the PNG from the grid view to the message.
StickerView.m
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
[collectionView deselectItemAtIndexPath:indexPath animated:YES];
NSString *file = stickers2[indexPath.item];
NSString *sticker = [file stringByReplacingOccurrencesOfString:#"#2x.png" withString:#""];
if (delegate != nil) [delegate didSelectSticker:sticker];
NSLog(#"Sticker Sent");
[self dismissViewControllerAnimated:YES completion:nil];
}
Chat.m
- (void)didSelectSticker:(NSString *)sticker
{
[self messageSend:sticker Video:nil Picture:nil Audio:nil];
}
messageSend
- (void)messageSend:(NSString *)text Video:(NSURL *)video Picture:(UIImage *)picture Audio:(NSString *)audio
{
Outgoing *outgoing = [[Outgoing alloc] initWith:groupId View:self.navigationController.view];
[outgoing send:text Video:video Picture:picture Audio:audio Sticker:sticker];
[JSQSystemSoundPlayer jsq_playMessageSentSound];
[self finishSendingMessage];
}
LOG
NSString *file = stickers2[indexPath.item];
NSLog (#"%d",indexPath.item);
NSLog (#"%#",file);
NSString *sticker = [file stringByReplacingOccurrencesOfString:#"#2x.png" withString:#"#2x.png"];
NSLog (#"%#",sticker);
if (delegate != nil) [delegate didSelectSticker:sticker];
NSLog(#"Sticker Sent");
[self dismissViewControllerAnimated:YES completion:nil];
indexPath returns a numerical value based on which sticker is selected. These are loaded into the mutableArray based on file name. If the file name contains a string of #"foo" it is loaded into the array. it could be one or one million value based on how many are loaded in. However it does correspond correctly to the selections. One is properly returned when object one is selected and 2 is properly selected and so on.
File name is returned as the correct file name.
Sticker was returned incorrectly based on your suspicious line of code, I have replaced it to not removed the extensions but did not delete it as of yet. Now returning the same value as File.
Outgoing
- (void)send:(NSString *)text Video:(NSURL *)video Picture:(UIImage *)picture Audio:(NSString *)audio Sticker:(NSString *)sticker
{
NSMutableDictionary *item = [[NSMutableDictionary alloc] init];
item[#"userId"] = [PFUser currentId];
item[#"name"] = [PFUser currentName];
item[#"date"] = Date2String([NSDate date]);
item[#"status"] = TEXT_DELIVERED;
item[#"video"] = item[#"thumbnail"] = item[#"picture"] = item[#"audio"] = item[#"latitude"] = item[#"longitude"] = #"";
item[#"video_duration"] = item[#"audio_duration"] = #0;
item[#"picture_width"] = item[#"picture_height"] = #0;
if (text != nil) [self sendTextMessage:item Text:text];
else if (video != nil) [self sendVideoMessage:item Video:video];
else if (picture != nil) [self sendPictureMessage:item Picture:picture];
else if (audio != nil) [self sendAudioMessage:item Audio:audio];
else if (sticker !=nil) [self sendSticker:item Sticker:sticker];
else [self sendLoactionMessage:item];
}
sendSticker
- (void)sendSticker:(NSMutableDictionary *)item Sticker:(NSString *)sticker
{
item[#"sticker"] = sticker;
NSLog(#"%#",sticker);
[self sendMessage:sticker]
}
NSLog never gets called so I know that this is not getting called. I must be missing something, after the user selects the sticker.
Sticker.h
#protocol StickersDelegate
- (void)didSelectSticker:(NSString *)sticker;
#end
#interface StickersView : UIViewController <UICollectionViewDataSource, UICollectionViewDelegate>
#property (nonatomic, assign) IBOutlet id<StickersDelegate>delegate;
#end
I see a couple of potential problem areas, but nothing definitive. I'd check the following either using a debugger or by adding additional logging:
What's the actual numerical value of indexPath.item? Does it represent a "valid" index into your array stickers2?
What is the value of file after you look it up? Is it a valid string, and does it correctly represent the name of an actual file on your file system?
What does the Outgoing class expect for the value of text in its -send:Video:Picture:Audio: method, a fully-qualified path name, or just a simple file name with no path? If the former, you'll either need to look up the system directory and construct the full file path before passing it in, or it would have needed to be constructed that way originally and inserted as such into stickers2, and if the latter, I'm assuming you will have needed to provided a "base" directory to Firebase/Parse somewhere.
This line is suspicious:
NSString *sticker = [file stringByReplacingOccurrencesOfString:#"#2x.png" withString:#""];
Do you mean to strip off the extension entirely? In other words, if you have a base filename of "foo#2x.png", then this will change "foo#2x.png" to "foo", is that what you really want. I'm guessing you might want this to be "foo.png" instead:
NSString *sticker = [file stringByReplacingOccurrencesOfString:#"#2x.png" withString:#".png"];
?
Hope this helps you track it down.
This question is about printing ALL content (including off screen content) of WKWebView. Currently (still, as of iOS 10.2 or OSX 10.12) there is NO working solution and none of the supposed solutions on Stackoverflow work. Only provide an answer here if you have verified for yourself that you can print OFF SCREEN CONTENT, and if you did then provide the working example code.
I'm trying to print ALL the content of a WKWebView or WebView on OSX 10.10 or above (Currently running on 10.11.2). For example, a wide html table where columns are out of view and off to the right. Earlier versions of OSX would automatically paginate and correctly print all the html.
I've tried using the solutions available here on Stackoverflow and elsewhere. All essentially say the same thing which is to print the documentView like so:
[[NSPrintOperation printOperationWithView:_webView.mainFrame.frameView.documentView printInfo:pInfo] runOperation];
This stopped working for both WKWebView or WebView in 10.10. If you do this:
[[NSPrintOperation printOperationWithView:_wkWebView printInfo:pInfo] runOperation];
You get pagination but the printout includes scroll bars WebView, and the other WKWebView gives you blank pages.
I can't find any mention whatsoever in Apple documentation about printing for WKWebView on OSX. Nor can I find any answer that is OSX specific and not iOS.
Does anyone have ANY idea how to print these on OSX?
UPDATE: This is a bug in WebView [Radar:23159060] (still open 2/2018) and WKWebView does not even appear to address printing on OSX. After examining the Open Source for this class on the net, I see that all of the classes that have anything to do with printing are in a conditional compilation block that only supports platform: iOS.
UPDATE Part Deux: Amazingly this ridiculous bug exists in ALL implementations of this class including those on iOS! I find it ridiculous that this is still not fixed at this late date despite the documentation's statement to use this (and only this class) in Apps that support iOS 8 or above. It is now IMPOSSIBLE to print all the on screen and off screen content of a WebView on either iOS or OSX. Fail Apple. Time to FIX THIS! We all know what Steve would've said about it....
UPDATE Part THREE :)- Yet still more amazing is that this issue is NOT resolved as of 10.15.2 and coming up on 4+ YEARS!!! this issue has been floating around (Apple waaaaaake up up up up....). It's kind of amazing considering they're getting very pushy about using WKWebView and over in iOS land even rejecting Apps that don't (unless you're trying to support iOS 7).
UPDATE Part FOUR (2020... can you believe it?!?): As of Big Sur, this is still an issue. I solved it by writing a work around see accepted answer below:
printOperationWithPrintInfo:
DOES NOT print all content which is off screen or scrolled out of view in either the Horizontal or vertical direction. It does however use your Print CSS which is a slight advantage over:
- (void)takeSnapshotWithConfiguration:(WKSnapshotConfiguration *)snapshotConfiguration
completionHandler:(void (^)(NSImage *snapshotImage, NSError *error))completionHandler;
To get it to work I did:
NSPrintInfo *pInfo = [[NSPrintInfo alloc] initWithDictionary:printInfoDict];
pInfo.horizontalPagination = NSPrintingPaginationModeAutomatic;
pInfo.verticalPagination = NSPrintingPaginationModeAutomatic;
pInfo.verticallyCentered = YES;
pInfo.horizontallyCentered = YES;
pInfo.orientation = NSPaperOrientationLandscape;
pInfo.leftMargin = 30;
pInfo.rightMargin = 30;
pInfo.topMargin = 30;
pInfo.bottomMargin = 30;
NSPrintOperation *po = [_webView printOperationWithPrintInfo:pInfo];
po.showsPrintPanel = YES;
po.showsProgressPanel = YES;
// Without the next line you get an exception. Also it seems to
// completely ignore the values in the rect. I tried changing them
// in both x and y direction to include content scrolled off screen.
// It had no effect whatsoever in either direction.
po.view.frame = _webView.bounds;
// [printOperation runOperation] DOES NOT WORK WITH WKWEBVIEW, use
[po runOperationModalForWindow:self.view.window delegate:self didRunSelector:#selector(printOperationDidRun:success:contextInfo:) contextInfo:nil];
**Also there is something going on I don't fully understand. It doesn't seem to matter what size your wkWebView is. If I size the App to hide some of the content it still seems to grab as much that IS off screen as will fit on the page specified, but it doesn't seem to know how to paginate content that will not fit on the page size onto other pages. So that appears to be where the issue is. There may be some way around this and if anyone has a clue post it here!!
I've successfully used the SPI -[WKWebView _printOperationWithPrintInfo:] passing the usual [NSPrintInfo sharedPrintInfo].
Note that you CAN'T use -runOperation on the returned NSPrintOperation. You must use -runOperationModalForWindow:.... which is quite similar. The problem resides in the WebKit internals that expects a running runloop and a preview to be made internally to know the number of pages.
It definitely works with offscreen content, if what you mean by offscreen is "not fully displayed on screen". I still have a WKWebView displayed in a window, but it's very tiny and only displays a very short fraction of the entire webview content (21 A4 pages!). Hope this helps!
PS: Tested on 10.12, 10.14 and 10.15.
Code is like this:
SEL printSelector = NSSelectorFromString(#"_printOperationWithPrintInfo:"); // This is SPI on WKWebView. Apparently existing since 10.11 ?
NSMutableDictionary *printInfoDict = [[[NSPrintInfo sharedPrintInfo] dictionary] mutableCopy];
printInfoDict[NSPrintJobDisposition] = NSPrintSaveJob; // means you want a PDF file, not printing to a real printer.
printInfoDict[NSPrintJobSavingURL] = [NSURL fileURLWithPath:[#"~/Desktop/wkwebview_print_test.pdf" stringByExpandingTildeInPath]]; // path of the generated pdf file
printInfoDict[NSPrintDetailedErrorReporting] = #YES; // not necessary
// customize the layout of the "printing"
NSPrintInfo *customPrintInfo = [[NSPrintInfo alloc] initWithDictionary:printInfoDict];
[customPrintInfo setHorizontalPagination: NSPrintingPaginationModeAutomatic];
[customPrintInfo setVerticalPagination: NSPrintingPaginationModeAutomatic];
[customPrintInfo setVerticallyCentered:NO];
[customPrintInfo setHorizontallyCentered:NO];
customPrintInfo.leftMargin = 0;
customPrintInfo.rightMargin = 0;
customPrintInfo.topMargin = 5;
customPrintInfo.bottomMargin = 5;
NSPrintOperation *printOperation = (NSPrintOperation*) [_webView performSelector:printSelector withObject:customPrintInfo];
[printOperation setShowsPrintPanel:NO];
[printOperation setShowsProgressPanel:NO];
// BOOL printSuccess = [printOperation runOperation]; // THIS DOES NOT WORK WITH WKWEBVIEW! Use runOperationModalForWindow: instead (asynchronous)
[printOperation runOperationModalForWindow:self.window delegate:self didRunSelector:#selector(printOperationDidRun:success:contextInfo:) contextInfo:nil]; // THIS WILL WORK, but is async
After 5 years I've managed to solve the original problem and which was forced by the fact that the MacOS 11 implementation of WKWebView printOperationWithPrintInfo still doesn't properly handle content scrolled out of view and off to the right.
The root issue seems to be that content outside the bounds of the clipping region (especially to the right) is not properly handled. This may be a WKWebView bug, because it seems to handle some content below the visible rect in the vertical direction.
After much digging, and seeing that others had been able to get the entire content of an NSView to print and properly paginate by having:
The view detached (not on screen).
Setting the frame to the size of the entire content.
Then calling printWithPrintInfo on the detached view.
I had an idea for a solution:
Extend WKWebView via a Category with functions that get all the content as image tiles. It does this on MacOS via JavaScript and on iOS by manipulating the UIScrollView associated with the WKWebView to get the full content size and then scrolling the various parts of the content into the visible area and snapshotting it as a grid of image tiles.
Create a subclass of NSView or UIView that draws all the tiles in their proper relation.
Call printWithPrintInfo on the detached view.
It works well on MacOS 10.14+ iOS 13+
On both platforms all the output is properly paginated (iOS requires use of UIPrintPageRenderer which is included in the associated GitHub project) and you can use the open as PDF in Preview and save it as a file, etc.
The only drawback I've encountered is that Print CSS is NOT used, not that that matters much given that Apple's support for Print CSS is currently minimal.
All the working code is on GitHub here: Full working source for iOS and MacOS
THIS Source is Out of Date See Github
The Header
//
// WKWebView+UtilityFunctions.h
// Created by Clifford Ribaudo on 12/24/20.
//
#import <WebKit/WebKit.h>
#ifdef _MAC_OS_ // Up to user to determine how they know this
#define IMAGE_OBJ NSImage
#define VIEW_OBJ NSView
#else
#define IMAGE_OBJ UIImage
#define VIEW_OBJ UIView
#endif
#interface TiledImageView : VIEW_OBJ
{
NSArray *_imageTiles;
}
-(void)printWithPrintInfo:(NSPrintInfo *)pi;
-(instancetype)initWithFrame:(CGRect)frame imageTiles:(NSArray<NSArray *> *)imageTiles;
#end
#interface WKWebView (UtilityFunctions)
-(void)HTMLPageMetrics:(void (^)(CGSize htmlDocSize, CGSize visibleSize, NSError *error))completionHandler;
-(void)currentScrollXY:(void (^)(float x, float y, NSError *error))completionHandler;
-(void)scrollHTMLTo:(float)x topY:(float)y completionHandler:(void (^)(NSError *error))completionHandler;
-(void)imageTilesForHTMLPage:(CGSize)pageSize visbleRect:(CGSize)visibleRect imgData:(NSMutableArray<NSArray *> *)tileData completionHandler:(void (^)(NSError *error))completionHandler;
-(void)imageTile:(CGRect)imgRect fromPageOfSize:(CGSize)pageSize inViewOfSize:(CGSize)viewSize completionHandler:(void (^)(IMAGE_OBJ *tileImage, NSError *error))completionHandler;
#end
The Implementation
//
// WKWebView+UtilityFunctions.m
// Created by Clifford Ribaudo on 12/24/20.
//
// Works with MacOS v10.14+ and ??iOS 13+
//
#import "WKWebView+UtilityFunctions.h"
#implementation TiledImageView
-(instancetype)initWithFrame:(CGRect)frame imageTiles:(NSArray<NSArray *> *)imageTiles
{
self = [super initWithFrame:NSRectFromCGRect(frame)];
if(self) {
_imageTiles = imageTiles;
}
return self;
}
-(BOOL)isFlipped {return YES;}
-(void)printWithPrintInfo:(NSPrintInfo *)pi
{
NSPrintOperation *po = [NSPrintOperation printOperationWithView:self];
po.printInfo = pi;
[po runOperation];
}
- (void)drawRect:(NSRect)rect
{
for(NSArray *imgData in _imageTiles)
{
NSRect drawRect = ((NSValue *)imgData[0]).rectValue;
IMAGE_OBJ *img = imgData[1];
[img drawInRect:drawRect];
}
}
#end
#implementation WKWebView (UtilityFunctions)
//
// Returns via Completion Handler:
// htmlDocSize - The size of the entire <HTML> element, visible or not
// visibleSize - The visible dimensions of the page, essentially WKWebView bounds minus HTML scroll bar dimensions
//
-(void)HTMLPageMetrics:(void (^)(CGSize htmlDocSize, CGSize visibleSize, NSError *error))completionHandler
{
//
// Anonymous Function - gets Size of entire HTML element and visible size.
// Result String = Full X, Full Y, Visible X, Visible Y
//
NSString *jsGetPageMetrics = #"(function(){return document.documentElement.scrollWidth + ',' + document.documentElement.scrollHeight + ',' + document.documentElement.clientWidth + ',' +document.documentElement.clientHeight;})();";
// Execute JS in WKWebView
[self evaluateJavaScript:jsGetPageMetrics completionHandler:^(id result, NSError *error)
{
CGSize htmlSize = CGSizeMake(0, 0);
CGSize visibleSize = CGSizeMake(0, 0);
if(!error && result)
{
NSArray<NSString *> *data = [[NSString stringWithFormat:#"%#", result] componentsSeparatedByString:#","];
htmlSize = CGSizeMake([data[0] floatValue], [data[1] floatValue]);
visibleSize = CGSizeMake([data[2] floatValue], [data[3] floatValue]);
}
else
NSLog(#"JS error getting page metrics: %#", error.description);
completionHandler(htmlSize, visibleSize, error);
}];
}
//
// Get <HTML> element current scroll position (x,y) and return to completeion handler:
// x = document.documentElement.scrollLeft
// y = document.documentElement.scrollTop
//
-(void)currentScrollXY:(void (^)(float X, float Y, NSError *error))completionHandler
{
NSString *jsGetPageMetrics = #"(function(){return document.documentElement.scrollLeft + ',' + document.documentElement.scrollTop;})();";
// Execute JS in WKWebView
[self evaluateJavaScript:jsGetPageMetrics completionHandler:^(id result, NSError *error) {
if(!error && result)
{
NSArray<NSString *> *data = [[NSString stringWithFormat:#"%#", result] componentsSeparatedByString:#","];
completionHandler([data[0] floatValue], [data[1] floatValue], error);
}
else {
NSLog(#"JS error getting page metrics: %#", error.localizedDescription);
completionHandler(0, 0, error);
}
}];
}
//
// Scroll the current HTML page to x, y using scrollTo(x,y) on the <HTML> element
// Optional Completion Handler to do something when scroll finished
//
-(void)scrollHTMLTo:(float)x topY:(float)y completionHandler:(void (^)(NSError *error))completionHandler
{
NSString *js = [NSString stringWithFormat:#"document.documentElement.scrollTo(%0.f, %0.f);", x, y];
// Execute JS in WKWebView
[self evaluateJavaScript:js completionHandler:^(id result, NSError *error)
{
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, .25 * NSEC_PER_SEC);
dispatch_after(delay, dispatch_get_main_queue(), ^{
if(completionHandler) completionHandler(error);
});
if(error) NSLog(#"JS error scrollTo %#", error.localizedDescription);
}];
}
//
// Called Recursively until tiles are obtained for the entire pageRect.
// Tiles are the size of visibleRect (WKWebView.bounts) but can be smaller.
// tileData - Array of arrays holding CGRect & Img.
//
-(void)imageTilesForHTMLPage:(CGSize)pageSize visbleRect:(CGSize)visibleSize imgData:(NSMutableArray<NSArray *> *)tileData completionHandler:(void (^)(NSError *error))completionHandler
{
__block CGRect currentRect; // In coordinates of pageSize (full).
if(tileData.count == 0) { // No image tiles yet. Start at top left of html page for visible WKWebView bounds
currentRect.origin.x = currentRect.origin.y = 0.0;
currentRect.size = visibleSize;
}
else {
NSArray *lastTile = [tileData lastObject]; // Calculate what the next tile rect is or call handler if done.
CGRect lastTileRect;
#ifdef _MAC_OS_
lastTileRect = ((NSValue *)lastTile[0]).rectValue;
#else
lastTileRect = ((NSValue *)lastTile[0]).CGRectValue;
#endif
// Check if anything more to get to right of last tile
if((lastTileRect.origin.x + lastTileRect.size.width) < pageSize.width)
{
currentRect.origin.x = lastTileRect.origin.x + lastTileRect.size.width + 1; // Next x to right of last tile
currentRect.origin.y = lastTileRect.origin.y; // Works on all rows
currentRect.size.height = lastTileRect.size.height;
currentRect.size.width = pageSize.width - currentRect.origin.x; // Get width of next tile to right of last
if(currentRect.size.width > visibleSize.width) // If more tiles to right use visible width
currentRect.size.width = visibleSize.width;
}
else if((lastTileRect.origin.y + lastTileRect.size.height) < pageSize.height) // New Row
{
currentRect.origin.x = 0; // Reset x back to left side of hmtl
currentRect.size.width = visibleSize.width; // Reset width back to view width
currentRect.origin.y = lastTileRect.origin.y + lastTileRect.size.height + 1; // Get y below last row
currentRect.size.height = pageSize.height - currentRect.origin.y;
if(currentRect.size.height > visibleSize.height) // If more rows below use row height
currentRect.size.height = visibleSize.height;
}
else {
completionHandler(nil);
return;
}
}
[self imageTile:currentRect fromPageOfSize:pageSize inViewOfSize:visibleSize completionHandler:^(NSImage *tileImage, NSError *error)
{
if(error || !tileImage) {
NSLog(#"Error getting image tiles %#", error.description);
completionHandler(error);
return;
}
#ifdef _MAC_OS_
[tileData addObject:#[[NSValue valueWithRect:NSRectFromCGRect(currentRect)], tileImage]];
#else
[tileData addObject:#[[NSValue valueWithCGRect:currentRect], tileImage]];
#endif
[self imageTilesForHTMLPage:(CGSize)pageSize visbleRect:(CGSize)visibleSize imgData:(NSMutableArray<NSArray *> *)tileData completionHandler:completionHandler];
}];
}
//
// ImgRect = location of rect in full page size. Has to be translated into what is visible and where.
// pageSize = Full size of HTML page, visible or not.
// viewSize = essentially the wkwebview.bounds.size - HTML scroll bars.
//
-(void)imageTile:(CGRect)imgRect fromPageOfSize:(CGSize)pageSize inViewOfSize:(CGSize)viewSize completionHandler:(void (^)(IMAGE_OBJ *tileImage, NSError *error))completionHandler
{
float x = imgRect.origin.x; // Always do this to make the desired rect visible in the rect of viewSize
float y = imgRect.origin.y;
CGRect rectToGetFromView;
rectToGetFromView.origin.x = 0;
rectToGetFromView.origin.y = 0;
rectToGetFromView.size = imgRect.size;
// If img is smaller than the viewport, determine where it is after scroll
if(imgRect.size.width < viewSize.width)
rectToGetFromView.origin.x = viewSize.width - imgRect.size.width;
if(imgRect.size.height < viewSize.height)
rectToGetFromView.origin.y = viewSize.height - imgRect.size.height;
[self scrollHTMLTo:x topY:y completionHandler:^(NSError *error)
{
if(!error) {
WKSnapshotConfiguration *sc = [WKSnapshotConfiguration new];
sc.rect = rectToGetFromView;
[self takeSnapshotWithConfiguration:sc completionHandler:^(IMAGE_OBJ *img, NSError *error)
{
if(error) NSLog(#"Error snapshotting image tile: %#", error.description);
completionHandler(img, error);
}];
}
else {
NSLog(#"Error scrolling for next image tile %#", error.description);
completionHandler(nil, error);
}
}];
}
#end
Usage
Use the Category in whatever handles printing for your WKWebView like so:
-(void)print:(id)sender
{
// Set this as per your needs
NSPrintInfo *pInfo = [NSPrintInfo sharedPrintInfo];
pInfo.verticallyCentered = YES;
pInfo.horizontallyCentered = NO;
pInfo.horizontalPagination = NSAutoPagination;
pInfo.verticalPagination = NSAutoPagination;
pInfo.orientation = NSPaperOrientationLandscape;
pInfo.bottomMargin = 30;
pInfo.topMargin = 30;
pInfo.leftMargin = 30;
pInfo.rightMargin = 30;
pInfo.scalingFactor = .60;
[_webView HTMLPageMetrics:^(CGSize htmlSize, CGSize visibleSize, NSError *error)
{
self->_imgTileData = [NSMutableArray new];
[self->_webView imageTilesForHTMLPage:htmlSize visbleRect:visibleSize imgData:self->_imgTileData completionHandler:^(NSError *error) {
if(!error) {
TiledImageView *tiv = [[TiledImageView alloc] initWithFrame:CGRectMake(0,0,htmlSize.width,htmlSize.height) imageTiles:self->_imgTileData];
[tiv printWithPrintInfo:pInfo];
}
}];
}
}
Here is the code as a Github Gist: Above code
And from this WKWebView with content below and also scrolled off to the right:
One gets this print dialog with proper pagination:
This isn't the correct answer because Apple needs to supply the correct answer with a working print method or .evaluateJavaScript("window.print()", completionHandler: nil)
But I have a stupid solution that "works" for me and perhaps it will help other people with a work around until then.
Step 1: Grab the HTML and fixup the <body> tag with <body onload='window.print()'>. If you are fetching the html from someplace and not loading your own, you'll want to use some regular expressions. I won't go into that.
Step 2: Save the html in a file someplace and keep the full path in a variable. In my example: filename
Step 3: Wire your print button to this code:
RunCommand(command: "/usr/bin/open \(filename)")
See code for RunCommand below. This leaves a dumb safari window laying around, but it makes it possible to get something printed without saving to a file and then remembering where you stuck it so you can open it with Safari on your own to do the printing.
func RunCommand(command: String) -> (success: Bool, result: String) {
let cc = command.components(separatedBy: " ")
let process = Process()
process.launchPath = cc[0]
var cp: [String] = []
for i in (1..<cc.count) {
cp.append(cc[i])
}
process.arguments = cp
let pipe = Pipe()
process.standardOutput = pipe
process.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
if (data.count > 0) {
let output = String(data: data, encoding: String.Encoding.utf8)
// let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
return (success: true, result: output!)
}
return (success: true, result: "")
}
It would be really nice if Apple could fix this. I have an in-house app that does all its reporting in HTML and reports are something you kind of want to print.
This is a basic implementation of printing content from WKWebView in macOS 11+. You will need a class which is retained in memory, because you'll have to use asynchronous calls. Make your class conform to WKNavigationDelegate to catch when the content has been loaded.
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
[webView display];
[self exportPDFtoURL:tempURL];
}
- (void)exportPDFtoURL:(NSURL*)url forPreview:(bool)preview {
// Save the url for asynchronous callback
self.pdfURL = url;
NSPrintInfo *printInfo = NSprintInfo.sharedPrintInfo;
[printInfo.dictionary addEntriesFromDictionary:#{
NSPrintJobDisposition: NSPrintSaveJob,
NSPrintJobSavingURL: url
}];
NSPrintOperation *printOperation [(WKWebView*)_webView printOperationWithPrintInfo:printInfo];
printOperation.view.frame = NSMakeRect(0,0, printInfo.paperSize.width, printInfo.paperSize.height);
printOperation.showsPrintPanel = NO;
printOperation.showsProgressPanel = YES;
// Print the content and call a selector afterwards
[printOperation runOperationModalForWindow:self.window delegate:self didRunSelector:#selector(printOperationDidRun:success:contextInfo:) contextInfo:nil];
}
- (void)printOperationDidRun:(id)operation success:(bool)success contextInfo:(nullable void *)contextInfo {
// Remove WKWebView from memory
self.webView.navigationDelegate = nil;
[webView.configuration.userContentController removeAllUserScripts];
// Do something with the URL here
}
I have exhausted every possible way to print the WKWebView directly with no success. The only workaround I can think of would be to convert the web page to a PDF object and then print said object. I will update with code if I get the workaround to work.
In iOS, I pass UIView.viewPrintFormatter() to UIActivityViewController to allow users to print everything
let myShare = [webView.viewPrintFormatter()]
let avc = UIActivityViewController(activityItems: myShare, applicationActivities: nil)
present(avc, animated: true, completion: nil)
this is my swift version (updated for OSX 11) of #Altimac's solution.
#available(OSX 11.0, *)
extension BrowserViewController
{
func print(pdf: Bool)
{
guard var printInfoDictionary = NSPrintInfo.shared.dictionary().mutableCopy() as? [NSPrintInfo.AttributeKey: Any] else { return }
printInfoDictionary[NSPrintInfo.AttributeKey.jobDisposition] = pdf ? NSPrintInfo.JobDisposition.preview : NSPrintInfo.JobDisposition.spool
printInfoDictionary[NSPrintInfo.AttributeKey.detailedErrorReporting] = true
let printInfo = NSPrintInfo(dictionary: printInfoDictionary)
printInfo.horizontalPagination = .automatic
printInfo.verticalPagination = .automatic
printInfo.isVerticallyCentered = false
printInfo.isHorizontallyCentered = false
printInfo.leftMargin = 10
printInfo.rightMargin = 10
printInfo.topMargin = 10
printInfo.bottomMargin = 10
let printOperation = theWebView.printOperation(with: printInfo)
printOperation.showsPrintPanel = false
printOperation.showsProgressPanel = false
printOperation.runModal(for: self.view.window!, delegate: self, didRun: nil, contextInfo: nil)
}
}
seems to work for me at least.
I'm trying to test out the iOS 8.1 handoff feature with NSUserActivity between my iPhone and my iPad. For this, I tried both implementing my own solution, and to use Apple's PhotoHandoff project. However, it's not working.
If I provide a webpageURL, the handover works fine, but when I try to use userData or addUserInfoEntriesFromDictionary nothing works, and I can't for the life of me figure out what the catch is to make the data work.
Sample code:
NSUserActivity *activity = [[NSUserActivity alloc] initWithActivityType:#"com.company.MyTestApp.activity"];
activity.title = #"My Activity";
activity.userInfo = # {};
// activity.webpageURL = [NSURL URLWithString:#"http://google.com"];
self.userActivity = activity;
[self.userActivity becomeCurrent];
[self.userActivity addUserInfoEntriesFromDictionary:# { #"nanananan": #[ #"totoro", #"monsters" ] }];
(I'm also unable to make it work with a Mac app with a corresponding activity type)
I hope you found the solution already, but in case somebody stumbles upon this problem too, here is a solution. (Actually not very different from the previous answer)
Create user activity without userInfo, it will be ignored:
NSUserActivity *activity = [[NSUserActivity alloc] initWithActivityType:#"..."];
activity.title = #"Test activity";
activity.delegate = self;
activity.needsSave = YES;
self.userActivity = activity;
[self.userActivity becomeCurrent];
Implement the delegate to react to needSave events:
- (void)userActivityWillSave:(NSUserActivity *)userActivity {
userActivity.userInfo = #{ #"KEY" : #"VALUE" };
}
When needsSave is set to YES this method will be called and userActivity will be updated.
Hope this helps.
To update the activity object’s userInfo dictionary, you need to configure its delegate and set its needsSave property to YES whenever the userInfo needs updating.
This process is described in the best practices section of the Adopting Handoff guide.
For example, with a simple UITextView, you need to specify the activity type ("com.company.app.edit") identifier in the Info.plist property list file in the NSUserActivityTypes array, then:
- (NSUserActivity *)customUserActivity
{
if (!_customUserActivity) {
_customUserActivity = [[NSUserActivity alloc] initWithActivityType:#"com.company.app.edit"];
_customUserActivity.title = #"Editing in app";
_customUserActivity.delegate = self;
}
return _customUserActivity;
}
- (void)textViewDidBeginEditing:(UITextView *)textView
{
[self.customUserActivity becomeCurrent];
}
- (void)textViewDidChange:(UITextView *)textView
{
self.customUserActivity.needsSave = YES;
}
- (BOOL)textViewShouldEndEditing:(UITextView *)textView
{
[self.customUserActivity invalidate];
return YES;
}
- (void)userActivityWillSave:(NSUserActivity *)userActivity
{
[userActivity addUserInfoEntriesFromDictionary:#{ #"editText" : self.textView.text }];
}
FWIW, I was having this issue. I was lucky that one of my Activity types worked and the other didn't:
Activity: Walking
(UserInfo x1,y1)
(UserInfo x2,y2)
(UserInfo x3,y3)
Activity: Standing
(UserInfo x4,y4)
Activity: Walking
etc.
I got userInfo if the handoff occured when standing but not walking. I got other properties such as webpageURL in all cases; just userInfo came through null.
The fix for me was to invalidate & recreate the NSUserActivity object every time (e.g. when Walking to x2/y2 from x1/y1), instead of only when Activity type changed (e.g. from walking to standing). This is very much not the way the doc is written, but fixed the issue on iOS 9.
UPDATE: This workaround doesn't work on iOS 8. You need to implement this via the userActivityWillSave delegate, as gregoryM specified. Per Apple's doc:
To update the activity object’s userInfo dictionary efficiently,
configure its delegate and set its needsSave property to YES whenever
the userInfo needs updating. At appropriate times, Handoff invokes the
delegate’s userActivityWillSave: callback, and the delegate can update
the activity state.
This isn't a "best practice", it is required!
[Note: issue occurred on iOS 9 devices running code built on Xcode 6.x. Haven't tested Xcode 7 yet, and issue may not occur on iOS 8.]
My issue is this: whenever an iPhone user is in call, or is using his or her phone as a hotspot, the iOS 7 status bar is enlarged, thus pushing my Phonegap application's UIWebView off the bottom of the screen. The enlarged status bar is termed the "in-call status bar". See below image:
Stack Overflow answers I have tried to remedy this:
Iphone- How to resize view when call status bar is toggled?
How In-Call status bar impacts UIViewController's view size ? (and how to handle it properly)
Additionally, there does not seem to be any sort of event fired by Phonegap that informs me of the status bar's change. Listening to the Phonegap "pause" event is useless, as 1) it's known to have quirks in iOS and 2) it doesn't really cover the hotspot case.
My Objective-C skills are very minimal, and I only resort to asking this sort of question after putting in the requisite 4+ hours Googling, Stack Overflowing, wailing, etc...
Gods of Stack Overflow, render unto me thine bounteous nerd fury.
Came up with the following solution based on Jef's suggestions. What you'll want to do is the following:
Observe the native didChangeStatusBarFrame delegate
Get size information about the statusbar via native statusBarFrame
Expose information to your webview by triggering an event that passes it
I have setup a Github repo with all the code you find in this answer.
Setup notification in AppDelegate
// Appdelegate.m
- (void)application:(UIApplication *)application didChangeStatusBarFrame:(CGRect)oldStatusBarFrame
{
NSMutableDictionary *statusBarChangeInfo = [[NSMutableDictionary alloc] init];
[statusBarChangeInfo setObject:#"statusbarchange"
forKey:#"frame"];
[[NSNotificationCenter defaultCenter] postNotificationName:#"statusbarchange"
object:self
userInfo:statusBarChangeInfo];
}
Make statusBarChange selector available
// MainViewController.h
#protocol StatusBarChange <NSObject>
-(void)onStatusbarChange:(NSNotification*)notification;
#end
Setup the listener. This gets the origin and size dictionaries from statusBarFrame whenever it changes and fires an event in the webview passing along this data.
// MainViewController.m
- (void)onStatusbarChange:(NSNotification*)notification
{
// Native code for
NSMutableDictionary *eventInfo = [self getStatusBarInfo];
[self notifiy:notification.name withInfo:eventInfo];
}
- (void)notifiy:(NSString*)event withInfo:(NSMutableDictionary*)info
{
NSString *json = [self toJSON:info];
NSString *cmd = [NSString stringWithFormat:#"cordova.fireWindowEvent('\%#\', %#)", event, json];
[self.webView stringByEvaluatingJavaScriptFromString:cmd];
}
- (NSMutableDictionary *)getStatusBarInfo
{
CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame];
NSMutableDictionary *statusBarInfo = [[NSMutableDictionary alloc] init];
NSMutableDictionary *size = [[NSMutableDictionary alloc] init];
NSMutableDictionary *origin = [[NSMutableDictionary alloc] init];
size[#"height"] = [NSNumber numberWithInteger:((int) statusBarFrame.size.height)];
size[#"width"] = [NSNumber numberWithInteger:((int) statusBarFrame.size.width)];
origin[#"x"] = [NSNumber numberWithInteger:((int) statusBarFrame.origin.x)];
origin[#"y"] = [NSNumber numberWithInteger:((int) statusBarFrame.origin.y)];
statusBarInfo[#"size"] = size;
statusBarInfo[#"origin"] = origin;
return statusBarInfo;
}
- (NSString *) toJSON:(NSDictionary *)dictionary {
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary options:NSJSONWritingPrettyPrinted error:&error];
return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
All this allows you to listen for window.statusbarchange event, e.g. like this:
// www/js/index.js
window.addEventListener('statusbarchange', function(e){
// Use e.size.height to adapt to the changing status bar
}, false)
I'd say that this always happens when you return from background, no?
In others words is it possible for the bar to enlarge without your app being at least briefly pushed to the background by incoming call etc?
If so surely you can query the status bar height in your delegates -(void)will(orDid)resume and adjust accordingly?
If it does happen without leaving the foreground that make a little more difficult, we'll need to work out which notifications to observe for, I know there's an audioSession interruption notif in the case of incoming calls, not sure about the hotspot thing but surely there is a notification for that too..
Edit ok here they are, choose one of these notifications to observe..
UIApplicationWillChangeStatusBarFrameNotification
UIApplicationDidChangeStatusBarFrameNotification
Or implement one of these callbacks in your delegate
-application:willChangeStatusBarFrame:
-application:didChangeStatusBarFrame:
I have added the .h .m and .bundle files for irate. I set the preview mode to YES so the Alert View pops up right when my app launches on my phone (I'm testing). It doesn't show the Alert View at all if I don't set the preview mode to YES.
So now it pops up the rating Alert View. In the .m file I tried editing the title, message and button text in the string and it still shows the original title, message and button text even though it does not exist in the .m because I have completely changed it. Does anyone know how to edit this text so that it will edit the text that appears on the Alert View. The code is below as it came from my download of irate. If I change the text in the strings it doesn't change when I test it still shows what is there right now. Going around in circles here and I'm guessing I'm missing something simple, any help would be awesome!
- (NSString *)messageTitle
{
return [_messageTitle ?: [self localizedStringForKey:iRateMessageTitleKey withDefault:#"Rate %#"] stringByReplacingOccurrencesOfString:#"%#" withString:self.applicationName];
}
- (NSString *)message
{
NSString *message = _message;
if (!message)
{
message = (self.appStoreGenreID == iRateAppStoreGameGenreID)? [self localizedStringForKey:iRateGameMessageKey withDefault:#"If you enjoy playing %#, would you mind taking a moment to rate it? It won’t take more than a minute. Thanks for your support!"]: [self localizedStringForKey:iRateAppMessageKey withDefault:#"If you enjoy using %#, would you mind taking a moment to rate it? It won’t take more than a minute. Thanks for your support!"];
}
return [message stringByReplacingOccurrencesOfString:#"%#" withString:self.applicationName];
}
- (NSString *)cancelButtonLabel
{
return _cancelButtonLabel ?: [self localizedStringForKey:iRateCancelButtonKey withDefault:#"No, Thanks"];
}
- (NSString *)rateButtonLabel
{
return _rateButtonLabel ?: [self localizedStringForKey:iRateRateButtonKey withDefault:#"Rate It Now"];
}
- (NSString *)remindButtonLabel
{
return _remindButtonLabel ?: [self localizedStringForKey:iRateRemindButtonKey withDefault:#"Remind Me Later"];
}
As suggested in the iRate documentation on GitHub, there are two ways you can override those strings:
1) you can override the default strings in your app delegate initialize class method:
+ (void)initialize
{
//overriding the default iRate strings
[iRate sharedInstance].messageTitle = NSLocalizedString(#"Rate MyApp", #"iRate message title");
[iRate sharedInstance].message = NSLocalizedString(#"If you like MyApp, please take the time, etc", #"iRate message");
[iRate sharedInstance].cancelButtonLabel = NSLocalizedString(#"No, Thanks", #"iRate decline button");
[iRate sharedInstance].remindButtonLabel = NSLocalizedString(#"Remind Me Later", #"iRate remind button");
[iRate sharedInstance].rateButtonLabel = NSLocalizedString(#"Rate It Now", #"iRate accept button");
}
2) The recommended way is that you can also create your own Localizable.strings file and add those strings found in iRate.h:
//localisation string keys
static NSString *const iRateMessageTitleKey = #"iRateMessageTitle";
static NSString *const iRateAppMessageKey = #"iRateAppMessage";
static NSString *const iRateGameMessageKey = #"iRateGameMessage";
static NSString *const iRateCancelButtonKey = #"iRateCancelButton";
static NSString *const iRateRemindButtonKey = #"iRateRemindButton";
static NSString *const iRateRateButtonKey = #"iRateRateButton";
For example, inside the Localizable.strings file:
"iRateMessageTitle" = "My own rating title";
"iRateAppMessage" = "My own rating message";