How to use drawRect in a particular area? - ios

I used this topic's animation in my project. The problem is I want to use a particular area for circle(grey area). To do it, I added a view(grey area) and the code is shown below. But it is locating different positions on x axis every time.(y is fixed). The second problem is I want to use the circle around the text. To do it, I placed the text and view to centre of the screen both vertically and horizontally. How can I fix these two problems?
Problem screenshots
These are the code that I used.
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clearColor()
// Use UIBezierPath as an easy way to create the CGPath for the layer.
// The path should be the entire circle.
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
// Setup the CAShapeLayer with the path, colors, and line width
circleLayer.path = circlePath.CGPath
circleLayer.fillColor = UIColor.clearColor().CGColor
circleLayer.strokeColor = randomColor().CGColor
circleLayer.lineWidth = 40;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
layer.addSublayer(circleLayer)
}
func animateCircle(duration: NSTimeInterval) {
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 0
animation.toValue = 1
// Do a linear animation (i.e. the speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// right value when the animation ends.
circleLayer.strokeEnd = 1.0
// Do the actual animation
circleLayer.addAnimation(animation, forKey: "animateCircle")
}
func addCircleView() {
let diceRoll = CGFloat(Int(arc4random_uniform(7))*50)
// Create a new CircleView
var circleView = CircleView(frame: CGRect(x: view.frame.origin.x/2, y: view.frame.origin.y/2, width: 200, height: 200))
extraView.addSubview(circleView)
println(view.frame.width)
println(view.frame.height)
// Animate the drawing of the circle over the course of 1 second
circleView.animateCircle(1.0)
}

Problem is Auto layout. You forgot set layout for circleView <=> extraView. See the code below.
func addCircleView() {
let diceRoll = CGFloat(Int(arc4random_uniform(7))*50)
// Create a new CircleView
var circleView = CircleView(frame: CGRect(x: view.frame.origin.x/2, y: view.frame.origin.y/2, width: 200, height: 200))
extraView.addSubview(circleView)
println(view.frame.width)
println(view.frame.height)
let constCenterX = NSLayoutConstraint(item: circleView, attribute: .CenterX, relatedBy: .Equal, toItem: extraView, attribute: .CenterX, multiplier: 1, constant: 0)
let constCenterY = NSLayoutConstraint(item: circleView, attribute: .CenterY, relatedBy: .Equal, toItem: extraView, attribute: .CenterY, multiplier: 1, constant: 0)
self.view.addConstraints([constCenterX, constCenterY])
// Animate the drawing of the circle over the course of 1 second
circleView.animateCircle(1.0)
//circleView.textLabel?.text = "Hello World!"
}
BTW: For the second question you should create a UILabel (centerX & centerY) in CircleView. Hope that helps!

Related

