How do I prevent a text stroke from cutting off at the edge of a UILabel? - objective-c

I'm adding a fairly wide stroke of a few pixels to text in a UILabel, and depending on the line spacing, if the very edges of the text touch the very edges of the label, the sides of the stroke can be cut off, if they go outside of the bounds of the label. How can I prevent this? Here's the code I'm using to apply the stroke (currently of 5px):
- (void) drawTextInRect: (CGRect) rect
{
UIColor *textColor = self.textColor;
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(c, 5);
CGContextSetLineJoin(c, kCGLineJoinRound);
CGContextSetTextDrawingMode(c, kCGTextStroke);
self.textColor = [UIColor colorWithRed: 0.165 green: 0.635 blue: 0.843 alpha: 1.0];
[super drawTextInRect: rect];
}
Here's an example of the clipping at the side of the label, I think what needs to happen is one of the following:
That when the text splits onto multiple lines, some space is given inside the label's frame for the stroke to occupy.
Or, that the stroke is allowed to overflow the outer bounds of the label.

Yup, clipping just didn't work.
What if you create insets on your UILabel sub-class though. You'd make the frame of the label however big you need it to be, then set your insets. When you draw the text, it will use the insets to give you padding around any edge you need.
The downside is, you won't be able to judge line wrapping at a glance in IB. You'd have to take your label and subtract your insets then you'll see what it would really look line on the screen.
.h
#interface MyLabel : UILabel
#property (nonatomic) UIEdgeInsets insets;
#end
.m
#implementation MyLabel
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
self.insets = UIEdgeInsetsMake(0, 3, 0, 3);
}
return self;
}
- (void)drawRect:(CGRect)rect {
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSaveGState(c);
CGRect actualTextContentRect = rect;
actualTextContentRect.origin.x += self.insets.left;
actualTextContentRect.origin.y += self.insets.top;
actualTextContentRect.size.width -= self.insets.right;
actualTextContentRect.size.height -= self.insets.bottom;
CGContextSetLineWidth(c, 5);
CGContextSetLineJoin(c, kCGLineJoinRound);
CGContextSetTextDrawingMode(c, kCGTextStroke);
self.textColor = [UIColor colorWithRed: 0.165 green: 0.635 blue: 0.843 alpha: 1.0];
[super drawTextInRect:actualTextContentRect];
CGContextRestoreGState(c);
self.textColor = [UIColor whiteColor];
[super drawTextInRect:actualTextContentRect];
}
Edit: Added full code for my UILabel subclass. Modified the code slightly to show both the large stroke and the the normal lettering.

You should implement sizeThatFits: on your UILabel subclass to return a slightly larger preferred size, taking the additional space required for the stroke into consideration. You can then either use the result of sizeThatFits: to calculate the label's frame correctly, or just call sizeToFit.

Related

UIButton displaying a triangle

I have a UIButton and i want it to display a triangle. Is there a function to make it a triangle? Since im not using a UIView class im not sure how to make my frame a triangle.
ViewController(m):
- (IBAction)makeTriangle:(id)sender {
UIView *triangle=[[UIView alloc] init];
triangle.frame= CGRectMake(100, 100, 100, 100);
triangle.backgroundColor = [UIColor yellowColor];
[self.view addSubview: triangle];
Do i have to change my layer or add points and connect them to make a triangle with CGRect?
If im being unclear or not specific add a comment. Thank you!
A button is a subclass of UIView, so you can make it any shape you want using a CAShape layer. For the code below, I added a 100 x 100 point button in the storyboard, and changed its class to RDButton.
#interface RDButton ()
#property (strong,nonatomic) UIBezierPath *shape;
#end
#implementation RDButton
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
self.titleEdgeInsets = UIEdgeInsetsMake(30, 0, 0, 0); // move the title down to make it look more centered
self.shape = [UIBezierPath new];
[self.shape moveToPoint:CGPointMake(0,100)];
[self.shape addLineToPoint:CGPointMake(100,100)];
[self.shape addLineToPoint:CGPointMake(50,0)];
[self.shape closePath];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = self.shape.CGPath;
shapeLayer.fillColor = [UIColor yellowColor].CGColor;
shapeLayer.strokeColor = [UIColor blueColor].CGColor;
shapeLayer.lineWidth = 2;
[self.layer addSublayer:shapeLayer];
}
return self;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if ([self.shape containsPoint:[touches.anyObject locationInView:self]])
[super touchesBegan:touches withEvent:event];
}
The touchesBegan:withEvent: override restricts the action of the button to touches within the triangle.
A view's frame is always a rect, which is a rectangle. Even if you apply a transform to it so it no longer looks like a rectangle, the view.frame property will still be a rectangle -- just the smallest possible rectangle that contains the new shape you have produced.
So if you want your UIButton to look like a triangle, the simplest solution is probably to set its type to UIButtonTypeCustom and then to set its image to be a png which shows a triangle and is transparent outside of the triangle.
Then the UIButton itself will actually be rectangle, but will look like a triangle.
If you want to get fancy, you can also customize touch delivery so that touches on the transparent part of the PNG are not recognized (as I believe they would be by default), but that might be a bit trickier.

