How can I 'cut' a transparent hole in a UIImage? - iphone

I'm trying to cut an transparent square in a UIImage, however I honestly have no idea where/how to start.
Any help would be greatly appreciated.
Thanks!

Presume that your image is being displayed in a view - probably a UIImageView. Then we can punch a rectangular hole in that view by masking the view's layer. Every view has a layer. We will apply to this view's layer a mask which is itself a layer containing an image, which we will generate in code. The image will be black except for a clear rectangle somewhere in the middle. That clear rectangle will cause the hole in the image view.
So, let self.iv be this UIImageView. Try running this code:
CGRect r = self.iv.bounds;
CGRect r2 = CGRectMake(20,20,40,40); // adjust this as desired!
UIGraphicsBeginImageContextWithOptions(r.size, NO, 0);
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextAddRect(c, r2);
CGContextAddRect(c, r);
CGContextEOClip(c);
CGContextSetFillColorWithColor(c, [UIColor blackColor].CGColor);
CGContextFillRect(c, r);
UIImage* maskim = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CALayer* mask = [CALayer layer];
mask.frame = r;
mask.contents = (id)maskim.CGImage;
self.iv.layer.mask = mask;
For example, in this image, the white square is not a superimposed square, it is a hole, showing the white of the window background behind it:
EDIT: I feel obligated, since I mentioned it in a comment, to show how to do the same thing with a CAShapeLayer. The result is exactly the same:
CGRect r = self.iv.bounds;
CGRect r2 = CGRectMake(20,20,40,40); // adjust this as desired!
CAShapeLayer* lay = [CAShapeLayer layer];
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, nil, r2);
CGPathAddRect(path, nil, r);
lay.path = path;
CGPathRelease(path);
lay.fillRule = kCAFillRuleEvenOdd;
self.iv.layer.mask = lay;

Here's a simple Swift function cut#hole#inView to copy and paste for 2017
func cut(hole: CGRect, inView view: UIView) {
let path: CGMutablePath = CGMutablePath()
path.addRect(view.bounds)
path.addRect(hole)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path
shapeLayer.fillRule = .evenOdd
view.layer.mask = shapeLayer
}

Just needed the Version from #Fattie, thanks again! Here is the updated Code for Swift 5.1:
private func cut(holeRect: CGRect, inView view: UIView) {
let combinedPath = CGMutablePath()
combinedPath.addRect(view.bounds)
combinedPath.addRect(holeRect)
let maskShape = CAShapeLayer()
maskShape.path = combinedPath
maskShape.fillRule = .evenOdd
view.layer.mask = maskShape
}
If you want the cutout to have rounded corners you can replace combinedPath.addRect(holeRect) with rectanglePath.addRoundedRect(in: holeRect, cornerWidth: 8, cornerHeight: 8).

Here's the updated code to cut a hole in an UIImage (instead of UIView) using Swift:
func cut(hole: CGRect, inView image: UIImage) -> UIImage? {
UIGraphicsBeginImageContext(image.size)
image.draw(at: CGPoint.zero)
let context = UIGraphicsGetCurrentContext()!
let bez = UIBezierPath(rect: hole)
context.addPath(bez.cgPath)
context.clip()
context.clear(CGRect(x:0,y:0,width: image.size.width,height: image.size.height))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}

Related

Getting masked layer as UIImage on Swift on top of UIImageView

I'm trying to get the UIImage of the mask that I applied to a UIImageView.
I'm adding the mask using UIBezierPath and want the actual masked layer as UIImage, not the whole image. Think of it as a crop feature.
I'm cropping the image using:
func cropImage() {
shapeLayer.fillColor = UIColor.black.cgColor
viewSource.imageView.layer.mask = shapeLayer
viewSource.imageView.layer.masksToBounds = true
UIGraphicsBeginImageContextWithOptions(viewSource.imageView.bounds.size, false, 1)
viewSource.imageView.layer.render(in: UIGraphicsGetCurrentContext()!)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.completionObservable.onNext(newImage)
}
This eventually gives me the masked image on top of the old dimensions (the initial imageView width and height). But I want to have only the masked image, excluding the white background around them.
The screens are as shown:
I know what you mean now. Here is the answer, just update the size of imageContext.
UIGraphicsBeginImageContextWithOptions((shapeLayer.path?.boundingBoxOfPath)!.size, false, 1)
If it's not so simple, can try CIImage pipeline to achieve.
let context = CIContext()
let m1 = newImage?.cgImage
let m = CIImage.init(cgImage: m1!)
let bounds = imageView.layer.bounds
let cgImage = context.createCGImage(m, from: CGRect.init(x: 0, y: bounds.size.height, width: bounds.size.width, height: bounds.size.height))
let newUIImage = UIImage.init(cgImage: cgImage!)
You may need to adjust transform.

UIBezierPath: How to add a border around a view with rounded corners?

