iOS creating PDF from UIViews - ios

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)
The text in a standard segmented control isn't as sharp:
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];

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:
New:
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

Related

IBDesignable UILabel setting text to NSLocalizedString in drawRect or willMoveToSuperview crashes Xcode

I am trying to retroactively localize an app by subclassing an IBDesignable to have a property like so:
#property IBInspectable NSString *localizedKey;
Then I thought I could easily override the proper function in the UILabel's lifecycle to do this:
if (self.localizedKey) self.text = NSLocalizedString(self.localizedKey, nil);
Here's an example of what I tried to do:
- (void) drawRect:(CGRect)rect {
NSLog(#"roundedlabel");
[super drawRect:rect];
[self.layer setCornerRadius:cornerRadius];
[self.layer setMasksToBounds:YES];
[self.layer setBorderWidth:borderWidth];
[self.layer setBorderColor:[borderColor CGColor]];
self.clipsToBounds = YES;
}
- (void) willMoveToSuperview:(UIView *)newSuperview {
[super willMoveToSuperview:newSuperview];
NSLog(#"roundedlabel");
if (self.localizedKey) self.text = NSLocalizedString(self.localizedKey, nil);
}
I also tried moving the if (self.localizedKey) to various locations in drawRect.
My plan was to then set up the Localizable.strings file manually with known localizedKeys assigned working through the XIB files. However, I am finding two things.
It seems as though the normal methods called in the life cycle
of a view are not being called for my IBDesignable UILabel subclass.
I have entered log statements into drawRect and
willMoveToSuperview, and these log statements never print out.
XCode almost always crashes & immediately closes when I try to open
a xib in Interface Builder that contains this subclassed label (so no error messages).
At run time, when I see a view that involves an affected xib, I don't see any sign of the localized string AND I don't see 'roundedlabel' printed anywhere in my log.
What's going on? In particular:
(1) How come these functions don't run at all for my subclassed IBDesignable UILabel subclass?
(2) Is there a way to do what I want to do?
I'd create an extension on UILabel and use runtime key values in the storyboard / XIB to set the localisation keys.
extension UILabel {
func setHBLocalisationKey(key: String) {
self.text = ...
}
}
or
#implementation UILabel (HBLocalisation)
- (void)setHBLocalisationKey:(NSString *)key {
self.text = NSLocalizedString(key, nil);
}
#end
This will now work on any label without requiring a custom subclass and it won't tamper with the Xcode UI which seems to be causing some issues with your current approach.

prepareForInterfaceBuilder doesn't update my storyboard

I've created a simple day calendar view and I'd like to see it rendering live on my storyboard, but I've managed to show only the code in -drawRect:.
Running on the simulator I get this image, that represents the correct final result, but in storyboard I get this.
On storyboard I get this
The CalendarDayView is a subclass of a another view whose purpose is to load a nib with a specific name and add it a as a subview. CalendarDayView is tagged as IB_DESIGNABLE.
It is added as a subview directly on the storyboard.
To show also the label inside the CalendarDayView in the -prepareForInterfaceBuilder method I've written some placeholder text.
- (void) prepareForInterfaceBuilder {
[super prepareForInterfaceBuilder];
self.yearLabel.text = #"2015";
self.monthLabel.text = #"FEBRUARY";
self.dayLabel.text = #"28";
}
Draw rect is shown fine
- (void) drawRect:(CGRect)rect {
self.backgroundColor = [UIColor clearColor];
UIBezierPath * path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:20];
[APP_CALENDAR_VIEW_COLOR setFill];
[path fill];
}
How can I achieve to show labels while editing my storyboard? even if I show the preview nothing is shown.

should adding a UILabel in a custom UIView be in drawRect or initWithFrame / custom setup method

I'm not a full-time iOS dev and have to make some changes to someone else's code. We have a custom view where a UILabel is added in drawRect like this (edited for brevity):
- (void)drawRect:(CGRect)rect
{
UILabel *myLabel=[[UILabel alloc] initWithFrame:CGRectMake(50.0f, 50.0f, 100.0f, 30.0f)];
myLabel.text=#"here is some text";
[self addSubview:myLabel];
}
I have never really seen this and thought that drawRect was ONLY for adding drawing operations (and have only seen UIBezierPaths). Should this be moved to initWithFrame (or a common setup method like setupMyView). Or is ok to leave in drawRect? Is there anything besides custom drawing that should be in drawRect?
Sorry for asking a somewhat basic question but even reading the Apple docs leave a bit to be desired.
Unless there is a very good reason to setup the view's subviews from within the drawRect method (I can think of none) I would strongly suggest leaving drawRect as purely a drawing method and move that addSubview stuff out of there! I would suggest overriding the -init method that is currently used to initiate the parent view. For example:
-(instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:(CGRect)frame]) {
UILabel *myLabel=[[UILabel alloc] initWithFrame:CGRectMake(50.0f, 50.0f, 100.0f, 30.0f)];
myLabel.text=#"here is some text";
[self addSubview:myLabel];
}
return self;
}
I would definitely not do anything like that in drawRect. One method I've used in the past is to add it to layoutSubviews, because at that point the view is aware of the true bounds/frame. You'll want to ensure that you only generate the view stuff once, as layoutSubviews is called many times, such as on rotation. I usually do something like the following, where _viewGenerated is an instance variable:
- (void)layoutSubviews {
[super layoutSubviews];
if (!_viewGenerated) {
[self generateView];
_viewGenerated = YES;
}
}
- (void)generateView {
// do everything with any labels, images, etc., here...
}