Swift 5 and UIKit draw, animate and split lines between 2 to 3 UIViews [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 1 year ago.
Improve this question
I have a view that may contains 2 or 3 UIViews.
I want to draw (and possibly animate a line from the bottom MidX of the higher view to the bottom one.
If I have 3 views I want the line to split and animate to both of them.
If I have a single view I want the line to go directly to the middle top of the bottom view all this using UIBezierPath and CAShapeLayer
All this considering screen height (4.7" -> 6.2") I have attached images to illustrate what I want to achieve.
Thanks for the help.
You're on the right track...
The problem with drawing a "split" line is that there is one start point and TWO end points. So, the resulting animation may not be what you really want.
Another approach would be to use TWO layers - one with the "left-side" split line and one with the "right-side" split line, then animate them together.
Here's an example of wrapping things into a "Connect" view subclass.
We'll use 3 layers: 1 for the single vertical connecting line and one each for the right-side and left-side lines.
We can also set the path points to the center of the view, and the left and right edges. That way we can constrain the Leading edge to the center of the left-box, and the trailing edge to the center of the right-box.
This view, by itself, will look like this (with a yellow background so we can see its frame):
or:
With the lines will be animated from the top.
class ConnectView: UIView {
// determines whether we want a single box-to-box line, or
// left and right split / stepped lines to two boxes
public var single: Bool = true
private let singleLineLayer = CAShapeLayer()
private let leftLineLayer = CAShapeLayer()
private let rightLineLayer = CAShapeLayer()
private var durationFactor: CGFloat = 0
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// add and configure sublayers
[singleLineLayer, leftLineLayer, rightLineLayer].forEach { lay in
layer.addSublayer(lay)
lay.lineWidth = 4
lay.strokeColor = UIColor.blue.cgColor
lay.fillColor = UIColor.clear.cgColor
}
}
override func layoutSubviews() {
super.layoutSubviews()
// for readablility, define the points for our lines
let topCenter = CGPoint(x: bounds.midX, y: 0)
let midCenter = CGPoint(x: bounds.midX, y: bounds.midY)
let botCenter = CGPoint(x: bounds.midX, y: bounds.maxY)
let midLeft = CGPoint(x: bounds.minX, y: bounds.midY)
let midRight = CGPoint(x: bounds.maxX, y: bounds.midY)
let botLeft = CGPoint(x: bounds.minX, y: bounds.maxY)
let botRight = CGPoint(x: bounds.maxX, y: bounds.maxY)
let singleBez = UIBezierPath()
let leftBez = UIBezierPath()
let rightBez = UIBezierPath()
// vertical line
singleBez.move(to: topCenter)
singleBez.addLine(to: botCenter)
// split / stepped line to the left
leftBez.move(to: topCenter)
leftBez.addLine(to: midCenter)
leftBez.addLine(to: midLeft)
leftBez.addLine(to: botLeft)
// split / stepped line to the right
rightBez.move(to: topCenter)
rightBez.addLine(to: midCenter)
rightBez.addLine(to: midRight)
rightBez.addLine(to: botRight)
// set the layer paths
// initializing strokeEnd to 0 for all three
singleLineLayer.path = singleBez.cgPath
singleLineLayer.strokeEnd = 0
leftLineLayer.path = leftBez.cgPath
leftLineLayer.strokeEnd = 0
rightLineLayer.path = rightBez.cgPath
rightLineLayer.strokeEnd = 0
// calculate total line lengths (in points)
// so we can adjust the "draw speed" in the animation
let singleLength = botCenter.y - topCenter.y
let doubleLength = singleLength + (midCenter.x - midLeft.x)
durationFactor = singleLength / doubleLength
}
public func doAnim() -> Void {
// reset the animations
[singleLineLayer, leftLineLayer, rightLineLayer].forEach { lay in
lay.removeAllAnimations()
lay.strokeEnd = 0
}
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = 2.0
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
if self.single {
// we want the apparent drawing speed to be the same
// for a single line as for a split / stepped line
// so change the animation duration
animation.duration *= durationFactor
// animate the single line layer
self.singleLineLayer.add(animation, forKey: animation.keyPath)
} else {
// animate the both left and right line layers
self.leftLineLayer.add(animation, forKey: animation.keyPath)
self.rightLineLayer.add(animation, forKey: animation.keyPath)
}
}
}
and a sample view controller showing it in action:
class ConnectTestViewController: UIViewController {
let vTop = UIView()
let vLeft = UIView()
let vCenter = UIView()
let vRight = UIView()
let testConnectView = ConnectView()
override func viewDidLoad() {
super.viewDidLoad()
// give the 4 views different background colors
// add them as subviews
// make them all 100x100 points
let colors: [UIColor] = [
.systemYellow,
.systemRed, .systemGreen, .systemBlue,
]
for (v, c) in zip([vTop, vLeft, vCenter, vRight], colors) {
v.backgroundColor = c
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
v.widthAnchor.constraint(equalToConstant: 100.0).isActive = true
v.heightAnchor.constraint(equalTo: v.widthAnchor).isActive = true
}
// add the clear-background Connect View
testConnectView.backgroundColor = .clear
testConnectView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(testConnectView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// horizontally center the top box near the top
vTop.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
vTop.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// horizontally center the center box, 200-pts below the top box
vCenter.topAnchor.constraint(equalTo: vTop.bottomAnchor, constant: 200.0),
vCenter.centerXAnchor.constraint(equalTo: g.centerXAnchor),
// align tops of left and right boxes with center box
vLeft.topAnchor.constraint(equalTo: vCenter.topAnchor),
vRight.topAnchor.constraint(equalTo: vCenter.topAnchor),
// position left and right boxes to left and right of center box
vLeft.trailingAnchor.constraint(equalTo: vCenter.leadingAnchor, constant: -20.0),
vRight.leadingAnchor.constraint(equalTo: vCenter.trailingAnchor, constant: 20.0),
// constrain Connect View
// Top to Bottom of Top box
testConnectView.topAnchor.constraint(equalTo: vTop.bottomAnchor),
// Bottom to Top of the row of 3 boxes
testConnectView.bottomAnchor.constraint(equalTo: vCenter.topAnchor),
// Leading to CenterX of Left box
testConnectView.leadingAnchor.constraint(equalTo: vLeft.centerXAnchor),
// Trailing to CenterX of Right box
testConnectView.trailingAnchor.constraint(equalTo: vRight.centerXAnchor),
])
// add a couple buttons at the bottom
let stack = UIStackView()
stack.spacing = 20
stack.distribution = .fillEqually
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
["Run Anim", "Show/Hide"].forEach { str in
let b = UIButton()
b.setTitle(str, for: [])
b.backgroundColor = .red
b.setTitleColor(.white, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
b.addTarget(self, action: #selector(buttonTap(_:)), for: .touchUpInside)
stack.addArrangedSubview(b)
}
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
stack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
stack.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
stack.heightAnchor.constraint(equalToConstant: 50.0),
])
}
#objc func buttonTap(_ sender: Any?) -> Void {
guard let b = sender as? UIButton,
let t = b.currentTitle
else {
return
}
if t == "Run Anim" {
// tap button to toggle between
// Top-to-Middle box line or
// Top-to-SideBoxes split / stepped line
testConnectView.single.toggle()
// run the animation
testConnectView.doAnim()
} else {
// toggle background of Connect View between
// clear and yellow
testConnectView.backgroundColor = testConnectView.backgroundColor == .clear ? .yellow : .clear
}
}
}
Running that will give this result:
The first button at the bottom will toggle the connection between Top-Center and Top-Left-Right (re-running the animation each time). The second button will toggle the view's background color between clear and yellow so we can see its frame.
Edit
If you want rounded "step" corners that look like this:
replace the layoutSubviews() code above with this:
override func layoutSubviews() {
super.layoutSubviews()
// for readablility, define the points for our lines
let topCenter = CGPoint(x: bounds.midX, y: 0)
let midCenter = CGPoint(x: bounds.midX, y: bounds.midY)
let botCenter = CGPoint(x: bounds.midX, y: bounds.maxY)
let midLeft = CGPoint(x: bounds.minX, y: bounds.midY)
let midRight = CGPoint(x: bounds.maxX, y: bounds.midY)
let botLeft = CGPoint(x: bounds.minX, y: bounds.maxY)
let botRight = CGPoint(x: bounds.maxX, y: bounds.maxY)
let singleBez = UIBezierPath()
let leftBez = UIBezierPath()
let rightBez = UIBezierPath()
// vertical line
singleBez.move(to: topCenter)
singleBez.addLine(to: botCenter)
// rounded "step" corners
let radius: CGFloat = 20.0
let leftArcP = CGPoint(x: midLeft.x + radius, y: midLeft.y)
let leftArcC = CGPoint(x: midLeft.x + radius, y: midLeft.y + radius)
let rightArcP = CGPoint(x: midRight.x - radius, y: midRight.y)
let rightArcC = CGPoint(x: midRight.x - radius, y: midRight.y + radius)
// split / stepped line to the left
leftBez.move(to: topCenter)
leftBez.addLine(to: midCenter)
leftBez.addLine(to: leftArcP)
leftBez.addArc(withCenter: leftArcC, radius: radius, startAngle: .pi * 1.5, endAngle: .pi, clockwise: false)
leftBez.addLine(to: botLeft)
// split / stepped line to the right
rightBez.move(to: topCenter)
rightBez.addLine(to: midCenter)
rightBez.addLine(to: rightArcP)
rightBez.addArc(withCenter: rightArcC, radius: radius, startAngle: .pi * 1.5, endAngle: 0, clockwise: true)
rightBez.addLine(to: botRight)
// set the layer paths
// initializing strokeEnd to 0 for all three
singleLineLayer.path = singleBez.cgPath
singleLineLayer.strokeEnd = 0
leftLineLayer.path = leftBez.cgPath
leftLineLayer.strokeEnd = 0
rightLineLayer.path = rightBez.cgPath
rightLineLayer.strokeEnd = 0
// calculate total line lengths (in points)
// so we can adjust the "draw speed" in the animation
let singleLength = botCenter.y - topCenter.y
let doubleLength = singleLength + (midCenter.x - midLeft.x)
durationFactor = singleLength / doubleLength
}
Well after some research I have come up with this solution for Swift 5:
class ViewController: UIViewController {
#IBOutlet weak var someView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
let start = CGPoint(x: self.someView.bounds.midX, y: self.someView.bounds.maxY)
let end = CGPoint(x: self.someView.layer.bounds.midX, y: (UIScreen.main.bounds.height / 2) - 100)
let linePath = UIBezierPath()
linePath.move(to: start)
linePath.addLine(to: end)
linePath.addLine(to: CGPoint(x: -50, y: (UIScreen.main.bounds.height / 2) - 100))
linePath.move(to: end)
linePath.addLine(to: CGPoint(x: 250, y: (UIScreen.main.bounds.height / 2) - 100))
linePath.addLine(to: CGPoint(x: lowerViewA.x, y: lowerViewA.y))
linePath.move(to: CGPoint(x: -50, y: (UIScreen.main.bounds.height / 2) - 100))
linePath.addLine(to: CGPoint(x: lowerViewB.x, y: lowerViewB.y))
let shapeLayer = CAShapeLayer()
shapeLayer.path = linePath.cgPath
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = 2
shapeLayer.lineJoin = CAShapeLayerLineJoin.bevel
self.someView.layer.addSublayer(shapeLayer)
//Basic animation if you want to animate the line drawing.
let pathAnimation: CABasicAnimation = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration = 4.0
pathAnimation.fromValue = 0.0
pathAnimation.toValue = 1.0
//Animation will happen right away
shapeLayer.add(pathAnimation, forKey: "strokeEnd")
}
}

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 - scale background while retaining top anchor