I am using UIBezierPath to have my imageview have round corners but I also want to add a border to the imageview. Keep in mind the top is a uiimage and the bottom is a label.
Currently using this code produces:
let rectShape = CAShapeLayer()
rectShape.bounds = myCell2.NewFeedImageView.frame
rectShape.position = myCell2.NewFeedImageView.center
rectShape.path = UIBezierPath(roundedRect: myCell2.NewFeedImageView.bounds,
byRoundingCorners: .TopRight | .TopLeft,
cornerRadii: CGSize(width: 25, height: 25)).CGPath
myCell2.NewFeedImageView.layer.mask = rectShape
I want to add a green border to that but I cant use
myCell2.NewFeedImageView.layer.borderWidth = 8
myCell2.NewFeedImageView.layer.borderColor = UIColor.greenColor().CGColor
because it cuts off the top left and top right corner of the border as seen in this image:
Is there a way too add in a border with UIBezierPath along with my current code?
You can reuse the UIBezierPath path and add a shape layer to the view. Here is an example inside a view controller.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Create a view with red background for demonstration
let v = UIView(frame: CGRectMake(0, 0, 100, 100))
v.center = view.center
v.backgroundColor = UIColor.redColor()
view.addSubview(v)
// Add rounded corners
let maskLayer = CAShapeLayer()
maskLayer.frame = v.bounds
maskLayer.path = UIBezierPath(roundedRect: v.bounds, byRoundingCorners: .TopRight | .TopLeft, cornerRadii: CGSize(width: 25, height: 25)).CGPath
v.layer.mask = maskLayer
// Add border
let borderLayer = CAShapeLayer()
borderLayer.path = maskLayer.path // Reuse the Bezier path
borderLayer.fillColor = UIColor.clearColor().CGColor
borderLayer.strokeColor = UIColor.greenColor().CGColor
borderLayer.lineWidth = 5
borderLayer.frame = v.bounds
v.layer.addSublayer(borderLayer)
}
}
The end result looks like this.
Note that this only works as expected when the view's size is fixed. When the view can resize, you will need to create a custom view class and resize the layers in layoutSubviews.
As it says above:
It is not easy to do this perfectly.
Here's a drop-in solution.
This
correctly addresses the issue that you are drawing HALF OF THE BORDER LINE
is totally usable in autolayout
completely re-works itself when the size of the view changes or animates
is totally IBDesignable - you can see it in realtime in your storyboard
for 2019 ...
#IBDesignable
class RoundedCornersAndTrueBorder: UIView {
#IBInspectable var cornerRadius: CGFloat = 10 {
didSet { setup() }
}
#IBInspectable var borderColor: UIColor = UIColor.black {
didSet { setup() }
}
#IBInspectable var trueBorderWidth: CGFloat = 2.0 {
didSet { setup() }
}
override func layoutSubviews() {
setup()
}
var border:CAShapeLayer? = nil
func setup() {
// make a path with round corners
let path = UIBezierPath(
roundedRect: self.bounds, cornerRadius:cornerRadius)
// note that it is >exactly< the size of the whole view
// mask the whole view to that shape
// note that you will ALSO be masking the border we'll draw below
let mask = CAShapeLayer()
mask.path = path.cgPath
self.layer.mask = mask
// add another layer, which will be the border as such
if (border == nil) {
border = CAShapeLayer()
self.layer.addSublayer(border!)
}
// IN SOME APPROACHES YOU would INSET THE FRAME
// of the border-drawing layer by the width of the border
// border.frame = bounds.insetBy(dx: borderWidth, dy: borderWidth)
// so that when you draw the line, ALL of the WIDTH of the line
// DOES fall within the actual mask.
// here, we will draw the border-line LITERALLY ON THE EDGE
// of the path. that means >HALF< THE LINE will be INSIDE
// the path and HALF THE LINE WILL BE OUTSIDE the path
border!.frame = bounds
let pathUsingCorrectInsetIfAny =
UIBezierPath(roundedRect: border!.bounds, cornerRadius:cornerRadius)
border!.path = pathUsingCorrectInsetIfAny.cgPath
border!.fillColor = UIColor.clear.cgColor
// the following is not what you want:
// it results in "half-missing corners"
// (note however, sometimes you do use this approach):
//border.borderColor = borderColor.cgColor
//border.borderWidth = borderWidth
// this approach will indeed be "inside" the path:
border!.strokeColor = borderColor.cgColor
border!.lineWidth = trueBorderWidth * 2.0
// HALF THE LINE will be INSIDE the path and HALF THE LINE
// WILL BE OUTSIDE the path. so MAKE IT >>TWICE AS THICK<<
// as requested by the consumer class.
}
}
So that's it.
Beginner help for the question in the comments ...
Make a "new Swift file" called "Fattie.swift". (Note, funnily enough it actually makes no difference what you call it. If you are at the stage of "don't know how to make a new file" seek basic Xcode tutorials.)
Put all of the above code in the file
You've just added a class "RoundedCornersAndTrueBorder" to your project.
On your story board. Add an ordinary UIView to your scene. In fact, make it actually any size/shape whatsoever, anything you prefer.
Look at the Identity Inspector. (If you do not know what that is, seek basic tutorials.) Simply change the class to "RoundedCornersAndTrueBorder". (Once you start typing "Roun...", it will guess which class you mean.
You're done - run the project.
Note that you have to, of course, add complete and correct constraints to the UIView, just as with absolutely anything you do in Xcode. Enjoy!
Similar solutions:
https://stackoverflow.com/a/57465440/294884 - image + rounded + shadows
https://stackoverflow.com/a/41553784/294884 - two-corner problem
https://stackoverflow.com/a/59092828/294884 - "shadows + hole" or "glowbox" problem
https://stackoverflow.com/a/57400842/294884 - the "border AND gap" problem
https://stackoverflow.com/a/57514286/294884 - basic "adding" beziers
And please also see the alternate answer below! :)
Absolutely perfect 2019 solution
Without further ado, here's exactly how you do this.
Don't actually use the "basic" layer that comes with the view
Make a new layer only for the image. You can now mask this (circularly) without affecting the next layer
Make a new layer for the border as such. It will safely not be masked by the picture layer.
The key facts are
With a CALayer, you can indeed apply a .mask and it only affects that layer
When drawing a circle (or indeed any border), have to attend very carefully to the fact that you only get "half the width" - in short never crop using the same path you draw with.
Notice the original cat image is exactly as wide as the horizontal yellow arrow. You have to be careful to paint the image so that the whole image appears in the roundel, which is smaller than the overall custom control.
So, setup in the usual way
import UIKit
#IBDesignable class GreenCirclePerson: UIView {
#IBInspectable var borderColor: UIColor = UIColor.black { didSet { setup() } }
#IBInspectable var trueBorderThickness: CGFloat = 2.0 { didSet { setup() } }
#IBInspectable var trueGapThickness: CGFloat = 2.0 { didSet { setup() } }
#IBInspectable var picture: UIImage? = nil { didSet { setup() } }
override func layoutSubviews() { setup() }
var imageLayer: CALayer? = nil
var border: CAShapeLayer? = nil
func setup() {
if (imageLayer == nil) {
imageLayer = CALayer()
self.layer.addSublayer(imageLayer!)
}
if (border == nil) {
border = CAShapeLayer()
self.layer.addSublayer(border!)
}
Now carefully make the layer for the circularly-cropped image:
// the ultimate size of our custom control:
let box = self.bounds.aspectFit()
let totalInsetOnAnyOneSide = trueBorderThickness + trueGapThickness
let boxInWhichImageSits = box.inset(by:
UIEdgeInsets(top: totalInsetOnAnyOneSide, left: totalInsetOnAnyOneSide,
bottom: totalInsetOnAnyOneSide, right: totalInsetOnAnyOneSide))
// just a note. that version of inset#by is much clearer than the
// confusing dx/dy variant, so best to use that one
imageLayer!.frame = boxInWhichImageSits
imageLayer!.contents = picture?.cgImage
imageLayer?.contentsGravity = .resizeAspectFill
let halfImageSize = boxInWhichImageSits.width / 2.0
let maskPath = UIBezierPath(roundedRect: imageLayer!.bounds,
cornerRadius:halfImageSize)
let maskLayer = CAShapeLayer()
maskLayer.path = maskPath.cgPath
imageLayer!.mask = maskLayer
Next as a completely separate layer, draw the border as you wish:
// now create the border
border!.frame = bounds
// To draw the border, you must inset it by half the width of the border,
// otherwise you'll be drawing only half the border. (Indeed, as an additional
// subtle problem you are clipping rather than rendering the outside edge.)
let halfWidth = trueBorderThickness / 2.0
let borderCenterlineBox = box.inset(by:
UIEdgeInsets(top: halfWidth, left: halfWidth,
bottom: halfWidth, right: halfWidth))
let halfBorderBoxSize = borderCenterlineBox.width / 2.0
let borderPath = UIBezierPath(roundedRect: borderCenterlineBox,
cornerRadius:halfBorderBoxSize)
border!.path = borderPath.cgPath
border!.fillColor = UIColor.clear.cgColor
border!.strokeColor = borderColor.cgColor
border!.lineWidth = trueBorderThickness
}
}
Everything works perfectly as in iOS standard controls:
Everything which is invisible is invisible; you can see-through the overall custom control to any material behind, there are no "half thickness" problems or missing image material, you can set the custom control background color in the usual way, etc etc. The inspector controls all work properly. (Phew!)
Similar solutions:
https://stackoverflow.com/a/57465440/294884 - image + rounded + shadows
https://stackoverflow.com/a/41553784/294884 - two-corner problem
https://stackoverflow.com/a/59092828/294884 - "shadows + hole" or "glowbox" problem
https://stackoverflow.com/a/57400842/294884 - the "border AND gap" problem
https://stackoverflow.com/a/57514286/294884 - basic "adding" beziers
**use this extension for round borders and corner**
extension UIView {
func roundCorners(corners: UIRectCorner, radius: CGFloat) {
let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask
}
func roundCornersWithBorder(corners: UIRectCorner, radius: CGFloat) {
let maskLayer = CAShapeLayer()
maskLayer.frame = bounds
maskLayer.path = UIBezierPath(roundedRect: bounds, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: radius, height: radius)).cgPath
layer.mask = maskLayer
// Add border
let borderLayer = CAShapeLayer()
borderLayer.path = maskLayer.path // Reuse the Bezier path
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.strokeColor = UIColor(red:3/255, green:33/255, blue:70/255, alpha: 0.15).cgColor
borderLayer.lineWidth = 2
borderLayer.frame = bounds
layer.addSublayer(borderLayer)
}
}
Use like this
myView.roundCornersWithBorder(corners: [.topLeft, .topRight], radius: 8.0)
myView.roundCorners(corners: [.topLeft, .topRight], radius: 8.0)
There sure is! Every view has a layer property (which you know from giving your layer rounded corners). Another two properties on layer are borderColor and borderWidth. Just by setting those you can add a border to your view! (The border will follow the rounded corners.) Be sure to use UIColor.CGColor for borderColor as a plain UIColor won't match the type.

