Connect two strokeEnd animations - ios

I am trying to make a custom activity indicator similar to the Android material design circular indeterminate activity indicator. Basically I want to draw the circle two times and erase it, but erasing and drawing does not happen at the same time or speed. This is what I have so far:
let progressLayer = CAShapeLayer()
progressLayer.strokeColor = UIColor.red().cgColor
progressLayer.fillColor = nil
progressLayer.lineWidth = 2
let drawAnimation = CABasicAnimation(keyPath: "strokeEnd")
drawAnimation.duration = duration / 2
drawAnimation.fromValue = 0
drawAnimation.toValue = 1
drawAnimation.isRemovedOnCompletion = false
drawAnimation.fillMode = kCAFillModeForwards
let eraseAnimation = CABasicAnimation(keyPath: "strokeStart")
eraseAnimation.duration = duration / 2
eraseAnimation.beginTime = 0.2
eraseAnimation.fromValue = 0
eraseAnimation.toValue = 0.4
eraseAnimation.isRemovedOnCompletion = false
eraseAnimation.fillMode = kCAFillModeForwards
let endDrawAnimation = CABasicAnimation(keyPath: "strokeEnd")
endDrawAnimation.beginTime = duration / 2
endDrawAnimation.duration = duration / 2
endDrawAnimation.fromValue = 0
endDrawAnimation.toValue = 1
endDrawAnimation.isRemovedOnCompletion = false
endDrawAnimation.fillMode = kCAFillModeForwards
let endEraseAnimation = CABasicAnimation(keyPath: "strokeStart")
endEraseAnimation.beginTime = duration / 2
endEraseAnimation.duration = duration / 4
endEraseAnimation.fromValue = 0.4
endEraseAnimation.toValue = 1
endEraseAnimation.isRemovedOnCompletion = false
endEraseAnimation.fillMode = kCAFillModeForwards
let endEraseAnimation2 = CABasicAnimation(keyPath: "strokeStart")
endEraseAnimation2.beginTime = duration * 3 / 4
endEraseAnimation2.duration = duration / 4
endEraseAnimation2.fromValue = 0
endEraseAnimation2.toValue = 1
endEraseAnimation2.isRemovedOnCompletion = false
endEraseAnimation2.fillMode = kCAFillModeForwards
let animations = CAAnimationGroup()
animations.duration = duration
animations.animations = [drawAnimation, eraseAnimation, endDrawAnimation, endEraseAnimation, endEraseAnimation2]
animations.isRemovedOnCompletion = false
animations.fillMode = kCAFillModeForwards
progressLayer.add(animations, forKey: "stroke")
The code does everything as expected, except for one issue. When the first strokeEnd animation is done and the second one starts there is sort of a flash meaning the part of the circle that was drawn till that point disappears and then drawing starts again from 0. Does anyone have any ideas how to fix this?

When the first strokeEnd animation is done and the second one starts
there is sort of a flash meaning the part of the circle that was drawn
till that point disappears and then drawing starts again from 0.
If you compare the setup of both drawAnimation and endDrawAnimation you will see that they are identical except for the beginTime property. That is what causes your flickering. They both start at 0 and they both end with 1. Since you have not specified the desired outcome I can just assume that you want endDrawAnimation to have a fromValue of 1.
let drawAnimation = CABasicAnimation(keyPath: "strokeEnd")
drawAnimation.duration = duration / 2
drawAnimation.fromValue = 0
drawAnimation.toValue = 1
drawAnimation.isRemovedOnCompletion = false
drawAnimation.fillMode = kCAFillModeForwards
let endDrawAnimation = CABasicAnimation(keyPath: "strokeEnd")
endDrawAnimation.beginTime = duration / 2 // Starts after the first animation and starts with 0 again.
endDrawAnimation.duration = duration / 2
endDrawAnimation.fromValue = 0
endDrawAnimation.toValue = 1
endDrawAnimation.isRemovedOnCompletion = false
endDrawAnimation.fillMode = kCAFillModeForwards
Suggestion for improvement
As far as I can see you want to create key frame animation meaning that you have predefined offsets at which the animation behaviour (speed, value, etc.) changes. You might want to consider using CAKeyframeAnimation instead.
I am unsure if you are aware of this but at the moment you have two animations running concurrently on strokeStart:
eraseAnimation that starts at 0.2 with a duration of 0.5 * duration
endEraseAnimation that starts at 0.5 * duration.
That means that the end of eraseAnimation (0.2 + 0.5 * duration) is always greater than the start of endEraseAnimation (0.5 * duration)

