Search code examples
ioscocoa-touchpdf-generationcalayer

iOS creating PDF from UIViews


I am currently creating PDF documents from a UIView in iOS by using CALayer and the renderInContext method.

The problem I am facing is the sharpness of labels. I have created a UILabel subclass that overrides drawLayer like so:

/** Overriding this CALayer delegate method is the magic that allows us to draw a vector version of the label into the layer instead of the default unscalable ugly bitmap */
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
    BOOL isPDF = !CGRectIsEmpty(UIGraphicsGetPDFContextBounds());
    if (!layer.shouldRasterize && isPDF)
        [self drawRect:self.bounds]; // draw unrasterized
    else
        [super drawLayer:layer inContext:ctx];
}

This method lets me draw nice crisp text, however, the problem is with other views that I don't have control over. Is there any method that would allow me to do something similar for labels embedded in UITableView or UIButton. I guess I'm looking for a way to iterate through the view stack and do something to let me draw sharper text.

Here is an example: This text renders nicely (my custom UILabel subclass) Imgur

The text in a standard segmented control isn't as sharp:

Imgur

Edit: I am getting the context to draw into my PDF as follows:

UIGraphicsBeginPDFContextToData(self.pdfData, CGRectZero, nil);
pdfContext = UIGraphicsGetCurrentContext();
UIGraphicsBeginPDFPageWithInfo(CGRectMake(0, 0, 612, 792), nil);
[view.layer renderInContext:pdfContext];

Solution

  • I ended up traversing the view hierarchy and setting every UILabel to my custom subclass that overrides drawLayer.

    Here is how I traverse the views:

    +(void) dumpView:(UIView*) aView indent:(NSString*) indent {
        if (aView) {
            NSLog(@"%@%@", indent, aView);      // dump this view
    
            if ([aView isKindOfClass:[UILabel class]])
                [AFGPDFDocument setClassForLabel:aView];
    
            if (aView.subviews.count > 0) {
                NSString* subIndent = [[NSString alloc] initWithFormat:@"%@%@",
                                   indent, ([indent length]/2)%2==0 ? @"| " : @": "];
                for (UIView* aSubview in aView.subviews)
                    [AFGPDFDocument dumpView:aSubview indent:subIndent];
            }
        }
    }
    

    And how I change the class:

    +(void) setClassForLabel: (UIView*) label {
        static Class myFancyObjectClass;
        myFancyObjectClass = objc_getClass("UIPDFLabel");
        object_setClass(label, myFancyObjectClass);
    }
    

    The comparison:

    Old:

    Image

    New:

    Imgur

    Not sure if there is a better way to do this, but it seems to work for my purposes.

    EDIT: Found a more generic way to do this that doesn't involve changing the class or traversing through the whole view hierarchy. I am using method swizzling. This method also lets you do cool things like surrounding every view with a border if you want. First I created a category UIView+PDF with my custom implementation of the drawLayer method, then in the load method I use the following:

    // The "+ load" method is called once, very early in the application life-cycle.
    // It's called even before the "main" function is called. Beware: there's no
    // autorelease pool at this point, so avoid Objective-C calls.
    Method original, swizzle;
    
    // Get the "- (void) drawLayer:inContext:" method.
    original = class_getInstanceMethod(self, @selector(drawLayer:inContext:));
    // Get the "- (void)swizzled_drawLayer:inContext:" method.
    swizzle = class_getInstanceMethod(self, @selector(swizzled_drawLayer:inContext:));
    // Swap their implementations.
    method_exchangeImplementations(original, swizzle);
    

    Worked from the example here: http://darkdust.net/writings/objective-c/method-swizzling