iOS: How to draw a speical shape - ios

How to draw the shape shown below using UIBezierPath or CG? I can think of several ways, but none are very good. It contains a small arrow/triangle and a circle. Also I need to be able to rotate this shape, so the angle of the arrow can not be hard coded. You can ignore the background light grey circle. Just the red shape (plus a UILabel is better)

First, subclass UIView and override drawRect:. This code is really close and should get you there with some tweaking:
//Get the graphics context
let context = UIGraphicsGetCurrentContext()
//Colors, I was a little off here
let color = UIColor(red: 0.880, green: 0.653, blue: 0.653, alpha: 1.000)
//The circle
var ovalPath = UIBezierPath(ovalInRect: CGRectMake(86, 31, 58, 56))
color.setFill()
ovalPath.fill()
//The pointed arrow on the left of the circle
var bezierPath = UIBezierPath()
bezierPath.moveToPoint(CGPointMake(87, 54))
bezierPath.addLineToPoint(CGPointMake(76, 60))
bezierPath.addLineToPoint(CGPointMake(87, 66))
bezierPath.addLineToPoint(CGPointMake(87, 54))
color.setFill()
bezierPath.fill()
//Now, draw the 17%
let textRect = CGRectMake(95, 45, 40, 29)
var textTextContent = NSString(string: "17%")
let textStyle = NSMutableParagraphStyle.defaultParagraphStyle().mutableCopy() as NSMutableParagraphStyle
textStyle.alignment = NSTextAlignment.Center
//Text components
let textFontAttributes = [NSFontAttributeName: UIFont.systemFontOfSize(19), NSForegroundColorAttributeName: UIColor.whiteColor(), NSParagraphStyleAttributeName: textStyle]
let textTextHeight: CGFloat = textTextContent.boundingRectWithSize(CGSizeMake(textRect.width, CGFloat.infinity), options: NSStringDrawingOptions.UsesLineFragmentOrigin, attributes: textFontAttributes, context: nil).size.height
CGContextSaveGState(context)
CGContextClipToRect(context, textRect);
textTextContent.drawInRect(CGRectMake(textRect.minX, textRect.minY + (textRect.height - textTextHeight) / 2, textRect.width, textTextHeight), withAttributes: textFontAttributes)
CGContextRestoreGState(context)
All of that gives you this:

Related

iOS swift: Image from label in TabBarItem

I'm trying to create a gray circle with name initials indie of it. And use it as tabBarItem image.
This is the function that I use to create the circle, and it almost works, as you can see in the image below, but it's a square not a circle:
static func createAvatarWithInitials(name: String, surname: String, size: CGFloat) -> UIImage {
let initialsLabel = UILabel()
initialsLabel.frame.size = CGSize(width: size, height: size)
initialsLabel.textColor = UIColor.white
initialsLabel.text = "\(name.characters.first!)\(surname.characters.first!)"
initialsLabel.textAlignment = .center
initialsLabel.backgroundColor = UIColor(red: 74.0/255, green: 77.0/255, blue: 78.0/255, alpha: 1.0)
initialsLabel.layer.cornerRadius = size/2
let scale = UIScreen.main.scale
UIGraphicsBeginImageContextWithOptions(initialsLabel.frame.size, false, scale)
initialsLabel.layer.render(in: UIGraphicsGetCurrentContext()!)
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
image.withRenderingMode(.alwaysOriginal)
return image
}
and this is how I call it inside the viewDidLoad of the tabBarController:
if index == positionForProfileItem {
tabBarItem.image = UIImage.createAvatarWithInitials(name: "John", surname: "Doe", size: CGFloat(20))
}
but I got an empty squared rectangle in the tab bar.. And I can't figure it out the reason, any help?
The UIImage withRenderingMode method returns a new image. It does not modify the sender.
Change the last two lines to just one:
return image.withRenderingMode(.alwaysOriginal)
But that is not what you wish to do. Since you want a template image of a circle with the text in the middle, you need to do the following:
static func createAvatarWithInitials(name: String, surname: String, size: CGFloat) -> UIImage {
UIGraphicsBeginImageContextWithOptions(CGSize(width: size, height: size), false, 0)
let ctx = UIGraphicsGetCurrentContext()!
// Draw an opague circle
UIColor.white.setFill()
let circle = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: size, height: size))
circle.fill()
// Setup text
let font = UIFont.systemFont(ofSize: 17)
let text = "\(name.characters.first!)\(surname.characters.first!)"
let textSize = (text as NSString).size(withAttributes: [ .font: font ])
// Erase text
ctx.setBlendMode(.clear)
let textRect = CGRect(x: (size - textSize.width) / 2, y: (size - textSize.height) / 2, width: textSize.width, height: textSize.height)
(text as NSString).draw(in: textRect, withAttributes: [ .font: font ])
let image: UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
return image
}
You have to add clipsToBounds=YES on your label for cornerRadius to have efect on your screen shoot, and also try to increase a little bit the size of your image from 20 to 30.

