Creating raining code Matrix effect - ios

Created a sub class of a CATextLayer within which I attached a fadeIn animation, which I than add to a CATextLayer to which I have attached a dropThru animation. The goal to try and create matrix movie raining code effect. Works reasonably well but for the fact that it slowly but surely drives itself into the ground, I suspect cause I keep adding more and more layers. How can I detect when an layer had left the screen so I may delete it.
Here is the code...
class CATextSubLayer: CATextLayer, CAAnimationDelegate {
private var starter:Float!
private var ender:Float!
required override init(layer: Any) {
super.init(layer: layer)
//UIFont.availableFonts()
self.string = randomString(length: 1)
self.backgroundColor = UIColor.black.cgColor
self.foregroundColor = UIColor.white.cgColor
self.alignmentMode = kCAAlignmentCenter
self.font = CTFontCreateWithName("AvenirNextCondensed-BoldItalic" as CFString?, fontSize, nil)
self.fontSize = 16
self.opacity = 0.0
makeFade()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
fatalError("init(coder:) has not been implemented")
}
func randomString(length: Int) -> String {
let letters : NSString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
let len = UInt32(letters.length)
var randomString = ""
for _ in 0 ..< length {
let rand = arc4random_uniform(len)
var nextChar = letters.character(at: Int(rand))
randomString += NSString(characters: &nextChar, length: 1) as String
}
return randomString
}
func makeFade() {
let rands = Double(arc4random_uniform(UInt32(4)))
let fadeInAndOut = CABasicAnimation(keyPath: "opacity")
fadeInAndOut.duration = 16.0;
fadeInAndOut.repeatCount = 1
fadeInAndOut.fromValue = 0.0
fadeInAndOut.toValue = 1
fadeInAndOut.isRemovedOnCompletion = true
fadeInAndOut.fillMode = kCAFillModeForwards;
fadeInAndOut.delegate = self
fadeInAndOut.beginTime = CACurrentMediaTime() + rands
self.add(fadeInAndOut, forKey: "opacity")
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
self.removeAllAnimations()
}
}
With the outer loop/View Controller ..
class ViewController: UIViewController, CAAnimationDelegate {
var beeb: CATextSubLayer!
var meeb: CATextLayer!
var lines = [Int]()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.black
// Do any additional setup after loading the view, typically from a nib.
meeb = CATextLayer()
for bing in stride(from:0, to: Int(view.bounds.width), by: 16) {
lines.append(bing)
}
for _ in 0 ..< 9 {
Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(makeBeeb), userInfo: nil, repeats: true)
}
}
func makeBeeb() {
let rands = Double(arc4random_uniform(UInt32(4)))
let beeb = CATextSubLayer(layer: meeb)
let randx = Int(arc4random_uniform(UInt32(lines.count)))
let monkey = lines[randx]
beeb.frame = CGRect(x: monkey, y: 0, width: 16, height: 16)
let dropThru = CABasicAnimation(keyPath: "position.y")
dropThru.duration = 12.0;
dropThru.repeatCount = 1
dropThru.fromValue = 1
dropThru.toValue = view.bounds.maxY
dropThru.isRemovedOnCompletion = true
dropThru.fillMode = kCAFillModeForwards;
dropThru.beginTime = CACurrentMediaTime() + rands
dropThru.delegate = self
beeb.add(dropThru, forKey: "position.y")
self.view.layer.addSublayer(beeb)
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
self.view.layer.removeAllAnimations()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}

As far as I understand your code you can remove the layer, when it's position animation ends. In this moment it should have left the bounds of the parent view.
Btw. Removing and adding layers costs performance. Instead removing you should reuse the layer for the next animation.

How can I detect when an layer had left the screen so I may delete it
You have already given the CABasicAnimation a delegate which is called when the animation finishes. That is your signal to remove the layer. (You are removing animations but not the layer itself.)

Related

Swift: Longpress Button Animation

