Creating a progress indicator with a rounded rectangle - swift

I am attempting to create a rounded-rectangle progress indicator in my app. I have previously implemented a circular indicator, but not like this shape. I would like it to look something like this (start point is at the top):
But I get this with 0 as the .strokeStart property of the layer:
My current code place in viewDidLoad():
let queueShapeLayer = CAShapeLayer()
let queuePath = UIBezierPath(roundedRect: addToQueue.frame, cornerRadius: addToQueue.layer.cornerRadius)
queueShapeLayer.path = queuePath.cgPath
queueShapeLayer.lineWidth = 5
queueShapeLayer.strokeColor = UIColor.white.cgColor
queueShapeLayer.fillColor = UIColor.clear.cgColor
queueShapeLayer.strokeStart = 0
queueShapeLayer.strokeEnd = 0.5
view.layer.addSublayer(queueShapeLayer)
addToQueue is the button which says 'Upvote'.
Unlike creating a circular progress indicator, I cannot set the start and end angle in the initialisation of a Bezier path.
How do I make the progress start from the top middle as seen in the first image?
Edit - added a picture without corner radius on:
It seems that the corner radius is creating the issue.
If you have any questions, please ask!

I found a solution so the loading indicator works for round corners:
let queueShapeLayer = CAShapeLayer()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Queue timer
let radius = addToQueue.layer.cornerRadius
let diameter = radius * 2
let totalLength = (addToQueue.frame.width - diameter) * 2 + (CGFloat.pi * diameter)
let queuePath = UIBezierPath(roundedRect: addToQueue.frame, cornerRadius: radius)
queueShapeLayer.path = queuePath.cgPath
queueShapeLayer.lineWidth = 5
queueShapeLayer.strokeColor = UIColor.white.cgColor
queueShapeLayer.fillColor = UIColor.clear.cgColor
queueShapeLayer.strokeStart = 0.25 - CGFloat.pi * diameter / 3 / totalLength // Change the '0.25' to 0.5, 0.75 etc. wherever you want the bar to start
queueShapeLayer.strokeEnd = queueShapeLayer.strokeStart + 0.5 // Change this to the value you want it to go to (in this case 0.5 or 50% loaded)
view.layer.addSublayer(queueShapeLayer)
}
After I had did this though, I was having problems that I couldn't animate the whole way round. To get around this, I created a second animation (setting strokeStart to 0) and then I placed completion blocks so I could trigger the animations at the correct time.
Tip:
Add animation.fillMode = CAMediaTimingFillMode.forwards & animation.isRemovedOnCompletion = false when using a CABasicAnimation for the animation to wait until you remove it.
I hope this formula helps anyone in the future!
If you need help, you can always message me and I am willing to help. :)

Related

Remove a function from SKScene when Touches Begin

I have a blue sky texture moving from the top of the screen to the bottom, repeating forever in a function. However, when touches begin, I would like the blue sky to completely fade out after a duration, then completely remove from the scene.
I am able to get the sky to fade out momentarily, then it seems as though half of the loop is removed, but I still get the sky coming down every other time. Here's a video of the problem: Blue Sky Video
In the video, touches begin when the pause button appears at the top left of the screen - before touches begin, the blue sky moves from top to bottom seamlessly as it should.
FUNCTION
func createBlueSky() {
let blueSkyTexture = SKTexture(imageNamed: "blueSky")
for i in 0 ... 1 {
let blueSky = SKSpriteNode(texture: blueSkyTexture)
blueSky.name = "blueSky"
blueSky.zPosition = -60
blueSky.anchorPoint = CGPoint.zero
blueSky.position = CGPoint(x: 0, y: (blueSkyTexture.size().height * CGFloat(i)))
worldNode.addChild(blueSky)
let moveDown = SKAction.moveBy(x: 0, y: -blueSkyTexture.size().height, duration: 10)
let moveReset = SKAction.moveBy(x: 0, y: blueSkyTexture.size().height, duration: 0)
let moveLoop = SKAction.sequence([moveDown, moveReset])
let moveForever = SKAction.repeatForever(moveLoop)
blueSky.run(moveForever)
}
}
I've added the above function to didMove(toView)
override func didMove
createBlueSky()
Then in my touches began I added this code to fade out and remove from parent.
I saw in another post that to access the blue sky from another function, I'd need to give it a name, which I did. Still no luck :/
override func touchesBegan
let blueSky = worldNode.childNode(withName: "blueSky") as! SKSpriteNode
let blueSkyFade = SKAction.fadeOut(withDuration: 3)
let blueSkyFadeWait = SKAction.wait(forDuration: 3)
let removeBlueSky = SKAction.removeFromParent()
blueSky.run(SKAction.sequence([blueSkyFadeWait, blueSkyFade, removeBlueSky]))
I'm very new to Swift, but I hope the question was specific enough. I'm happy to provide any other necessary information.
You are creating 2 blueSky nodes, but you only fade and remove 1 of them. You need to enumerate through all of the nodes with the name you are looking for. To do this, you call enumerateChildNodes(withName:}
let blueSkyFade = SKAction.fadeOut(withDuration: 3)
let blueSkyFadeWait = SKAction.wait(forDuration: 3)
let removeBlueSky = SKAction.removeFromParent()
enumerateChleNodes(withName:"blueSky")
{
(blueSky,stop) in
blueSky.run(SKAction.sequence([blueSkyFadeWait, blueSkyFade, removeBlueSky]))
}