Core Graphics CGImage mask not affecting

What I'm essentially trying to do is have a text label 'cut' a text-shaped hole through the view. I've tried using self.mask = uiLabel but those refused to position the text correctly so I'm approaching this through Core Graphics.
Here's the code that isn't working (in the draw(_ rect: CGRect)):
let context = (UIGraphicsGetCurrentContext())!
// Set mask background color
context.setFillColor(UIColor.black.cgColor)
context.fill(rect)
context.saveGState()
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let attributes = [
NSParagraphStyleAttributeName: paragraphStyle,
NSFontAttributeName: UIFont.systemFont(ofSize: 16, weight: UIFontWeightMedium),
NSForegroundColorAttributeName: UIColor.white
]
let string = NSString(string: "LOGIN")
// This wouldn't vertically align so we calculate the string size and create a new rect in which it is vertically aligned
let size = string.size(attributes: attributes)
let position = CGRect(
x: rect.origin.x,
y: rect.origin.y + (rect.size.height - size.height) / 2,
width: rect.size.width,
height: size.height
)
context.translateBy(x: 0, y: rect.size.height)
context.scaleBy(x: 1, y: -1)
string.draw(
in: position,
withAttributes: attributes
)
let mask = (context.makeImage())!
context.restoreGState()
// Redraw with created mask
context.clear(rect)
context.saveGState()
// !!!! Below line is the problem
context.clip(to: rect, mask: mask)
context.restoreGState()
Essentially I've successfully created the code to create a CGImage (the mask variable) which is the mask I want to apply to the whole image.
The marked line when replaced with context.draw(mask, in: rect) (to view the mask) correctly displays. The mask shows (correctly) as:
:
However once I try to apply this mask (using the context.clip(to: rect, mask: mask)) nothing happens!. Actual result:
Desired result is:
but for some reason the mask is not being correctly applied.
This code seems like it should work as I've read the docs over and over again. I've additionally tried to create the mask in a separate CGContext which didn't work. Also when I tried to convert the CGImage (mask) to CGColorSpaceCreateDeviceGray() using .copy(colorSpace:), it returned nil. I've been at this for two days so any help is appreciated
If you want the label to have fully translucent text, you can use blend modes instead of masks.
public class KnockoutLabel: UILabel {
public override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
context.setBlendMode(.clear)
self.drawText(in: rect)
}
}
Make sure to set isOpaque to false though; by default the view assumes it is opaque, since you use an opaque background color.
I'm not actually sure that you will enjoy the code below. It was made by using the PaintCode. Text will be always at the center of rounded rectangle. Hope it will help you.
Code, put it inside draw(_ rect: CGRect):
//// General Declarations
let context = UIGraphicsGetCurrentContext()
//// Color Declarations - just to make a gradient same as your button have
let gradientColor = UIColor(red: 0.220, green: 0.220, blue: 0.223, alpha: 1.000)
let gradientColor2 = UIColor(red: 0.718, green: 0.666, blue: 0.681, alpha: 1.000)
let gradientColor3 = UIColor(red: 0.401, green: 0.365, blue: 0.365, alpha: 1.000)
//// Gradient Declarations
let gradient = CGGradient(colorsSpace: nil, colors: [gradientColor.cgColor, gradientColor3.cgColor, gradientColor2.cgColor] as CFArray, locations: [0, 0.55, 1])!
//// Subframes
let group: CGRect = CGRect(x: rect.minX, y: rect.minY, width: rect.width, height: rect.height)
//// Group
context.saveGState()
context.beginTransparencyLayer(auxiliaryInfo: nil)
//// Rectangle Drawing
let rectanglePath = UIBezierPath(roundedRect: group, cornerRadius: 5)
context.saveGState()
rectanglePath.addClip()
let rectangleRotatedPath = UIBezierPath()
rectangleRotatedPath.append(rectanglePath)
var rectangleTransform = CGAffineTransform(rotationAngle: -99 * -CGFloat.pi/180)
rectangleRotatedPath.apply(rectangleTransform)
let rectangleBounds = rectangleRotatedPath.cgPath.boundingBoxOfPath
rectangleTransform = rectangleTransform.inverted()
context.drawLinearGradient(gradient,
start: CGPoint(x: rectangleBounds.minX, y: rectangleBounds.midY).applying(rectangleTransform),
end: CGPoint(x: rectangleBounds.maxX, y: rectangleBounds.midY).applying(rectangleTransform),
options: [])
context.restoreGState()
//// Text Drawing
context.saveGState()
context.setBlendMode(.destinationOut)
let textRect = group
let textTextContent = "LOGIN"
let textStyle = NSMutableParagraphStyle()
textStyle.alignment = .center
let textFontAttributes = [
NSFontAttributeName: UIFont.systemFont(ofSize: 16, weight: UIFontWeightBold),
NSForegroundColorAttributeName: UIColor.black,
NSParagraphStyleAttributeName: textStyle,
]
let textTextHeight: CGFloat = textTextContent.boundingRect(with: CGSize(width: textRect.width, height: CGFloat.infinity), options: .usesLineFragmentOrigin, attributes: textFontAttributes, context: nil).height
context.saveGState()
context.clip(to: textRect)
textTextContent.draw(in: CGRect(x: textRect.minX, y: textRect.minY + (textRect.height - textTextHeight) / 2, width: textRect.width, height: textTextHeight), withAttributes: textFontAttributes)
context.restoreGState()
context.restoreGState()
context.endTransparencyLayer()
context.restoreGState()
Results:

