Calling function in ViewDidLoad through NSTimer - swift - ios

I'm trying to animate some bezier paths in swift, and I need to get some of them to launch after a set delay. To do this, I have to write both of these sets of code in the viewDidLoad function.
Here is some sample code with the same idea:
override func viewDidLoad() {
super.viewDidLoad
func testFunc() {
println("Hello")
}
var frontOfBaseTimer = NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: Selector("testFunc"), userInfo: nil, repeats: false)
}
This would crash the app 3 seconds after the view loaded. The error message leads me to believe that the only problem in this case is the target property.
What should I change here to get this to work?

If you're bound and determined to use your own delay loop, consider using GCD and the dispatch_after method. That method takes a closure and invokes the closure after a specified delay, which is pretty much exactly what you want. You would pass nil for the queue parameter so your closure would be run on the main queue.
I created a global function delay that lets me invoke dispatch_async painlessly without having to figure out it's confusing parameters:
func delay(delay: Double, block:()->())
{
let nSecDispatchTime = dispatch_time(DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC)));
let queue = dispatch_get_main_queue()
dispatch_after(nSecDispatchTime, queue, block)
}
You call it like this:
delay(2.0)
{
//code to fire after a delay
}

try this:-
override func viewDidLoad() {
super.viewDidLoad()
var frontOfBaseTimer =
NSTimer.scheduledTimerWithTimeInterval(3,
target: self,
selector:"testFunc",
userInfo: nil,
repeats: false)
}
func testFunc(){
println("hello its me")
}

Do like this way, Your app get crash because, timer call the function which has been added in viewdidload method.
override func viewDidLoad() {
super.viewDidLoad()
var frontOfBaseTimer =
NSTimer.scheduledTimerWithTimeInterval(3,
target: self,
selector: Selector("testFunc"),
userInfo: nil,
repeats: false)
}
func testFunc() {
println("Hello")
}

I just tested it and confirmed that you can't use a nested function as the selector for an NSTimer. The method the timer calls needs to be defined at the global scope of the target.
If you're trying to do animation, why not use UIView animation? That family of methods includes a delay parameter.
You could also use Core Animation. You can animate a path very easily by installing the CGPath from a bezierPath into a shape layer, then changing the shape layer's path as part of a CABasicAnimation.
Using Core Animation makes for very smooth, clean animations.
I have a project called RandomBlobs on Github that shows how to animate Bezier paths using CAShapeLayers and CABasicAnimations. It's written in Objective-C, but the techniques are directly applicable in Swift.
You can see a short video of the bezier path animation on Youtube.

Related

Does iOS/UIKit have built in support for scheduling scroll like dinging

