How to draw vertical text on CGContext? - ios

I want to draw a vertical text. I had tried different way to rotate string using rotate by but its not working
Here is my code
override func draw(with box: PDFDisplayBox, to context: CGContext) {
let name = "Sunny"
// Draw original content
super.draw(with: box, to: context)
// Draw rotated overlay string
UIGraphicsPushContext(context)
context.saveGState()
let pageBounds = self.bounds(for: box)
context.translateBy(x: pageBounds.size.width, y: 0)
context.scaleBy(x: -1.0, y: -1.0)
context.rotate(by: CGFloat.pi / 4.0)
let string: NSString = name as! NSString
let attributes = [
NSAttributedString.Key.foregroundColor: UIColor(red: 0.5, green: 0.5, blue: 0.5, alpha: 0.5),
NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 64)
]
string.draw(at: CGPoint(x:250, y:40), withAttributes: attributes)
context.restoreGState()
UIGraphicsPopContext()
}
How can i draw?

We can draw the vertical string using the below code, in which I have converted given string by adding line separators between the characters.
override func draw(with box: PDFDisplayBox, to context: CGContext) {
// Draw original content
super.draw(with: box, to: context)
// Draw rotated overlay string
UIGraphicsPushContext(context)
context.saveGState()
let pageBounds = self.bounds(for: box)
context.translateBy(x: 0.0, y: pageBounds.size.height)
context.scaleBy(x: 1.0, y: -1.0)
context.rotate(by: CGFloat.pi / -60.0)
// Convert string by adding line separators.
let string = "Sunny".trimmingCharacters(in: .whitespaces)
let asArray = Array(string)
let verticalString = asArray.map { "\($0)" }.joined(separator: "\n")
let attributes: [NSAttributedString.Key: Any] = [
NSAttributedString.Key.foregroundColor: #colorLiteral(red: 0.4980392157, green: 0.4980392157, blue: 0.4980392157, alpha: 0.5),
NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 64)
]
verticalString.draw(at: CGPoint(x: 250, y: 40), withAttributes: attributes)
context.restoreGState()
UIGraphicsPopContext()
}
Output of vertical string in PDF

Related

Why is CILinearGradient resulting in a very NON-linear gradient?