How can I scale a UIImageView while retaining the top anchor?
I tried:
UIView.animate(withDuration: 0.3, animations: {
self.backGroundImage.contentMode = .scaleToFill
self.backGroundImage.transform = CGAffineTransform(scaleX: 3, y: 3)
self.backGroundImage.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
})
That scales the image but the anchor point is the middle of the image. It seems like setting the topAnchor doesn't do anything. Is there a way to set some sort of "scaleAnchor" or some other solution?
In the end I would like to achieve this:
Update
This is how I constrain the view:
NSLayoutConstraint.activate([
// constrain background image
backGroundImage.topAnchor.constraint(equalTo: view.topAnchor),
backGroundImage.bottomAnchor.constraint(equalTo: view.bottomAnchor),
backGroundImage.leadingAnchor.constraint(equalTo: view.leadingAnchor),
backGroundImage.trailingAnchor.constraint(equalTo: view.trailingAnchor), )]
This works for me in Swift 5.1:
public extension UIView {
func applyTransform(withScale scale: CGFloat, anchorPoint: CGPoint) {
let xPadding = (scale - 1) * (anchorPoint.x - 0.5) * bounds.width
let yPadding = (scale - 1) * (anchorPoint.y - 0.5) * bounds.height
transform = CGAffineTransform(scaleX: scale, y: scale).translatedBy(x: xPadding, y: yPadding)
}
}
Example:
yourView.applyTransform(withScale: 3, anchorPoint: CGPoint(x: 0.5, y: 0))
The transform matrix is applied after the frame has been calculated from the constraints. If you want to scale things up consistently I suggest just setting a multiple on the height and width constraints instead. Alternatively you can move the anchor point by using CALayers.anchorPoint and then apply you transform so that it scales from the top middle instead of the center (which is the default).
EDIT -- example:
import PlaygroundSupport
class V: UIViewController {
let square = UIView()
override func viewDidLoad() {
super.viewDidLoad()
square.translatesAutoresizingMaskIntoConstraints = false
square.backgroundColor = .red
view.addSubview(square)
NSLayoutConstraint.activate([
square.centerXAnchor.constraint(equalTo: view.centerXAnchor),
square.centerYAnchor.constraint(equalTo: view.centerYAnchor),
square.widthAnchor.constraint(equalToConstant: 100),
square.heightAnchor.constraint(equalToConstant: 100),
])
square.layer.anchorPoint = .init(x: 0.5, y: 0)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
UIView.animate(withDuration: 10) {
self.square.transform = .init(scaleX: 3, y: 3)
}
}
}
PlaygroundPage.current.liveView = V()