Related

Falling Animation for Swift(iOS without using storyboard)

I'm new to iOS and trying to practice falling animation like Yolo app.
What Yolo app has is on sign up page, it has bunch of falling emojis at different velocity seamlessly.
So if I want to implement the falling animation for each emojis and without using storyboard and SwiftUI(so with Appkit), what's the common practice here.
Is there any library I can use?
let emitterLayer = CAEmitterLayer()
emitterLayer.emitterPosition = CGPoint(x: 200, y: 200)
let cell = CAEmitterCell()
cell.birthRate = 10
cell.lifetime = 1
cell.velocity = 100
cell.scale = 0.1
cell.emissionRange = CGFloat.pi * 2.0
cell.contents = UIImage(named: "yes")!.cgImage
emitterLayer.emitterCells = [cell]
view.layer.addSublayer(emitterLayer)

iOS Charts : Bar chart spacing and position

Objective
I'm trying to display data from two sets (ACTIVE and REST) in a bar chart.
The sets are alternating (meaning there is one ACTIVE interval, followed by a REST interval, etc...). Rather, there is alwats one REST set between every two ACTIVE sets.
The code I've used is below, for reference.
I am, however, running into problems with the bar positions and spacing.
Issue: centering bar at x-value
First issue, the bar is not centered at the corresponding x-value. In the example below, the first orange bar has x = 1.
It clearly starts at value 1 on the x axis (its leftmost side corresponds to about 1), but I want it to be centered around the 1 value on the x-axis
Issue: bar spacing
For some reason, the bars intersection and the spacing is not equal (see screenshot).
Note the white space between bar 2 and 3, and the absence of space between bar 1 and 2.
The relevant code here is :
let barWidth = 1.0
let barSpace = 0.10
let groupSpace = 0.1
chartData.groupBars(fromX: 1, groupSpace: groupSpace, barSpace: barSpace)
chartData.barWidth = barWidth
Indexing bars and BarChartDataEntry
Each set (REST and ACTIVE) comes from a different array of values.
If I press on the 1st REST bar, I would like to obtain the corresponding value of 0. on the 1st ACTIVE bar, the value 0 also. 2nd ACTIVE bar, value 1...
However, in chartValueSelected, using the entry.x value gives the bar's x-position on the axis (NOT the x value I've set in the code), which is casing errors.
How can I get the value of the selected bar's index in the set it belongs to ?
Code
//
// MARK: Chart Setup
//
func setupBarChart() {
let intervals = self.session!.getIntervals()
// Set chart delegate
intervalBarChartView.delegate = self
//
// Create pairs (x, y) of values
//
var values_ACTIVE : [BarChartDataEntry] = []
var values_REST : [BarChartDataEntry] = []
for i in 0...(intervals.count - 1) {
let newValue_ACTIVE = intervals[i].duration!.doubleValue
// let newIndex_ACTIVE = Double(2*i+1)
let newIndex_ACTIVE = Double(i)
values_ACTIVE += [BarChartDataEntry(x: newIndex_ACTIVE, y: newValue_ACTIVE)]
if i < (intervals.count - 1) {
let newValue_REST = (intervals[i+1].startTime!.doubleValue) - intervals[i].getEndTime()!
// let newIndex_REST = Double(2*i+2)
let newIndex_REST = Double(i)
values_REST += [BarChartDataEntry(x: newIndex_REST, y: newValue_REST)]
}
}
// Create data sets
let dataSet_ACTIVE = BarChartDataSet(values: values_ACTIVE, label: "ACTIVE")
let dataSet_REST = BarChartDataSet(values: values_REST, label: "REST")
//Set chart data
let chartData = BarChartData()
chartData.addDataSet(dataSet_REST)
chartData.addDataSet(dataSet_ACTIVE)
self.intervalBarChartView.data = chartData
// Bar sizes
let barWidth = 1.0
let barSpace = 0.10
let groupSpace = 0.1
chartData.groupBars(fromX: 0, groupSpace: groupSpace, barSpace: barSpace)
chartData.barWidth = barWidth
// Bar Colors
dataSet_ACTIVE.colors = [runOrange]
dataSet_REST.colors = [RunGreen]
self.intervalBarChartView.gridBackgroundColor = NSUIColor.white
// Enable/Disable show values and position of values
chartData.setDrawValues(false)
intervalBarChartView.drawValueAboveBarEnabled = false
// Bar Axes:
intervalBarChartView.xAxis.axisMinimum = 0.0
intervalBarChartView.xAxis.axisMaximum = 10
intervalBarChartView.leftAxis.axisMinimum = 0.0
intervalBarChartView.rightAxis.enabled = false
// Bar Axes: GridLines
self.intervalBarChartView.xAxis.drawGridLinesEnabled = false
self.intervalBarChartView.xAxis.drawAxisLineEnabled = false
self.intervalBarChartView.rightAxis.drawAxisLineEnabled = true
self.intervalBarChartView.rightAxis.drawGridLinesEnabled = false
self.intervalBarChartView.leftAxis.drawGridLinesEnabled = false
self.intervalBarChartView.leftAxis.drawAxisLineEnabled = false
// Bar Text
self.intervalBarChartView.chartDescription?.text = "Barchart Demo"
// Control interaction
self.intervalBarChartView.doubleTapToZoomEnabled = false
}
EDIT
Here is what I get with two different compilations of settings :
With these settings :
// Bar sizes
let barWidth = 0.3
let barSpace = 0.10
let groupSpace = 0.1
The gap between the bars is (more or less) equal:
However, with these setttings:
let groupSpace = 0.3
let barSpace = 0.05
let barWidth = 0.3
I get a wider gap between bars 2 and 3 (compared to 1 and 2)
You need to change your barWidth calculation so that you can achieve what you expect in your output.
Logic of GroupBarChart Bar & Spacing calculation:
(baSpace + barWidth) * dataSet count + groupSpace = 1.0
So in your case you need to update above value like this:
let groupSpace = 0.3
let barSpace = 0.05
let barWidth = 0.3
// (0.3 + 0.05) * 2 + 0.3 = 1.00 -> interval per "group"
let startVal = 0
chartData.barWidth = barWidth;
chartData.groupBars(fromX: Double(startVal), groupSpace: groupSpace, barSpace: barSpace)
By doing this you will get proper cantered bar position in your bar chart.
Also one more option is only set BarWidth for your charts like below:
let barChartData = BarChartData(dataSet: barChartDataSet)
barChartData.barWidth = 0.4
Hope this will helps!

