Just started with Swift (and OOO in general about a week ago so bear with me).
Following this "tutorial" (https://www.youtube.com/watch?v=bwDJspl5sbg), I was able to get the following code which draws a grid of circles.
//get the bounds of the view we're in
let viewWidth = self.view.bounds.width
let viewHeight = self.view.bounds.height
//set parameters for gride of circles
let margin: CGFloat = 25
let xMargin = margin
let yMargin = margin
let circleBreathingSpace: CGFloat = 5
let circleLineWidth: CGFloat = 4
let across: CGFloat = 6
let gridRowWidth: CGFloat = viewWidth - (2.0 * margin)
let cellWidth: CGFloat = gridRowWidth / across
let cellHeight: CGFloat = cellWidth
let down: CGFloat = (viewHeight - (2 * yMargin)) / cellHeight
let circleRadius: CGFloat = (cellWidth - (2 * circleBreathingSpace)) / 2
//draw the grid
for xx in 0..<Int(across){
for yy in 0..<Int(down){
let circleX: CGFloat = xMargin + ((CGFloat(xx) + 0.5) * cellWidth)
let circleY: CGFloat = yMargin + ((CGFloat(yy) + 0.5) * cellHeight)
//Draw Circle [ref: http://stackoverflow.com/questions/29616992/how-do-i-draw-a-circle-in-ios-swift ]
let circlePath = UIBezierPath(arcCenter: CGPoint(x: circleX,y: circleY),
radius: circleRadius, startAngle: CGFloat(0),
endAngle:CGFloat(M_PI * 2),
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
//change the fill color
shapeLayer.fillColor = UIColor.blue.cgColor
//you can change the stroke color
shapeLayer.strokeColor = UIColor.black.cgColor
//you can change the line width
shapeLayer.lineWidth = 6.0
view.layer.addSublayer(shapeLayer)
}
}
Question Now what I'm wondering is how I can give a "reference" to each of these individual circles to manipulate their properties later on.
Context
I basically want to have a grid of these circles slowly disappear according to an input of time. So if I have 20 minutes as an input, I'll get 20 circles and each will take 60 seconds to disappear. That's just context for what I'm doing, not necessarily the question itself (although any direction in that area wouldn't hurt either :) )
My Thoughts My idea was maybe to create an array and add the circle currently being created to this array. Then I could loop through the array after and slowly decrease the size of each circle making them shrink and disappear one by one. Even if that is a feasible solution, I'm not quite sure how to go about it.
Thanks in advance!
Yes, you could create an array for those layers:
var shapeLayers = [CAShapeLayer]()
Then, as you create shape layers, you can add
them to that array.
Alternatively, you might want to use an array of arrays (in effect, a two dimensional array) so that it more accurately mirrors the visual representation. You could do something like above, creating an array and adding shapes to the array for each row, and then create another array to hold those individual arrays for the rows of shape layers. But maybe more elegant, you can use map
calls to build the array of array of CAShapeLayer
objects.
The basic idea is that you take a range of integers to build an array. Let's consider a simple example where I want a simple array of 10 Int
objects, 0, 2, 4, ... 18:
let array = (0 ..< 10).map { (i) -> Int in
return i * 2
}
But this works for building an array of CAShapeLayer
objects, too:
let shapeArray = (0 ..< 10).map { (xx) -> CAShapeLayer in
let shapeLayer = CAShapeLayer()
// ... configure that shapeLayer here
return shapeLayer
}
So that builds a "row" of CAShapeLayer
objects, as xx
progresses from 0 to 10.
You can then nest that inside an map
for all of the rows, too. So the inner map
returns an array of CAShapeLayer
objects, but the outer map
returns an array of those arrays, and in ending up with a [[CAShapeLayer]]
:
let arrayOfArrays = (0 ..< 20).map { (yy) -> [CAShapeLayer] in
return (0 ..< 10).map { (xx) -> CAShapeLayer in
let shapeLayer = CAShapeLayer()
// ... configure that shapeLayer here
return shapeLayer
}
}
You can then iterate through those like so:
for shapeLayers in arrayOfArrays {
for shapeLayer in shapeLayers {
// do something with `shapeLayer`, e.g.
view.layer.addSublayer(shapeLayer)
}
}
So, going back to your example:
let viewWidth = view.bounds.width
let viewHeight = view.bounds.height
//set parameters for gride of circles
let margin: CGFloat = 25
let xMargin = margin
let yMargin = margin
let circleBreathingSpace: CGFloat = 5
let circleLineWidth: CGFloat = 6
let across: CGFloat = 6
let gridRowWidth: CGFloat = viewWidth - (2.0 * margin)
let cellWidth: CGFloat = gridRowWidth / across
let cellHeight: CGFloat = cellWidth
let down: CGFloat = (viewHeight - (2 * yMargin)) / cellHeight
let circleRadius: CGFloat = (cellWidth - (2 * circleBreathingSpace)) / 2
//build the grid
let circles = (0 ..< Int(down)).map { (yy) -> [CAShapeLayer] in
return (0 ..< Int(across)).map { (xx) -> CAShapeLayer in
let circleX: CGFloat = xMargin + ((CGFloat(xx) + 0.5) * cellWidth)
let circleY: CGFloat = yMargin + ((CGFloat(yy) + 0.5) * cellHeight)
let path = UIBezierPath(arcCenter: CGPoint(x: circleX, y: circleY),
radius: circleRadius, startAngle: 0,
endAngle: .pi * 2,
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.blue.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = circleLineWidth
return shapeLayer
}
}
for row in circles {
for shapeLayer in row {
view.layer.addSublayer(shapeLayer)
}
}
By the way, if you want them to disappear over time, you might consider a repeating timer. And I'd animate them going to clear
color:
var row = 0
var col = 0
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
let shapeLayer = circles[col][row]
let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = 1
animation.toValue = 0
animation.duration = 2
shapeLayer.opacity = 0
shapeLayer.add(animation, forKey: nil)
row += 1
if row >= circles[0].count {
row = 0
col += 1
if col >= circles.count {
timer.invalidate()
}
}
}
That yields:
Note, it might be tempting to use the nested for
loop pattern (like we did when adding the shape layers to the layer hierarchy), creating a separate timer for each circle to be removed, but that introduces two issues:
First, if you have to cancel all of these, you don't really want to have to iterate through arrays of timers in order to cancel them. It's nicer to be able to invalidate
a single repeating timer. Second, the firing of multiple timers scheduled in the future can be thrown off by "timer coalescing" (where, in an effort to save power, the OS will start firing off distant timers in groups). Yes, you can use GCD timers and turn off timer coalescing, but repeating timer eliminates the issue altogether.
I notice that you wanted the circles to shrink, rather than fade, like I did above. So you could do the following:
let viewWidth = view.bounds.width
let viewHeight = view.bounds.height
//set parameters for gride of circles
let margin: CGFloat = 25
let xMargin = margin
let yMargin = margin
let circleBreathingSpace: CGFloat = 5
let circleLineWidth: CGFloat = 6
let across: CGFloat = 6
let gridRowWidth: CGFloat = viewWidth - (2.0 * margin)
let cellWidth: CGFloat = gridRowWidth / across
let cellHeight: CGFloat = cellWidth
let down: CGFloat = (viewHeight - (2 * yMargin)) / cellHeight
let circleRadius: CGFloat = (cellWidth - (2 * circleBreathingSpace)) / 2
//build the grid
let circles = (0 ..< Int(down)).map { (yy) -> [CAShapeLayer] in
(0 ..< Int(across)).map { (xx) -> CAShapeLayer in
let circleX: CGFloat = xMargin + ((CGFloat(xx) + 0.5) * cellWidth)
let circleY: CGFloat = yMargin + ((CGFloat(yy) + 0.5) * cellHeight)
let path = UIBezierPath(arcCenter: .zero,
radius: circleRadius, startAngle: 0,
endAngle: .pi * 2,
clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.position = CGPoint(x: circleX, y: circleY)
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.blue.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = circleLineWidth
return shapeLayer
}
}
//add grid to view's layer
for row in circles {
for shapeLayer in row {
view.layer.addSublayer(shapeLayer)
}
}
Note, I tweaked that so that the path was about .zero
, but moved the position
of the layer to where I wanted the circle. That allows me to simply adjust the scale of the transform
of the layer when I want them to disappear:
var row = 0
var col = 0
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
let shapeLayer = circles[col][row]
let transformAnimation = CABasicAnimation(keyPath: "transform")
let transform = CATransform3DMakeScale(0, 0, 1)
transformAnimation.fromValue = shapeLayer.transform
transformAnimation.toValue = transform
transformAnimation.duration = 2
shapeLayer.transform = transform
shapeLayer.add(transformAnimation, forKey: nil)
row += 1
if row >= circles[0].count {
row = 0
col += 1
if col >= circles.count {
timer.invalidate()
}
}
}
That yields: