In my app I need to have a UIView whose form is initially a circle but then it can be altered into an ellipse by changing its height or width. So, I have overridden the drawRect() method like the following:
override func drawRect(rect: CGRect) {
if (self.isRound) {
let ctx:CGContext = UIGraphicsGetCurrentContext()!
CGContextSetRGBStrokeColor(ctx, 0.0, 0.0, 0.0, 1.0)
CGContextSetLineWidth(ctx, 0.5)
CGContextAddEllipseInRect(ctx, rect)
CGContextStrokePath(ctx)
self.layer.cornerRadius = frame.size.width / 2
}
}
which works quite well, returning this result:
I would like to know how to hide, if possibile, those 4 corners so that only the ellipse itself is strictly visibile. Is there a way?
UPDATE and SOLUTION
This is the solution I have found, looking here in all Apple Core Graphics functions concerning ellipses. First of all, if my UIView is supposed to be an ellipse, in its constructor I take the following trick concerning alpha parameter:
if (self.isRound) {
self.backgroundColor = UIColor(red: 0.0, green: 0.6, blue: 1.0, alpha: 0.0)
}
else {
self.backgroundColor = UIColor(red: 0.0, green: 0.6, blue: 1.0, alpha: 0.4)
}
In this way the background rectangle is not showed at all. Then, this is my drawRect() method:
override func drawRect(rect: CGRect) {
if (self.isRound) {
let ctx:CGContext = UIGraphicsGetCurrentContext()!
CGContextSetRGBFillColor(ctx, 0.0, 0.6, 1.0, 0.4) /*!!!*/
CGContextFillEllipseInRect(ctx, rect) /*use Fill and not Stroke */
CGContextStrokePath(ctx)
}
}
which gives me this result:
You need a clipping path, have a look at CGContextClip
Related
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.
I want to put a gradient background in all my views.
The way that I thought to do this without the need to create a IBOutlet of a view in all view controllers is to create a new class from UIView, and inside the draw() I put the code that makes a CAGradientLayer in the view.
So, in the interface builder I just need to select the background view and set its class as my custom class. It works so far.
My question is if I can do that without problems. Somebody knows is it ok?
Because the model file that inherit from UIView come with the comment:
//Only override draw() if you perform custom drawing.
And I don't know if create a layer counts. Or if draw() is only to low level drawing or something like that. Do not know nothing about the best usage of the draw() func.
This is the code:
override func draw(_ rect: CGRect) {
let layer = CAGradientLayer()
layer.frame = CGRect(x: 0, y: 0, width: self.frame.width, height: self.frame.height)
let color1 = UIColor(red: 4/255, green: 39/255, blue: 105/255, alpha: 1)
let color2 = UIColor(red: 1/255, green: 91/255, blue: 168/255, alpha: 1)
layer.colors = [color1.cgColor, color2.cgColor]
self.layer.addSublayer(layer)
}
You can use #IBDesignable and #IBInspectable to configure the startColor and endColor properties from the Storyboard. Use CGGradient which specifies colors and locations instead of CAGradientLayer if you want to draw in draw(_ rect:) method. The Simple Gradient class and draw(_ rect: CGRect) function would look like this
import UIKit
#IBDesignable
class GradientView: UIView {
#IBInspectable var startColor: UIColor = UIColor(red: 4/255, green: 39/255, blue: 105/255, alpha: 1)
#IBInspectable var endColor: UIColor = UIColor(red: 1/255, green: 91/255, blue: 168/255, alpha: 1)
override func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
let colors = [startColor.cgColor, endColor.cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let colorLocations: [CGFloat] = [0.0, 1.0]
let gradient = CGGradient(colorsSpace: colorSpace,
colors: colors as CFArray,
locations: colorLocations)!
let startPoint = CGPoint.zero
let endPoint = CGPoint(x: 0, y: bounds.height)
context.drawLinearGradient(gradient,
start: startPoint,
end: endPoint,
options: [CGGradientDrawingOptions(rawValue: 0)])
}
}
you can read more about it here and tutorial
You shouldn't be adding a CAGradientLayer inside the draw function.
If you want to add a gradient to a view then the easiest way is to do it with a layer (like you are doing) but not like this...
You should add it to your view as part of the init method and then update its frame in the layout subviews method.
Something like this...
class MyView: UIView {
let gradientLayer: CAGradientLayer = {
let l = CAGradientLayer()
let color1 = UIColor(red: 4/255, green: 39/255, blue: 105/255, alpha: 1)
let color2 = UIColor(red: 1/255, green: 91/255, blue: 168/255, alpha: 1)
l.colors = [color1.cgColor, color2.cgColor]
return l
}()
override init(frame: CGRect) {
super.init(frame: frame)
layer.addSublayer(gradientLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = bounds
}
}
I would create UIView extension and apply it when needed.
extension UIView {
func withGradienBackground(color1: UIColor, color2: UIColor, color3: UIColor, color4: UIColor) {
let layerGradient = CAGradientLayer()
layerGradient.colors = [color1.cgColor, color2.cgColor, color3.cgColor, color4.cgColor]
layerGradient.frame = bounds
layerGradient.startPoint = CGPoint(x: 1.5, y: 1.5)
layerGradient.endPoint = CGPoint(x: 0.0, y: 0.0)
layerGradient.locations = [0.0, 0.3, 0.4, 1.0]
layer.insertSublayer(layerGradient, at: 0)
}
}
Here you can change func declaration to specify startPoint, endPointand locations params as variables.
After that just call this method like
gradienView.withGradienBackground(color1: UIColor.green, color2: UIColor.red, color3: UIColor.blue, color4: UIColor.white)
P.S.
Please note that you can have as much colors as you want in colors array
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.
I need to draw an animated linear gradient that is updated on every frame, using CADisplayLink. Drawing directly into a full-screen context with CoreGraphics is slow, I get around 40fps at full CPU load on an iPad Air.
What's the fastest way to do this?
Update
As #Kurt Revis pointed out in a comment on the question, the proper - and fastest - way to do this is to use CAGradientLayer. Add a CAGradientLayer to your view that fills its bounds, then:
func updateGradient() {
// Sample random gradient
let gradientLocs:[CGFloat] = [0.0, 0.5, 1.0]
let gradientColors:[CGColorRef] = [
UIColor.whiteColor().CGColor,
UIColor.init(colorLiteralRed: 0.0, green: 0.0, blue: 0.25*Float(arc4random())/Float(UINT32_MAX), alpha: 1.0).CGColor,
UIColor.blackColor().CGColor
]
// Disable implicit animations if this is called via CADisplayLink
CATransaction.begin()
CATransaction.setDisableActions(true)
// Draw to existing CAGradientLayer
gradientBackgroundLayer.colors = gradientColors
gradientBackgroundLayer.locations = gradientLocs
CATransaction.commit()
}
Old answer
After some experimentation, I ended up using CoreGraphics to draw the gradient into a 1px wide CGImage and then rely on UIImageView to do the scaling. I get solid 60fps at about 7-11% CPU load, depending on how many colors the gradient contains:
Subclass UIImageView with contentMode set to ScaleToFill, then call the following via CADisplayLink for a continuous animation.
func updateGradient() {
// Sample random gradient
let gradientLocs:[CGFloat] = [0.0, 0.5, 1.0]
let gradientColors:[CGColorRef] = [
UIColor.whiteColor().CGColor,
UIColor.init(colorLiteralRed: 0.0, green: 0.0, blue: 0.25*Float(arc4random())/Float(UINT32_MAX), alpha: 1.0).CGColor,
UIColor.blackColor().CGColor
]
// Create context at 1px width and display height
let gradientWidth:Double = 1
let gradientHeight:Double = Double(UIScreen.mainScreen().bounds.height)
UIGraphicsBeginImageContext(CGSize(width: gradientWidth, height: gradientHeight))
let buffer:CGContextRef = UIGraphicsGetCurrentContext()!
// Draw gradient into buffer
let colorspace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradientCreateWithColors(colorspace, gradientColors, gradientLocs)
CGContextDrawLinearGradient(
buffer,
gradient,
CGPoint(x: 0.0, y: 0.0),
CGPoint(x: 0.0, y: gradientHeight),
CGGradientDrawingOptions.DrawsAfterEndLocation
)
// Let UIImageView superclass handle scaling
image = UIImage.init(CGImage: CGBitmapContextCreateImage(buffer)!)
// Avoid memory leaks...
UIGraphicsEndImageContext()
}
Caveat: this approach will only work for horizontal or vertical linear gradients.
Are there better ways of doing this?
I'm trying to add opacity to my view. When I add a CAGradientlayer, I roughly get, what I planned, but have a few issues.
I can't change the color. No matter what values I use, I'll always get a white layer
Even though I wanted it to be distributed over the whole view, it seems like it has an offset to the left.
My code is as follows:
let maskLayer = CAGradientLayer()
maskLayer.anchorPoint = CGPoint(x: 0.0, y: 0.0)
maskLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
maskLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
maskLayer.colors = [UIColor(white: 0.0, alpha: 0.0).CGColor, UIColor(white: 1.0, alpha: 1.0).CGColor, UIColor(white: 0.0, alpha: 0.0).CGColor]
maskLayer.locations = [0.0, 0.5, 1.0]
maskLayer.bounds = CGRectMake(0, 0, self.frame.width, self.frame.height)
self.layer.mask = maskLayer
What am I doing wrong?
Thanks
About the white, this is because a layer mask does not contribute to color, only transparency. Essentially, only the alpha component has an effect, and the R,G,B components of the layer mask are ignored.
About the offset, could it be because your view is getting resized, and your layer isn't kept up to date? If so you could see the answer to this other question: CALayers didn't get resized on its UIView's bounds change. Why?