CAEmitterCell emits in two opposite orientation (one is wrong)

I want CAEmitterCell emits in a specific angle. I use emitterCell.emissionLongitude = -CGFloat.pi to specify the orientation angle is -pi(which is left), but it emits to right as well.
func setUpEmitterCell() {
emitterCell.contents = UIImage(named: "spark_small")?.cgImage
emitterCell.velocity = 50.0
emitterCell.velocityRange = 500.0
let zeroDegreesInRadians = degreesToRadians(0.0)
emitterCell.spin = degreesToRadians(130.0)
emitterCell.spinRange = zeroDegreesInRadians
emitterCell.emissionRange = degreesToRadians(10.0)
emitterCell.emissionLongitude = -CGFloat.pi
emitterCell.lifetime = 1.0
emitterCell.birthRate = 1000.0
emitterCell.xAcceleration = 0.0
emitterCell.yAcceleration = 0.0
}
This is the problem:
emitterCell.velocity = 50.0
// but then:
emitterCell.velocityRange = 500.0
You are allowing the velocity to range so widely from its original value of 50 that it can be negative. When it is, the cells are emitted in the opposite direction (that is what negative velocity means).

CGAffineTransformRotate issue with image

I'm trying to create a very simple clock. When I apply a CGAffineTransformRotate it looks great, however on the 30th - 31st second the hand goes crazy.
An example of it going wrong.
Here is the code I'm using to animate it:
lengthInSeconds += 1
let angle = CGFloat(6.degreesToRadians * Double(lengthInSeconds))
var transform = CGAffineTransformMakeTranslation(0.0, (clockFace.frame.width/4))
secondsHand.layer.anchorPoint = CGPointMake(0.5, 1.0)
transform = CGAffineTransformRotate(transform, angle)
let spring = CASpringAnimation(keyPath: "transform.rotation")
spring.damping = 2.0//3.0
spring.duration = 0.4
spring.mass = 0.1
spring.initialVelocity = 0.0
secondsHand.layer.addAnimation(spring, forKey: "rotation")
secondsHand.transform = transform
Anyone have any idea as to why it would rotate awkwardly like this? Just to add, it happens on all passes, so at 30 seconds, 1 minute 30, 2 minutes 30 etc.

