Swift - Poor gradient quality in UIView size animation - ios

I have the following structure in place:
main view contains subview1; subview1 is laid out using constraints
subview1 contains subview2; subview2 also has constraints and I need to animate the size of subview2
in subview2 I call CGContextDrawRadialGradient to draw a circle with a gradient
The code in subview1 is:
let circle = GradientCircle(frame: CGRect(origin: CGPointZero, size: frame.size), withColor: color)
addSubview(circle)
circle.centerXAnchor.constraintEqualToAnchor(centerXAnchor).active = true
circle.centerYAnchor.constraintEqualToAnchor(centerYAnchor).active = true
let widthConstraint = circle.widthAnchor.constraintEqualToConstant(frame.width * 3)
let heightConstraint = circle.heightAnchor.constraintEqualToConstant(frame.width * 3)
circle.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activateConstraints([heightConstraint, widthConstraint])
layoutIfNeeded()
UIView.animateWithDuration(3.0, delay: 0, options: [.Repeat], animations: {
widthConstraint.constant = 2
heightConstraint.constant = 2
self.layoutIfNeeded()
}, completion: nil)
Here's a gif to show what the problem is: http://i.stack.imgur.com/LWP7d.gif
Why does this drop in quality occur and how can I fix it? Obviously, I'm new in Swift and I don't fully understand the whole layout and animation thing, so please help if you can, it's slowly driving me insane :)
EDIT:
Here's the GradientCircle class (which is subview2 in the description above), maybe the problem is with how the gradient is drawn:
class GradientCircle : UIView {
var color: UIColor
init(frame: CGRect, withColor: UIColor) {
color = withColor
super.init(frame: frame)
backgroundColor = UIColor.clearColor()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func drawRect(rect: CGRect) {
let context = UIGraphicsGetCurrentContext()
let locations: [CGFloat] = [0.0, 1.0]
let colorspace = CGColorSpaceCreateDeviceRGB()
let colors = [color.CGColor, GraphicsUtils.darkGrayColor().CGColor]
let gradient = CGGradientCreateWithColors(colorspace, colors, locations)
let startPoint = CGPoint(x: rect.width / 2, y: rect.height / 2)
let endPoint = CGPoint(x: rect.width / 2, y: rect.height / 2)
let startRadius: CGFloat = 0
let endRadius: CGFloat = rect.width / 2
CGContextClearRect(context, rect)
CGContextDrawRadialGradient(context, gradient, startPoint, startRadius, endPoint, endRadius, CGGradientDrawingOptions.DrawsAfterEndLocation)
}

Related

Create a layer with increasing height - swift

I want to make a custom slider that has increasing height i.e it's height starts from 4.0 and goes to 6.0.
I have written code for creating a layer but I cannot find a way to increase its height in this manner. Here is my code :
let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
ctx.addPath(path.cgPath)
ctx.setFillColor(UIColor.red.cgColor)
ctx.fillPath()
let lowerValuePosition = slider.positionForValue(slider.lowerValue)
let upperValuePosition = slider.positionForValue(slider.upperValue)
let rect = CGRect(x: 0, y: 0,
width: (bounds.width - 36),
height: bounds.height)
ctx.fill(rect)
Because the Minimum and Maximum (left-side & right-side) "Track" images stretch, you may not be able to get what you want with a default UISlider.
Not too tough to get around it though.
Basically:
create a custom view with your "rounded wedge" shape
overlay a UISlider on that custom view
"fill" the percentage of the shape when the slider value changes
Here's the idea, before overlaying them:
When we want to overlay the slider on the wedge, set the slider Min/Max track images to clear and it looks like this:
We can use a little trick to handle "filling" the shape by percentage:
use a gradient background layer
mask it with the shape
set the gradient colors to red, red, gray, gray
set the color locations to [0.0, pct, pct, 1.0]
That way we get a clean edge, instead of a gradient fade.
Here's a complete example -- no #IBOutlet or #IBAction connections, so just set a view controller's custom class to WedgeSliderViewController:
class RoundedWedgeSliderView: UIView {
var leftRadius: CGFloat = 4.0
var rightRadius: CGFloat = 6.0
// mask shape
private var cMask = CAShapeLayer()
var pct: Float = 0.0 {
didSet {
let p = pct as NSNumber
// disable layer built-in animation so the update won't "lag"
CATransaction.begin()
CATransaction.setDisableActions(true)
// update gradient locations
gradientLayer.locations = [
0.0, p, p, 1.0
]
CATransaction.commit()
}
}
// allows self.layer to be a CAGradientLayer
override class var layerClass: AnyClass { return CAGradientLayer.self }
private var gradientLayer: CAGradientLayer {
return self.layer as! CAGradientLayer
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// gradient colors will be
// red, red, gray, gray
let colors = [
UIColor.red.cgColor,
UIColor.red.cgColor,
UIColor(white: 0.9, alpha: 1.0).cgColor,
UIColor(white: 0.9, alpha: 1.0).cgColor,
]
gradientLayer.colors = colors
// initial gradient color locations
gradientLayer.locations = [
0.0, 0.0, 0.0, 1.0
]
// horizontal gradient
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
}
override func layoutSubviews() {
super.layoutSubviews()
let r = bounds
// define the "Rounded Wedge" shape
let leftCenter = CGPoint(x: r.minX + leftRadius, y: r.midY)
let rightCenter = CGPoint(x: r.maxX - rightRadius, y: r.midY)
let bez = UIBezierPath()
bez.addArc(withCenter: leftCenter, radius: leftRadius, startAngle: .pi * 0.5, endAngle: .pi * 1.5, clockwise: true)
bez.addArc(withCenter: rightCenter, radius: rightRadius, startAngle: .pi * 1.5, endAngle: .pi * 0.5, clockwise: true)
bez.close()
// set the mask layer's path
cMask.path = bez.cgPath
// mask self's layer
layer.mask = cMask
}
}
class WedgeSliderViewController: UIViewController {
let mySliderView = RoundedWedgeSliderView()
let theSlider = UISlider()
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(mySliderView)
view.addSubview(theSlider)
mySliderView.translatesAutoresizingMaskIntoConstraints = false
theSlider.translatesAutoresizingMaskIntoConstraints = false
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain slider 100-pts from top, 40-pts on each side
theSlider.topAnchor.constraint(equalTo: g.topAnchor, constant: 100.0),
theSlider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
theSlider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
// constrain mySliderView width to the slider width minus 16-pts
// (so we have 8-pt "padding" on each side for the thumb to cover)
mySliderView.widthAnchor.constraint(equalTo: theSlider.widthAnchor, constant: -16.0),
// constrain mySliderView to same height as the slider, centered X & Y
mySliderView.heightAnchor.constraint(equalTo: theSlider.heightAnchor),
mySliderView.centerXAnchor.constraint(equalTo: theSlider.centerXAnchor),
mySliderView.centerYAnchor.constraint(equalTo: theSlider.centerYAnchor),
])
// set left- and right-side "track" images to empty images
theSlider.setMinimumTrackImage(UIImage(), for: .normal)
theSlider.setMaximumTrackImage(UIImage(), for: .normal)
// add target for the slider
theSlider.addTarget(self, action: #selector(self.sliderValueChanged(_:)), for: .valueChanged)
// set intitial values
theSlider.value = 0.0
mySliderView.pct = 0.0
// end-radii of mySliderView defaults to 4.0 and 6.0
// un-comment next line to see the difference
//mySliderView.rightRadius = 10.0
}
#objc func sliderValueChanged(_ sender: Any) {
if let s = sender as? UISlider {
// update mySliderView when the slider changes
mySliderView.pct = s.value
}
}
}
You have to override the trackRect method of your superclass UISlider in the subclass like below and return the bounds and size that you require:
/**
UISlider Subclass
*/
class CustomSlider: UISlider {
override func trackRect(forBounds bounds: CGRect) -> CGRect {
return CGRect(origin: #oigin#, size: CGSize(width: #width#, height: #height#))
}
}

Swift: rainbow colour circle

Hi i am trying to write colour picker in swift that looks like this.
But so far I managed this.
Draw circle was easy, heres code...
fileprivate func setupScene(){
let circlePath: UIBezierPath = UIBezierPath(arcCenter: CGPoint(x: self.wheelView.frame.width/2, y: self.wheelView.frame.height/2), radius: CGFloat(self.wheelView.frame.height/2), startAngle: CGFloat(0), endAngle:CGFloat(Double.pi * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
//color inside circle
shapeLayer.fillColor = UIColor.clear.cgColor
//colored border of circle
shapeLayer.strokeColor = UIColor.purple.cgColor
//width size of border
shapeLayer.lineWidth = 10
wheelView.layer.addSublayer(shapeLayer)
}
#IBOutlet var wheelView: UIView!
But now I don't know how to insert rainbow colours ... I tried CAGradientLayer but it was not visible. Any good advice?
Details
Xcode 9.1, swift 4
Xcode 10.2.1 (10E1001), Swift 5
Solution
The code was taken from https://github.com/joncardasis/ChromaColorPicker
import UIKit
class RainbowCircle: UIView {
private var radius: CGFloat {
return frame.width>frame.height ? frame.height/2 : frame.width/2
}
private var stroke: CGFloat = 10
private var padding: CGFloat = 5
//MARK: - Drawing
override func draw(_ rect: CGRect) {
super.draw(rect)
drawRainbowCircle(outerRadius: radius - padding, innerRadius: radius - stroke - padding, resolution: 1)
}
init(frame: CGRect, lineHeight: CGFloat) {
super.init(frame: frame)
stroke = lineHeight
}
required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) }
/*
Resolution should be between 0.1 and 1
*/
private func drawRainbowCircle(outerRadius: CGFloat, innerRadius: CGFloat, resolution: Float) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.saveGState()
context.translateBy(x: self.bounds.midX, y: self.bounds.midY) //Move context to center
let subdivisions:CGFloat = CGFloat(resolution * 512) //Max subdivisions of 512
let innerHeight = (CGFloat.pi*innerRadius)/subdivisions //height of the inner wall for each segment
let outterHeight = (CGFloat.pi*outerRadius)/subdivisions
let segment = UIBezierPath()
segment.move(to: CGPoint(x: innerRadius, y: -innerHeight/2))
segment.addLine(to: CGPoint(x: innerRadius, y: innerHeight/2))
segment.addLine(to: CGPoint(x: outerRadius, y: outterHeight/2))
segment.addLine(to: CGPoint(x: outerRadius, y: -outterHeight/2))
segment.close()
//Draw each segment and rotate around the center
for i in 0 ..< Int(ceil(subdivisions)) {
UIColor(hue: CGFloat(i)/subdivisions, saturation: 1, brightness: 1, alpha: 1).set()
segment.fill()
//let lineTailSpace = CGFloat.pi*2*outerRadius/subdivisions //The amount of space between the tails of each segment
let lineTailSpace = CGFloat.pi*2*outerRadius/subdivisions
segment.lineWidth = lineTailSpace //allows for seemless scaling
segment.stroke()
//Rotate to correct location
let rotate = CGAffineTransform(rotationAngle: -(CGFloat.pi*2/subdivisions)) //rotates each segment
segment.apply(rotate)
}
context.translateBy(x: -self.bounds.midX, y: -self.bounds.midY) //Move context back to original position
context.restoreGState()
}
}
Usage
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let rainbowCircle = RainbowCircle(frame: CGRect(x: 50, y: 50, width: 240, height: 420), lineHeight: 5)
rainbowCircle.backgroundColor = .clear
view.addSubview(rainbowCircle)
}
}
Result