Drawing an object of NSString in a Rectangle.

I'm trying to put a string in a rectangular form via drawInRect:withAttributes: method. The official documentation writes: "Draws the receiver with the font and other display characteristics of the given attributes, within the specified rectangle in the currently focused UIView."
This action should be done on a button event. But it doesn't work at all. Nothing is shown.
The code that I use:
- (IBAction)buttonPressed:(id)sender
{
// some area on the top (a part of UIView)
CGRect rect = CGRectMake(50, 50, 100, 100);
// my text
NSString *str = #"Eh, what's wrong with this code?";
// crappy attributes
NSDictionary *attributes = #{NSFontAttributeName: [UIFont boldSystemFontOfSize:8]};
// and finally passing a message
[str drawInRect:rect withAttributes:attributes];
}
Is there a particular kind of areas that might be used for drawing strings in rects? Or is it possible to draw a string in a rect where I'd like it to be? Please, help me understand it better!
But it doesn't work at all. Nothing is shown.
That's because no drawing context is set up when you run that code. You're looking at drawing as something that you can do whenever you feel like it, but in fact drawing is something that you should only do when the system asks you to. At other times, you should instead remember what you want to draw, and then draw it when the time comes.
When is "the time" to draw? It's when your view's -drawRect: method is called. Since your method is an action, it looks like you're probably trying to do the drawing in a view controller. What you want to do instead is to subclass UIView and override -drawRect: to draw what you want.
It doesn't work that way, you first need to make a CGContextRef where you will draw the text.
Your code is fine, just place everything within drawRect.

iOS: Why is drawRect: being called twice, and why does this ivar value seemingly change without any reason?

I have created a UIView subclass in order to implement a custom drawRect method. By putting some logs in the code I found that drawRect is actually getting called twice as the view first gets set up by its view controller. Why is this?
The other issue is that my UIView subclass has an ivar named needsToDrawTools. My designated initializer for this subclass sets the value of needsToDrawTools to YES. The drawRect method then checks this value. If YES, it draws the tools and then sets the value of needsToDrawTools to NO so that it never re-draws the tools.
BUT, somehow the value of needsToDrawTools is NO by the time drawRect is called. Nowhere in my code am I setting it to NO, other than from within the if(needsToDrawTools) statement inside drawRect. But since needsToDrawTools is already NO by the time it reaches that if statement, the code inside the statement never even runs. If I remove that IF statement altogether, then it does run of course and I see what I expect in the view. But I don't want to remove the IF statement because that will result in re-drawing of things that don't need to be re-drawn.
Here's my code:
- (id)initWithParentViewController:(NewPhotoEditingViewController *)vc
{
self = [super init];
if (self) {
parentVC = vc;
needsToDrawTools = YES;
NSLog(#"needsToDrawTools: %i",needsToDrawTools); //Console result: 1
}
return self;
}
#pragma mark - Drawing
- (void)drawRect:(CGRect)rect
{
NSLog(#"needsToDrawTools: %i",needsToDrawTools); //Console result: 0 !!!!!
if (needsToDrawTools){
NSLog(#"drawingTools"); //Never shows up in the console
UIBezierPath *toolPointDragger = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(75, 100, 30, 30)];
[[UIColor blackColor] setStroke];
toolPointDragger.lineWidth = 6;
[toolPointDragger stroke];
[[UIColor blueColor] setFill];
[toolPointDragger fill];
needsToDrawTools = NO;
}
}
So again, my two questions are:
Why is drawRect being called twice? I assume it gets called the first time automatically as part of the view loading process, but I don't know why it then gets called again.
How does needsToDrawTools end up with a value of NO?
It sounds like you've got more than one instance of this view. Perhaps you're creating one programmatically and loading one from a nib? Objective-C will set all ivars to zero (or nil, or NO) when an object is created, and if you're loading an instance of your view from a nib, it won't be initialized with your -(id)initWithParentViewController: and needsToDrawTools should be NO for that view.

Resources