equivalent way drawing method to UIGraphicsGetCurrentContext - ios

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.

Related

How to apply color filter to a view

I've been trying to wrap my head around Core Image filters & layers but I couldn't find a solution to what I'm trying to achieve.
So I'm building an app which has a component where the user needs to draw over random shapes (here the shape is a circle for the sake of simplicity). When the user goes over the edge of the shape the stroke color must change from yellow to red.
what I'm trying to get
And this is what I have so far:
what I have
Here's how I set the "mask":
#IBOutlet private weak var tempImageView: UIImageView! {
willSet {
let maskImage: UIImage = UIImage(named: "circle2")!
let mask = CALayer()
mask.opacity = 0.8
mask.contents = maskImage.cgImage
mask.contentsGravity = CALayerContentsGravity.resizeAspect
mask.bounds = CGRect(x: 0,
y: 0,
width: newValue.frame.width,
height: newValue.frame.height)
mask.anchorPoint = CGPoint(x: 0, y: 0)
mask.position = CGPoint(x: 0, y: 0)
newValue.layer.addSublayer(mask)
}
}
and here's where I render the drawing:
func drawLine(from fromPoint: CGPoint, to toPoint: CGPoint) {
UIGraphicsBeginImageContext(view.frame.size)
guard let context = UIGraphicsGetCurrentContext() else {
return
}
tempImageView.image?.draw(in: view.bounds)
context.setLineCap(.round)
context.setBlendMode(.normal)
context.setLineWidth(brushWidth)
context.move(to: fromPoint)
context.setStrokeColor(UIColor.red.cgColor)
context.addLine(to: toPoint)
context.strokePath()
tempImageView.image = UIGraphicsGetImageFromCurrentImageContext()
tempImageView.alpha = 1.0
UIGraphicsEndImageContext()
}
However all this does is it fades the red color when inside the shape instead of giving it a different color, which I haven't been able to figure out how to do.

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 = ...
}
}

CALayer sublayer shows up but context drawing within it not working

I created a custom UIView class where I override the draw(_ rect: CGRect) function. In it, I added a gradient (CAGradientLayer) sublayer (which works without much problem) and then add another sublayer above it, a custom 'CloseLayer' class that inherits from CALayer.
That second sublayer I add gets correctly added in front with the right dimensions, however the line drawing I implemented in the draw(in ctx: CGContext) function of the custom CALayer class doesn't seem to be doing anything. I have been looking at several resources and googling for the past few days and am started to get a bit discouraged. Below is my code for the UIView class (that contains the two sublayers) and the custom layer class I want drawing to occur in. Any help is much appreciated!
class GraphAView: UIView {
// trading 9:00AM EST to 6:00PM EST
// Normal: 9:30AM EST to 4:00PM EST
// 9 hours total, 5 min intervals (12 intervals per hour, 108 total)
let HonchoGreyComponents : [CGFloat] = [0.2,0.2,0.2,1.0]
let graphBackGreyComponents: [CGFloat] = [0.8,0.8,0.8,1.0]
override func draw(_ rect: CGRect) {
setUpBack()
}
func setUpBack(){
let RGBSpace = CGColorSpaceCreateDeviceRGB()
let HonchoGrey = CGColor(colorSpace: RGBSpace, components: HonchoGreyComponents)
let graphBackGrey = CGColor(colorSpace: RGBSpace, components: graphBackGreyComponents)
let Gradlayer = CAGradientLayer()
Gradlayer.frame = self.bounds
Gradlayer.colors = [HonchoGrey!,graphBackGrey!,graphBackGrey!,HonchoGrey!]
//layer.locations = [NSNumber(value: PreMarketEnd),NSNumber(value: RegMarketEnd)]
Gradlayer.locations = [0,0.055555, 0.7777777]
Gradlayer.startPoint = CGPoint(x: 0, y: 0)
Gradlayer.endPoint = CGPoint(x: 1, y: 0)
self.layer.insertSublayer(Gradlayer, at: 0)
let closingLay = CloseLayer()
closingLay.frame = self.bounds
let yer = UIColor(red: 0.3, green: 0.6, blue: 0.8, alpha: 1.0)
//closingLay.backgroundColor = UIColor.blue.cgColor
//closingLay.backgroundColor = UIColor(white: 0.4, alpha: 0.0).cgColor
closingLay.backgroundColor = yer.cgColor
self.layer.insertSublayer(closingLay, above: Gradlayer)
closingLay.setNeedsDisplay()
self.layer.setNeedsDisplay()
}
}
class CloseLayer: CALayer {
let ClosinglineWidth: CGFloat = 4.0
let dashPattern: [CGFloat] = [2,2]
override func draw(in ctx: CGContext) {
let CloseContext = UIGraphicsGetCurrentContext()
let middleY = self.frame.height / 2
// let width = self.frame.width
// let context = UIGraphicsGetCurrentContext()
CloseContext?.setLineWidth(ClosinglineWidth)
CloseContext?.setLineDash(phase: 1, lengths: dashPattern)
CloseContext?.move(to: CGPoint(x: 0, y: middleY))
CloseContext?.addLine(to: CGPoint(x: self.frame.width, y: middleY)) // 358 width
let closingLineColor = UIColor.red.cgColor
CloseContext?.setStrokeColor(closingLineColor)
CloseContext?.strokePath()
}
func drawLine(start: CGPoint, end: CGPoint){
}
}
The problem is this line:
let CloseContext = UIGraphicsGetCurrentContext()
There is no current context. Your job is to draw into ctx, the context you were handed.
Long story short, change that line to:
let CloseContext : CGContext? = ctx
...and bingo, you will see your line. (But that is just for instant gratification. In the end, you should get rid of CloseContext everywhere and subsitute ctx.)

