Create PDF of Dynamic Size with Typography Using Uiview Template(S)

How to Convert UIView to PDF within iOS?

Note that the following method creates just a bitmap of the view; it does not create actual typography.

(void)createPDFfromUIView:(UIView*)aView saveToDocumentsWithFileName:(NSString*)aFilename
{
// Creates a mutable data object for updating with binary data, like a byte array
NSMutableData *pdfData = [NSMutableData data];

// Points the pdf converter to the mutable data object and to the UIView to be converted
UIGraphicsBeginPDFContextToData(pdfData, aView.bounds, nil);
UIGraphicsBeginPDFPage();
CGContextRef pdfContext = UIGraphicsGetCurrentContext();

// draws rect to the view and thus this is captured by UIGraphicsBeginPDFContextToData

[aView.layer renderInContext:pdfContext];

// remove PDF rendering context
UIGraphicsEndPDFContext();

// Retrieves the document directories from the iOS device
NSArray* documentDirectories = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask,YES);

NSString* documentDirectory = [documentDirectories objectAtIndex:0];
NSString* documentDirectoryFilename = [documentDirectory stringByAppendingPathComponent:aFilename];

// instructs the mutable data object to write its context to a file on disk
[pdfData writeToFile:documentDirectoryFilename atomically:YES];
NSLog(@"documentDirectoryFileName: %@",documentDirectoryFilename);
}

Also make sure you import:
QuartzCore/QuartzCore.h

UIView with Auto layout, get his size

sizeToFit is the wrong API to use if you're using autolayout.

The correct api to use for autolayout is UIView systemLayoutSizeFittingSize:, most likely passing UILayoutFittingCompressedSize. This will return a CGSize which is the minimum size that can bound the view with its specified constraints.

Check this link

EDIT: In response to the additional code that was posted...

I've never used the Masonry library before but it's neat!

UILabels provide an intrinsicContentSize that is large enough to display their text. In order to calculate this size for multi-line text the label needs to know a single dimension that will be fixed, and that is the desired-width dimension via the preferredMaxLayoutWidth property. If you don't provide a preferredMaxLayoutWidth value (defaults to 0), the label calculates an intrinsic content size AS IF it were laying out a single line of text.

In your code you never set the label's preferredMaxLayoutWidth so any layout performed by autolayout that relies on the intrinsicContentSize of the label will not be what you're expecting.

I took your code and distilled it into a new example, which you should be able to copy/paste into a clean viewcontroller subclass:

#import "Masonry.h"

@interface TSViewController ()
@end

@implementation TSViewController
{
UIScrollView* _scrollView;
UIView* _containerView;

UILabel* _titleLabel;
UILabel* _contentLabel;
}

- (void) viewDidLoad
{
[super viewDidLoad];

self.view.backgroundColor = [UIColor darkGrayColor];

// create/configure our scrollview
_scrollView = [UIScrollView new];
_scrollView.translatesAutoresizingMaskIntoConstraints = NO;
_scrollView.backgroundColor = [UIColor scrollViewTexturedBackgroundColor];
[self.view addSubview: _scrollView];

// create/configure our container
_containerView = [UIView new];
_containerView.translatesAutoresizingMaskIntoConstraints = NO;
_containerView.backgroundColor = [UIColor yellowColor];
[_scrollView addSubview: _containerView];

// create/configure our content - title and content labels
_titleLabel = [UILabel new];
_titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
_titleLabel.numberOfLines = 1;
_titleLabel.font = [UIFont fontWithName: @"Courier" size: 80];
_titleLabel.text = @"Lorem Ipsum";
_titleLabel.backgroundColor = [UIColor lightGrayColor];
_titleLabel.textAlignment = NSTextAlignmentCenter;
[_containerView addSubview: _titleLabel];

_contentLabel = [UILabel new];
_contentLabel.translatesAutoresizingMaskIntoConstraints = NO;
_contentLabel.numberOfLines = 0;
_contentLabel.font = [UIFont fontWithName: @"Courier" size: 40];
_contentLabel.text = self.loremIpsum;
[_containerView addSubview: _contentLabel];

// configure constraints for each view:

[_scrollView mas_makeConstraints: ^(MASConstraintMaker *make) {

// glue the scrollview to its superview with a 20 point inset:

make.top.equalTo( self.view.mas_top ).with.offset( 20 );
make.left.equalTo( self.view.mas_left ).with.offset( 20 );
make.right.equalTo( self.view.mas_right ).with.offset( -20 );
make.bottom.equalTo( self.view.mas_bottom ).with.offset( -20 );
}];

[_containerView mas_makeConstraints: ^(MASConstraintMaker *make) {

// per the iOS 6.0 Release Notes, this is how to use auto-layout for a container view
// inside a scrollview. basically, tie the edges of the containerview to the scrollview.
// http://developer.apple.com/library/ios/#releasenotes/General/RN-iOSSDK-6_0/
// see section "Here are some notes regarding Auto Layout support for UIScrollView"

make.top.equalTo( _scrollView.mas_top );
make.left.equalTo( _scrollView.mas_left );
make.right.equalTo( _scrollView.mas_right );
make.bottom.equalTo( _scrollView.mas_bottom );

// match the width of the containerview to the scrollview width:
make.width.equalTo( _scrollView.mas_width );

// match the height of the containerview to the intrinsic height of the contentlabel + 100 points for our fixed height title

// (this is the magical part.)
make.height.equalTo( _contentLabel.mas_height ).with.offset( 100 );
}];

[_titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {

// tie the contentlabel edges to the sides of its container:
make.top.equalTo( _containerView.mas_top ).with.offset(0);
make.left.equalTo( _containerView.mas_left ).with.offset(0);
make.right.equalTo( _containerView.mas_right ).with.offset(0);

// fixed height
make.height.equalTo( @100 );
}];

[_contentLabel mas_makeConstraints:^(MASConstraintMaker *make) {

// tie top edge to the bottom of our title
make.top.equalTo( _titleLabel.mas_bottom );

// tie the remaining edges to the sides of the container:
make.left.equalTo( _containerView.mas_left );
make.right.equalTo( _containerView.mas_right );
make.bottom.equalTo( _containerView.mas_bottom );
}];
}