I have a Button that acts as an SOS Button. I would like to only accept longpresses on that button (something like two seconds long press) and animate the button while pressing to let the user know he has to longpress.
The Button is just a round Button:
let SOSButton = UIButton()
SOSButton.backgroundColor = Colors.errorRed
SOSButton.setImage(UIImage(systemName: "exclamationmark.triangle.fill"), for: .normal)
SOSButton.translatesAutoresizingMaskIntoConstraints = false
SOSButton.tintColor = Colors.justWhite
SOSButton.clipsToBounds = true
SOSButton.layer.cornerRadius = 25
SOSButton.addTarget(self, action: #selector(tappedSOSButton(sender:)), for: .touchUpInside)
SOSButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(SOSButton)
which looks something like this:
Now, when the button is getting long-pressed, I'd like to animate a stroke like a circular progress view. It would start from 0* and fill the whole circle to finally look like this:
I know it looks the same because the background is white, but there is a white stroke around it.
If the user lets go of the button before the circle fills up, it should animate back to zero in the same speed. If the user holds on long enough, only then should the action get executed.
How would I go about designing such a button? I have not found anything I can work off right now. I know I can animate stuff but animating while long-pressing seems like I'd need to implement something very custom.
Interested in hearing ideas.
You can create a custom class of UIView and add layer to it.
class CircularProgressBar: UIView {
private var circularPath: UIBezierPath = UIBezierPath()
var progressLayer: CAShapeLayer!
var progress: Double = 0 {
willSet(newValue) {
progressLayer?.strokeEnd = CGFloat(newValue)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func layoutSubviews() {
super.layoutSubviews()
removeAllSubviewAndSublayers()
setupCircle()
}
private func removeAllSubviewAndSublayers() {
layer.sublayers?.forEach { $0.removeFromSuperlayer() }
subviews.forEach { $0.removeFromSuperview() }
}
func setupCircle() {
let x = self.frame.width / 2
let y = self.frame.height / 2
let center = CGPoint(x: x, y: y)
circularPath = UIBezierPath(arcCenter: center, radius: x, startAngle: -0.5 * CGFloat.pi, endAngle: 1.5 * CGFloat.pi, clockwise: true)
progressLayer = CAShapeLayer()
progressLayer.path = circularPath.cgPath
progressLayer.strokeColor = UIColor.white.cgColor
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.lineWidth = x/10
progressLayer.lineCap = .round
progressLayer.strokeEnd = 0
layer.addSublayer(progressLayer)
}
func addStroke(duration: Double = 2.0) {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = duration
animation.beginTime = CACurrentMediaTime()
progressLayer.add(animation, forKey: "strokeEnd")
}
func removeStroke(duration: Double = 0.0) {
let revAnimation = CABasicAnimation(keyPath: "strokeEnd")
revAnimation.duration = duration
revAnimation.fromValue = progressLayer.presentation()?.strokeEnd
revAnimation.toValue = 0.0
progressLayer.removeAllAnimations()
progressLayer.add(revAnimation, forKey: "strokeEnd")
}
}
In UIViewController create a UIImageView and CircularProgressBar. Set isUserInteractionEnabled to true of imageView and add progressView to it.
In viewDidLayoutSubviews() method set the frame of progressView equal to bounds of imageView. You also need to set Timer to execute action. Here is the full code.
class ViewController: UIViewController {
let imageView = UIImageView(image: UIImage(named: "icon_sos")!)
let progressView = CircularProgressBar()
var startTime: Date?
var endTime: Date?
var longPress: UILongPressGestureRecognizer?
var timer: Timer?
let longPressDuration: Double = 2.0
override func viewDidLoad() {
super.viewDidLoad()
longPress = UILongPressGestureRecognizer(target: self, action: #selector(longPressAction(_:)))
longPress?.minimumPressDuration = 0.01
imageView.frame = CGRect(x: 100, y: 100, width: 30, height: 30)
imageView.isUserInteractionEnabled = true
imageView.addGestureRecognizer(longPress!)
imageView.addSubview(progressView)
self.view.addSubview(imageView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
progressView.frame = imageView.bounds
}
private func setupTimer() {
timer = Timer.scheduledTimer(timeInterval: self.longPressDuration, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: false)
}
#objc private func fireTimer() {
longPress?.isEnabled = false
longPress?.isEnabled = true
progressView.removeStroke()
if let timer = timer {
timer.invalidate()
self.timer = nil
}
// execute button action here
print("Do something")
}
#objc private func longPressAction(_ sender: UILongPressGestureRecognizer) {
if sender.state == .began {
print("Long Press Began: ", Date())
startTime = Date()
self.progressView.addStroke(duration: self.longPressDuration)
setupTimer()
}
if sender.state == .changed {
print("Long Press Changed: ", Date())
}
if sender.state == .cancelled {
print("Long Press Cancelled: ", Date())
endTime = Date()
if let startTime = startTime, let endTime = endTime {
let interval = DateInterval(start: startTime, end: endTime)
progressView.removeStroke(duration: interval.duration.magnitude)
if interval.duration.magnitude < self.longPressDuration {
timer?.invalidate()
timer = nil
}
}
}
if sender.state == .ended {
print("Long Press Ended: ", Date())
endTime = Date()
if let startTime = startTime, let endTime = endTime {
let interval = DateInterval(start: startTime, end: endTime)
progressView.removeStroke(duration: interval.duration.magnitude)
if interval.duration.magnitude < self.longPressDuration {
timer?.invalidate()
timer = nil
}
}
}
}
}

Custom SKShapeNode is not appearing on scene when being called from update function

// SpawnBlock Init
class SpawnBlock : SKShapeNode {
var timer: Timer?
init(circleOfRadius radius: CGFloat) {
super.init()
self.alpha = 1
self.zPosition = 2
self.fillColor = UIColor.blue
self.strokeColor = UIColor.blue
self.physicsBody = SKPhysicsBody(circleOfRadius: radius)
self.physicsBody?.affectedByGravity = false
self.physicsBody?.categoryBitMask = Collisions.spawnBlock.rawValue
self.physicsBody?.contactTestBitMask = Collisions.block.rawValue
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// Spawn Functions
func spawnABlock() {
let spawnBlock = SpawnBlock(circleOfRadius: 10)
let randomPos = CGPoint(x: CGFloat(arc4random_uniform(UInt32(self.frame.width))), y: CGFloat(arc4random_uniform(UInt32(self.frame.height))))
print("\(randomPos)")
spawnBlock.timer = Timer.scheduledTimer(withTimeInterval: 10, repeats: false, block: { (Timer) in
print("timer done")
spawnBlock.removeFromParent()
})
spawnBlock.position = randomPos
self.addChild(spawnBlock)
}
override func update(_ currentTime: TimeInterval) {
count += 1
print("\(count)")
if count == maxCount {
count = 0
spawnABlock()
}
}
When run in the simulator, the node count increases, the timer works and deinits the sprites, but they do not appear on the scene. Is there a piece I am missing or something?
override init() {
super.init()
}
convenience init(width: CGFloat) {
self.init()
self.init(circleOfRadius: width / 2)
print("initialsed")
self.alpha = 1
self.zPosition = 2
self.fillColor = UIColor.blue
self.strokeColor = UIColor.blue
self.physicsBody = SKPhysicsBody(circleOfRadius: width / 2)
self.physicsBody?.affectedByGravity = false
self.physicsBody?.categoryBitMask = Collisions.spawnBlock.rawValue
self.physicsBody?.collisionBitMask = Collisions.block.rawValue
self.physicsBody?.fieldBitMask = Collisions.noField.rawValue
}
Adding these code blocks in for the original initializing function spawns in the blocks.

Display a view of loading

I user Swift2 and Xcode 7.1
I want to show my view loading by calling a function. My view chragement is a class that inherits from UIView.
I would like my class is instantiated throughout my Controller automatically.
My class :
class loading: UIView {
let circle = UIView()
let anim = CAKeyframeAnimation(keyPath: "position")
init() { }
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func clear(){
self.hidden = true
// Animation ...
}
func display(){
self.hidden = false
// Animation ...
}
private func scaleAndColor(){ }
private func createAnim(){ }
private func createCircle(frame: CGRect){ }
}
What I would like to :
class ViewController: UIViewController {
// nothing to declare
override func viewDidLoad() {
super.viewDidLoad()
// nothing to do
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func anFunction(){
load() // display my loadView
clear() // clear my loadView
}
}
Here's a Loader Singleton I built in Swift 2.1
import Foundation
import UIKit
class MGSwiftLoader: UIView {
static let sharedInstance = MGSwiftLoader(frame: CGRectZero)
private var backgroundView: UIView!
private var label: UILabel!
private let activityIndicator = UIActivityIndicatorView()
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
super.init(frame: frame)
//See through view
self.backgroundColor = UIColor.clearColor()
self.opaque = false
//BackgroundView will contain the UIVisualEffectView, label and activityIndicator
backgroundView = UIView()
backgroundView.backgroundColor = UIColor.clearColor()
backgroundView.opaque = false
backgroundView.layer.cornerRadius = 5.0
self.addSubview(backgroundView)
backgroundView.snp_makeConstraints { (make) -> Void in
make.center.equalTo(self)
make.height.equalTo(100.0)
}
//Blur
let blurEffect = UIBlurEffect(style: .Light)
let blurredEffectView = UIVisualEffectView(effect: blurEffect)
blurredEffectView.layer.cornerRadius = 5.0
blurredEffectView.clipsToBounds = true
backgroundView.addSubview(blurredEffectView)
blurredEffectView.snp_makeConstraints { (make) -> Void in
make.edges.equalTo(backgroundView)
}
//ActivityIndicator
activityIndicator.color = UIColor.lightGrayColor()
backgroundView.addSubview(activityIndicator)
activityIndicator.transform = CGAffineTransformMakeScale(1.15, 1.15)
activityIndicator.snp_makeConstraints { (make) -> Void in
make.center.equalTo(backgroundView)
}
}
//Shows the loder - The view takes all the screen disabling touches but we only see the backgroundView with its subviews
func show() {
self.hidden = false
//Take all of the MainWindow screen
let application = UIApplication.sharedApplication()
let frontWindow = application.keyWindow
frontWindow?.addSubview(self)
self.snp_makeConstraints { (make) -> Void in
make.edges.equalTo(frontWindow!)
}
activityIndicator.startAnimating()
//Show top activity indicator
application.networkActivityIndicatorVisible = true
//Scale animation - you can add any animation you want
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = NSNumber(float: 0.0)
scaleAnimation.toValue = NSNumber(float: 1.0)
scaleAnimation.duration = 0.33
scaleAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
self.layer.addAnimation(scaleAnimation, forKey: "scaleAnimation")
}
func showWithSubTitle(text: String) {
show()
if label != nil {
label.removeFromSuperview()
}
activityIndicator.snp_remakeConstraints { (make) -> Void in
make.centerX.equalTo(backgroundView)
make.centerY.equalTo(backgroundView).offset(-10.0)
}
label = UILabel()
label.textColor = UIColor.lightGrayColor()
label.textAlignment = NSTextAlignment.Center
label.text = text
label.customFont(.Light, size: 15.0)
backgroundView.addSubview(label)
label.snp_makeConstraints { (make) -> Void in
make.bottom.equalTo(backgroundView).offset(-10.0)
make.right.equalTo(backgroundView).offset(-20.0)
make.left.equalTo(backgroundView).offset(20.0)
}
}
func hide() {
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.fromValue = NSNumber(float: 1.0)
scaleAnimation.toValue = NSNumber(float: 0.0)
scaleAnimation.duration = 0.33
scaleAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
scaleAnimation.delegate = self
scaleAnimation.fillMode = kCAFillModeForwards
scaleAnimation.removedOnCompletion = false
self.layer.addAnimation(scaleAnimation, forKey: "scaleAnimation")
self.hidden = true
//Hide top activity indicator
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
}
}
You just use it by calling:
MGSwiftLoader.sharedInstance.show()
or
MGSwiftLoader.sharedInstance.showWithSubTitle("Your Loader Text")
And hide it with:
MGSwiftLoader.sharedInstance.hide()
Keep in mind that I'm using SnapKit to build the constrains manually (http://snapkit.io)

CALayer Subclass Repeating Animation

I'm attempting to create a CALayer subclass that performs an animation every x seconds. In the example below I'm attempting to change the background from one random color to another but when running this in the playground nothing seems to happen
import UIKit
import XCPlayground
import QuartzCore
let view = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 200, height: 200))
XCPShowView("view", view)
class CustomLayer: CALayer {
var colors = [
UIColor.blueColor().CGColor,
UIColor.greenColor().CGColor,
UIColor.yellowColor().CGColor
]
override init!() {
super.init()
self.backgroundColor = randomColor()
let animation = CABasicAnimation(keyPath: "backgroundColor")
animation.fromValue = backgroundColor
animation.toValue = randomColor()
animation.duration = 3.0
animation.repeatCount = Float.infinity
addAnimation(animation, forKey: "backgroundColor")
}
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private func randomColor() -> CGColor {
let index = Int(arc4random_uniform(UInt32(colors.count)))
return colors[index]
}
}
let layer = CustomLayer()
layer.frame = view.frame
view.layer.addSublayer(layer)
The parameters of a repeating animation are only setup once, so you can't change the color on each repeat. Instead of a repeating animation, you should implement the delegate method,
animationDidStop:finished:, and call the animation again from there with a new random color. I haven't tried this in a playground, but it works ok in an app. Notice that you have to implement init!(layer layer: AnyObject!) in addition to the other init methods you had.
import UIKit
class CustomLayer: CALayer {
var newColor: CGColorRef!
var colors = [
UIColor.blueColor().CGColor,
UIColor.greenColor().CGColor,
UIColor.yellowColor().CGColor
]
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init!(layer: AnyObject!) {
super.init(layer: layer)
}
override init!() {
super.init()
backgroundColor = randomColor()
newColor = randomColor()
self.animateLayerColors()
}
func animateLayerColors() {
let animation = CABasicAnimation(keyPath: "backgroundColor")
animation.fromValue = backgroundColor
animation.toValue = newColor
animation.duration = 3.0
animation.delegate = self
addAnimation(animation, forKey: "backgroundColor")
}
override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
backgroundColor = newColor
newColor = randomColor()
self.animateLayerColors()
}
private func randomColor() -> CGColor {
let index = Int(arc4random_uniform(UInt32(colors.count)))
return colors[index]
}
}

How to create progress bar in sprite kit?

I want to create my own progress bar in Sprite Kit.
I figured I will need to images - one fully empty progress bar and filled progress bar.
I have those images, I put filled one on top of empty one, they are regular SKSPriteNodes now I can't figure out how do I cut my filled image where I need?
How do I cut SKSpriteNode image at certain point? Maybe texture?
I would recommend looking into SKCropNode. For a visual aid how SKCropNode works, look it up in the Apple Programming Guide. I have read through the entire document multiple times and it is a particularly good read.
SKCropNode is basically an SKNode which you add to your scene, but its children can be cropped by a mask. This mask is set in the maskNode property of the SKCropNode. In this way, you only need one texture image. I would subclass SKCropNode to implement functionality to move or resize the mask, so you can easily update its appearance.
#interface CustomProgressBar : SKCropNode
/// Set to a value between 0.0 and 1.0.
- (void) setProgress:(CGFloat) progress;
#end
#implementation CustomProgressBar
- (id)init {
if (self = [super init]) {
self.maskNode = [SKSpriteNode spriteNodeWithColor:[SKColor whiteColor] size:CGSizeMake(300,20)];
SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithImageNamed:#"progressBarImage"];
[self addChild:sprite];
}
return self;
}
- (void) setProgress:(CGFloat) progress {
self.maskNode.xScale = progress;
}
#end
In your scene:
#import "CustomProgressBar.h"
// ...
CustomProgressBar * progressBar = [CustomProgressBar new];
[self addChild:progressBar];
// ...
[progressBar setProgress:0.3];
// ...
[progressBar setProgress:0.7];
Note: this code doesn't move the mask (so the sprite will be cropped on either side) but I'm sure you get the idea.
Quite simply: you need a frame image (optional) and a "bar" image. The bar image out to be a single, solid color and as high as you need it and 1 or 2 pixels wide. A SKShapeNode as bar will do as well.
Just making the bar and animating is simply a matter of changing the SKSpriteNode's size property. For example to make the bar represent progress between 0 and 100 just do:
sprite.size = CGSizeMake(progressValue, sprite.size.height);
Update the size whenever progressValue changes.
You'll notice the image will increase in width to both left and right, to make it stretch only to the right change the anchorPoint to left-align the image:
sprite.anchorPoint = CGPointMake(0.0, 0.5);
That is all. Draw a frame sprite around it to make it look nicer.
that is my ProgressBar in swift :
import Foundation
import SpriteKit
class IMProgressBar : SKNode{
var emptySprite : SKSpriteNode? = nil
var progressBar : SKCropNode
init(emptyImageName: String!,filledImageName : String)
{
progressBar = SKCropNode()
super.init()
let filledImage = SKSpriteNode(imageNamed: filledImageName)
progressBar.addChild(filledImage)
progressBar.maskNode = SKSpriteNode(color: UIColor.whiteColor(),
size: CGSize(width: filledImage.size.width * 2, height: filledImage.size.height * 2))
progressBar.maskNode?.position = CGPoint(x: -filledImage.size.width / 2,y: -filledImage.size.height / 2)
progressBar.zPosition = 0.1
self.addChild(progressBar)
if emptyImageName != nil{
emptySprite = SKSpriteNode.init(imageNamed: emptyImageName)
self.addChild(emptySprite!)
}
}
func setXProgress(xProgress : CGFloat){
var value = xProgress
if xProgress < 0{
value = 0
}
if xProgress > 1 {
value = 1
}
progressBar.maskNode?.xScale = value
}
func setYProgress(yProgress : CGFloat){
var value = yProgress
if yProgress < 0{
value = 0
}
if yProgress > 1 {
value = 1
}
progressBar.maskNode?.yScale = value
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
//How to use :
let progressBar = IMProgressBar(emptyImageName: "emptyImage",filledImageName: "filledImage")
or
let progressBar = IMProgressBar(emptyImageName: nil,filledImageName: "filledImage")
and add this progressBar to any SKNode :
self.addChild(progressBar)
//That's all.
Assuming HealthBarNode is a subclass of SKSpriteNode with a public property health that varies between 0.0 and 1.0 and whose parental property texture is generated from the entire color bar image of width _textureWidth (a private property), you could do something like this:
- (void)setHealth:(CGFloat)fraction
{
self.health = MIN(MAX(0.0, fraction), 1.0); // clamp health between 0.0 and 1.0
SKTexture *textureFrac = [SKTexture textureWithRect:CGRectMake(0, 0, fraction, 1.0) inTexture:self.texture];
// check docs to understand why you can pass in self.texture as the last parameter every time
self.size = CGSizeMake(fraction * _textureWidth, self.size.height);
self.texture = textureFrac;
}
Setting the health to a new value will cause the health bar (added as a child to the main scene, say) to get cropped properly.
I built a small library to deal with this exact scenario! Here is SpriteBar: https://github.com/henryeverett/SpriteBar
There is no "cutting" an image/texture.
An alternative to what Cocos offered is to make a couple of textures and interchange them into your node depending on health. I did a game where the health bar changed texture every 10 points (range was 0-100). After some trial and error though, I just ended up doing what Cocos already suggested.
I did it like this, and it works perfectly.
So first, I declared a SKSpriteNode:
baseBar = [SKSpriteNode spriteNodeWithColor:[UIColor redColor] size:CGSizeMake(CGRectGetMidX(self.frame)-40, self.frame.size.height/10)];
//The following will make the health bar to reduce from right to left
//Change it to (1,0.5) if you want to have it the other way
//But you'd have to play with the positioning as well
[baseBar setAnchorPoint:CGPointMake(0, 0.5)];
CGFloat goodWidth, goodHeight;
goodHeight =self.frame.size.height-(baseBar.frame.size.height*2/3);
goodWidth =self.frame.size.width-(10 +baseBar.frame.size.width);
[baseBar setPosition:CGPointMake(goodWidth, goodHeight)];
[self addChild:baseBar];
I then added a 'Frame' for the bar, with an SKShapeNode, without fill colour (clearcolour), and a stroke colour:
//The following was so useful
SKShapeNode *edges = [SKShapeNode shapeNodeWithRect:baseBar.frame];
edges.fillColor = [UIColor clearColor];
edges.strokeColor = [UIColor blackColor];
[self addChild:edges];
When I wanted to reduce the health, I did the following:
if (playerHealthRatio>0) {
playerHealthRatio -= 1;
CGFloat ratio = playerHealthRatio / OriginalPlayerHealth;
CGFloat newWidth =baseBar.frame.size.width*ratio;
NSLog(#"Ratio: %f newwidth: %f",ratio,newWidth);
[baseBar runAction:[SKAction resizeToWidth:newWidth duration:0.5]];
}else{
// NSLog(#"Game over");
}
Simple, clean and not complicated at all.
Swift 4:
( my answer 1 -> make a rapid and simple progress bar)
To make a simple progress bar based to colors you can subclass a simple SKNode without using SKCropNode:
class SKProgressBar: SKNode {
var baseSprite: SKSpriteNode!
var coverSprite: SKSpriteNode!
override init() {
super.init()
}
convenience init(baseColor: SKColor, coverColor: SKColor, size: CGSize ) {
self.init()
self.baseSprite = SKSpriteNode(color: baseColor, size: size)
self.coverSprite = SKSpriteNode(color: coverColor, size: size)
self.addChild(baseSprite)
self.addChild(coverSprite)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setProgress(_ value:CGFloat) {
print("Set progress bar to: \(value)")
guard 0.0 ... 1.0 ~= value else { return }
let originalSize = self.baseSprite.size
var calculateFraction:CGFloat = 0.0
self.coverSprite.position = self.baseSprite.position
if value == 0.0 {
calculateFraction = originalSize.width
} else if 0.01..<1.0 ~= value {
calculateFraction = originalSize.width - (originalSize.width * value)
}
self.coverSprite.size = CGSize(width: originalSize.width-calculateFraction, height: originalSize.height)
if value>0.0 && value<1.0 {
self.coverSprite.position = CGPoint(x:(self.coverSprite.position.x-calculateFraction)/2,y:self.coverSprite.position.y)
}
}
}
Usage:
self.energyProgressBar = SKProgressBar.init(baseColor: .white, coverColor: .blue, size: CGSize(width:200,height:50))
addChild(self.energyProgressBar)
// other code to see progress changing..
let wait = SKAction.wait(forDuration: 2.0)
let action1 = SKAction.run {
self.energyProgressBar.setProgress(0.7)
}
let action2 = SKAction.run {
self.energyProgressBar.setProgress(0.0)
}
let action3 = SKAction.run {
self.energyProgressBar.setProgress(1.0)
}
let action4 = SKAction.run {
self.energyProgressBar.setProgress(0.5)
}
let action5 = SKAction.run {
self.energyProgressBar.setProgress(0.1)
}
let sequence = SKAction.sequence([wait,action1,wait,action2,wait,action3,wait,action4,wait,action5])
self.run(sequence)
Output:
Swift 4
( my answer 3 -> old SpriteBar project fully translated to swift)
To make a progress bar based to SKTextureAtlas you can use the Objective C project called SpriteBar maded by Henry Everett.
I've forked and fully translated this project, this is the source:
class SpriteBar: SKSpriteNode {
var textureReference = ""
var atlas: SKTextureAtlas!
var availableTextureAddresses = Array<Int>()
var timer = Timer()
var timerInterval = TimeInterval()
var currentTime = TimeInterval()
var timerTarget: AnyObject!
var timerSelector: Selector!
init() {
let defaultAtlas = SKTextureAtlas(named: "sb_default")
let firstTxt = defaultAtlas.textureNames[0].replacingOccurrences(of: "#2x", with: "")
let texture = defaultAtlas.textureNamed(firstTxt)
super.init(texture: texture, color: .clear, size: texture.size())
self.atlas = defaultAtlas
commonInit()
}
convenience init(textureAtlas: SKTextureAtlas?) {
self.init()
self.atlas = textureAtlas
commonInit()
}
func commonInit() {
self.textureReference = "progress"
resetProgress()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func closestAvailableToPercent(_ percent:Int)->Int {
var closest = 0
for thisPerc in self.availableTextureAddresses {
if labs(Int(thisPerc) - percent) < labs(closest - percent) {
closest = Int(thisPerc)
}
}
return closest
}
func percentFromTextureName(_ string:String) -> Int? {
let clippedString = string.replacingOccurrences(of: "#2x", with: "")
let pattern = "(?<=\(textureReference)_)([0-9]+)(?=.png)"
let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive)
let matches = regex?.matches(in: clippedString, options: [], range: NSRange(location: 0, length: clippedString.count))
// If the matches don't equal 1, you have done something wrong.
if matches?.count != 1 {
NSException(name: NSExceptionName(rawValue: String("SpriteBar: Incorrect texture naming.")), reason: "Textures should follow naming convention: \(textureReference)_#.png. Failed texture name: \(string)", userInfo: nil).raise()
}
for match: NSTextCheckingResult? in matches ?? [NSTextCheckingResult?]() {
let matchRange = match?.range(at: 1)
let range = Range(matchRange!, in: clippedString)!
return Int(clippedString[range.lowerBound..<range.upperBound])
}
return nil
}
func resetProgress() {
self.texture = self.atlas.textureNamed("\(self.textureReference)_\(closestAvailableToPercent(0)).png")
self.availableTextureAddresses = []
for name in self.atlas.textureNames {
self.availableTextureAddresses.append(self.percentFromTextureName(name)!)
}
self.invalidateTimer()
self.currentTime = 0
}
func setProgress(_ progress:CGFloat) {
// Set texure
let percent: CGFloat = CGFloat(lrint(Double(progress * 100)))
let name = "\(textureReference)_\(self.closestAvailableToPercent(Int(percent))).png"
self.texture = self.atlas.textureNamed(name)
// If we have reached 100%, invalidate the timer and perform selector on passed in object.
if fabsf(Float(progress)) >= fabsf(1.0) {
if timerTarget != nil && timerTarget.responds(to: timerSelector) {
typealias MyTimerFunc = #convention(c) (AnyObject, Selector) -> Void
let imp: IMP = timerTarget.method(for: timerSelector)
let newImplementation = unsafeBitCast(imp, to: MyTimerFunc.self)
newImplementation(self.timerTarget, self.timerSelector)
}
timer.invalidate()
}
}
func setProgressWithValue(_ progress:CGFloat, ofTotal maxValue:CGFloat) {
self.setProgress(progress/maxValue)
}
func numberOfFrames(inAnimation animationName: String) -> Int {
// Get the number of frames in the animation.
let allAnimationNames = atlas.textureNames
let nameFilter = NSPredicate(format: "SELF CONTAINS[cd] %#", animationName)
return ((allAnimationNames as NSArray).filtered(using: nameFilter)).count
}
func startBarProgress(withTimer seconds: TimeInterval, target: Any?, selector: Selector) {
resetProgress()
timerTarget = target as AnyObject
timerSelector = selector
// Split the progress time between animation frames
timerInterval = seconds / TimeInterval((numberOfFrames(inAnimation: textureReference) - 1))
timer = Timer.scheduledTimer(timeInterval: timerInterval, target: self, selector: #selector(self.timerTick(_:)), userInfo: seconds, repeats: true)
}
#objc func timerTick(_ timer: Timer) {
// Increment timer interval counter
currentTime += timerInterval
// Make sure we don't exceed the total time
if currentTime <= timer.userInfo as! Double {
setProgressWithValue(CGFloat(currentTime), ofTotal: timer.userInfo as! CGFloat)
}
}
func invalidateTimer() {
timer.invalidate()
}
}
Usage:
let progressBarAtlas = SKTextureAtlas.init(named: "sb_default")
self.energyProgressBar = SpriteBar(textureAtlas: progressBarAtlas)
self.addChild(self.energyProgressBar)
self.energyProgressBar.size = CGSize(width:350, height:150)
self.energyProgressBar.position = CGPoint(x:self.frame.width/2, y:self.frame.height/2)
let wait = SKAction.wait(forDuration: 2.0)
let action1 = SKAction.run {
self.energyProgressBar.setProgress(0.7)
}
let action2 = SKAction.run {
self.energyProgressBar.setProgress(0.0)
}
let action3 = SKAction.run {
self.energyProgressBar.setProgress(1.0)
}
let action4 = SKAction.run {
self.energyProgressBar.setProgress(0.5)
}
let action5 = SKAction.run {
self.energyProgressBar.setProgress(0.1)
}
let action6 = SKAction.run {
self.energyProgressBar.startBarProgress(withTimer: 10, target: self, selector: #selector(self.timeOver))
}
let sequence = SKAction.sequence([wait,action1,wait,action2,wait,action3,wait,action4,wait,action5,wait,action6])
self.run(sequence)
To have more details you can find my GitHUB repo here
Swift 4:
( my answer 2 -> make a complex progress bar using textures)
To make a complex progress bar based to texture and colors you can subclass a simple SKNode. About this case, SpriteKit for now (swift v4.1.2) doesn't have a method to directly cutting a SKTexture. We need to use another method called texture(from:crop:)
class SKProgressImageBar: SKNode {
var baseSprite: SKSpriteNode!
var coverSprite: SKSpriteNode!
var originalCoverSprite: SKSpriteNode!
override init() {
super.init()
}
convenience init(baseImageName:String="", coverImageName:String="", baseColor: SKColor, coverColor: SKColor, size: CGSize ) {
self.init()
self.baseSprite = baseImageName.isEmpty ? SKSpriteNode(color: baseColor, size: size) : SKSpriteNode(texture: SKTexture(imageNamed:baseImageName), size: size)
self.coverSprite = coverImageName.isEmpty ? SKSpriteNode(color: coverColor, size: size) : SKSpriteNode(texture: SKTexture(imageNamed:coverImageName), size: size)
self.originalCoverSprite = self.coverSprite.copy() as! SKSpriteNode
self.addChild(baseSprite)
self.addChild(coverSprite)
self.coverSprite.zPosition = 2.0
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setProgress(_ value:CGFloat) {
print("Set progress bar to: \(value)")
guard 0.0 ... 1.0 ~= value else { return }
self.coverSprite.texture = self.originalCoverSprite.texture
let originalSize = self.baseSprite.size
var calculateFraction:CGFloat = 0.0
self.coverSprite.position = self.baseSprite.position
if value == 1.0 {
calculateFraction = originalSize.width
} else if 0.01..<1.0 ~= value {
calculateFraction = originalSize.width * value
}
let coverRect = CGRect(origin: self.baseSprite.frame.origin, size: CGSize(width:calculateFraction,height:self.baseSprite.size.height))
if let parent = self.parent, parent is SKScene, let parentView = (parent as! SKScene).view {
if let texture = parentView.texture(from: self.originalCoverSprite, crop: coverRect) {
let sprite = SKSpriteNode(texture:texture)
self.coverSprite.texture = sprite.texture
self.coverSprite.size = sprite.size
}
if value == 0.0 {
self.coverSprite.texture = SKTexture()
self.coverSprite.size = CGSize.zero
}
if value>0.0 && value<1.0 {
let calculateFractionForPosition = originalSize.width - (originalSize.width * value)
self.coverSprite.position = CGPoint(x:(self.coverSprite.position.x-calculateFractionForPosition)/2,y:self.coverSprite.position.y)
}
}
}
}
Usage:
some texture just to make an example:
baseTxt.jpeg:
coverTxt.png:
Code:
self.energyProgressBar = SKProgressImageBar.init(baseImageName:"baseTxt.jpeg", coverImageName: "coverTxt.png", baseColor: .white, coverColor: .blue, size: CGSize(width:200,height:50))
//self.energyProgressBar = SKProgressImageBar.init(baseColor: .white, coverColor: .blue, size: CGSize(width:200,height:50))
self.addChild(self.energyProgressBar)
self.energyProgressBar.position = CGPoint(x:self.frame.width/2, y:self.frame.height/2)
let wait = SKAction.wait(forDuration: 2.0)
let action1 = SKAction.run {
self.energyProgressBar.setProgress(0.7)
}
let action2 = SKAction.run {
self.energyProgressBar.setProgress(0.0)
}
let action3 = SKAction.run {
self.energyProgressBar.setProgress(1.0)
}
let action4 = SKAction.run {
self.energyProgressBar.setProgress(0.5)
}
let action5 = SKAction.run {
self.energyProgressBar.setProgress(0.1)
}
let sequence = SKAction.sequence([wait,action1,wait,action2,wait,action3,wait,action4,wait,action5])
self.run(sequence)
Output:
with colors:
with textures:
A simple class using two sprite nodes
class PBProgressBar: SKNode {
private var baseNode : SKSpriteNode!
private var progressNode : SKSpriteNode!
private var basePosition: CGPoint!
var progress: CGFloat!
init(progress: CGFloat = 0.45, position: CGPoint = CGPoint.zero) {
super.init()
self.progress = progress
self.basePosition = position
configureProgress()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureProgress() {
baseNode = SKSpriteNode(color: .white, size: CGSize(width: 10, height: 100))
baseNode.anchorPoint = CGPoint.zero
let heightFraction = baseNode.size.height * progress
baseNode.position = basePosition
progressNode = SKSpriteNode(color: .blue, size: CGSize(width: 10, height: heightFraction))
progressNode.anchorPoint = CGPoint.zero
baseNode.addChild(progressNode)
self.addChild(baseNode)
}
}

Resources