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
}
}
}
}
}
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]
}
}
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)
}
}