Set size of UIView based on content size

Is it possible to draw something on a UIView, then, after the drawing is completed, resize the view to be the same size as the thing that was previously drawn?
For example, if I draw a circle on a UIView, I would like to crop the UIView to the dimensions of the circle that I just drew on the view.
UPDATE
I am looking into using a CAShapeLayer as a possible solution. Does anyone know how to convert a UIBezierPath to a CAShapeLayer then set the position of the CAShapeLayer?
I have tried:
shapeLayer.path = bezierPath.CGPath;
shapeLayer.position = CGPointMake(0, 0);
but this does not work.
Yes you can do that. Have a look at this example that will answer both your questions.
First of all you need to add a UIView called myView and attach it to an IBOutlet ivar.
Define this global values for demonstration purposes:
#define POSITION CGPointMake(50.0,50.0)
#define SIZE CGSizeMake(100.0,100.0)
Declare two methods, one that will draw a shape in myView and another one that will resize the view to adapt it to the drawn shape:
-(void) circle
{
CAShapeLayer *layerData = [CAShapeLayer layer];
layerData.fillColor = [UIColor greenColor].CGColor;
UIBezierPath * dataPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0.0, 0.0, SIZE.width, SIZE.height)];
layerData.path = dataPath.CGPath;
layerData.position = CGPointMake(POSITION.x, POSITION.y);
[myView.layer addSublayer:layerData];
}
-(void) resize
{
((CAShapeLayer *)[myView.layer.sublayers objectAtIndex:0]).position = CGPointMake(0.0, 0.0);
myView.frame = CGRectMake(POSITION.x + myView.frame.origin.x , POSITION.y + myView.frame.origin.y, SIZE.width, SIZE.height);
}
Finally, in viewWillAppear: call both methods:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self circle];
[self resize];
}
You can run the same code calling only circle and calling both methods. In both cases the drawn circle will be at the same exact position but in the second case myView has been resized to have the same size as the drawn shape.
Hope it helps.

Perspective transform on UILabel

I'm trying to add some perspective on a UILabel, to make it display as if it's painted over an angled face.
Here's my test label:
And after applying the transform:
My code:
#implementation MyViewController
- (void)viewDidLoad {
[super viewDidLoad];
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(100.f, 250.f, 100.f, 30.f)];
label.text = #"Hello";
label.textAlignment = NSTextAlignmentCenter;
label.backgroundColor = [UIColor greenColor];
[label tiltDegrees:20.f];
[self.view addSubview:label];
[label release];
}
#end
#implementation UIView (Perspective)
-(void) tiltDegrees:(CGFloat)degrees {
CATransform3D aTransform = CATransform3DIdentity;
CGFloat zDistance = 100; // affects the sharpness of the transform
aTransform.m34 = 1.0 / -zDistance;
aTransform = CATransform3DRotate(aTransform, degrees * M_PI / 180.0f, 1.0f, 0.0f, 0.0f);
self.layer.transform = aTransform;
}
#end
I'm stuck with a bunch of problems:
The text inside the label doesn't seem to apply the transformation in the same way as the UILabel itself does. It's out of frame (clipped), not centered, and the geometry doesn't seem right. Maybe size is wrong too?
It is blurred. How could I apply the transform on the text vector before it is rasterized. I think that would produce crisp results.
I can't seem to work out the coordination between zDistance and the degrees of rotation, so that they result in a correct perspective. How can I calculate zDistance correctly based on degrees?
Any help to solve (1) (2) (3) would be greatly appreciated.
Many Thanks
I don't know if you already solved this, but the problem is basically that your text label is passing through the gray layer behind it, hence why it seems to clip in the middle of the text.
Translate the text layer towards the camera to solve this problem.