I want to implement an action that when I press and hold begins to repeatedly do an action (similar to a scroll button on a Desktop UI). Is there first class support for this in the UIGestureRecognizer/events framework, or do I just roll my own?
E.g.
var timer:Timer?
func killDing() {
self.timer?.invalidate()
self.timer = nil
}
func startDing() {
self.killTimer()
self.timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) {
self.ding() // this is where the repeated code happens
}
}
override func beginTracking(_ touch:UITouch, with event:UIEvent?) -> Bool {
self.startDing()
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
super.endTracking(touch, with: event)
self.killDing()
}
I can of course do this with a LongPressGestureRecognizer as well. My question is whether I need to roll my own ding loop as shown above, or if there's something more first class in UIKit that I'm currently not aware of and should be taking advantage of.
I think you are on the right way. You can use use timers to repeat some actions, but you should add the created timer into a run loop with a common mode, without this mode, the run loop will not call the timer's action while a user is touching the screen
let timer = Timer(timerInterval: interval, repeats: true, block: block)
RunLoop.current.add(timer, forMode: . common)
Also you can use CADisplayLink, to call your action. You can find example of using CADisplayLink in my library, witch can help to you implement animation based on CADisplayLink:
let link = CADisplayLink(target: self, selector: #selector(updateAction(sender:)));
link.add(to: RunLoop.main, forMode: .commonModes);

Weird Swift's #selector behavior. Value of parameter totally different through #selector

I'm new to Swift's #selector paradigm. Ultimately I'm trying to move SKSpriteNode of a spaceShip over an CGMutablePath and add exhaust fumes as SKEmitterNodes on the path that the spaceShip has travelled.
However I'm stuck on #selector paradigm. I'm trying to send position of a the SpaceShipSprite via a timer to a addFumeToPosition(_ point: CGPoint) function so later I can add the SKEmitternode to that position.
For some reason that I cannot comprehend the #selector thing sends out something totally different than what I'm putting into the variable. Please see prints below. What's going on here?
let spaceShipSprite = SKSpriteNode(texture: SKTexture(image: #imageLiteral(resourceName: "spaceShip")))
spaceShipSprite.position = CGPoint(x: 142.0, y:160.0)
spaceShipSprite.name = "spaceShip"
self.addChild(spaceShipSprite)
print("spaceShipSprite.position before timer point x:\(spaceShipSprite.position.x) y:\(spaceShipSprite.position.y)")
let gameTimer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector:#selector(addFumeToPosition(_: )), userInfo: spaceShipSprite.position, repeats: true)
print("spaceShipSprite.position before timer point x:\(spaceShipSprite.position.x) y:\(spaceShipSprite.position.y)")
...
func addFumeToPosition(_ point: CGPoint){
print("spaceShipSprite.position during point x:\(point.x) y:\(point.y)")
}
spaceShipSprite.position before timer point x:142.0 y:160.0
spaceShipSprite.position after timer point x:142.0 y:160.0
spaceShipSprite.position during timer point x:196092657881833.0 y:1000000000.0
You cannot do that.
The two supported forms of target/action are
Without parameter
func addFumeToPosition()
With one parameter representing the affected class (in this case the Timer instance)
func addFumeToPosition(_ timer: Timer)
If a parameter is passed it must be the Timer instance.
All custom parameters have to be passed and handled via the userInfo parameter.
func addFumeToPosition(_ timer: Timer){
let point = timer.userInfo as! CGPoint
print("spaceShipSprite.position during point x:\(point.x) y:\(point.y)")
}
Note: In Swift 4 you might have to add the #objc attribute to the function.

Definitively, do you have to invalidate() a CADisplayLink when the controller disappears?

Say you have an everyday CADisplayLink
class Test: UIViewController {
private var _ca : CADisplayLink?
#IBAction func frames() {
_ca?.invalidate()
_ca = nil
_ca = CADisplayLink(
target: self,
selector: #selector(_step))
_ca?.add(to: .main, forMode: .commonModes)
}
#objc func _step() {
let s = Date().timeIntervalSince1970
someAnime.seconds = CGFloat(s)
}
Eventually the view controller is dismissed.
Does anyone really definitively know,
do you have to explicitly call .invalidate() (and indeed nil _ca) when the view controller is dismissed?
(So perhaps in deinit, or viewWillDisappear, or whatever you prefer.)
The documentation is worthless, and I'm not smart enough to be able to look in to the source. I've never found anyone who truly, definitively, knows the answer to this question.
Do you have to explicitly invalidate, will it be retained and keep running if the VC goes away?
A run loop keeps strong references to any display links that are added to it. See add(to:forMode:) documentation:
The run loop retains the display link. To remove the display link from all run loops, send an invalidate() message to the display link.
And a display link keeps strong reference to its target. See invalidate() documentation:
Removing the display link from all run loop modes causes it to be released by the run loop. The display link also releases the target.
So, you definitely have to invalidate(). And if you're using self as the target of the display link, you cannot do this in deinit (because the CADisplayLink keeps a strong reference to its target).
A common pattern if doing this within a view controller is to set up the display link in viewDidAppear and remove it in viewDidDisappear.
For example:
private weak var displayLink: CADisplayLink?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
startDisplayLink()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
stopDisplayLink()
}
private func startDisplayLink() {
stopDisplayLink() // stop previous display link if one happens to be running
let link = CADisplayLink(target: self, selector: #selector(handle(displayLink:)))
link.add(to: .main, forMode: .commonModes)
displayLink = link
}
private func stopDisplayLink() {
displayLink?.invalidate()
}
#objc func handle(displayLink: CADisplayLink) {
// do something
}
Method definition of invalidate():
Removing the display link from all run loop modes causes it to be
released by the run loop. The display link also releases the target.
For me this means that displaylink holds the target and run loop holds DispayLink.
Also, according to this link I found, it looks like its rather important to call invalidate() for cleanup of CADisplayLink.
We can actually validate using XCode's wonderful Memory graph debugger:
I have created a test project where a DetailViewController is being pushed on a navigation stack.
class DetailViewController: UIViewController {
private var displayLink : CADisplayLink?
override func viewDidAppear() {
super.viewDidAppear()
startDisplayLink()
}
func startDisplayLink() {
startTime = CACurrentMediaTime()
displayLink = CADisplayLink(target: self,
selector: #selector(displayLinkDidFire))
displayLink?.add(to: .main, forMode: .commonModes)
}
}
This initiates CADispalyLink when view gets appeared.
If we check the memory graph, we can see that DetailViewController is still in the memory and CADisplayLink holds its reference. Also, the DetailViewController holds the reference to CADisplayLink.
If we now call invalidate() on viewDidDisappear() and check the memory graph again, we can see that the DetailViewController has been deallocated successfully.
This to me suggests that invalidate is a very important method in CADisplayLink and should be called to dealloc CADisplayLink to prevent retain cycles and memory leaks.

How to properly manage groups of sprites onscreen

I'm building my first game in Swift and I wanted to know how to go about handling multiple on screen sprites at once. My game pushes sprites on to screen with addChild continuously, so there are many active at once. I realized that I didn't have a proper way of simultaneously affecting all of them- like if I wanted to affect a physics property of all enemy sprites at once. So far I created an empty array var enemySprites = [enemyType1]() at the begining of GameScene and have been appending the sprite instances to it instead of using addChild to draw them directly to the scene. However, I'm not able to simply loop through and draw them to screen with:
for enemy in enemySprites{
addChild(enemy)
}
this bit of code is in the override func update(currentTime: CFTimeInterval) function, so maybe I'm just misplacing it? Any help on how to go about this would be great!
Sam,
Here's some sample code to update enemies when your lives reach 0:
First, we set a property observer on the lives property so we can call a function when you lose all lives:
var lives = 3 {
didSet {
if lives == 0 {
updateEnemies()
}
}
And then a function to enumerate over all the enemies and change each one's velocity to (0, 0):
func update enemies() {
enumerateChildNodesWithName("type1") {
node, stop in
let enemy = node as! SKSpriteNode
enemy.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
}
}
Instead of use update method, you could use a timer. From sources:
public class func scheduledTimerWithTimeInterval(ti: NSTimeInterval, target aTarget: AnyObject, selector aSelector: Selector, userInfo: AnyObject?, repeats yesOrNo: Bool) -> NSTimer
So if you follow Apple guide, it will be for example:
NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: Selector("spawnAlien:"), userInfo: myParameter, repeats: true)
func spawnAlien(timer : NSTimer) {
if let myUserInfo = timer.userInfo {
print(myUserInfo) // a parameters passed to help you to the alien creation
}
timer.invalidate()
}
BUT according to Whirlwind I agree with him and with LearnCocos2d work, sprite-kit don't work well with timers (as explained in the link by LearnCocos2d) and the better way, especially as you say you develop your first game, it's to use SKAction, a combination of actions to achieve the similar behavior obtained by NSTimer.
I've think about a function or an extension, let me know if it's work as expected:
extension SKAction {
class func scheduledTimerWithTimeInterval(time:NSTimeInterval, selector: Selector, repeats:Bool)->SKAction {
let call = SKAction.customActionWithDuration(0.0) { node, _ in
node.performSelector(selector)
}
let wait = SKAction.waitForDuration(time)
let seq = SKAction.sequence([wait,call])
let callSelector = repeats ? SKAction.repeatActionForever(seq) : seq
return callSelector
}
}
Usage:
let spawn = SKAction.scheduledTimerWithTimeInterval(time, selector: #selector(GenericArea.spawnAlien), repeats: true)
self.runAction(spawn,withKey: "spawnAlien")

Passing parameters to a method called by NSTimer in Swift

I'm trying to pass an argument to a method that is called by NSTimer in my code. It is throwing an exception. This is how I'm doing it. Circle is my custom class.
var circle = Circle()
var timer = NSTimer.scheduledTimerWithInterval(1.0, target: self, selector: animate, userInfo: circle, repeats: true)
Below is the method that is being called
func animate(circle: Circle) -> Void{
//do stuff with circle
}
Note: The method is in the same class that it is being called. So I believe i've set the target correctly.
The selector you use with NSTimer is passed the NSTimer object as it's one and only parameter. Put the circle object in it as userInfo and you can extract it when the timer fires.
var circle = Circle()
var timer = NSTimer.scheduledTimerWithInterval(1.0, target: self, selector: "animate:", userInfo: circle, repeats: true)
func animate(timer:NSTimer){
var circle = timer.userInfo as Circle
//do stuff with circle
}
Your selector needs to be a string unless that's supposed to be an ivar. Also, your animate function has the wrong signature. The following changes should get you moving again:
var circle = Circle()
var timer = NSTimer.scheduledTimerWithInterval(1.0, target: self, selector: "animate", userInfo: circle, repeats: true)
func animate(circle: Circle) -> () {
//do stuff with circle
}
The function really doesn't need to return the empty tuple; it can be written without the -> ()
I've also seen the selector string wrapped in a "Selector()" method: Selector("animate"). It works either way.
I've been messing with NSTimer and closures myself and wrote an article on it: Using Swift's Closures With NSTimer

Resources