Bad quality for my Radial Gradient - ios

I am drawing a radial gradient in UIView's drawRect function which is pretty simple, the problem I have is that on device (iPhone 6S plus) gradient quality is pretty bad.
Do you have an idea why?
EDIT: I noticed that gradient is bad if my view or color has an alpha value less than 1.0.
private func updateGradient() {
let colorSpace = CGColorSpaceCreateDeviceRGB()
let colors:[CGColor] = [haloColor.CGColor, UIColor.clearColor().CGColor]
gradient = CGGradientCreateWithColors(colorSpace, colors, [0.0,0.8])
setNeedsDisplay()
}
override public func drawRect(rect: CGRect) {
if let gradient = self.gradient {
let context = UIGraphicsGetCurrentContext()
let options: CGGradientDrawingOptions = [.DrawsAfterEndLocation]
let center = CGPoint(x: bounds.midX, y: -70)
CGContextDrawRadialGradient(context, gradient, center, 0, center, size.height*3, options)
}
}

Related

Apply Beveled Radial Gradient to UIView

I'm currently using the following code here
#IBDesignable class RadialGradientView: UIView {
#IBInspectable var outsideColor: UIColor = UIColor.red
#IBInspectable var insideColor: UIColor = UIColor.green
override func draw(_ rect: CGRect) {
let colors = [insideColor.cgColor, outsideColor.cgColor] as CFArray
let endRadius = sqrt(pow(frame.width/2, 2) + pow(frame.height/2, 2))
let center = CGPoint(x: bounds.size.width / 2, y: bounds.size.height / 2)
let gradient = CGGradient(colorsSpace: nil, colors: colors, locations: nil)
let context = UIGraphicsGetCurrentContext()
context?.drawRadialGradient(gradient!, startCenter: center, startRadius: 0.0, endCenter: center, endRadius: endRadius, options: CGGradientDrawingOptions.drawsBeforeStartLocation)
}
}
This code will draw a radial gradient that looks like this:
What I am trying to do is make it look like below (which is how it looks in the current desktop version of the app).
You will notice that in the desktop version the gradient has blue at the top and bottom and the white area is stretched to include more of the text area so it's easier to read.
How can I recreate this effect? I have tried for hours playing with different numbers and settings in the current code to no avail so far (such as changing the endRadius).
Any help or ideas are appreciated.

How to fix inconsistent color-to-clear radial gradient layer