NSView with fill ( pattern image) scrolls when window changes size

I have an NSView with a drawRect
- (void)drawRect:(CGRect)rect {
// Drawing code
NSPoint origin = [self visibleRect].origin;
[[NSGraphicsContext currentContext]
setPatternPhase:NSMakePoint(origin.x, origin.y)];
[[NSColor colorWithPatternImage: self.image] set];
[NSBezierPath fillRect: [self bounds]];
}
It draws my pattern perfectly, but i can see the pattern scroll when i change the the size of my window.
i have tried to set the view isFlipped to YES but that doesn't change anything.
You need to do some off-screen drawing first and then draw that result onto the view. For example you can use a blank NSImage of the exact same size as the view, draw the pattern on that image and then draw that image on the view.
Your code may look something like that:
- (void)drawRect:(NSRect)dirtyRect
{
// call super
[super drawRect:dirtyRect];
// create blank image and lock drawing on it
NSImage* bigImage = [[[NSImage alloc] initWithSize:self.bounds.size] autorelease];
[bigImage lockFocus];
// draw your image patter on the new blank image
NSColor* backgroundColor = [NSColor colorWithPatternImage:bgImage];
[backgroundColor set];
NSRectFill(self.bounds);
[bigImage unlockFocus];
// draw your new image
[bigImage drawInRect:self.bounds
fromRect:NSZeroRect
operation:NSCompositeSourceOver
fraction:1.0f];
}
// I think you may also need to flip your view
- (BOOL)isFlipped
{
return YES;
}
Swift
A lot has changed, now things are easier, unfortunately part of objective-C's patrimony is lost and when it comes to Cocoa, Swift is like an orphan child. Anyways, based on Neovibrant's we can deduct the solution.
Subclass NSView
Override draw method
Call parent method (this is important)
Set a fill on buffer within the bounds of the view
Draw fill on buffer
code
override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect)
let bgimage : NSImage = /* Set the image you want */
let background = NSColor.init(patternImage: bgimage)
background.setFill()
bgimage.draw(in: self.bounds, from: NSZeroRect, operation: .sourceOver, fraction: 1.0)
}

CALayer - clear internal rendering context in favor of custom drawing code

When subclassing an CALayer and implementing the drawInContext method, I would assume that any drawing I do within there is all that will show up, but instead if I set (for example) borderWidth/borderColor then CALayer will draw a border on it's own above all my custom drawing code.
This is a CALayer subclass:
#implementation MyCustomCALayer
- (id)init
{
self = [super init];
if(self)
{
[self setNeedsDisplayOnBoundsChange:YES];
}
return self;
}
- (void)drawInContext:(CGContextRef)context
{
CGRect rect = CGContextGetClipBoundingBox(context);
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
CGContextFillRect(context, rect);
}
#end
Created in a UIView something like:
- (void)ensureLayer
{
if(myLayer)
return;
myLayer = [[[MyCustomCALayer alloc] init] autorelease];
myLayer.borderColor = [UIColor greenColor].CGColor;
myLayer.borderWidth = 1;
myLayer.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
}
- (void)layoutSublayersOfLayer:(CALayer *)layer
{
[super layoutSublayersOfLayer:layer];
[self ensureLayer];
if(![[layer.sublayers objectAtIndex:0] isEqual:myLayer])
[layer insertSublayer:myLayer atIndex:0];
}
What happens, is the MyCustomCALayer fills a rectangle with red, this is what I would expect to see and nothing else, since i've implemented the drawInContext method, but instead I see a red rectangle with a green border on top, always on top, i've tried just about every combination I can think of to get rid of the green border being drawn and cannot figure it out.
My reasoning is I would like to use the borderWidth and borderColor and other properties of the CALayer instead of creating my own properties, because the code that I need to draw contains a border, a fill, etc... but the rendering I need to do is not a simple shape. So the only way i've found around this is to set the borderWidth to 0 and add my own property to my subclass, like myBorderWidth, which is ridiculous.
This is done with the latest iOS SDK, but i'd imagine it's the same for Mac.
Hope this makes sense, any thoughts?
Thanks!
You’re out of luck; CoreAnimation doesn’t support overriding its implementation of rendering for the basic layer properties. Please do file a bug.