Core Graphics- Using CGGradient with strokes and text

So here is my code for creating a particular gradient:
let newBlueOne = UIColor(red: 0.141, green: 0.776, blue: 0.863, alpha: 1.000)
let newBlueTwo = UIColor(red: 0.318, green: 0.290, blue: 0.616, alpha: 1.000)
And here is the code for the gradient itself:
let newBlue = CGGradientCreateWithColors(CGColorSpaceCreateDeviceRGB(), [newBlue1.CGColor, newBlue2.CGColor], [0, 1])!
Here is the code for the rectangle I'm trying to make:
let rectanglePath = UIBezierPath(roundedRect: CGRect(x: 22, y: 35, width: 194, height: 38), cornerRadius: 19)
CGContextSaveGState(context)
rectanglePath.addClip()
CGContextDrawLinearGradient(context, translucent1, CGPoint(x: 119, y: 35), CGPoint(x: 119, y: 73), CGGradientDrawingOptions())
CGContextRestoreGState(context)
rectanglePath.lineWidth = 1
rectanglePath.stroke()
I want to use that newBlue gradient above as the stroke colour for this rectangle created. I also want to change the colour of some text to that newBlue gradient. Unfortunately, I can't quite figure it out. Can anyone help?
I appreciate your responses and look forward to reading them. Thank you so much :)
You can not directly stroke a path with a gradient.
To do what you want, you will need to go to the Core Graphics Layer, and use the CGContext Functions.
First, you create your path, and set its various properties, such as line weight.
Then, you use the CGContextReplacePathWithStrokedPath(context) function. Look it up in the CGContext documentation.
Then, you use the resultant path as a clip path.
And then, you paint that area with your gradient.
Like This:
override func drawRect(rect: CGRect) {
let newBlueOne = UIColor(red: 0.141, green: 0.776, blue: 0.863, alpha: 1.000)
let newBlueTwo = UIColor(red: 0.318, green: 0.290, blue: 0.616, alpha: 1.000)
let newBlue = CGGradientCreateWithColors(CGColorSpaceCreateDeviceRGB(), [newBlueOne.CGColor, newBlueTwo.CGColor], [0, 1])!
let lineWeight: CGFloat = 20.0
let context: CGContext = UIGraphicsGetCurrentContext()!
let rect: CGRect = CGRectMake(40.0, 40.0, 200.0, 200.0)
CGContextAddRect(context, rect)
CGContextSetLineWidth(context, lineWeight)
CGContextReplacePathWithStrokedPath(context)
CGContextClip(context)
let startPoint: CGPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMinY(rect))
let endPoint: CGPoint = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect))
CGContextDrawLinearGradient(context, newBlue, startPoint, endPoint, CGGradientDrawingOptions(rawValue: 0))
}
I only addressed painting the border. I don't know about the text.