- (void) viewWillLayoutSubviews
{
// perform a scrollview layout so the containerView width will be set
[_scrollView setNeedsLayout];
[_scrollView layoutIfNeeded];

// update the preferred layout width of the content label;
// this affects the label's intrinsicContentSize and will force our constraints to be recalculated
CGFloat preferredWidth = _containerView.frame.size.width;
_contentLabel.preferredMaxLayoutWidth = preferredWidth;

// we don't need this since the scrollview is fully using autolayout to adjust the content-size. but just to show that it works:

CGSize fittingSize = [_containerView systemLayoutSizeFittingSize: UILayoutFittingCompressedSize];

NSLog( @"container fitting size: %@", NSStringFromCGSize( fittingSize ));
}

- (NSString*) loremIpsum
{
return @"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing <br>Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type<br> and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.";
}

@end

And, here's the output from systemLayoutSizeFittingSize when the constraints are all working nicely:

2013-08-05 15:01:16.309 testLabelConstraints[8265:c07] container fitting size: {721, 6868}

Objective-C Issues with UIWebView to PDF

I've done this in the past using UIPrintPageRenderer. It's a more versetile way of creating a PDF from a UIWebView, and it's been working well for me so far. I've tested this solution with Xcode 6 and iOS 8.2. Also, tried printing the resulting PDF and everything printed out fine.

When I read the OP, I did some testing with various page sizes, to see if I can get a blank PDF. There are a few key items that I identified, that could contribute to a blank PDF file. I've identified them in the code.

When webViewDidFinishLoad() gets called, the view might not be 100% loaded. A check is necessary, to see if the view is still loading. This is important, as it might be the source of your problem. If it's not, then we are good to go. There is a very important note here. Some web pages are loaded dynamically (defined in the page itself). Take youtube.com for example. The page displays almost immediately, with a "loading" screen. This will trick our web view, and it's "isLoading" property will be set to "false", while the web page is still loading content dynamically. This is a pretty rare case though, and in the general case this solution will work well. If you need to generate a PDF file from such a dynamic loading web page, you might need to move the actual generation to a different spot. Even with a dynamic loading web page, you will end up with a PDF showing the loading screen, and not an empty PDF file.

Another key aspect is setting the printableRect and pageRect. Note that those are set separately. If the printableRect is smaller than the paperRect, you will end up with some padding around the content - see code for example. Here is a link to Apple's API doc with some short descriptions for both.

The example code below adds a Category to UIPrintPageRenderer to create the actual PDF data. The code in this sample has been put together using various resources online in the past, and I wasn't able to find which ones were used to credit them properly.

@interface UIPrintPageRenderer (PDF)
- (NSData*) createPDF;
@end

@implementation UIPrintPageRenderer (PDF)
- (NSData*) createPDF
{
NSMutableData *pdfData = [NSMutableData data];
UIGraphicsBeginPDFContextToData( pdfData, self.paperRect, nil );
[self prepareForDrawingPages: NSMakeRange(0, self.numberOfPages)];
CGRect bounds = UIGraphicsGetPDFContextBounds();
for ( int i = 0 ; i < self.numberOfPages ; i++ )
{
UIGraphicsBeginPDFPage();
[self drawPageAtIndex: i inRect: bounds];
}
UIGraphicsEndPDFContext();
return pdfData;
}
@end