Animating a circular progress circle smoothly via buttons

I am experiencing odd visual quirks with my progress circle. I would like the green circle (CAShapeLayer) to smoothly animate in increments of fifths. 0/5 = no green circle. 5/5 = full green circle. The problem is the green circle is animating past where it should be, then abruptly shrinks back to the proper spot. This happens when both the plus and minus button are pressed. There is a gif below demonstrating what is happening.
The duration of each animation should last 0.25 seconds, and should smoothly animate both UP and DOWN (depending if the plus or minus button is pressed) from wherever the animation ended last (which is currently not happening.)
In my UIView, I draw the circle, along with the method to animate the position of the progress:
class CircleView: UIView {
let progressCircle = CAShapeLayer()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
}
override func draw(_ rect: CGRect) {
let circlePath = UIBezierPath(ovalIn: self.bounds)
progressCircle.path = circlePath.cgPath
progressCircle.strokeColor = UIColor.green.cgColor
progressCircle.fillColor = UIColor.red.cgColor
progressCircle.lineWidth = 10.0
// Add the circle to the view.
self.layer.addSublayer(progressCircle)
}
func animateCircle(circleToValue: CGFloat) {
let fifths:CGFloat = circleToValue / 5
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = 0.25
animation.fromValue = fifths
animation.byValue = fifths
animation.fillMode = kCAFillModeBoth
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
progressCircle.strokeEnd = fifths
// Create the animation.
progressCircle.add(animation, forKey: "strokeEnd")
}
}
In my VC:
I init the circle and starting point with:
let myDrawnCircle = CircleView()
var startingPointForCircle = CGFloat()
viewDidAppear:
startingPointForCircle = 0.0
myDrawnCircle.frame = CGRect(x: 100, y: 100, width: 150, height: 150)
myDrawnCircle.backgroundColor = UIColor.white
myDrawnCircle.animateCircle(circleToValue: startingPointForCircle)
self.view.addSubview(myDrawnCircle)
textLabel.text = "\(startingPointForCircle)"
And the actual buttons that do the cool animating:
#IBAction func minusButtonPressed(_ sender: UIButton) {
if startingPointForCircle > 0 {
startingPointForCircle -= 1
textLabel.text = "\(startingPointForCircle)"
myDrawnCircle.animateCircle(circleToValue: startingPointForCircle)
}
}
#IBAction func plusButtonPressed(_ sender: UIButton) {
if startingPointForCircle < 5 {
startingPointForCircle += 1
textLabel.text = "\(startingPointForCircle)"
myDrawnCircle.animateCircle(circleToValue: startingPointForCircle)
}
}
Here's a gif showing you what's going on with the jerky animations.
How do I make my animations smoothly animate TO the proper value FROM the proper value?
My guess is it has something to do with using a key path. I don't know how exactly to fix it for using a key path, but you could try a different tack, using a custom view, and animating the value passed to the method used for drawing an arc.
The code below was generated by PaintCode. The value passed for arc specifies the point on the circle to draw the arc to. This is used in the drawRect method of a custom UIView. Make a CGFloat property of the custom view and simply change that value, and call setNeedsDisplay on the view after your animation code.
func drawCircleProgress(frame frame: CGRect = CGRect(x: 0, y: 0, width: 40, height: 40), arc: CGFloat = 270) {
//// General Declarations
let context = UIGraphicsGetCurrentContext()
//// Color Declarations
let white = UIColor(red: 1.000, green: 1.000, blue: 1.000, alpha: 1.000)
let white50 = white.colorWithAlpha(0.5)
//// Oval Drawing
CGContextSaveGState(context)
CGContextTranslateCTM(context, frame.minX + 20, frame.minY + 20)
CGContextRotateCTM(context, -90 * CGFloat(M_PI) / 180)
let ovalRect = CGRect(x: -19.5, y: -19.5, width: 39, height: 39)
let ovalPath = UIBezierPath()
ovalPath.addArcWithCenter(CGPoint(x: ovalRect.midX, y: ovalRect.midY), radius: ovalRect.width / 2, startAngle: 0 * CGFloat(M_PI)/180, endAngle: -arc * CGFloat(M_PI)/180, clockwise: true)
white.setStroke()
ovalPath.lineWidth = 1
ovalPath.stroke()
CGContextRestoreGState(context)
}
The desired results were as simple as commenting out fromValue / byValue in the animateCircle method. This was pointed out by #dfri - "set both fromValue and toValue to nil then: "Interpolates between the previous value of keyPath in the target layer’s presentation layer and the current value of keyPath in the target layer’s presentation layer."
//animation.fromValue = fifths
//animation.byValue = fifths

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)