I'm a relatively new Swift developer and I am using the CILinearGradient CIFilter to generate gradients that I can then use as backgrounds and textures. I was pretty happy with the way it was working, until I realized that the gradients coming out of it seem to be heavily skewed towards away from the black end of the spectrum.
At first I thought I was nuts, but then I created pure black-to-white and white-to-black gradients and put them on screen next to each other. I took a screenshot and brought it into Photoshop. then I looked at the color values. You can see that the ends of each gradient line up (pure black over pure white on one end, and the opposite on the other), but the halfway point of each gradient is significantly skewed towards the black end.
Is this an issue with the CIFilter or am I doing something wrong? Thanks to anyone with any insight on this!
Here's my code:
func gradient2colorIMG(UIcolor1: UIColor, UIcolor2: UIColor, width: CGFloat, height: CGFloat) -> CGImage? {
if let gradientFilter = CIFilter(name: "CILinearGradient") {
let startVector:CIVector = CIVector(x: 0 + 10, y: 0)
let endVector:CIVector = CIVector(x: width - 10, y: 0)
let color1 = CIColor(color: UIcolor1)
let color2 = CIColor(color: UIcolor2)
let context = CIContext(options: nil)
if let currentFilter = CIFilter(name: "CILinearGradient") {
currentFilter.setValue(startVector, forKey: "inputPoint0")
currentFilter.setValue(endVector, forKey: "inputPoint1")
currentFilter.setValue(color1, forKey: "inputColor0")
currentFilter.setValue(color2, forKey: "inputColor1")
if let output = currentFilter.outputImage {
if let cgimg = context.createCGImage(output, from: CGRect(x: 0, y: 0, width: width, height: height)) {
let gradImage = cgimg
return gradImage
}
}
}
}
return nil
}
and then I call it in SpriteKit using this code (but this is just so I can see them on the screen to compare the CGImages that are output by the function) ...
if let gradImage = gradient2colorIMG(UIcolor1: UIColor(red: 255.0 / 255.0, green: 255.0 / 255.0, blue: 255.0 / 255.0, alpha: 1.0), UIcolor2: UIColor(red: 0.0 / 255.0, green: 0.0 / 255.0, blue: 0.0 / 255.0, alpha: 1.0), width: 250, height: 80) {
let sampleback = SKShapeNode(path: CGPath(roundedRect: CGRect(x: 0, y: 0, width: 250, height: 80), cornerWidth: 5, cornerHeight: 5, transform: nil))
sampleback.fillColor = .white
sampleback.fillTexture = SKTexture(cgImage: gradImage)
sampleback.zPosition = 200
sampleback.position = CGPoint(x: 150, y: 50)
self.addChild(sampleback)
}
if let gradImage2 = gradient2colorIMG(UIcolor1: UIColor(red: 0.0 / 255.0, green: 0.0 / 255.0, blue: 0.0 / 255.0, alpha: 1.0), UIcolor2: UIColor(red: 255.0 / 255.0, green: 255.0 / 255.0, blue: 255.0 / 255.0, alpha: 1.0), width: 250, height: 80) {
let sampleback2 = SKShapeNode(path: CGPath(roundedRect: CGRect(x: 0, y: 0, width: 250, height: 80), cornerWidth: 5, cornerHeight: 5, transform: nil))
sampleback2.fillColor = .white
sampleback2.fillTexture = SKTexture(cgImage: gradImage2)
sampleback2.zPosition = 200
sampleback2.position = CGPoint(x: 150, y: 150)
self.addChild(sampleback2)
}
As another follow-up, I tried doing a red-blue gradient (so purely a change in hue) and it is perfectly linear (see below). The issue seems to be around the brightness.
A red-blue gradient DOES ramp its hue in a perfectly linear fashion
Imagine that black is 0 and white is 1. Then the problem here is that we intuitively think that 50% of black "is" a grayscale value of 0.5 — and that is not true.
To see this, consider the following core image experiment:
let con = CIContext(options: nil)
let white = CIFilter(name:"CIConstantColorGenerator")!
white.setValue(CIColor(color:.white), forKey:"inputColor")
let black = CIFilter(name:"CIConstantColorGenerator")!
black.setValue(CIColor(color:UIColor.black.withAlphaComponent(0.5)),
forKey:"inputColor")
let atop = CIFilter(name:"CISourceAtopCompositing")!
atop.setValue(white.outputImage!, forKey:"inputBackgroundImage")
atop.setValue(black.outputImage!, forKey:"inputImage")
let cgim = con.createCGImage(atop.outputImage!,
from: CGRect(x: 0, y: 0, width: 201, height: 50))!
let image = UIImage(cgImage: cgim)
let iv = UIImageView(image:image)
self.view.addSubview(iv)
iv.frame.origin = CGPoint(x: 100, y: 150)
What I've done here is to lay a 50% transparency black swatch on top of a white swatch. We intuitively imagine that the result will be a swatch that will read as 0.5. But it isn't; it's 0.737, the very same shade that is appearing at the midpoint of your gradients:
The reason is that everything here is happening, not in some mathematical vacuum, but in a color space adjusted for a specific gamma.
Now, you may justly ask: "But where did I specify this color space? This is not what I want!" Aha. You specified it in the first line, when you created a CIContext without overriding the default working color space.
Let's fix that. Change the first line to this:
let con = CIContext(options: [.workingColorSpace : NSNull()])
Now the output is this:
Presto, that's your 0.5 gray!
So what I'm saying is, if you create your CIContext like that, you will get the gradient you are after, with 0.5 gray at the midpoint. I'm not saying that that is any more "right" than the result you are getting, but at least it shows how to get that particular result with the code you already have.
(In fact, I think what you were getting originally is more "right", as it is adjusted for human perception.)
The midpoint of the CILinearGradient appears to correspond to 188, 188, 188, which looks like the “absolute whiteness” rendition of middle gray, which is not entirely unreasonable. (The CISmoothLinearGradient offers a smoother transition, but it doesn’t have the midpoint at 0.5, 0.5, 0.5, either.) As an aside, the “linear” in CILinearGradient and CISmoothLinearGradient refer to the shape of the gradient (to differentiate it from a “radial” gradient), not the nature of the color transitions within the gradient.
However if you want a gradient whose midpoint is 0.5, 0.5, 0.5, you can use CGGradient:
func simpleGradient(in rect: CGRect) -> UIImage {
return UIGraphicsImageRenderer(bounds: rect).image { context in
let colors = [UIColor.white.cgColor, UIColor.black.cgColor]
let colorSpace = CGColorSpaceCreateDeviceGray() // or RGB works, too
guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: nil) else { return }
context.cgContext.drawLinearGradient(gradient, start: .zero, end: CGPoint(x: rect.maxX, y: 0), options: [])
}
}
Alternatively, if you want a gradient background, you might define a UIView subclass that uses a CAGradientLayer as its backing layer:
class GradientView: UIView {
override class var layerClass: AnyClass { return CAGradientLayer.self }
var gradientLayer: CAGradientLayer { return layer as! CAGradientLayer }
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
func configure() {
gradientLayer.colors = [UIColor.white.cgColor, UIColor.black.cgColor]
gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
}
}