SKPhysicsBody objects collisions are not aligned

I have a player. He has to jump over some blocks.
However, it seems like the SKPhysicsBody(rectangleOfSize: ... ) isn't aligned properly. When the character hits the upper half of the block it just passes through and he can't pass below it because it collides with an invisible SKPhysicsBody.
Depending on how I put the anchor point it sometimes even happens that the physics body is to the left or right of it's SKSpriteNode.
My code for the blocks:
self.smallBlock.anchorPoint = CGPointMake(0.5, 0.5)
self.smallBlock.position = CGPointMake(CGRectGetMaxX(self.frame) + self.smallBlock.size.width, CGRectGetMinY(self.frame) + self.runningBar.size.height * 2)
self.smallBlock.physicsBody = SKPhysicsBody(rectangleOfSize: self.smallBlock.size)
self.smallBlock.physicsBody?.dynamic = false
self.smallBlock.physicsBody?.categoryBitMask = colliderType.Block.rawValue
self.smallBlock.physicsBody?.contactTestBitMask = colliderType.Character.rawValue
self.smallBlock.physicsBody?.collisionBitMask = colliderType.Character.rawValue
self.smallBlock.physicsBody?.usesPreciseCollisionDetection = true
self.addChild(self.smallBlock)
self.character.anchorPoint = CGPointMake(0, 0.25)
self.character.position = CGPointMake(CGRectGetMinX(self.frame) - self.character.size.width, CGRectGetMinY(self.frame) + self.character.size.height)
self.initPosX = CGRectGetMinX(self.frame) + (self.character.size.width * 1.25)
Code for the character
self.character.anchorPoint = CGPointMake(0, 0.25)
self.character.position = CGPointMake(CGRectGetMinX(self.frame) - self.character.size.width, CGRectGetMinY(self.frame) + self.character.size.height)
self.initPosX = CGRectGetMinX(self.frame) + (self.character.size.width * 1.25)
self.character.physicsBody = SKPhysicsBody(rectangleOfSize: self.character.size)
self.character.physicsBody?.affectedByGravity = true
self.character.physicsBody?.categoryBitMask = colliderType.Character.rawValue
self.character.physicsBody?.contactTestBitMask = colliderType.Block.rawValue
self.character.physicsBody?.collisionBitMask = colliderType.Block.rawValue
self.character.physicsBody?.usesPreciseCollisionDetection = true
self.character.physicsBody?.affectedByGravity = true
self.character.physicsBody?.allowsRotation = false
self.addChild(self.character)
Solution:
I've added skView.showsPhysics = true to my View Controller. When you run the app in the simulator it will outline every object like shown here.
It turns out setting custom anchor points messes up the rectangleOfSize-positions.

Resources