NSGradient drawInBezierPath iOS equivalent

Is there an iOS equivalent to - (void)drawInBezierPath:(NSBezierPath *)path angle:(CGFloat)angle?
I need to draw a gradient inside a UIBezierPath and cannot get it to work. The gradient draws all over the screen.
This example creates a gradient inside a UIBezierPath in Swift 3. It draws a slanting line with a green/white gradient.
import UIKit
class DrawingView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.isOpaque = false
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func draw(_ rect: CGRect) {
let startPoint = CGPoint(x:100, y:100)
let endPoint = CGPoint(x: 300, y:400)
let context = UIGraphicsGetCurrentContext()!
context.setStrokeColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0);
// create a line
context.move(to: startPoint)
context.addLine(to: endPoint)
context.setLineWidth(4)
// use the line created above as a clipping mask
context.replacePathWithStrokedPath()
context.clip()
// create a gradient
let locations: [CGFloat] = [ 0.0, 0.5 ]
let colors = [UIColor.green.cgColor,
UIColor.white.cgColor]
let colorspace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorspace,
colors: colors as CFArray, locations: locations)
let gradientStartPoint = CGPoint(x: rect.midX, y: rect.minY)
let gradientEndPoint = CGPoint(x: rect.midX, y: rect.maxY)
context.drawLinearGradient(gradient!,
start: gradientStartPoint, end: gradientEndPoint,
options: .drawsBeforeStartLocation)
UIGraphicsEndImageContext()
}
}

Resources