How to draw a rectangle inside a tableviewcell

Probably it's a weird question, but I gave up. Situation: I have a TableView with a prototype cell. Inside the cell (I have a custom class) I want to have a background, but not the whole cell, and a label on it. The size of the background would be changed when I know the length of the text. The background is a rectangle, with a gradient fill, corner radius, gradient fill. How should I draw this rect? Should I do it with UIKit or with CoreGraphics? My first thought was to import the image but because i have to make it bigger if there is a long text, I decided to make it programmaticaly. Thanks in advance.
Try this solution:
// Add a label with sizeToFit
let label = UILabel()
label.text = "Add some text that you want"
label.sizeToFit()
// Add a rectangle view
let rectangle = UIView(frame: CGRect(x: 0, y: 0, width: label.frame.size.width, height: 40))
// Add gradient
let gradientLayer = CAGradientLayer()
gradientLayer.frame = rectangle.bounds
let color1 = UIColor.yellowColor().CGColor as CGColorRef
let color2 = UIColor(red: 1.0, green: 0, blue: 0, alpha: 1.0).CGColor as CGColorRef
let color3 = UIColor.clearColor().CGColor as CGColorRef
let color4 = UIColor(white: 0.0, alpha: 0.7).CGColor as CGColorRef
gradientLayer.colors = [color1, color2, color3, color4]
gradientLayer.locations = [0.0, 0.25, 0.75, 1.0]
rectangle.layer.addSublayer(gradientLayer)
// Add corner radius
gradientLayer.cornerRadius = 10
// Add the label to your rectangle
rectangle.addSubview(label)
// Add the rectangle to your cell
cell.addSubview(rectangle)

CGContextSetShadowWithColor puts shadows on top of the text casting the shadow

I am using CGContextSetShadowWithColor and core graphics to draw shadowed text. The shadow appears, but it also seems to "muddy" the actual text itself which is casting the shadow (which should be pure white). It is as if it is casting the shadow on top of the text (but not quite).
Like this:
If I redraw the text in the same position with shadowing off, I can overwrite the muddy text with clean white text, so it is a work-around, but I am wondering:
Am I am doing something wrong, or is this a bug?
let shadowOffset : CGSize = CGSize (width: 4, height: 4)
UIGraphicsBeginImageContextWithOptions(CGSize(width: 800, height: 200), false, 1.0)
let ctx = UIGraphicsGetCurrentContext()
CGContextTranslateCTM(ctx, 0, 200);
CGContextScaleCTM(ctx, 1.0, -1.0);
CGContextSetAlpha(ctx, 1.0)
CGContextSetShadowWithColor(ctx, shadowOffset, 5, UIColor(red: 0, green: 0, blue: 0, alpha: 0.5).CGColor)
CGContextSetAllowsAntialiasing (ctx, true)
let attr:CFDictionaryRef = [
NSFontAttributeName : UIFont(name: fontName, size: fontSize)!,
NSForegroundColorAttributeName:UIColor.whiteColor()]
let line = CTLineCreateWithAttributedString(CFAttributedStringCreate(nil, "1234567890", attr))
let bounds = CTLineGetBoundsWithOptions(line, CTLineBoundsOptions.UseOpticalBounds)
CGContextSetLineWidth(ctx, 1)
CGContextSetTextDrawingMode(ctx, kCGTextFillStroke)
CGContextSetTextPosition(ctx, 100.0, 100.0)
CTLineDraw(line, ctx)
//Uncomment to clean-up text
//CGContextSetShadowWithColor(ctx, shadowOffset, 0, nil)
//CGContextSetTextPosition(ctx, 100.0, 100.0)
//CTLineDraw(line, ctx)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
When calling to CGContextSetTextDrawingMode, set the drawing mode to kCGTextFill. To my understanding, the shadow you see is casted by the stroke of the text.

Resources