Swift - How to create a view with a shape cropped in it

I'm trying to achieve the result shown in the image using swift 1.2 and xcode 6.
Basically I want to create a view with a shape cut in it to be able to see the the view below to make a tutorial for my app.
I know how to create a circular shape but i don't know how to cut it out in a view.
I need a complete example on how to do it please.
Thanks in advance
Even though there is an answer, i'd like to share my way:
// Let's say that you have an outlet to the image view called imageView
// Create the white view
let whiteView = UIView(frame: imageView.bounds)
let maskLayer = CAShapeLayer() //create the mask layer
// Set the radius to 1/3 of the screen width
let radius : CGFloat = imageView.bounds.width/3
// Create a path with the rectangle in it.
let path = UIBezierPath(rect: imageView.bounds)
// Put a circle path in the middle
path.addArcWithCenter(imageView.center, radius: radius, startAngle: 0.0, endAngle: CGFloat(2*M_PI), clockwise: true)
// Give the mask layer the path you just draw
maskLayer.path = path.CGPath
// Fill rule set to exclude intersected paths
maskLayer.fillRule = kCAFillRuleEvenOdd
// By now the mask is a rectangle with a circle cut out of it. Set the mask to the view and clip.
whiteView.layer.mask = maskLayer
whiteView.clipsToBounds = true
whiteView.alpha = 0.8
whiteView.backgroundColor = UIColor.whiteColor()
//If you are in a VC add to the VC's view (over the image)
view.addSubview(whiteView)
// Annnnnd you're done.
//assume you create a UIImageView and content image before execute this code
let sampleMask = UIView()
sampleMask.frame = self.view.frame
sampleMask.backgroundColor = UIColor.black.withAlphaComponent(0.6)
//assume you work in UIViewcontroller
self.view.addSubview(sampleMask)
let maskLayer = CALayer()
maskLayer.frame = sampleMask.bounds
let circleLayer = CAShapeLayer()
//assume the circle's radius is 150
circleLayer.frame = CGRect(x:0 , y:0,width: sampleMask.frame.size.width,height: sampleMask.frame.size.height)
let finalPath = UIBezierPath(roundedRect: CGRect(x:0 , y:0,width: sampleMask.frame.size.width,height: sampleMask.frame.size.height), cornerRadius: 0)
let circlePath = UIBezierPath(ovalIn: CGRect(x:sampleMask.center.x - 150, y:sampleMask.center.y - 150, width: 300, height: 300))
finalPath.append(circlePath.reversing())
circleLayer.path = finalPath.cgPath
circleLayer.borderColor = UIColor.white.withAlphaComponent(1).cgColor
circleLayer.borderWidth = 1
maskLayer.addSublayer(circleLayer)
sampleMask.layer.mask = maskLayer
Here is sample code for how you can make a circle Mask for a UIView:
let sampleView = UIView(frame: UIScreen.mainScreen().bounds)
let maskLayer = CALayer()
maskLayer.frame = sampleView.bounds
let circleLayer = CAShapeLayer()
//assume the circle's radius is 100
circleLayer.frame = CGRectMake(sampleView.center.x - 100, sampleView.center.y - 100, 200, 200)
let circlePath = UIBezierPath(ovalInRect: CGRectMake(0, 0, 200, 200))
circleLayer.path = circlePath.CGPath
circleLayer.fillColor = UIColor.blackColor().CGColor
maskLayer.addSublayer(circleLayer)
sampleView.layer.mask = maskLayer
Here is what I made in the playground:
The easiest way to do this would be to create a png image with partly transparent white around the outside and a clear circle in the middle. Then stack 2 image views on top of each other, with the masking image on top, and set its "opaque" flag to false.
You could also do this by creating a CAShapeLayer and set it up to use a translucent white color, then install a shape that is the square with the hole cut out of it shape. You'd install that shape layer on top of your image view's layer.
The most general-purpose way to do that would be to create a custom subclass of UIImageView and have the init method of your subclass create and install the shape layer. I just created a gist yesterday that illustrated creating a custom subclass of UIImageView. Here is the link: ImageViewWithGradient gist
That gist creates a gradient layer. It would be a simple matter to adapt it to create a shape layer instead, and if you modified the layoutSubviews method you could make it adapt the view and path if the image view gets resized.
EDIT:
Ok, I took the extra step of creating a playground that creates a cropping image view. You can find that at ImageViewWithMask on github
The resulting image for my playground looks like this:
class MakeTransparentHoleOnOverlayView: UIView {
#IBOutlet weak var transparentHoleView: UIView!
// MARK: - Drawing
override func draw(_ rect: CGRect) {
super.draw(rect)
if self.transparentHoleView != nil {
// Ensures to use the current background color to set the filling color
self.backgroundColor?.setFill()
UIRectFill(rect)
let layer = CAShapeLayer()
let path = CGMutablePath()
// Make hole in view's overlay
// NOTE: Here, instead of using the transparentHoleView UIView we could use a specific CFRect location instead...
path.addRect(transparentHoleView.frame)
path.addRect(bounds)
layer.path = path
layer.fillRule = kCAFillRuleEvenOdd
self.layer.mask = layer
}
}
override func layoutSubviews () {
super.layoutSubviews()
}
// MARK: - Initialization
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
}