iOS animate drawing of multiple circles, only one displays

Based on the answer to this question: Animate drawing of a circle
I now want to display two of these circles simultaneously on the same screen but in two different views. If I just want to animate one circle there are no problems. However if I try to add a second, only the second one is visible.
This is the Circle class:
import UIKit
var circleLayer: CAShapeLayer!
class Circle: UIView {
init(frame: CGRect, viewLayer: CALayer) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
// Use UIBezierPath as an easy way to create the CGPath for the layer.
// The path should be the entire circle.
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: (3.0 * .pi)/2.0, endAngle: CGFloat((3.0 * .pi)/2.0 + (.pi * 2.0)), clockwise: true)
// Setup the CAShapeLayer with the path, colors, and line width
circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.green.cgColor
circleLayer.lineWidth = 8.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
viewLayer.addSublayer(circleLayer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func animateCircle(duration: TimeInterval) {
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 0
animation.toValue = 1
// Do a linear animation (i.e The speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// Right value when the animation ends
circleLayer.strokeEnd = 1.0
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
}
func removeCircle() {
circleLayer.strokeEnd = 0.0
}
}
And here is how I call it from my ViewController:
var rythmTimer: Circle?
var adrenalineTimer: Circle?
override func viewDidLoad() {
// Create two timers as circles
self.rythmTimer = Circle(frame: CGRect(x: 0, y: 0, width: 100, height: 100), viewLayer: view1.layer)
if let rt = rythmTimer {
view1.addSubview(rt)
rt.center = CGPoint(x: self.view1.bounds.midX, y: self.view1.bounds.midY);
}
self.adrenalineTimer = Circle(frame: CGRect(x: 0, y: 0, width: 100, height: 100), viewLayer: view2.layer)
if let at = adrenalineTimer {
view2.addSubview(at)
at.center = CGPoint(x: self.view2.bounds.midX, y: self.view2.bounds.midY)
}
}
If I remove the code for the adrenalineTimer I can see the circle drawn by the rythmTimer. If I keep it the rythmTimer will be displayed in view2 instead of view1 and will have the duration/color of the rythmTimer
It seems you set the circle path with wrong origin points. In addition, you do it before you place the circle on the view.
Add this code to the animate function instead:
func animateCircle(duration: TimeInterval) {
let circlePath = UIBezierPath(arcCenter: CGPoint(x: self.frame.origin.x + self.frame.size.width / 2, y: self.frame.origin.y + self.frame.size.height / 2), radius: (frame.size.width - 10)/2, startAngle: (3.0 * .pi)/2.0, endAngle: CGFloat((3.0 * .pi)/2.0 + (.pi * 2.0)), clockwise: true)
circleLayer.path = circlePath.cgPath
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
...
The main problem is that you have declared circleLayer outside of the class:
import UIKit
var circleLayer: CAShapeLayer!
class Circle: UIView {
init(frame: CGRect, viewLayer: CALayer) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
...
}
}
The result is that you only ever have ONE circleLayer object (instance).
If you move it inside the class, then each instance of Circle will have its own instance of circleLayer:
import UIKit
class Circle: UIView {
var circleLayer: CAShapeLayer!
init(frame: CGRect, viewLayer: CALayer) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
...
}
}
There are a number of other odd things you're doing, but that is why you only get one animated circle.
Edit: Here's a modified version of your code that will allow you to use auto-layout instead of fixed / hard-coded sizes and positions. You can run this directly in a Playground page:
import UIKit
import PlaygroundSupport
class Circle: UIView {
var circleLayer: CAShapeLayer!
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
// Setup the CAShapeLayer with colors and line width
circleLayer = CAShapeLayer()
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.green.cgColor
circleLayer.lineWidth = 8.0;
// We haven't set the path yet, so don't draw initially
circleLayer.strokeEnd = 0.0
// Add the layer to the self's layer
self.layer.addSublayer(circleLayer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func layoutSubviews() {
super.layoutSubviews()
// Use UIBezierPath as an easy way to create the CGPath for the layer.
// The path should be the entire circle.
// this will update whenever the frame size changes (makes it easy to use with auto-layout)
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: (3.0 * .pi)/2.0, endAngle: CGFloat((3.0 * .pi)/2.0 + (.pi * 2.0)), clockwise: true)
circleLayer.path = circlePath.cgPath
}
func animateCircle(duration: TimeInterval) {
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 0
animation.toValue = 1
// Do a linear animation (i.e The speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// Right value when the animation ends
circleLayer.strokeEnd = 1.0
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
}
func removeCircle() {
circleLayer.strokeEnd = 0.0
}
}
class MyViewController : UIViewController {
var rythmTimer: Circle?
var adrenalineTimer: Circle?
var theButton: UIButton = {
let b = UIButton()
b.setTitle("Tap Me", for: .normal)
b.translatesAutoresizingMaskIntoConstraints = false
b.backgroundColor = .red
return b
}()
var view1: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .blue
return v
}()
var view2: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .orange
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
view.addSubview(theButton)
// constrain button to Top: 32 and centerX
NSLayoutConstraint.activate([
theButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 32.0),
theButton.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0.0),
])
// add an action for the button tap
theButton.addTarget(self, action: #selector(didTap(_:)), for: .touchUpInside)
view.addSubview(view1)
view.addSubview(view2)
NSLayoutConstraint.activate([
view1.widthAnchor.constraint(equalToConstant: 120.0),
view1.heightAnchor.constraint(equalTo: view1.widthAnchor, multiplier: 1.0),
view1.centerXAnchor.constraint(equalTo: view.centerXAnchor),
view1.topAnchor.constraint(equalTo: theButton.bottomAnchor, constant: 40.0),
view2.widthAnchor.constraint(equalTo: view1.widthAnchor, multiplier: 1.0),
view2.heightAnchor.constraint(equalTo: view1.widthAnchor, multiplier: 1.0),
view2.centerXAnchor.constraint(equalTo: view.centerXAnchor),
view2.topAnchor.constraint(equalTo: view1.bottomAnchor, constant: 40.0),
])
rythmTimer = Circle(frame: CGRect.zero)
adrenalineTimer = Circle(frame: CGRect.zero)
if let rt = rythmTimer,
let at = adrenalineTimer {
view1.addSubview(rt)
view2.addSubview(at)
rt.translatesAutoresizingMaskIntoConstraints = false
at.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
rt.widthAnchor.constraint(equalToConstant: 100.0),
rt.heightAnchor.constraint(equalToConstant: 100.0),
rt.centerXAnchor.constraint(equalTo: view1.centerXAnchor),
rt.centerYAnchor.constraint(equalTo: view1.centerYAnchor),
at.widthAnchor.constraint(equalToConstant: 100.0),
at.heightAnchor.constraint(equalToConstant: 100.0),
at.centerXAnchor.constraint(equalTo: view2.centerXAnchor),
at.centerYAnchor.constraint(equalTo: view2.centerYAnchor),
])
}
}
// on button tap, change the text in the label(s)
#objc func didTap(_ sender: Any?) -> Void {
if let rt = rythmTimer,
let at = adrenalineTimer {
rt.removeCircle()
at.removeCircle()
rt.animateCircle(duration: 2.0)
at.animateCircle(duration: 2.0)
}
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