CATextLayer position and Transformation issue

I am trying to build a control like attached circle image with multiple segment having equal space for each part. Number of segments can change depend upon provided array.
I have developed this so far using CAShapeLayer and UIBezierPath. Also added text in the center of each shape Layer.
I have added my code so far for generating this control.
let circleLayer = CALayer()
func createCircle(_ titleArray:[String]) {
update(bounds: bounds, titleArray: titleArray)
containerView.layer.addSublayer(circleRenderer.circleLayer)
}
func update(bounds: CGRect, titleArray: [String]) {
let position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
circleLayer.position = position
circleLayer.bounds = bounds
update(titleArray: titles)
}
func update(titleArray: [String]) {
let center = CGPoint(x: circleLayer.bounds.size.width / 2.0, y: circleLayer.bounds.size.height / 2.0)
let radius:CGFloat = min(circleLayer.bounds.size.width, circleLayer.bounds.size.height) / 2
let segmentSize = CGFloat((Double.pi*2) / Double(titleArray.count))
for i in 0..<titleArray.count {
let startAngle = segmentSize*CGFloat(i) - segmentSize/2
let endAngle = segmentSize*CGFloat(i+1) - segmentSize/2
let midAngle = (startAngle+endAngle)/2
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.random.cgColor
let bezierPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
bezierPath.addLine(to: center)
shapeLayer.path = bezierPath.cgPath
let height = titleArray[i].height(withConstrainedWidth: radius-20, font: UIFont.systemFont(ofSize: 15))
let frame = shapeLayer.path?.boundingBoxOfPath ?? CGRect.zero
let textLayer = TextLayer()
textLayer.frame = CGRect(x: 0, y: 0, width: 70, height: height)
textLayer.position = CGPoint(x: frame.center.x, y: frame.center.y)
textLayer.fontSize = 15
textLayer.contentsScale = UIScreen.main.scale
textLayer.alignmentMode = .center
textLayer.string = titleArray[i]
textLayer.isWrapped = true
textLayer.backgroundColor = UIColor.black.cgColor
textLayer.foregroundColor = UIColor.white.cgColor
textLayer.transform = CATransform3DMakeRotation(midAngle, 0.0, 0.0, 1.0)
shapeLayer.addSublayer(textLayer)
circleLayer.addSublayer(shapeLayer)
}
}
circleLayer is added as superlayer, containing full area of UIView.
My requirement is to add text centered vertically and horizontally within shape with angle. I am facing issue with centring text within shape while angle is fine.
Thanks
Edit:
If I remove textLayer rotation code, then It look like this image.
Your labels are not "centered" because you're using the geometric center of the wedge bounding-box:
What you need to do is calculate an "inner" circle, with 1/2 of the full radius, and then find the points on that circle to place your labels.
So, first we calculate the circle:
Then bisect each angle and find the point on the circle:
Then calculate the bounding-box for the label (I used max-width of radius * 0.6), put the center of that frame on the point on the circle, and then rotate the text layer:
And the result, without the "guides":
Note: For these images, I used radius * 0.55 - or just slightly further out than exactly 1/2 of the radius - for the "inner circle". This gave me just slightly better appearance, due to the wedges narrowing as we get to the center of the circle. Changing that to radius * 0.6 might even look better.
Here is the code to generate this view:
struct Wedge {
var color: UIColor = .cyan
var label: String = ""
}
class WedgeView: UIView {
var wedges: [Wedge] = []
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
}
override func layoutSubviews() {
super.layoutSubviews()
layer.sublayers?.forEach { $0.removeFromSuperlayer() }
setup()
}
func setup() -> Void {
// initialize local variables
var startAngle: CGFloat = 0
var outerRadius: CGFloat = 0.0
var halfRadius: CGFloat = 0.0
// initialize local constants
let viewCenter: CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let diameter = bounds.width
let fontHeight: CGFloat = ceil(12.0 * (bounds.height / 300.0))
let textLayerFont = UIFont.systemFont(ofSize: fontHeight, weight: .light)
outerRadius = diameter * 0.5
halfRadius = outerRadius * 0.55
let labelMaxWidth:CGFloat = outerRadius * 0.6
startAngle = -(.pi * (1.0 / CGFloat(wedges.count)))
for i in 0..<wedges.count {
let endAngle = startAngle + 2 * .pi * (1.0 / CGFloat(wedges.count))
let shape = CAShapeLayer()
let path: UIBezierPath = UIBezierPath()
path.addArc(withCenter: viewCenter, radius: outerRadius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
path.addLine(to: viewCenter)
path.close()
shape.path = path.cgPath
shape.fillColor = wedges[i].color.cgColor
shape.strokeColor = UIColor.black.cgColor
self.layer.addSublayer(shape)
let textLayer = CATextLayer()
textLayer.font = textLayerFont
textLayer.fontSize = fontHeight
let string = wedges[i].label
textLayer.string = string
textLayer.foregroundColor = UIColor.white.cgColor
textLayer.backgroundColor = UIColor.black.cgColor
textLayer.isWrapped = true
textLayer.alignmentMode = CATextLayerAlignmentMode.center
textLayer.contentsScale = UIScreen.main.scale
let bisectAngle = startAngle + ((endAngle - startAngle) * 0.5)
let p = CGPoint.pointOnCircle(center: viewCenter, radius: halfRadius, angle: bisectAngle)
var textLayerframe = CGRect(x: 0, y: 0, width: labelMaxWidth, height: 0)
let h = string.getLableHeight(labelMaxWidth, usingFont: textLayerFont)
textLayerframe.size.height = h
textLayerframe.origin.x = p.x - (textLayerframe.size.width * 0.5)
textLayerframe.origin.y = p.y - (textLayerframe.size.height * 0.5)
textLayer.frame = textLayerframe
self.layer.addSublayer(textLayer)
textLayer.transform = CATransform3DMakeRotation(bisectAngle, 0.0, 0.0, 1.0)
// uncomment this block to show the dashed-lines
/*
let biLayer = CAShapeLayer()
let dash = UIBezierPath()
dash.move(to: viewCenter)
dash.addLine(to: p)
biLayer.strokeColor = UIColor.yellow.cgColor
biLayer.lineDashPattern = [4, 4]
biLayer.path = dash.cgPath
self.layer.addSublayer(biLayer)
*/
startAngle = endAngle
}
// uncomment this block to show the half-radius circle
/*
let tempLayer: CAShapeLayer = CAShapeLayer()
tempLayer.path = UIBezierPath(ovalIn: bounds.insetBy(dx: outerRadius - halfRadius, dy: outerRadius - halfRadius)).cgPath
tempLayer.fillColor = UIColor.clear.cgColor
tempLayer.strokeColor = UIColor.green.cgColor
tempLayer.lineWidth = 1.0
self.layer.addSublayer(tempLayer)
*/
}
}
class WedgesWithRotatedLabelsViewController: UIViewController {
let wedgeView: WedgeView = WedgeView()
var wedges: [Wedge] = []
let colors: [UIColor] = [
UIColor(red: 1.00, green: 0.00, blue: 0.00, alpha: 1.0),
UIColor(red: 0.00, green: 0.50, blue: 0.00, alpha: 1.0),
UIColor(red: 0.00, green: 0.00, blue: 1.00, alpha: 1.0),
UIColor(red: 1.00, green: 0.50, blue: 0.50, alpha: 1.0),
UIColor(red: 0.00, green: 0.75, blue: 0.00, alpha: 1.0),
UIColor(red: 0.50, green: 0.50, blue: 1.00, alpha: 1.0),
]
let labels: [String] = [
"This is long text for Label 1",
"Label 2",
"Longer Label 3",
"Label 4",
"Label 5",
"Label 6",
]
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
for (c, s) in zip(colors, labels) {
wedges.append(Wedge(color: c, label: s))
}
wedgeView.wedges = wedges
view.addSubview(wedgeView)
wedgeView.translatesAutoresizingMaskIntoConstraints = false
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
wedgeView.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.8),
wedgeView.heightAnchor.constraint(equalTo: wedgeView.widthAnchor),
wedgeView.centerXAnchor.constraint(equalTo: g.centerXAnchor),
wedgeView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
}
}
Couple of "helper" extensions used in the above code:
// get a random color
extension UIColor {
static var random: UIColor {
return UIColor(red: .random(in: 0...1),
green: .random(in: 0...1),
blue: .random(in: 0...1),
alpha: 1.0)
}
}
// get the point on a circle at specific radian
extension CGPoint {
static func pointOnCircle(center: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
let x = center.x + radius * cos(angle)
let y = center.y + radius * sin(angle)
return CGPoint(x: x, y: y)
}
}
// get height of word-wrapping string with max-width
extension String {
func getLableHeight(_ forWidth: CGFloat, usingFont: UIFont) -> CGFloat {
let constraintRect = CGSize(width: forWidth, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: usingFont], context: nil)
return ceil(boundingBox.height)
}
}