Moving an object along a curve in iPhone development

I wanted to animate an image object by moving it along a particular curve. It is not a general or random curve but rather a curve which follows a particular path on screen.
Currently, Im manually specifying the list of x and y co-ordinates of the path along which i want the image object to move by setting its frame each time. This is a laborious process in the sense that im setting the specific x and y coordinates of the path and moving the image along it. Is there a more efficient way to do this?
Is there a way that i can specify,say, just about 15 - 20 points and have a curve traced along those to move the object? Any other way to acheive this? Any help would be much appreciated. Thanks.
You could use a combination of UIBezierPath and CAKeyFrameAnimation.
I found a very useful blog post dealing with this subject.
http://oleb.net/blog/2010/12/animating-drawing-of-cgpath-with-cashapelayer/
Here's a simplified version of what I used (it just animates the drawing of a square):
UIBezierPath *customPath = [UIBezierPath bezierPath];
[customPath moveToPoint:CGPointMake(100,100)];
[customPath addLineToPoint:CGPointMake(200,100)];
[customPath addLineToPoint:CGPointMake(200,200)];
[customPath addLineToPoint:CGPointMake(100,200)];
[customPath addLineToPoint:CGPointMake(100,100)];
UIImage *movingImage = [UIImage imageNamed:#"foo.png"];
CALayer *movingLayer = [CALayer layer];
movingLayer.contents = (id)movingImage.CGImage;
movingLayer.anchorPoint = CGPointZero;
movingLayer.frame = CGRectMake(0.0f, 0.0f, movingImage.size.width, movingImage.size.height);
[self.view.layer addSublayer:movingLayer];
CAKeyframeAnimation *pathAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
pathAnimation.duration = 4.0f;
pathAnimation.path = customPath.CGPath;
pathAnimation.calculationMode = kCAAnimationLinear;
[movingLayer addAnimation:pathAnimation forKey:#"movingAnimation"];
In Swift-3 version of #Jilouc :-
override func viewDidLoad() {
super.viewDidLoad()
addAdditiveAnimation()
initiateAnimation()
}
//curve which follows a particular path
func pathToTrace() -> UIBezierPath {
let path = UIBezierPath(ovalIn: CGRect(x: 120 , y: 120, width: 100, height: 100))
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 1.0
self.view.layer.addSublayer(shapeLayer)
return path
}
func addAdditiveAnimation() {
let movement = CAKeyframeAnimation(keyPath: "position")
movement.path = pathToTrace().cgPath
movement.duration = 5
movement.repeatCount = HUGE
movement.calculationMode = kCAAnimationPaced
movement.timingFunctions = [CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)]
self.movement = movement
}
func createLayer() -> CALayer {
let layer = CALayer()
let image = UIImage(named: "launch.png")
layer.frame = CGRect(x: 0 , y: 0, width: (image?.size.width),height: (image?.size.height))
layer.position = CGPoint(x: 5, y: 5)
layer.contents = image?.cgImage
layer.anchorPoint = .zero
layer.backgroundColor = UIColor.red.cgColor
//layer.cornerRadius = 5
self.view.layer.addSublayer(layer)
return layer
}
func initiateAnimation() {
let layer = createLayer()
layer.add(self.movement, forKey: "Object Movement")
}
Github Demo

How do I add a gradient to the text of a UILabel, but not the background?

hey, I want to be able to have a gradient fill on the text in a UILabel I know about CGGradient but i dont know how i would use it on a UILabel's text
i found this on google but i cant manage to get it to work
http://silverity.livejournal.com/26436.html
I was looking for a solution and DotSlashSlash has the answer hidden in one of the comments!
For the sake of completeness, the answer and the simplest solution is:
UIImage *myGradient = [UIImage imageNamed:#"textGradient.png"];
myLabel.textColor = [UIColor colorWithPatternImage:myGradient];
(Skip to bottom for full class source code)
Really useful answers by both Brad Larson and Bach. The second worked for me but it requires an image to be present in advance. I wanted something more dynamic so I combined both solutions into one:
draw the desired gradient on a UIImage
use the UIImage to set the color pattern
The result works and in the screenshot below you can see some Greek characters rendered fine too. (I have also added a stroke and a shadow on top of the gradient)
Here's the custom init method of my label along with the method that renders a gradient on a UIImage (part of the code for that functionality I got from a blog post I can not find now to reference it):
- (id)initWithFrame:(CGRect)frame text:(NSString *)aText {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
self.text = aText;
self.textColor = [UIColor colorWithPatternImage:[self gradientImage]];
}
return self;
}
- (UIImage *)gradientImage
{
CGSize textSize = [self.text sizeWithFont:self.font];
CGFloat width = textSize.width; // max 1024 due to Core Graphics limitations
CGFloat height = textSize.height; // max 1024 due to Core Graphics limitations
// create a new bitmap image context
UIGraphicsBeginImageContext(CGSizeMake(width, height));
// get context
CGContextRef context = UIGraphicsGetCurrentContext();
// push context to make it current (need to do this manually because we are not drawing in a UIView)
UIGraphicsPushContext(context);
//draw gradient
CGGradientRef glossGradient;
CGColorSpaceRef rgbColorspace;
size_t num_locations = 2;
CGFloat locations[2] = { 0.0, 1.0 };
CGFloat components[8] = { 0.0, 1.0, 1.0, 1.0, // Start color
1.0, 1.0, 0.0, 1.0 }; // End color
rgbColorspace = CGColorSpaceCreateDeviceRGB();
glossGradient = CGGradientCreateWithColorComponents(rgbColorspace, components, locations, num_locations);
CGPoint topCenter = CGPointMake(0, 0);
CGPoint bottomCenter = CGPointMake(0, textSize.height);
CGContextDrawLinearGradient(context, glossGradient, topCenter, bottomCenter, 0);
CGGradientRelease(glossGradient);
CGColorSpaceRelease(rgbColorspace);
// pop context
UIGraphicsPopContext();
// get a UIImage from the image context
UIImage *gradientImage = UIGraphicsGetImageFromCurrentImageContext();
// clean up drawing environment
UIGraphicsEndImageContext();
return gradientImage;
}
I'll try to complete that UILabel subclass and post it.
EDIT:
The class is done and it's on my GitHub repository. Read about it here!
Swift 4.1
class GradientLabel: UILabel {
var gradientColors: [CGColor] = []
override func drawText(in rect: CGRect) {
if let gradientColor = drawGradientColor(in: rect, colors: gradientColors) {
self.textColor = gradientColor
}
super.drawText(in: rect)
}
private func drawGradientColor(in rect: CGRect, colors: [CGColor]) -> UIColor? {
let currentContext = UIGraphicsGetCurrentContext()
currentContext?.saveGState()
defer { currentContext?.restoreGState() }
let size = rect.size
UIGraphicsBeginImageContextWithOptions(size, false, 0)
guard let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(),
colors: colors as CFArray,
locations: nil) else { return nil }
let context = UIGraphicsGetCurrentContext()
context?.drawLinearGradient(gradient,
start: CGPoint.zero,
end: CGPoint(x: size.width, y: 0),
options: [])
let gradientImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
guard let image = gradientImage else { return nil }
return UIColor(patternImage: image)
}
}
Usage:
label.gradientColors = [UIColor.blue.cgColor, UIColor.red.cgColor]
SWIFT 3+
This solution is based on #Dimitris's answer. It is an extension on the UILabel class that will create a gradient over the label's text per your passed startColor and endColor. The UILabel extension is below:
extension UILabel {
func applyGradientWith(startColor: UIColor, endColor: UIColor) -> Bool {
var startColorRed:CGFloat = 0
var startColorGreen:CGFloat = 0
var startColorBlue:CGFloat = 0
var startAlpha:CGFloat = 0
if !startColor.getRed(&startColorRed, green: &startColorGreen, blue: &startColorBlue, alpha: &startAlpha) {
return false
}
var endColorRed:CGFloat = 0
var endColorGreen:CGFloat = 0
var endColorBlue:CGFloat = 0
var endAlpha:CGFloat = 0
if !endColor.getRed(&endColorRed, green: &endColorGreen, blue: &endColorBlue, alpha: &endAlpha) {
return false
}
let gradientText = self.text ?? ""
let name:String = NSFontAttributeName
let textSize: CGSize = gradientText.size(attributes: [name:self.font])
let width:CGFloat = textSize.width
let height:CGFloat = textSize.height
UIGraphicsBeginImageContext(CGSize(width: width, height: height))
guard let context = UIGraphicsGetCurrentContext() else {
UIGraphicsEndImageContext()
return false
}
UIGraphicsPushContext(context)
let glossGradient:CGGradient?
let rgbColorspace:CGColorSpace?
let num_locations:size_t = 2
let locations:[CGFloat] = [ 0.0, 1.0 ]
let components:[CGFloat] = [startColorRed, startColorGreen, startColorBlue, startAlpha, endColorRed, endColorGreen, endColorBlue, endAlpha]
rgbColorspace = CGColorSpaceCreateDeviceRGB()
glossGradient = CGGradient(colorSpace: rgbColorspace!, colorComponents: components, locations: locations, count: num_locations)
let topCenter = CGPoint.zero
let bottomCenter = CGPoint(x: 0, y: textSize.height)
context.drawLinearGradient(glossGradient!, start: topCenter, end: bottomCenter, options: CGGradientDrawingOptions.drawsBeforeStartLocation)
UIGraphicsPopContext()
guard let gradientImage = UIGraphicsGetImageFromCurrentImageContext() else {
UIGraphicsEndImageContext()
return false
}
UIGraphicsEndImageContext()
self.textColor = UIColor(patternImage: gradientImage)
return true
}
}
And usage:
let text = "YAAASSSSS!"
label.text = text
if label.applyGradientWith(startColor: .red, endColor: .blue) {
print("Gradient applied!")
}
else {
print("Could not apply gradient")
label.textColor = .black
}
SWIFT 2
class func getGradientForText(text: NSString) -> UIImage {
let font:UIFont = UIFont(name: "YourFontName", size: 50.0)!
let name:String = NSFontAttributeName
let textSize: CGSize = text.sizeWithAttributes([name:font])
let width:CGFloat = textSize.width // max 1024 due to Core Graphics limitations
let height:CGFloat = textSize.height // max 1024 due to Core Graphics limitations
//create a new bitmap image context
UIGraphicsBeginImageContext(CGSizeMake(width, height))
// get context
let context = UIGraphicsGetCurrentContext()
// push context to make it current (need to do this manually because we are not drawing in a UIView)
UIGraphicsPushContext(context!)
//draw gradient
let glossGradient:CGGradientRef?
let rgbColorspace:CGColorSpaceRef?
let num_locations:size_t = 2
let locations:[CGFloat] = [ 0.0, 1.0 ]
let components:[CGFloat] = [(202 / 255.0), (197 / 255.0), (52 / 255.0), 1.0, // Start color
(253 / 255.0), (248 / 255.0), (101 / 255.0), 1.0] // End color
rgbColorspace = CGColorSpaceCreateDeviceRGB();
glossGradient = CGGradientCreateWithColorComponents(rgbColorspace, components, locations, num_locations);
let topCenter = CGPointMake(0, 0);
let bottomCenter = CGPointMake(0, textSize.height);
CGContextDrawLinearGradient(context, glossGradient, topCenter, bottomCenter, CGGradientDrawingOptions.DrawsBeforeStartLocation);
// pop context
UIGraphicsPopContext();
// get a UIImage from the image context
let gradientImage = UIGraphicsGetImageFromCurrentImageContext();
// clean up drawing environment
UIGraphicsEndImageContext();
return gradientImage;
}
Props to #Dimitris
The example you provide relies on private text drawing functions that you don't have access to on the iPhone. The author provides an example of how to do this using public API in a subsequent post. His later example uses a gradient image for the color of the text. (Unfortunately, it appears his blog has since been removed, but see Bach's answer here for the approach he used.)
If you still want to draw the gradient for your text color in code, it can be done by subclassing UILabel and overriding -drawRect: to have code like the following within it:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextTranslateCTM(context, 0.0f, self.bounds.size.height);
CGContextScaleCTM(context, 1.0f, -1.0f);
CGContextSelectFont(context, "Helvetica", 20.0f, kCGEncodingMacRoman);
CGContextSetTextDrawingMode(context, kCGTextClip);
CGContextSetTextPosition(context, 0.0f, round(20.0f / 4.0f));
CGContextShowText(context, [self.text UTF8String], strlen([self.text UTF8String]));
CGContextClip(context);
CGGradientRef gradient;
CGColorSpaceRef rgbColorspace;
size_t num_locations = 2;
CGFloat locations[2] = { 0.0, 1.0 };
CGFloat components[8] = { 1.0, 1.0, 1.0, 1.0, // Start color
1.0, 1.0, 1.0, 0.1 }; // End color
rgbColorspace = CGColorSpaceCreateDeviceRGB();
gradient = CGGradientCreateWithColorComponents(rgbColorspace, components, locations, num_locations);
CGRect currentBounds = self.bounds;
CGPoint topCenter = CGPointMake(CGRectGetMidX(currentBounds), 0.0f);
CGPoint midCenter = CGPointMake(CGRectGetMidX(currentBounds), CGRectGetMidY(currentBounds));
CGContextDrawLinearGradient(context, gradient, topCenter, midCenter, 0);
CGGradientRelease(gradient);
CGColorSpaceRelease(rgbColorspace);
CGContextRestoreGState(context);
One shortcoming of this approach is that the Core Graphics functions I use don't handle Unicode text properly.
What the code does is it flips the drawing context vertically (the iPhone inverts the normal Quartz coordinate system on for the Y axis), sets the text drawing mode to intersect the drawn text with the clipping path, clips the area to draw to the text, and then draws a gradient. The gradient will only fill the text, not the background.
I tried using NSString's -drawAtPoint: method for this, which does support Unicode, but all the characters ran on top of one another when I switched the text mode to kCGTextClip.
Here's what I'm doing in Swift 3
override func viewDidLoad() {
super.viewDidLoad()
timerLabel.textColor = UIColor(patternImage: gradientImage(size: timerLabel.frame.size, color1: CIColor(color: UIColor.green), color2: CIColor(color: UIColor.red), direction: .Left))
}
func gradientImage(size: CGSize, color1: CIColor, color2: CIColor, direction: GradientDirection = .Up) -> UIImage {
let context = CIContext(options: nil)
let filter = CIFilter(name: "CILinearGradient")
var startVector: CIVector
var endVector: CIVector
filter!.setDefaults()
switch direction {
case .Up:
startVector = CIVector(x: size.width * 0.5, y: 0)
endVector = CIVector(x: size.width * 0.5, y: size.height)
case .Left:
startVector = CIVector(x: size.width, y: size.height * 0.5)
endVector = CIVector(x: 0, y: size.height * 0.5)
case .UpLeft:
startVector = CIVector(x: size.width, y: 0)
endVector = CIVector(x: 0, y: size.height)
case .UpRight:
startVector = CIVector(x: 0, y: 0)
endVector = CIVector(x: size.width, y: size.height)
}
filter!.setValue(startVector, forKey: "inputPoint0")
filter!.setValue(endVector, forKey: "inputPoint1")
filter!.setValue(color1, forKey: "inputColor0")
filter!.setValue(color2, forKey: "inputColor1")
let image = UIImage(cgImage: context.createCGImage(filter!.outputImage!, from: CGRect(x: 0, y: 0, width: size.width, height: size.height))!)
return image
}
There is a really simple solution for this! Here's how you add gradient colors to UILabel text.
We will achieve this in just two steps:
Create Gradient Image
Apply Gradient Image As textColor to UILabel
1.Create Gradient Image
extension UIImage {
static func gradientImageWithBounds(bounds: CGRect, colors: [CGColor]) -> UIImage {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.colors = colors
UIGraphicsBeginImageContext(gradientLayer.bounds.size)
gradientLayer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image!
}
}
Use this as follows:
let gradientImage = UIImage.gradientImageWithBounds(bounds: myLabel.bounds, colors: [firstColor.cgColor, secondColor.cgColor])
⠀
2.Apply Gradient Image As textColor to UILabel
myLabel.textColor = UIColor.init(patternImage: gradientImage)
⠀
Note:
If you want the gradient to be horizontal, just add these two lines to gradientLayer instance:
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
⠀
Note 2:
The UIImage extension function works with other UIViews too; not just UILabel! So feel free to use this method no matter which UIView you use to apply gradient color.
yourLabel.textColor = UIColor(patternImage: UIImage(named: "ur gradient image name ")!)
SwiftUI
Although we use Text in SwiftUI instead of UILabel, If you consider how to apply a gradient on a Text, you should apply it as a mask. But since gradients are stretchable, you can make a simple extension like this:
extension View {
func selfSizeMask<T: View>(_ mask: T) -> some View {
ZStack {
self.opacity(0)
mask.mask(self)
}.fixedSize()
}
}
Demo
And then you can assign any gradient or other type of view as a self-size mask like:
Text("Gradient is on FIRE !!!")
.selfSizeMask(
LinearGradient(
gradient: Gradient(colors: [.red, .yellow]),
startPoint: .bottom,
endPoint: .top)
)
This method contains some bonus advantages that you can see here in this answer
Simplest Swift 3 Solution
Add an image to your project assets or create one programmatically then do the following:
let image = UIImage(named: "myGradient.png")!
label.textColor = UIColor.init(patternImage: image)
You could sub-class out UILable and do the draw method yourself. That would probably be the more difficult approach, there might be an easier way.