Circle UIView resize with animation keeping Circle shape

I have a UIView which I am making the background a circular shape with:
self.colourView.layer.cornerRadius = 350
self.colourView.clipsToBounds = true
With the view being a 700 by 700 square.
I am then trying to change the size of this view using:
UIView.animate(withDuration: zoomLength,
delay: 0,
options: .curveEaseIn,
animations: {
self.colorViewWidth.constant = 100
self.colorViewHeight.constant = 100
self.colourView.layer.cornerRadius = 50
self.colourView.clipsToBounds = true
self.view.layoutIfNeeded()
},
completion: { finished in
})
})
The problem with this is the corner radius of the view switch to 50 instantly. So that as the view shrinks in size it doesn't look like a circle shrinking but a square with rounded corners until it reaches the 100 by 100 size.
How can I take a view that circule in shape and shrink it down while maintaining the circlare shape during the animation.
You could always animate the corner radius as well with a CABasicAnimation along side the UIView animations. See example below.
import UIKit
class ViewController: UIViewController {
//cause 'merica
var colorView = UIView()
var button = UIButton()
var colorViewWidth : NSLayoutConstraint!
var colorViewHeight : NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
colorView.frame = CGRect(x: 0, y: 0, width: 750, height: 750)
colorView.center = self.view.center
colorView.backgroundColor = .blue
colorView.layer.cornerRadius = 350
colorView.clipsToBounds = true
colorView.layer.allowsEdgeAntialiasing = true
colorView.translatesAutoresizingMaskIntoConstraints = false
//set up constraints
colorViewWidth = NSLayoutConstraint(item: colorView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 750)
colorViewWidth.isActive = true
colorViewHeight = NSLayoutConstraint(item: colorView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 750)
colorViewHeight.isActive = true
self.view.addSubview(colorView)
colorView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
colorView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor).isActive = true
//button for action
button = UIButton(frame: CGRect(x: 0, y: 0, width: self.view.bounds.width - 20, height: 50))
button.center = self.view.center
button.center.y = self.view.bounds.height - 70
button.addTarget(self, action: #selector(ViewController.pressed(sender:)), for: .touchUpInside)
button.setTitle("Animate", for: .normal)
button.setTitleColor(.blue, for: .normal)
self.view.addSubview(button)
}
func pressed(sender:UIButton) {
self.colorViewWidth.constant = 100
self.colorViewHeight.constant = 100
UIView.animate(withDuration: 1.0,
delay: 0,
options: .curveEaseIn,
animations: {
self.view.layoutIfNeeded()
},
completion: { finished in
})
//animate cornerRadius and update model
let animation = CABasicAnimation(keyPath: "cornerRadius")
animation.duration = 1.0
//super important must match
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
animation.fromValue = colorView.layer.cornerRadius
animation.toValue = 50
colorView.layer.add(animation, forKey: nil)
//update model important
colorView.layer.cornerRadius = 50
}
}
Result:
Try to put your image inside a viewDidLayoutSubviews()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
colourView.layer.cornerRadius = colourView.frame.size.width/2
colourView.layer.masksToBounds = true
colourView.layer.borderWidth = 3.0
colourView.layer.borderColor = UIColor(red: 142.0/255.5, green: 142.0/255.5, blue: 142.0/255.5, alpha: 1.0).cgColor
}
let me know how it goes
Cheers
Regarding animation you need to create the CAShapeLayer object and then set the cgpath of the layer object.
let circleLayer = CAShapeLayer()
let radius = view.bounds.width * 0.5
//Value of startAngle and endAngle should be in the radian.
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
circleLayer.path = path.cgPath
The above will draw the circle for you. After that you can apply the animation on layer object.
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = fromValue//you can update the value here by default it will be 0
animation.toValue = toValue //you can update the value here by default it will be 1
animation.duration = CFTimeInterval(remainingTime)
circleLayer
circleLayer.removeAnimation(forKey: "stroke")
circleLayer.add(animation, forKey: "stroke")

Resources