When trying to draw a radial gradient from white to clear, I obtain different results depending on whether I use UIColor(white:alpha:), UIColor.white.withAlpha.Components(alpha:) or UIColor.color. How Can I get the same gradient with a clear color as with a plain color?
I order to draw a radial gradient, I have overridden the draw(context:) method (see below). My code seems to work fine when using plain colours for a gradient but works "mysteriously" when using a clear color.
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
ctx.saveGState()
let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colorFill as CFArray, locations: self.locations as! [CGFloat] )
let startPoint = CGPoint(x: startX, y: startY )
let endPoint = CGPoint(x: endX , y: endY)
let drawingOption : CGGradientDrawingOptions = radius > 0 ? [] : .drawsAfterEndLocation
//self.masksToBounds = true
if gradientType == .radial {
ctx.drawRadialGradient(gradient!, startCenter: startPoint, startRadius: 0.0, endCenter: endPoint, endRadius: self.frame.width / 2, options: drawingOption)
} else {
self.startPoint = startPoint
self.endPoint = endPoint
}
}
Here's the results I obtain depending on the input colours for the gradient:
[UIColor.white.cgColor,UIColor.black.cgColor]
(desired result with clear color instead of black)
[UIColor.white.cgColor, UIColor.clear.cgColor]
[UIColor.white.cgColor, UIColor.white.withAlphaComponent(0.0).cgColor]
[UIColor(white: 0.0, alpha: 1).cgColor, UIColor(white: 1.0, alpha: 0).cgColor]
Could someone can explain why I don't get the same output as with only plain colours (output I wish to obtain)?
Thanks a lot for taking the time to read and reply!
The layer is not drawn only in the draw(in context:) method. This line of code confirms some properties had been set prior to the call of the method, which has priorly triggered the drawing of the layer (self.locations):
CGColorSpaceCreateDeviceRGB(), colors: colorFill as CFArray, locations: self.locations as! [CGFloat] )
By removing the set of self.colors and self.locations properties in the custom initializer and storing the values in Type Properties instead before calling self.setNeedDisplay(), the layer is only being drawn in the draw(in context:) method.
class CustomLayer {
override init() {
super.init()
needsDisplayOnBoundsChange = true
}
override init(layer: Any) {
super.init(layer: layer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
var colors = []()
var colorMap = [NSNumber]()
custom init() {
colors = [color,color]
colorMap = [0.0, 1.0]
self.setNeedDisplay()
}
override func draw(in ctx: CGContext) {
super.draw(in: ctx)
ctx.saveGState()
guard let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors as CFArray, locations: colorMap as! [CGFloat] ) else {return}
let startPoint = CGPoint(x: startX, y: startY )
let endPoint = CGPoint(x: endX , y: endY)
self.masksToBounds = true
if gradientType == .radial {
ctx.drawRadialGradient(gradient, startCenter: startPoint, startRadius: 0.0, endCenter: endPoint, endRadius: self.frame.width / 2, options: drawingOption)
} else {
self.locations = colorMap
self.colors = colors
self.startPoint = startPoint
self.endPoint = endPoint
}
}
}
Cheers!

How to blend multiple UIView in iOS?

I want to blend multiple UIView in such a way that it can move, rotate and resize smoothly.
I already achieved this with the code below but it's not as smooth as other application like PicsArt & SnapSeed.
- (void)drawRect:(CGRect)rect {
// Drawing code
[super drawRect:rect];
UIImage *viewImage = [self captureView:self.superview withFrame:self.frame];
[viewImage drawInRect:rect];
[self.imageToBlend drawInRect:rect blendMode:self.blendMode alpha:self.alphaBlend];
// Other code....
}
You can't blend views this way; they have separate layer hierarchies that are resolved independently. Move the gradient and text on CALayer objects and blend those (or draw them by hand inside the same view). For example:
class MyView: UIView {
let gradientLayer: CAGradientLayer = {
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor.red.cgColor,
UIColor.blue.cgColor]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
return gradientLayer
}()
let textLayer: CATextLayer = {
let textLayer = CATextLayer()
let astring = NSAttributedString(string: "Text Example",
attributes: [.font: UIFont(name: "MarkerFelt-Wide", size: 60)!])
textLayer.string = astring
textLayer.alignmentMode = kCAAlignmentCenter
textLayer.bounds = astring.boundingRect(with: CGSize(width: .max, height: .max),
options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
return textLayer
}()
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
gradientLayer.bounds = bounds
gradientLayer.render(in: context)
context.saveGState()
context.setBlendMode(.softLight)
context.translateBy(x: center.x - textLayer.bounds.width / 2,
y: center.y - textLayer.bounds.height / 2)
textLayer.position = center
textLayer.render(in: context)
context.restoreGState()
}
}
}
you won't get a proper answer by searching try your own logic.
By using context, you can use a gesture and all other.
Capture and blend each time when drawRect: sounds too heavy.
It seems that you want to blend some image to its backend with various blend modes and alpha.
In that case, you'd better to blend entire view, and use view.layer.mask to show only masked area of the blend result.
So you can just move, rotate and resize the view.layer.mask, and no capturing or blending is needed. It should be much faster.
Something like this.
class MyView: UIImageView {
var backendImage: UIImage?
var blendImage: UIImage?
var blendMode: CGBlendMode = .normal
var alphaBlend: Float = 0.5
func setup(backend: UIView) {
self.backendImage = self.captureView(backend)
self.image = self.createBlendImage()
self.layer.mask = self.createMask()
}
func captureView(view: UIView) -> UIImage {
// capture view
}
func createBlendImage() -> UIImage {
// prepare ImageContext, draw two image, get image, and so on...
}
func createMask() -> CALayer {
let mask = CAShapeLayer()
// setup the shape of mask
}
func moveMask(origin: CGPoint, scale: Float, rotation: Float) {
// change mask
self.layer.mask.frame = ...
self.layer.mask.transform = ...
}
}