And here is what I have in webViewDidFinishLoad()

- (void)webViewDidFinishLoad:(UIWebView *)webViewIn {
NSLog(@"web view did finish loading");

// webViewDidFinishLoad() could get called multiple times before
// the page is 100% loaded. That's why we check if the page is still loading
if (webViewIn.isLoading)
return;

UIPrintPageRenderer *render = [[UIPrintPageRenderer alloc] init];
[render addPrintFormatter:webViewIn.viewPrintFormatter startingAtPageAtIndex:0];

// Padding is desirable, but optional
float padding = 10.0f;

// Define the printableRect and paperRect
// If the printableRect defines the printable area of the page
CGRect paperRect = CGRectMake(0, 0, PDFSize.width, PDFSize.height);
CGRect printableRect = CGRectMake(padding, padding, PDFSize.width-(padding * 2), PDFSize.height-(padding * 2));

[render setValue:[NSValue valueWithCGRect:paperRect] forKey:@"paperRect"];
[render setValue:[NSValue valueWithCGRect:printableRect] forKey:@"printableRect"];

// Call the printToPDF helper method that will do the actual PDF creation using values set above
NSData *pdfData = [render createPDF];

// Save the PDF to a file, if creating one is successful
if (pdfData) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *path = [paths objectAtIndex:0];

NSString *pdfPath = [path stringByAppendingPathComponent:[NSString stringWithFormat:@"Purchase Order.pdf"]];

[pdfData writeToFile:pdfPath atomically:YES];
}
else
{
NSLog(@"error creating PDF");
}
}

PDFSize is defined as a constant, set to a standard A4 page size. It can be edited to meet your needs.

#define PDFSize CGSizeMake(595.2,841.8)

UIWebView dynamic content size

This post has been updated for Swift 5 & WKWebView


So this is a really great function you wrote there, OP!

Here is just a shorter, more elegant version of your code:

// make sure to declare the delegate when creating your webView (add UIWebViewDelegate to class declaration as well)
myWebView.delegate = self

func webViewDidFinishLoad(webView: UIWebView) {
webView.frame.size.height = 1
webView.frame.size = webView.sizeThatFits(CGSize.zero)
}

Migrating to WKWebView

1) import WebKit
2) make your ViewController inherit from WKNavigationDelegate
3) hook up the WKWebView’s delegate: webView.navigationDelegate = self
4) implement the following protocol function:

webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)

After migrating from UIWebView to WKWebView, above approach doesn’t seem to work anymore.

What you can do instead, is change the line with webView.sizeThatFits(CGSize.zero) to:

webView.frame.size = webView.scrollView.contentSize

The full code for WKWebView would then be:

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
webView.frame.size.height = 1
webView.frame.size = webView.scrollView.contentSize
}

How to determine the content size of a WKWebView?

I think I read every answer on this subject and all I had was part of the solution. Most of the time I spent trying to implement KVO method as described by @davew, which occasionally worked, but most of the time left a white space under the content of a WKWebView container. I also implemented @David Beck suggestion and made the container height to be 0 thus avoiding the possibility that the problem occurs if the container height is larger that that of the content. In spite of that I had that occasional blank space.
So, for me, "contentSize" observer had a lot of flaws. I do not have a lot of experience with web technologies so I cannot answer what was the problem with this solution, but i saw that if I only print height in the console but do not do anything with it (eg. resize the constraints), it jumps to some number (e.g. 5000) and than goes to the number before that highest one (e.g. 2500 - which turns out to be the correct one). If I do set the height constraint to the height which I get from "contentSize" it sets itself to the highest number it gets and never gets resized to the correct one - which is, again, mentioned by @David Beck comment.

After lots of experiments I've managed to find a solution that works for me:

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
if complete != nil {
self.webView.evaluateJavaScript("document.body.scrollHeight", completionHandler: { (height, error) in
self.containerHeight.constant = height as! CGFloat
})
}

})
}

Of course, it is important to set the constraints correctly so that scrollView resizes according to the containerHeight constraint.

As it turns out didFinish navigation method never gets called when I wanted, but having set document.readyState step, the next one (document.body.offsetHeight) gets called at the right moment, returning me the right number for height.

Anyway to include file in HTML for meta settings?

I don't think there is a solution using only HTML,


but there is a simple solution that is using JavaScript.

like this:

  1. create a script file,
  2. and put the code of the meta tag

    inside a string
  3. add that code string with a .innerHTML
  4. put the same script link <script src="mymeta.js"> inside <head> tag.

let metaString = `
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>my repetitive title </title>
`;

document.head.innerHTML += metaString;
<head>
<!-- the meta tags we will be added automatically-->
<script src="mymetatags.js"></script>
</head>

<body>
hello world
</body>


Related Topics



Leave a reply



Submit