iOS UIProgressView with gradient

is it possible to create a custom ui progress view with a gradient from left to right?
I've tried it with the following code:
let gradientLayer = CAGradientLayer()
gradientLayer.frame = self.frame
gradientLayer.anchorPoint = CGPoint(x: 0, y: 0)
gradientLayer.position = CGPoint(x: 0, y: 0)
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0);
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.0);
gradientLayer.colors = [
UIColor.red,
UIColor.green
]
// Convert to UIImage
self.layer.insertSublayer(gradientLayer, at: 0)
self.progressTintColor = UIColor.clear
self.trackTintColor = UIColor.black
But unfortunately the gradient is not visible. Any other ideas?
Looking at UIProgressView documentation, there's this property:
progressImage
If you provide a custom image, the progressTintColor property is ignored.
With that in mind, the laziest way to do this would be to create your gradient image and set it as the progressImage
I adapted this extension to make it a little cleaner, scaleable, and safer.
fileprivate extension UIImage {
static func gradientImage(with bounds: CGRect,
colors: [CGColor],
locations: [NSNumber]?) -> UIImage? {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
gradientLayer.colors = colors
// This makes it horizontal
gradientLayer.startPoint = CGPoint(x: 0.0,
y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0,
y: 0.5)
UIGraphicsBeginImageContext(gradientLayer.bounds.size)
gradientLayer.render(in: UIGraphicsGetCurrentContext()!)
guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return nil }
UIGraphicsEndImageContext()
return image`
}
}
Now that we've got a way to create a gradient image "on the fly", here's how to use it:
let gradientImage = UIImage.gradientImage(with: progressView.frame,
colors: [UIColor.red.cgColor, UIColor.green.cgColor],
locations: nil)
From there, you'd just set your progressView's progressImage, like so:
// I'm lazy...don't force unwrap this
progressView.progressImage = gradientImage!
progressView.setProgress(0.75, animated: true)
I had the same problem and solved it by creating a gradient custom view which I then convert to an image and assign it as the progress view track image.
I then flip the progress horizontally so that the progress bar becomes the background and the track image becomes the foreground.
This has the visual effect of revealing the gradient image underneath.
You just have to remember to invert your percentages which is really simple, see example buttons and code below:
SWIFT 3 Example:
class ViewController: UIViewController {
#IBOutlet weak var progressView: UIProgressView!
#IBAction func lessButton(_ sender: UIButton) {
let percentage = 20
let invertedValue = Float(100 - percentage) / 100
progressView.setProgress(invertedValue, animated: true)
}
#IBAction func moreButton(_ sender: UIButton) {
let percentage = 80
let invertedValue = Float(100 - percentage) / 100
progressView.setProgress(invertedValue, animated: true)
}
override func viewDidLoad() {
super.viewDidLoad()
//create gradient view the size of the progress view
let gradientView = GradientView(frame: progressView.bounds)
//convert gradient view to image , flip horizontally and assign as the track image
progressView.trackImage = UIImage(view: gradientView).withHorizontallyFlippedOrientation()
//invert the progress view
progressView.transform = CGAffineTransform(scaleX: -1.0, y: -1.0)
progressView.progressTintColor = UIColor.black
progressView.progress = 1
}
}
extension UIImage{
convenience init(view: UIView) {
UIGraphicsBeginImageContext(view.frame.size)
view.layer.render(in: UIGraphicsGetCurrentContext()!)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
self.init(cgImage: (image?.cgImage)!)
}
}
#IBDesignable
class GradientView: UIView {
private var gradientLayer = CAGradientLayer()
private var vertical: Bool = false
override func draw(_ rect: CGRect) {
super.draw(rect)
// Drawing code
//fill view with gradient layer
gradientLayer.frame = self.bounds
//style and insert layer if not already inserted
if gradientLayer.superlayer == nil {
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = vertical ? CGPoint(x: 0, y: 1) : CGPoint(x: 1, y: 0)
gradientLayer.colors = [UIColor.green.cgColor, UIColor.red.cgColor]
gradientLayer.locations = [0.0, 1.0]
self.layer.insertSublayer(gradientLayer, at: 0)
}
}
}
George figured out a very clever method. If you want a more easy solution, open UIProgressView document, there is a property named progressImage.
so, i just make it work like this:
progressView.progressImage = UIImage(named: "your_gradient_progress_icon")
progressView.trackTintColor = UIColor.clear
after that:
progressView.setProgress(currentProgress, animated: true)

Rotation And Gradient in CoreGraphics and context

I'm definitely missing some parts of Core Graphics context and cannot clear things out by myself.
Here is my code in Xcode Playground
import UIKit
class MyView08 : UIView {
override func drawRect(rect: CGRect) {
let cornerRaduis : CGFloat = 20
let pathRect = CGRectInset(self.bounds, 20, 20)
let rectanglePath = UIBezierPath(roundedRect: pathRect, cornerRadius: cornerRaduis)
var context = UIGraphicsGetCurrentContext()
CGContextSaveGState(context)
rotate(rectanglePath, context: context)
CGContextRestoreGState(context)
context = UIGraphicsGetCurrentContext()
CGContextSaveGState(context)
// drawGrad(rectanglePath, context: context)
CGContextRestoreGState(context)
}
func rotate(rectanglePath : UIBezierPath, context : CGContext) {
let rotation = CGAffineTransformMakeRotation(CGFloat(M_PI) / 4.0)
CGContextConcatCTM(context, rotation)
UIColor.greenColor().setFill()
rectanglePath.fill()
}
func drawGrad (rectanglePath : UIBezierPath, context : CGContext) {
let startColor = UIColor.redColor()
let endColor = UIColor.greenColor()
let gradientColors : CFArray = [startColor.CGColor, endColor.CGColor]
let gradientLocations : [CGFloat] = [0.0, 1.0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradientCreateWithColors(colorSpace, gradientColors, gradientLocations)
let topPoint = CGPointMake(self.bounds.size.width / 2, 20)
let bottomPoint = CGPointMake(self.bounds.size.width / 2, self.bounds.size.height - 20)
rectanglePath.addClip()
CGContextDrawLinearGradient(context, gradient, bottomPoint, topPoint, 0)
}
}
let myViewRect = CGRect(x: 0, y: 0, width: 300, height: 300)
let myView = MyView08(frame: myViewRect)
If i uncomment this line
drawGrad(rectanglePath, context: context)
Everything is going bad, Xcode trying to execute code almost 2 thousands times and after that it fails.
I'm trying to rotate rectangle and draw a gradient.
You are experiencing an infinite recursion. I don't know why, but it has to do with your testing procedure. The problem lies in how you are testing - not in your code. Don't test this kind of code in a playground. Playgrounds are not reality.
(I could generalize and say, don't use playgrounds ever. They are the work of the devil. But I'm not going to. Ooops, did I say that out loud?)
Anyway, I copied and pasted your code into a fresh app project (and I uncommented the drawGrad line), and it ran just fine with no recursion issues of the kind you describe.
Note that you will need to put your view into the interface in order for it to do anything. I added this line in order to do that:
self.view.addSubview(myView)
Apart from that, my code is identical to yours, and there's no problem.

Resources