How to scale to be at the same distance in all devices?

I'm having problems opening my game in different divices , in the 6s iphone plus looks much bigger the circle the center, also the small circle that is on the line changes position , I would like that the center circle was the same size and that the small circle always this half on the line.
import SpriteKit
struct Circle {
var position:CGPoint
var radius:CGFloat
}
class GameScene: SKScene {
let node = SKNode()
let sprite = SKShapeNode(circleOfRadius: 6)
var rotation:CGFloat = CGFloat(M_PI)
var circles:[Circle] = []
var circuloFondo = SKSpriteNode()
var orbita = SKSpriteNode()
let padding2:CGFloat = 26.0
let padding3:CGFloat = 33.5
let padding5:CGFloat = 285.5
var circulo = SKSpriteNode()
override func didMoveToView(view: SKView) {
scaleMode = .ResizeFill
backgroundColor = UIColor(red: 0.3, green: 0.65, blue: 0.9, alpha: 1)
orbita = SKSpriteNode(imageNamed: "orbita2")
orbita.size = CGSize(width:view.frame.size.width - padding2 , height: view.frame.size.width - padding2)
orbita.color = UIColor.whiteColor()
orbita.colorBlendFactor = 1
orbita.alpha = 1
orbita.position = view.center
self.addChild(orbita)
orbita.zPosition = 3
circuloFondo = SKSpriteNode(imageNamed: "circuloFondo")
circuloFondo.size = CGSize(width:view.frame.size.width - padding5 , height: view.frame.size.width - padding5)
circuloFondo.color = UIColor.whiteColor()
circuloFondo.alpha = 1
circuloFondo.position = view.center
self.addChild(circuloFondo)
circuloFondo.zPosition = 0
let radius1:CGFloat = (view.frame.size.width - padding3)/2 - 1
let radius2:CGFloat = (view.frame.size.width - padding5)/2 + 6.5
circles.append(Circle(position: view.center, radius: radius1))
circles.append(Circle(position: view.center, radius: radius2))
addChild(node)
node.addChild(sprite)
if let circle = nextCircle() {
node.position = circle.position
sprite.fillColor = SKColor.whiteColor()
sprite.zPosition = 4.0
sprite.position = CGPoint(x:circle.radius, y:0)
rotate()
}
You can get the width of the screen like this:
let screenSize: CGRect = UIScreen.mainScreen().bounds
let screenWidth = screenSize.width
and then set elements in your UI to be a proportion of the screenWidth. For instance:
let radius1:CGFloat = screenWidth/4
//this would always give you a radius that is one quarter of the screen width
I've used this method a few times with success, hope it works for you.
Your main problem is your scene mode is set to ResizeFill. This will cause you so many headaches as you will have to do all the scaling yourself, hence the circles are different sizes on different devices. Having scale mode resizeFill will also affect things such as the physics engine or fontSizes which you will need to adjust for on every device.
I would recommend you use scene scale mode .AspectFill with the default scene size of 1024*768 (landscape) or 768*1024 (portrait). This is the same as the xCode default game template.
This way everything will look exactly the same on all iPhones. On iPads there will be slightly more screen space at the top and bottom which you simply cover with your background. The main trick is that you position your stuff from the center.
Furthermore you can use the universal assets in the asset catalogue and everything will look great and not blurry.
The only thing that you might have to adjust for this way is that on iPads you might need to move some buttons up/down if you want them on the top/bottom edge.
I strongly recommend you consider this as I can talk from experience that using scale mode ResizeFill is really bad. I have been through this pain with 2 games before I rewrote them because they were so inconsistent on all devices causing me so many bugs in the process. Lets not talk about the time I wasted testing on all devices, adjusting values until it felt right.
Hope this helps.

Swift : Animate SKEmitterNode

I am trying to create an explosion effect using SKEmitterNode, I followed some of this tutorial (http://www.waveworks.de/kill-your-characters-animating-explosions-with-skanimations-in-sprite-kit/), but it didn't seem to work. Here is what i have:
var superDabParticle = SKEmitterNode(fileNamed: "superDabParticle.sks")
func setUpEmiterNode(){
superDabParticle?.position = CGPoint(x: self.size.width * 0.5, y: self.size.height * 0.5)
let superDabParticleEffectNode = SKEffectNode()
superDabParticle?.zPosition = 1
superDabParticle?.setScale(0)
superDabParticleEffectNode.addChild(superDabParticle!)
self.addChild(superDabParticleEffectNode)
}
func animateEmiterNode(){
let birth = SKAction.runBlock { self.superDabParticle?.particleBirthRate = 0 }
superDabParticle!.runAction(SKAction.sequence([SKAction.waitForDuration(0.1), SKAction.scaleBy(1.5, duration: 0.1), birth]))
}
So i need to call setUpEmiterNode() when the app first launches up, then when the user taps a button, call animateEmiterNode to have the particles fly from the bottom of the screen up to about 75% of the screen, then fall back down; giving a sort of explosion under water effect. Im using the SPARK emitter effect. If you know my issue or have any other ways that are better to do this, please assist!!

Is there a way to spawn nodes uniformly?

I have a game where circles are moving up and down lines like so:
http://i.stack.imgur.com/5zKS4.png
, and Im using a .plist file to generate them, but its a little monotonous.Is there another way to generate several columns of circles uniformly without using a .plist file? Will post code if necessary.
You could write a function like this:
func createCirclesOnLine(line : CGFloat){
//Set this to however low you want the circles to start.
var currentY : CGFloat = -20.0
//Set this to however high you want the circles to go
let maxY = self.size.height + 100
//Set this to the spacing you want between the circles
let spacing : CGFloat = 50
while currentY < maxY {
let circle = SKShapeNode(circleOfRadius: 20)
circle.position = CGPoint(x: line, y: currentY)
self.addChild(circle)
currentY += spacing
}
}
And then pass it the line (think longitude) of wherever you want the circles to be drawn:
let line = self.size.width / 2
createCirclesOnLine(line)
Is that what you're looking for?
EDIT: To answer your question about how to use this multiple times:
If you want multiple lines of circles on the screen, you can simply decide what lines (again, think longitude) you want to place the circles on. For instance, if I wanted three equally spaced lines of circles on the screen, I would do the following:
let line = self.size.width / 4
createCirclesOnLine(line)
createCirclesOnLine(line * 2)
createCirclesOnLine(line * 3)
If you try this and don't see all of the lines, you might need to add this
scene.size = skView.bounds.size
to the viewDidLoad method in your ViewController.swift right under
skView.ignoresSiblingOrder = true
since you are running the app in portrait mode.

How to generate a new node when another one hits a certain y value

I have a circle moving up a line, and when that circle reaches a certain y point, how can I make it so that another node would generate from below?
Here is the code I currently have for populating the circles, but I am not able to use it with a physics body, as it generates too many nodes and slows down my app:
func createCirclesOnLine(line: CGFloat) {
var currentY : CGFloat = -110
let maxY = self.size.width * 15
let spacing : CGFloat = 120
while currentY < maxY {
let circle = SKSpriteNode(imageNamed: "first#2x")
circle.physicsBody?.dynamic = false
circle.position = CGPointMake(line, currentY)
//circle.physicsBody?.restitution = -900
circle.size = CGSizeMake(75, 75)
// circle.physicsBody = SKPhysicsBody(rectangleOfSize: circle.size)
let up = SKAction.moveByX(0, y: 9000, duration: 90)
circle.runAction(up)
foregroundNode.addChild(circle)
currentY += CGFloat((random() % 400) + 70)
}
Will post more code if necessary.
There are two ways you can go about this. One is to simply check every circle's y position to see if it's above the screen. You'll need a reference to the circles so...
class GameScene: SKScene {
var circles = Array<SKSpriteNode>()
...
In your createCirlcesOnLine function, add each circle to the array as you create it.
...
self.addChild(circle)
circles.append(circle)
Then, in your update method, enumerate through the circles to see if any of them are above the top of the screen.
override func update(currentTime: NSTimeInterval) {
for circle in circles {
if circle.position.y > self.size.height + circle.size.height/2 {
//Send circle back to the bottom using the circle's position property
}
}
}
This solution will work but causes a lot of unnecessary checks on every update cycle.
A second more efficient (and slightly more complicated) recommendation is to add an invisible node above the top of the screen that stretches the screen width. When the circle collides with it, just move it to the bottom of the screen. Look into implementing the SKPhysicsContactDelegate protocol and what needs to happen for that to work. If you run into problems with this solution, post a separate question with those issues.