CGContext renders text upside down

I am rendering a NSAttributedString with CGContext, however when it renders the text is upside down :(
This is what I came up with:
override func draw(with box: PDFDisplayBox, in context: CGContext) {
UIGraphicsPushContext(context)
context.saveGState()
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let attributes = [
NSAttributedString.Key.paragraphStyle: paragraphStyle,
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 80.0),
NSAttributedString.Key.foregroundColor: UIColor.red
]
NSAttributedString(string: self.widgetStringValue!, attributes: attributes).draw(in: self.bounds)
context.restoreGState()
UIGraphicsPopContext()
}
I have tried adding this:
context.translateBy(x: 0.0, y: bounds.height)
context.scaleBy(x: 1.0, y: -1.0)
But then the text does not appear on my screen at all :(
What am I doing wrong?
UPDATE
As requested here is my full TextAnnotation class:
class TextAnnotation: PDFAnnotation {
var currentText: String?
override init(bounds: CGRect, forType annotationType: PDFAnnotationSubtype, withProperties properties: [AnyHashable : Any]?) {
super.init(bounds: bounds, forType: annotationType, withProperties: properties)
self.widgetFieldType = PDFAnnotationWidgetSubtype(rawValue: PDFAnnotationWidgetSubtype.text.rawValue)
self.font = UIFont.systemFont(ofSize: 80)
self.isMultiline = true
self.widgetStringValue = "Text Here"
self.currentText = self.widgetStringValue!
}
override func draw(with box: PDFDisplayBox, in context: CGContext) {
UIGraphicsPushContext(context)
context.saveGState()
let pageBounds = bounds
context.translateBy(x: 0, y: pageBounds.height)
context.scaleBy(x: 1, y: -1)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let attributes: [NSAttributedString.Key: Any] = [
.paragraphStyle: paragraphStyle,
.font: UIFont.systemFont(ofSize: 80),
.foregroundColor: UIColor.red
]
widgetStringValue!.draw(in: pageBounds, withAttributes: attributes)
context.restoreGState()
UIGraphicsPopContext()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
In my case, I was subclassing a PDFAnnotation. The key was using the correct y-value (which was a pain to figure out) for the translateBy call in my draw method:
override func draw(with box: PDFDisplayBox, in context: CGContext) {
let text = NSString(string: "Hello!")
UIGraphicsPushContext(context)
context.saveGState()
context.translateBy(x: 0, y: bounds.height + (2 * bounds.minY))
context.scaleBy(x: 1, y: -1.0)
text.draw(in: bounds.offsetBy(dx: 2, dy: 0), withAttributes: attributes)
context.restoreGState()
UIGraphicsPopContext()
}
In a PDFAnnotation, you should just add the translateBy(x:y:) and scaledBy(x:y:) as you’ve discussed:
override func draw(with box: PDFDisplayBox, in context: CGContext) {
UIGraphicsPushContext(context)
context.saveGState()
context.translateBy(x: 0, y: bounds.height)
context.scaleBy(x: 1, y: -1)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let attributes: [NSAttributedString.Key: Any] = [
.paragraphStyle: paragraphStyle,
.font: UIFont.systemFont(ofSize: 80),
.foregroundColor: UIColor.red
]
widgetStringValue?.draw(in: bounds, withAttributes: attributes)
context.restoreGState()
UIGraphicsPopContext()
}
In a PDFPage you should use bounds(for:) to get the bounds within that PDFDisplayBox, and then use translateBy(x:y:) and scaledBy(x:y:) as you’ve discussed:
override func draw(with box: PDFDisplayBox, to context: CGContext) {
UIGraphicsPushContext(context)
context.saveGState()
let pageBounds = bounds(for: box)
context.translateBy(x: 0, y: pageBounds.height)
context.scaleBy(x: 1, y: -1)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let attributes: [NSAttributedString.Key: Any] = [
.paragraphStyle: paragraphStyle,
.font: UIFont.systemFont(ofSize: 80),
.foregroundColor: UIColor.red
]
text.draw(in: pageBounds, withAttributes: attributes)
context.restoreGState()
UIGraphicsPopContext()
}

Use radial gradient as an extention for UIView

My goal is to make radial gradient extension for UIView. Here is my code:
extension UIView {
func drawRadialGradient() {
let colors = Colors.gradientColors as CFArray
let gradient = CGGradient(colorsSpace: nil, colors: colors, locations: nil)
guard let gradientValue = gradient else{ return }
let endRadius: CGFloat? = max(frame.width, frame.height) / 2
guard let endRadiusValue = endRadius else{ return }
let bottomcenterCoordinates = CGPoint(x: frame.width / 2, y: frame.height)
let getCurrentContext = UIGraphicsGetCurrentContext()
guard let currentContext = getCurrentContext else{ return }
currentContext.drawRadialGradient(gradientValue, startCenter: bottomcenterCoordinates, startRadius: 0.0, endCenter: bottomcenterCoordinates, endRadius: endRadiusValue, options: CGGradientDrawingOptions.drawsAfterEndLocation)
let radialGradientLayer = CALayer(layer: currentContext)
radialGradientLayer.frame = bounds
radialGradientLayer.masksToBounds = true
self.layer.insertSublayer(radialGradientLayer, at: 1)
}
}
When I call this function in viewDidLoad() or viewWillAppear() the compiler contains no mistakes and no warnings, the function just does not work out. i call it as following:
override func viewDidLoad() {
super.viewDidLoad()
view.drawRadialGradient()
}
For example, I have created an extension function for drawing a Linear Gradient on the UIView and it works, I call it the same way as radial gradient function:
func drawLinearGradient() {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.frame
gradientLayer.colors = Colors.gradientColors
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.95)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.05)
self.layer.insertSublayer(gradientLayer, at: 0)
}
For colors I have created a structure:
struct Colors {
static let firstColor = colorPicker(red: 70, green: 183, blue: 0)
static let secondColor = colorPicker(red: 0, green: 170, blue: 116)
static let thirdColor = colorPicker(red: 20, green: 0, blue: 204)
static let gradientColors = [firstColor.cgColor, secondColor.cgColor, thirdColor.cgColor]
static func colorPicker(red: CGFloat, green: CGFloat, blue: CGFloat) -> UIColor {
let color = UIColor(red: red / 255, green: green / 255, blue: blue / 255, alpha: 1.0)
return color
}
}
Please, give me a piece of advice on how to realize it as an extension.
One main thing I can see in your code, is that you try to do the drawing in viewDidLoad. Don't do that, on top of other problems, the frame size is not properly set yet at that moment. If you want the UIView to do the drawing, then derive a class from UIView, and do the drawing in the draw method of that class.
If you want the radialGradientLayer CALayer that you created to do the drawing (it currently is just empty), then derive a subclass from CALayer, and implement its drawInContext method.

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:

Resources