Is it possible to add a UILabel to a CALayer without subclassing and drawing it in drawInContext:?
Thanks!
CATextLayer *label = [[CATextLayer alloc] init];
[label setFont:#"Helvetica-Bold"];
[label setFontSize:20];
[label setFrame:validFrame];
[label setString:#"Hello"];
[label setAlignmentMode:kCAAlignmentCenter];
[label setForegroundColor:[[UIColor whiteColor] CGColor]];
[layer addSublayer:label];
[label release];
I don't think you can add a UIView subclass to a CALayer object. However if you want to draw text on a CALayer object, it can be done using the drawing functions provided in NSString UIKit additions as shown below. While my code is done in the delegate's drawLayer:inContext method, the same can be used in subclass' drawInContext: method. Is there any specific UILabel functionality that you want to leverage?
- (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
CGContextSetFillColorWithColor(ctx, [[UIColor darkTextColor] CGColor]);
UIGraphicsPushContext(ctx);
/*[word drawInRect:layer.bounds
withFont:[UIFont systemFontOfSize:32]
lineBreakMode:UILineBreakModeWordWrap
alignment:UITextAlignmentCenter];*/
[word drawAtPoint:CGPointMake(30.0f, 30.0f)
forWidth:200.0f
withFont:[UIFont boldSystemFontOfSize:32]
lineBreakMode:UILineBreakModeClip];
UIGraphicsPopContext();
}
Just to document my approach, I did it like this in Swift 4+ :
let textlayer = CATextLayer()
textlayer.frame = CGRect(x: 20, y: 20, width: 200, height: 18)
textlayer.fontSize = 12
textlayer.alignmentMode = .center
textlayer.string = stringValue
textlayer.isWrapped = true
textlayer.truncationMode = .end
textlayer.backgroundColor = UIColor.white.cgColor
textlayer.foregroundColor = UIColor.black.cgColor
caLayer.addSublayer(textlayer) // caLayer is and instance of parent CALayer
Your UILabel already has a CALayer behind it. If you are putting together several CALayers, you can just add the UILabel's layer as a sublayer of one of those (by using its layer property).
If it's direct text drawing in a layer that you want, the UIKit NSString additions that Deepak points to are the way to go. For an example of this in action, the Core Plot framework has a Mac / iPhone platform-independent CALayer subclass which does text rendering, CPTextLayer.
Add a CATextLayer as a sublayer and set the string property. That would be easiest and you can easily use a layout manager to make it very generic.
The answers below are fine, just make sure you add otherwise you text will be blurry:
textLayer.contentsScale = UIScreen.main.scale
Final code for Swift:
let textLayer = CATextLayer()
textLayer.frame = CGRect(x: 0, y: 0, width: 60, height: 15)
textLayer.fontSize = 12
textLayer.string = "my text"
textLayer.foregroundColor = UIColor.red.cgColor
textLayer.contentsScale = UIScreen.main.scale
class MyCALayer: CALayer {
......
override func draw(in ctx: CGContext) {
UIGraphicsPushContext(ctx)
let text = "這是一段普通的文字"
let textAttrs: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: 20), .foregroundColor: UIColor.blue]
var drawPoint = CGPoint(x: 0, y: 0)
for char in text {
let word = NSAttributedString(string: String(char),
attributes: textAttrs)
let wordBounds = word.boundingRect(with: CGSize(width: .max, height: .max), context: nil)
word.draw(at: drawPoint)
drawPoint = CGPoint(x: drawPoint.x + wordBounds.width, y: drawPoint.y)
let whitespace = NSAttributedString(string: " ", attributes: textAttrs)
let whitespaceBounds = whitespace.boundingRect(with: CGSize(width: .max, height: .max), context: nil)
whitespace.draw(at: drawPoint)
drawPoint = CGPoint(x: drawPoint.x + whitespaceBounds.width, y: drawPoint.y)
}
UIGraphicsPopContext()
}
......
}
The result
Always remember to remove previous sublayers, if you gonna add another one, to prevent duplicating views:
if let sublayers = layer.sublayers {
for sublayer in sublayers {
sublayer.removeFromSuperlayer()
}
}
Related
This is what I'm getting with this code
private func setupBorders(){
let path = UIBezierPath(roundedRect: mainTableView.bounds, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: 20, height: 20))
let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
mainTableView.layer.mask = maskLayer
mainTableView.layer.borderColor = UIColor.darkGray.cgColor
mainTableView.layer.borderWidth = 1.0
}
MainTableView is a uiview containing the notepad table and the table header. If I can get it to work for any UIView then it will work for this one. Much appreciation to anyone who can help!
Edit: In case its not clear, the problem is the border disappears on the rounded corners.
A mask layer is not enough to solve your requirement, because the layer border will not respect to the layer mask. Instead you should create a view for drawing the backgound and the border, and it should clip its contents along the border, too.
In storyboard drag a UIView to your ViewController, set constrains as you want, link it to NewView and try this,
import UIKit
import QuartzCore
class ViewController: UIViewController{
#IBOutlet var NewView :UIView!
override func viewDidLoad() {
super.viewDidLoad()
let screenSize: CGRect = UIScreen.mainScreen().bounds
let HeightFloat :CGFloat = screenSize.height - 60
let WidthFloat :CGFloat = screenSize.width - 50
let NewRect :CGRect = CGRectMake(10, 20, WidthFloat, HeightFloat)
let maskPath = UIBezierPath(roundedRect: NewRect,
byRoundingCorners: [.TopLeft, .TopRight],
cornerRadii: CGSize(width: 10, height: 10))
let shape = CAShapeLayer()
shape.path = maskPath.CGPath
shape.fillColor = UIColor.clearColor().CGColor
shape.strokeColor = UIColor.darkGrayColor().CGColor
shape.lineWidth = 2
shape.lineJoin = kCALineJoinRound
NewView.layer.mask = shape
}
}
You will get your output,
I'm currently developing a simple photoshop like application on iphone. When I want to flatten my layers, the labels are at the good position but with a bad font size.
Here's my code to flatten :
UIGraphicsBeginImageContext(CGSizeMake(widthDocument,widthDocument));
for (UILabel *label in arrayLabel) {
[label drawTextInRect:label.frame];
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
Anybody can help me ?
From: pulling an UIImage from a UITextView or UILabel gives white image.
// iOS
- (UIImage *)grabImage {
// Create a "canvas" (image context) to draw in.
UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, 0.0); // high res
// Make the CALayer to draw in our "canvas".
[[self layer] renderInContext: UIGraphicsGetCurrentContext()];
// Fetch an UIImage of our "canvas".
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// Stop the "canvas" from accepting any input.
UIGraphicsEndImageContext();
// Return the image.
return image;
}
// Swift extension w/ usage. credit #Heberti Almeida in below comments
extension UIImage {
class func imageWithLabel(label: UILabel) -> UIImage {
UIGraphicsBeginImageContextWithOptions(label.bounds.size, false, 0.0)
label.layer.renderInContext(UIGraphicsGetCurrentContext()!)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img
}
}
The usage:
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 30, height: 22))
label.text = bulletString
let image = UIImage.imageWithLabel(label)
I have created a Swift extension to render the UILabel into a UIImage:
extension UIImage {
class func imageWithLabel(label: UILabel) -> UIImage {
UIGraphicsBeginImageContextWithOptions(label.bounds.size, false, 0.0)
label.layer.renderInContext(UIGraphicsGetCurrentContext()!)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img
}
}
EDIT: Improved Swift 4 version
extension UIImage {
class func imageWithLabel(_ label: UILabel) -> UIImage {
UIGraphicsBeginImageContextWithOptions(label.bounds.size, false, 0)
defer { UIGraphicsEndImageContext() }
label.layer.render(in: UIGraphicsGetCurrentContext()!)
return UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
}
}
The usage is simple:
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 30, height: 22))
label.text = bulletString
let image = UIImage.imageWithLabel(label)
you need to render the label in a context
replace
[label drawTextInRect:label.frame];
with
[label.layer renderInContext:UIGraphicsGetCurrentContext()];
and you will need to create one image per label, so move the for loop to outside the begin and end context calls
for Swift, I use this UIView-extension:
// Updated for Swift 3.0
extension UIView {
var grabbedImage:UIImage? {
get {
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
layer.render(in: UIGraphicsGetCurrentContext()!)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img
}
}
}
You can use renderInContext on any CALayer. It will draw your view's layer on the context. So that you can change it to an image. Also if you do renderInContext on a view , all its subviews will be drawn onto the context. So instead of iterating through all the labels, while adding the labels, you can add those to a containerView. And just need to do renderInContext on that containerView.
extension UIImage {
class func imageFrom(text: String, width: CGFloat, font: UIFont, textAlignment: NSTextAlignment, lineBreakMode: NSLineBreakMode) -> UIImage? {
let label = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
label.text = text
label.font = font
label.textAlignment = textAlignment
label.lineBreakMode = lineBreakMode
label.sizeToFit()
UIGraphicsBeginImageContextWithOptions(label.bounds.size, false, 0.0)
label.layer.render(in: UIGraphicsGetCurrentContext()!)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img
}
}
Using:
let iv = UIImageView(image: UIImage.imageFrom(text: "Test For \nYarik", width: 537.0, font: UIFont.systemFont(ofSize: 30), textAlignment: .center, lineBreakMode: .byWordWrapping))
iv.contentMode = .scaleAspectFit
I bring the issue forward which I face. I am creating a UITextField programmatically as below.
UItextField *mobileNumberField = [[UITextField alloc] initWithFrame:CGRectMake(10, 195, 300, 41)];
mobileNumberField.delegate = self;
mobileNumberField.borderStyle = UITextBorderStyleRoundedRect;
[mobileNumberField.layer setCornerRadius:14.0f];
mobileNumberField.placeholder = #"Mobile Number";
[self.paymentsHomeView addSubview:mobileNumberField];
The output is the attached image.
I dont know why is it breaking at the corners. Help me to fix my text field like the image attached below.
Just remove this line...
mobileNumberField.borderStyle = UITextBorderStyleRoundedRect;
and add this code also..
[mobileNumberField setBackgroundColor:[UIColor whiteColor]];
[mobileNumberField.layer setBorderColor:[UIColor grayColor].CGColor];
[mobileNumberField.layer setBorderWidth:1.0];
Update your like below.
UITextField *mobileNumberField = [[UITextField alloc] initWithFrame:CGRectMake(10, 195, 300, 41)];
mobileNumberField.delegate = self;
mobileNumberField.layer.borderWidth = 1.0f;
mobileNumberField.layer.borderColor = [UIColor lightGrayColor].CGColor;
mobileNumberField.
// mobileNumberField.borderStyle = UITextBorderStyleRoundedRect;
[mobileNumberField.layer setCornerRadius:14.0f];
mobileNumberField.placeholder = #"Mobile Number";
[self.paymentsHomeView addSubview:mobileNumberField];
textField.layer.cornerRadius=textfield.frame.size.height/2;
textField.clipsToBounds=YES;
The reason the corners are cut is because there is an enclosing view to the text field. When you set the corner radius, applies to THAT view and thus the corners of the inside text field seem to be cut - in reality they have not even changed.
The solution is to put the UITextField inside UIView, set textfield borderstyle to none. Then apply the border and corner radius specification to the uiview. Note the borderColor, which is very close, if not the same, to the UITextField borderColor.
As of writing, tested and works in Xcode 7.3.1, Swift 2.2, iOS 8 and 9.
Swift:
textField.borderStyle = UITextBorderStyle.None
textBorderView.layer.cornerRadius = 5
textBorderView.layer.borderWidth = 1
textBorderView.layer.borderColor = UIColor.lightGrayColor().colorWithAlphaComponent(0.2).CGColor
The following code has given me the following result in Swift 5, XCode 11.4.
Definitely more bells and whistles can be added. I think this good MVP
naviTextField = UITextField.init(frame: CGRect(x: 0, y: 0, width: (self.navigationController?.navigationBar.frame.size.width)!, height: 21))
self.navigationItem.titleView = naviTextField
naviTextField.becomeFirstResponder()
naviTextField.placeholder = "Type target name here"
naviTextField.borderStyle = .roundedRect
naviTextField.layer.cornerRadius = 5.0
naviTextField.textAlignment = .center
Here is the solution of your problem
UITextField * txtField = [[UITextField alloc]initWithFrame:CGRectMake(0, 0, 200, 50)];
[txtField setBorderStyle:UITextBorderStyleNone];
[txtField.layer setMasksToBounds:YES];
[txtField.layer setCornerRadius:10.0f];
[txtField.layer setBorderColor:[[UIColor lightGrayColor]CGColor]];
[txtField.layer setBorderWidth:1];
[txtField setTextAlignment:UITextAlignmentCenter];
[txtField setContentVerticalAlignment:UIControlContentVerticalAlignmentCenter];
[self.view addSubview:txtField];
Swift 3 solution:
I have written separate function to set border and corner radius to any layer in swift, you have to just pass the layer of any view, border width, corner radius and border color to the following function
` func setBorderAndCornerRadius(layer: CALayer, width: CGFloat, radius: CGFloat,color : UIColor ) {
layer.borderColor = color.cgColor
layer.borderWidth = width
layer.cornerRadius = radius
layer.masksToBounds = true
}
`
swift solution:
textField.layer.borderWidth = anyWidth
textField.layer.borderColor = anyColor
textField.layer.cornerRadius = textField.frame.size.height/2
textField.clipsToBounds = true
All above answer is good , but here I am adding the code through #IBDesignable.
#IBDesignable class DesignableUITextField: UITextField {
// Provides left padding for images
override func leftViewRect(forBounds bounds: CGRect) -> CGRect {
var textRect = super.leftViewRect(forBounds: bounds)
textRect.origin.x += leftPadding
return textRect
}
// Provides right padding for images
override func rightViewRect(forBounds bounds: CGRect) -> CGRect {
var textRect = super.rightViewRect(forBounds: bounds)
textRect.origin.x -= rightPadding
return textRect
}
#IBInspectable var leftImage: UIImage? {
didSet {
updateView()
}
}
#IBInspectable var rightImage: UIImage? {
didSet {
updateRightView()
}
}
#IBInspectable var leftPadding: CGFloat = 0
#IBInspectable var rightPadding: CGFloat = 0
#IBInspectable var gapPadding: CGFloat = 0
#IBInspectable var color: UIColor = UIColor.lightGray {
didSet {
updateView()
}
}
#IBInspectable var cornerRadius: CGFloat = 0
#IBInspectable var borderColor: UIColor? = .lightGray
override func draw(_ rect: CGRect) {
layer.cornerRadius = cornerRadius
layer.masksToBounds = true
layer.borderWidth = 1
layer.borderColor = borderColor?.cgColor
}
//#IBInspectable var roundCornersRadius: CGFloat = 0 {
// didSet{
// roundCornersRadiusTextField(radius: roundCornersRadius)
// }
// }
func roundCornersRadiusTextField(radius:CGFloat) {
roundCorners(corners: [UIRectCorner.topLeft, UIRectCorner.topRight, UIRectCorner.bottomLeft, UIRectCorner.bottomRight], radius:radius)
}
func roundBottomCornersRadius(radius:CGFloat) {
roundCorners(corners: [UIRectCorner.topLeft, UIRectCorner.topRight], radius:radius)
}
func updateView() {
if let image = leftImage {
leftViewMode = UITextField.ViewMode.always
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
imageView.contentMode = .scaleAspectFit
imageView.image = image
// Note: In order for your image to use the tint color, you have to select the image in the Assets.xcassets and change the "Render As" property to "Template Image".
imageView.tintColor = color
leftView = imageView
} else {
leftViewMode = UITextField.ViewMode.never
leftView = nil
}
// Placeholder text color
attributedPlaceholder = NSAttributedString(string: placeholder != nil ? placeholder! : "", attributes:[NSAttributedString.Key.foregroundColor: color])
}
func updateRightView() {
if let image = rightImage {
rightViewMode = UITextField.ViewMode.always
let imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
imageView.contentMode = .scaleAspectFit
imageView.image = image
// Note: In order for your image to use the tint color, you have to select the image in the Assets.xcassets and change the "Render As" property to "Template Image".
imageView.tintColor = color
rightView = imageView
} else {
rightViewMode = UITextField.ViewMode.never
rightView = nil
}
// Placeholder text color
attributedPlaceholder = NSAttributedString(string: placeholder != nil ? placeholder! : "", attributes:[NSAttributedString.Key.foregroundColor: color])
}
func roundCorners(corners:UIRectCorner, radius:CGFloat) {
let bounds = self.bounds
let maskPath = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let maskLayer = CAShapeLayer()
maskLayer.frame = bounds
maskLayer.path = maskPath.cgPath
self.layer.mask = maskLayer
let frameLayer = CAShapeLayer()
frameLayer.frame = bounds
frameLayer.path = maskPath.cgPath
frameLayer.strokeColor = UIColor.darkGray.cgColor
frameLayer.fillColor = UIColor.init(red: 247, green: 247, blue: 247, alpha: 0).cgColor
self.layer.addSublayer(frameLayer)
}
private var textPadding: UIEdgeInsets {
let p: CGFloat = leftPadding + gapPadding + (leftView?.frame.width ?? 0)
return UIEdgeInsets(top: 0, left: p, bottom: 0, right: 5)
}
override open func textRect(forBounds bounds: CGRect) -> CGRect {
return bounds.inset(by: textPadding)
}
override open func placeholderRect(forBounds bounds: CGRect) -> CGRect {
return bounds.inset(by: textPadding)
}
override open func editingRect(forBounds bounds: CGRect) -> CGRect {
return bounds.inset(by: textPadding)
}}
I'm currently working on drawing vertical Chinese text in a label. Here's what I am trying to achieve, albeit with Chinese Characters:
I've been planning to draw each character, rotate each character 90 degrees to the left, then rotating the entire label via affine transformations to get the final result. However, it feels awfully complicated. Is there an easier way to draw the text without complicated CoreGraphics magic that I'm missing?
Well, You can do like below:
labelObject.numberOfLines = 0;
labelObject.lineBreakMode = NSLineBreakByCharWrapping;
and setFrame with -- height:100, width:20 It will work fine..
It works
UILabel *lbl = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 30, 100)];
lbl.transform = CGAffineTransformMakeRotation((M_PI)/2);
Tried the method offered by Simha.IC but it didn't work well for me. Some characters are thinner than others and get placed two on a line. E.g.
W
ai
ti
n
g
The solution for me was to create a method that transforms the string itself into a multiline text by adding \n after each character. Here's the method:
- (NSString *)transformStringToVertical:(NSString *)originalString
{
NSMutableString *mutableString = [NSMutableString stringWithString:originalString];
NSRange stringRange = [mutableString rangeOfString:mutableString];
for (int i = 1; i < stringRange.length*2 - 2; i+=2)
{
[mutableString insertString:#"\n" atIndex:i];
}
return mutableString;
}
Then you just setup the label like this:
label.text = [self transformStringToVertical:myString];
CGRect labelFrame = label.frame;
labelFrame.size.width = label.font.pointSize;
labelFrame.size.height = label.font.lineHeight * myString.length;
label.frame = labelFrame;
Enjoy!
If you would like to rotate the whole label (including characters), you can do so as follows:
First add the QuartzCore library to your project.
Create a label:
UILabel* label = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 30.0)];
[label setText:#"Label Text"];
Rotate the label:
[label setTransform:CGAffineTransformMakeRotation(-M_PI / 2)];
Depending on how you'd like to position the label you may need to set the anchor point. This sets the point around which a rotation occurs. Eg:
[label.layer setAnchorPoint:CGPointMake(0.0, 1.0)];
This is another way to draw vertical text, by subclassing UILabel. But it is some kind different of what the question want.
Objective-C
#implementation MyVerticalLabel
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
CGContextRef context = UIGraphicsGetCurrentContext();
CGAffineTransform transform = CGAffineTransformMakeRotation(-M_PI_2);
CGContextConcatCTM(context, transform);
CGContextTranslateCTM(context, -rect.size.height, 0);
CGRect newRect = CGRectApplyAffineTransform(rect, transform);
newRect.origin = CGPointZero;
NSMutableParagraphStyle *textStyle = [[NSMutableParagraphStyle defaultParagraphStyle] mutableCopy];
textStyle.lineBreakMode = self.lineBreakMode;
textStyle.alignment = self.textAlignment;
NSDictionary *attributeDict =
#{
NSFontAttributeName : self.font,
NSForegroundColorAttributeName : self.textColor,
NSParagraphStyleAttributeName : textStyle,
};
[self.text drawInRect:newRect withAttributes:attributeDict];
}
#end
A sample image is following:
Swift
It can put on the storyboard, and watch the result directly. Like the image, it's frame will contain the vertical text. And text attributes, like textAlignment, font, work well too.
#IBDesignable
class MyVerticalLabel: UILabel {
override func drawRect(rect: CGRect) {
guard let text = self.text else {
return
}
// Drawing code
let context = UIGraphicsGetCurrentContext()
let transform = CGAffineTransformMakeRotation( CGFloat(-M_PI_2))
CGContextConcatCTM(context, transform)
CGContextTranslateCTM(context, -rect.size.height, 0)
var newRect = CGRectApplyAffineTransform(rect, transform)
newRect.origin = CGPointZero
let textStyle = NSMutableParagraphStyle.defaultParagraphStyle().mutableCopy() as! NSMutableParagraphStyle
textStyle.lineBreakMode = self.lineBreakMode
textStyle.alignment = self.textAlignment
let attributeDict: [String:AnyObject] = [
NSFontAttributeName: self.font,
NSForegroundColorAttributeName: self.textColor,
NSParagraphStyleAttributeName: textStyle,
]
let nsStr = text as NSString
nsStr.drawInRect(newRect, withAttributes: attributeDict)
}
}
Swift 4
override func draw(_ rect: CGRect) {
guard let text = self.text else {
return
}
// Drawing code
if let context = UIGraphicsGetCurrentContext() {
let transform = CGAffineTransform( rotationAngle: CGFloat(-Double.pi/2))
context.concatenate(transform)
context.translateBy(x: -rect.size.height, y: 0)
var newRect = rect.applying(transform)
newRect.origin = CGPoint.zero
let textStyle = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
textStyle.lineBreakMode = self.lineBreakMode
textStyle.alignment = self.textAlignment
let attributeDict: [NSAttributedStringKey: AnyObject] = [NSAttributedStringKey.font: self.font, NSAttributedStringKey.foregroundColor: self.textColor, NSAttributedStringKey.paragraphStyle: textStyle]
let nsStr = text as NSString
nsStr.draw(in: newRect, withAttributes: attributeDict)
}
}
Swift 5
More easy way with CGAffineTransform
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var verticalText: UILabel
override func viewDidLoad() {
verticalText.transform = CGAffineTransform(rotationAngle:CGFloat.pi/2)
}
}
import UIKit
class VerticalLabel : UILabel {
private var _text : String? = nil
override var text : String? {
get {
return _text
}
set {
self.numberOfLines = 0
self.textAlignment = .center
self.lineBreakMode = .byWordWrapping
_text = newValue
if let t = _text {
var s = ""
for c in t {
s += "\(c)\n"
}
super.text = s
}
}
}
}
All I want is a one pixel black border around my white UILabel text.
I got as far as subclassing UILabel with the code below, which I clumsily cobbled together from a few tangentially related online examples. And it works but it's very, very slow (except on the simulator) and I couldn't get it to center the text vertically either (so I hard-coded the y value on the last line temporarily). Ahhhh!
void ShowStringCentered(CGContextRef gc, float x, float y, const char *str) {
CGContextSetTextDrawingMode(gc, kCGTextInvisible);
CGContextShowTextAtPoint(gc, 0, 0, str, strlen(str));
CGPoint pt = CGContextGetTextPosition(gc);
CGContextSetTextDrawingMode(gc, kCGTextFillStroke);
CGContextShowTextAtPoint(gc, x - pt.x / 2, y, str, strlen(str));
}
- (void)drawRect:(CGRect)rect{
CGContextRef theContext = UIGraphicsGetCurrentContext();
CGRect viewBounds = self.bounds;
CGContextTranslateCTM(theContext, 0, viewBounds.size.height);
CGContextScaleCTM(theContext, 1, -1);
CGContextSelectFont (theContext, "Helvetica", viewBounds.size.height, kCGEncodingMacRoman);
CGContextSetRGBFillColor (theContext, 1, 1, 1, 1);
CGContextSetRGBStrokeColor (theContext, 0, 0, 0, 1);
CGContextSetLineWidth(theContext, 1.0);
ShowStringCentered(theContext, rect.size.width / 2.0, 12, [[self text] cStringUsingEncoding:NSASCIIStringEncoding]);
}
I just have a nagging feeling that I'm overlooking a simpler way to do this. Perhaps by overriding "drawTextInRect", but I can't seem to get drawTextInRect to bend to my will at all despite staring at it intently and frowning really really hard.
I was able to do it by overriding drawTextInRect:
- (void)drawTextInRect:(CGRect)rect {
CGSize shadowOffset = self.shadowOffset;
UIColor *textColor = self.textColor;
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(c, 1);
CGContextSetLineJoin(c, kCGLineJoinRound);
CGContextSetTextDrawingMode(c, kCGTextStroke);
self.textColor = [UIColor whiteColor];
[super drawTextInRect:rect];
CGContextSetTextDrawingMode(c, kCGTextFill);
self.textColor = textColor;
self.shadowOffset = CGSizeMake(0, 0);
[super drawTextInRect:rect];
self.shadowOffset = shadowOffset;
}
A simpler solution is to use an Attributed String like so:
Swift 4:
let strokeTextAttributes: [NSAttributedStringKey : Any] = [
NSAttributedStringKey.strokeColor : UIColor.black,
NSAttributedStringKey.foregroundColor : UIColor.white,
NSAttributedStringKey.strokeWidth : -2.0,
]
myLabel.attributedText = NSAttributedString(string: "Foo", attributes: strokeTextAttributes)
Swift 4.2:
let strokeTextAttributes: [NSAttributedString.Key : Any] = [
.strokeColor : UIColor.black,
.foregroundColor : UIColor.white,
.strokeWidth : -2.0,
]
myLabel.attributedText = NSAttributedString(string: "Foo", attributes: strokeTextAttributes)
On a UITextField you can set the defaultTextAttributes and the attributedPlaceholder as well.
Note that the NSStrokeWidthAttributeName has to be negative in this case, i.e. only the inner outlines work.
After reading the accepted answer and the two corrections to it and the answer from Axel Guilmin, I decided to compile an overall solution in Swift, that suits me:
import UIKit
class UIOutlinedLabel: UILabel {
var outlineWidth: CGFloat = 1
var outlineColor: UIColor = UIColor.whiteColor()
override func drawTextInRect(rect: CGRect) {
let strokeTextAttributes = [
NSStrokeColorAttributeName : outlineColor,
NSStrokeWidthAttributeName : -1 * outlineWidth,
]
self.attributedText = NSAttributedString(string: self.text ?? "", attributes: strokeTextAttributes)
super.drawTextInRect(rect)
}
}
You can add this custom UILabel class to an existing label in the Interface Builder and change the thickness of the border and its color by adding User Defined Runtime Attributes like this:
Result:
There is one issue with the answer's implementation. Drawing a text with stroke has a slightly different character glyph width than drawing a text without stroke, which can produce "uncentered" results. It can be fixed by adding an invisible stroke around the fill text.
Replace:
CGContextSetTextDrawingMode(c, kCGTextFill);
self.textColor = textColor;
self.shadowOffset = CGSizeMake(0, 0);
[super drawTextInRect:rect];
with:
CGContextSetTextDrawingMode(context, kCGTextFillStroke);
self.textColor = textColor;
[[UIColor clearColor] setStroke]; // invisible stroke
self.shadowOffset = CGSizeMake(0, 0);
[super drawTextInRect:rect];
I'm not 100% sure, if that's the real deal, because I don't know if self.textColor = textColor; has the same effect as [textColor setFill], but it should work.
Disclosure: I'm the developer of THLabel.
I've released a UILabel subclass a while ago, which allows an outline in text and other effects. You can find it here: https://github.com/tobihagemann/THLabel
A Swift 4 class version based off the answer by kprevas
import Foundation
import UIKit
public class OutlinedText: UILabel{
internal var mOutlineColor:UIColor?
internal var mOutlineWidth:CGFloat?
#IBInspectable var outlineColor: UIColor{
get { return mOutlineColor ?? UIColor.clear }
set { mOutlineColor = newValue }
}
#IBInspectable var outlineWidth: CGFloat{
get { return mOutlineWidth ?? 0 }
set { mOutlineWidth = newValue }
}
override public func drawText(in rect: CGRect) {
let shadowOffset = self.shadowOffset
let textColor = self.textColor
let c = UIGraphicsGetCurrentContext()
c?.setLineWidth(outlineWidth)
c?.setLineJoin(.round)
c?.setTextDrawingMode(.stroke)
self.textColor = mOutlineColor;
super.drawText(in:rect)
c?.setTextDrawingMode(.fill)
self.textColor = textColor
self.shadowOffset = CGSize(width: 0, height: 0)
super.drawText(in:rect)
self.shadowOffset = shadowOffset
}
}
It can be implemented entirely in the Interface Builder by setting the UILabel's custom class to OutlinedText. You will then have the ability to set the outline's width and color from the Properties pane.
If your goal is something like this:
Here is how I achieved it: I added a new label of a custom class as a Subview to my current UILabel (inspired by this answer).
Just copy & paste it into your project and you are good to go:
extension UILabel {
func addTextOutline(usingColor outlineColor: UIColor, outlineWidth: CGFloat) {
class OutlinedText: UILabel{
var outlineWidth: CGFloat = 0
var outlineColor: UIColor = .clear
override public func drawText(in rect: CGRect) {
let shadowOffset = self.shadowOffset
let textColor = self.textColor
let c = UIGraphicsGetCurrentContext()
c?.setLineWidth(outlineWidth)
c?.setLineJoin(.round)
c?.setTextDrawingMode(.stroke)
self.textAlignment = .center
self.textColor = outlineColor
super.drawText(in:rect)
c?.setTextDrawingMode(.fill)
self.textColor = textColor
self.shadowOffset = CGSize(width: 0, height: 0)
super.drawText(in:rect)
self.shadowOffset = shadowOffset
}
}
let textOutline = OutlinedText()
let outlineTag = 9999
if let prevTextOutline = viewWithTag(outlineTag) {
prevTextOutline.removeFromSuperview()
}
textOutline.outlineColor = outlineColor
textOutline.outlineWidth = outlineWidth
textOutline.textColor = textColor
textOutline.font = font
textOutline.text = text
textOutline.tag = outlineTag
sizeToFit()
addSubview(textOutline)
textOutline.frame = CGRect(x: -(outlineWidth / 2), y: -(outlineWidth / 2),
width: bounds.width + outlineWidth,
height: bounds.height + outlineWidth)
}
}
USAGE:
yourLabel.addTextOutline(usingColor: .red, outlineWidth: 6)
it also works for a UIButton with all its animations:
yourButton.titleLabel?.addTextOutline(usingColor: .red, outlineWidth: 6)
If you want to animate something complicated, the best way is to programmaticly take a screenshot of it an animate that instead!
To take a screenshot of a view, you'll need code a little like this:
UIGraphicsBeginImageContext(mainContentView.bounds.size);
[mainContentView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
Where mainContentView is the view you want to take a screenshot of. Add viewImage to a UIImageView and animate that.
Hope that speeds up your animation!!
N
As MuscleRumble mentioned, the accepted answer's border is a bit off center. I was able to correct this by setting the stroke width to zero instead of changing the color to clear.
i.e. replacing:
CGContextSetTextDrawingMode(c, kCGTextFill);
self.textColor = textColor;
self.shadowOffset = CGSizeMake(0, 0);
[super drawTextInRect:rect];
with:
CGContextSetTextDrawingMode(c, kCGTextFillStroke);
self.textColor = textColor;
CGContextSetLineWidth(c, 0); // set stroke width to zero
self.shadowOffset = CGSizeMake(0, 0);
[super drawTextInRect:rect];
I would've just commented on his answer but apparently I'm not "reputable" enough.
This won't create an outline per-se, but it will put a shadow around the text, and if you make the shadow radius small enough it could resemble an outline.
label.layer.shadowColor = [[UIColor blackColor] CGColor];
label.layer.shadowOffset = CGSizeMake(0.0f, 0.0f);
label.layer.shadowOpacity = 1.0f;
label.layer.shadowRadius = 1.0f;
I don't know whether it is compatible with older versions of iOS..
Anyway, I hope it helps...
if ALL you want is a one pixel black border around my white UILabel text,
then
i do think you're making the problem harder than it is...
I don't know by memory which 'draw rect / frameRect' function you should use, but it will be easy for you to find. this method just demonstrates the strategy (let the superclass do the work!):
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
[context frameRect:rect]; // research which rect drawing function to use...
}
I found an issue with the main answer. The text position is not necessarily centered correctly to sub-pixel location, so that the outline can be mismatched around the text. I fixed it using the following code, which uses CGContextSetShouldSubpixelQuantizeFonts(ctx, false):
- (void)drawTextInRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
[self.textOutlineColor setStroke];
[self.textColor setFill];
CGContextSetShouldSubpixelQuantizeFonts(ctx, false);
CGContextSetLineWidth(ctx, self.textOutlineWidth);
CGContextSetLineJoin(ctx, kCGLineJoinRound);
CGContextSetTextDrawingMode(ctx, kCGTextStroke);
[self.text drawInRect:rect withFont:self.font lineBreakMode:NSLineBreakByWordWrapping alignment:self.textAlignment];
CGContextSetTextDrawingMode(ctx, kCGTextFill);
[self.text drawInRect:rect withFont:self.font lineBreakMode:NSLineBreakByWordWrapping alignment:self.textAlignment];
}
This assumes that you defined textOutlineColor and textOutlineWidth as properties.
Here is the another answer to set outlined text on label.
extension UILabel {
func setOutLinedText(_ text: String) {
let attribute : [NSAttributedString.Key : Any] = [
NSAttributedString.Key.strokeColor : UIColor.black,
NSAttributedString.Key.foregroundColor : UIColor.white,
NSAttributedString.Key.strokeWidth : -2.0,
NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: 12)
] as [NSAttributedString.Key : Any]
let customizedText = NSMutableAttributedString(string: text,
attributes: attribute)
attributedText = customizedText
}
}
set outlined text simply using the extension method.
lblTitle.setOutLinedText("Enter your email address or username")
it is also possible to subclass UILabel with the following logic:
- (void)setText:(NSString *)text {
[self addOutlineForAttributedText:[[NSAttributedString alloc] initWithString:text]];
}
- (void)setAttributedText:(NSAttributedString *)attributedText {
[self addOutlineForAttributedText:attributedText];
}
- (void)addOutlineForAttributedText:(NSAttributedString *)attributedText {
NSDictionary *strokeTextAttributes = #{
NSStrokeColorAttributeName: [UIColor blackColor],
NSStrokeWidthAttributeName : #(-2)
};
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithAttributedString:attributedText];
[attrStr addAttributes:strokeTextAttributes range:NSMakeRange(0, attrStr.length)];
super.attributedText = attrStr;
}
and if you set text in Storyboard then:
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
// to apply border for text from storyboard
[self addOutlineForAttributedText:[[NSAttributedString alloc] initWithString:self.text]];
}
return self;
}
Why don't you create a 1px border UIView in Photoshop, then set a UIView with the image, and position it behind your UILabel?
Code:
UIView *myView;
UIImage *imageName = [UIImage imageNamed:#"1pxBorderImage.png"];
UIColor *tempColour = [[UIColor alloc] initWithPatternImage:imageName];
myView.backgroundColor = tempColour;
[tempColour release];
It's going to save you subclassing an object and it's fairly simple to do.
Not to mention if you want to do animation, it's built into the UIView class.
To put a border with rounded edges around a UILabel I do the following:
labelName.layer.borderWidth = 1;
labelName.layer.borderColor = [[UIColor grayColor] CGColor];
labelName.layer.cornerRadius = 10;
(don't forget to include QuartzCore/QuartzCore.h)