equivalent way drawing method to UIGraphicsGetCurrentContext

I have a swift class that outputs pie chart, I want to take out drawing logic out of drawRect method, since UIGraphicsGetCurrentContext returns nil outside of the draw method I need to change way of drawing my pie chart view,
What is the appropriate way to do this? My PieChart class looks like;
override func draw(_ rect: CGRect) {
let context: CGContext = UIGraphicsGetCurrentContext()!
drawLinearGradient(context)
drawCenterCircle(context)
let circle: CAShapeLayer = drawStroke()
let pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration = percentage
pathAnimation.toValue = percentage
pathAnimation.isRemovedOnCompletion = rendered ? false : (percentageLabel > 20 ? false : true)
pathAnimation.isAdditive = true
pathAnimation.fillMode = kCAFillModeForwards
circle.add(pathAnimation, forKey: "strokeEnd")
}
private func drawLinearGradient(_ context: CGContext) {
let circlePoint: CGRect = CGRect(x: 0, y: 0,
width: bounds.size.width, height: bounds.size.height)
context.addEllipse(in: circlePoint)
context.clip()
let colorSpace: CGColorSpace = CGColorSpaceCreateDeviceRGB()
let colorArray = [colors.0.cgColor, colors.1.cgColor]
let colorLocations: [CGFloat] = [0.0, 1.0]
let gradient = CGGradient(colorsSpace: colorSpace, colors: colorArray as CFArray, locations: colorLocations)
let startPoint = CGPoint.zero
let endPoint = CGPoint(x: 0, y: self.bounds.height)
context.drawLinearGradient(gradient!,
start: startPoint,
end: endPoint,
options: CGGradientDrawingOptions.drawsAfterEndLocation)
}
private func drawCenterCircle(_ context: CGContext) {
let circlePoint: CGRect = CGRect(x: arcWidth, y: arcWidth,
width: bounds.size.width-arcWidth*2,
height: bounds.size.height-arcWidth*2)
context.addEllipse(in: circlePoint)
UIColor.white.setFill()
context.fillPath()
}
private func drawStroke() -> CAShapeLayer {
let circle = CAShapeLayer()
self.layer.addSublayer(circle)
return circle
}
You can change your code to produce a UIImage containing the pie chart, and then place that image inside an image view:
UIGraphicsBeginImageContext(size)
let context = UIGraphicsGetCurrentContext()
// ... here goes your drawing code, just as before
let pieImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let pieView = UIImageView(image: pieImage)
Note that if you need to update the chart very frequently (multiple times per second), this solution might bring a performance penalty because the image creation takes a bit more time then the pure rendering.

Swift - how to create a non-pixelated radial gradient?

I'm trying to create a radial / circular gradient for my app.
I used a solution similar to the confirmed answer here
However, the resulting gradient looks pixelated.
What am I doing wrong? Is there are more modern way or a good 3rd party framework to create smooth, simple radial gradients?
Thanks!
My code (based on snippets published on Stack Overflow):
class RadialGradientLayer: CALayer {
init(innerColor: UIColor, outerColor: UIColor) {
self.innerColor = innerColor.CGColor
self.outerColor = outerColor.CGColor
super.init()
needsDisplayOnBoundsChange = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("This class does not support NSCoding")
}
var innerColor: CGColor
var outerColor: CGColor
override func drawInContext(ctx: CGContext) {
CGContextSaveGState(ctx)
let colorSpace = CGColorSpaceCreateDeviceRGB()
let locations:[CGFloat] = [0.0, 1.0]
let colors = [innerColor, outerColor]
let gradient = CGGradientCreateWithColors(colorSpace, colors, locations)
let center = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
let radius = min(bounds.width / 2.0, bounds.height / 2.0)
CGContextDrawRadialGradient(ctx, gradient, center, 0.0, center, radius, CGGradientDrawingOptions(rawValue: 0))
}
}
You need to make sure the CGContext is scaled for the devices screen.
Take a look at the answers to this question - CGContext and retina display
This will help you fix the issue.

Resources