I'm working on a project in Swift that requires strict control of image display and removal timings in certain sections. I'm finding that when I set the image property of an NSImageView from inside a block that's fired by a Timer, the actual display of the image on the screen is delayed by up to a second after the assignment is complete. (This is measured by eyeballing it and using a stopwatch to gauge the time between when an NSLog line is written and when the image actually appears on-screen.)
Triggering image display with a click appears to happen instantaneously, whether it's done by setting the image property of an existing NSImageView, or constructing one on the spot and adding it as a subview.
I have attempted to reduce the behavior down to a fairly simple test case, which, after basic setup of the view (loading the images into variables and laying out several target image locations, which are NSImageViews stored to the targets array), sets a Timer, with an index into the targets array stored in its userInfo property, to trigger the following method:
#objc func testATimer(fromTimer: Timer) {
if let targetLocation = fromTimer.userInfo as? Int {
NSLog("Placing target \(targetLocation)")
targets[targetLocation].image = targetImage
OperationQueue.main.addOperation {
let nextLocation = targetLocation + 1
if (nextLocation < self.targets.count) {
NSLog("Setting timer for target \(nextLocation)")
let _ = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(ViewController.testATimer(fromTimer:)), userInfo: nextLocation, repeats: false)
}
}
}
}
The amount of time observed between the appearance of the log entry "Placing target x" and that of the image that is set to be displayed in the very next line seems to average between 0.3 and 0.6 seconds. This is far beyond the delay that this project can tolerate, as the image will only be on screen for a total of 1 second at a time.
My best guess as to why this is happening is that it is something to do with Timers running on a separate thread from the main display thread; however, I do not have enough experience with multi-threaded programming to know exactly how to mitigate that.
Any assistance would be most appreciated, and if I've left out information that would make answering easier (or possible) I'm more than happy to give it.
Well, after poking at it with some helpful people on in #macdev on irc.freenode.net, I found that the source of the problem was the program scaling the image down on the fly every time it set it to the NSImageView. Reducing the size of the image ahead of time solved the problem. (As did setting the image once, then hiding and showing the NSImageView instead.)
Related
My app is dynamically creating sublayers ("myLayer1" below). I am using the following code to set it, which works well
view.layer.insertSublayer(myLayer1, at: UInt32(sublayersCount))
Obviously each time it runs, there is a new myLayer1, and the old myLayer1 is lost
I want to store these OLD myLayer1's, and cycle through them with an IBACTION ("back" button)
I tried something like the following, to give me 5 possibilities
myLayer5 = myLayer4
myLayer4 = myLayer3
myLayer3 = myLayer2
myLayer2 = myLayer1
As a test, I dynamically created a few layers, and then ran this via my IBACTION:
view.layer.insertSublayer(myLayer2, at: UInt32(sublayersCount))
However, it did not display the past layer
What is the best way to achieve this?
1- If you do
view.layer.insertSublayer(myLayer2, at: UInt32(sublayersCount))
Then it's equivalent to
view.layer.addSublayer(myLayer2)
2-
The added layer hides the previous because you may need to set
myLayer2.opacity = // 0.0 --- 1.0
3-
Instead of creating vars use
view.layer.sublayers
to access all current sublayers
I have a long CoreData process (GlobalData.shared.resetData function) that takes around 4 seconds and I want to present a loading indicator.
Both action (core data process and showing loading indicator) must run in Main Queue,
Do you know why the showing loading indicator always happened after CoreData process?
#IBAction func resetTapped(_ sender: UIButton) {
tableView.backgroundColor = .green
showLoading(loadingText: nil)
GlobalData.shared.resetData(completion: {
self.refreshGlobalData()
})
}
I added that line to change table background color for testing. Background color always changed after core data process (GlobalData.shared.resetData function).
When you change the background color like that, you're changing the "model", but you won't see the results until the next time the main thread redraws, which won't happen until your code relinquishes control of the run loop (finishes running). You could try dispatch_async-ing the Core Data op, which may allow the run loop to draw before starting your operation. But really, as commenters have mentioned, don't do something that takes four seconds on the main thread.
I'm looking for a solution for my problem:
I have defined a animation as SCNAction with the action editor in Xcode. Now I want to control it with a slider. So basically manipulating the slider shows a specific time/frame in the animation.
Right now, I can just pause/play the animation or reset it to the beginning. I achieved this with Swift code addressing the SCNAction from the action editor.
As far as I researched, there is no possibility
to animate a SCNAction to a specific time immediately and stop right there. That would work.
I'm also interested in other attempts if this is not achievable with SCNActions. It may be that this is not realizable at all. Thank you.
I found a solution: I loaded a CAAnimation from a .dae file(For this look here:https://stackoverflow.com/a/24924702/9625548, bit outdated but still usable).
With the SCNAnimationPlayer I can address the CAAnimation with a key. With the following
code I can control my animation with a UISlider:
let moment: TimeInterval = TimeInterval(self.animationTimeSlider.value)
guard let animationPlayer: SCNAnimationPlayer = node.animationPlayer(forKey: key) else{
return
}
animationPlayer.animation.timeOffset = moment
animationPlayer.play()
animationPlayer.speed = 0.0
Basically, I read the thumb and convert it to TimerInterval. I'm looking for the SCNAnimationPlayer and aborting if no SCNAnimationPlayer is found for the given key. Then I set the .timeOffset to the moment I want to display. With play(), the animation starts from the beginning + .timeOffset. Finally, set the .speed to zero. the animation is not running but shows the moment.
Attention: If you let your animation running and also manipulating it in that way, you have to reset it. Otherwise it is getting messed up. You can catch that case with a boolean that saves if you let your animation run normally. Then you know you have to reset it before manipulating with the slider. For example like this:
node.removeAllAnimations()
node.addAnimation(animation, forKey: key)
self.sceneView.scene.rootNode.animationPlayer(forKey: key)?.paused = true
I have this two functions that measure the elapsed time when the phone is locked or the app is in background:
func saveTimeInBackground(){
startMeasureTime = Int(Date.timeIntervalSinceReferenceDate)
}
func timeOnAppActivated(){
stopMeasureTime = Int(Date.timeIntervalSinceReferenceDate)
elapsedTime = stopMeasureTime - startMeasureTime
seconds = seconds - elapsedTime + 2
if seconds > 0 {
timerLbl.text = "time: \(seconds)"
} else {
seconds = 0
timerLbl.text = "time: \(seconds)"
}
}
and then in the viewDidLoad() i have observers that are trigger the functions when the app becomes active/inactive:
NotificationCenter.default.addObserver(self, selector: #selector(saveTimeInBackground), name: Notification.Name.UIApplicationWillResignActive, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(timeOnAppActivated), name: Notification.Name.UIApplicationDidBecomeActive, object: nil)
The problem is that when the app becomes active there are 2 seconds (approximately) of difference so i've added 2 seconds and it seems to work fine, but only if the elapsed time is > 15 seconds.
If i lock the phone and immediately unlock it the there are like 5 or more seconds that are missing. For example, if there are 50 seconds left, when i lock and immediately unlock it there are like 42 seconds left.
Can anyone please explain, what i am doing wrong?
Edit: The logic of the app is this:
It starts a match between 2 players with 60 seconds for a game. The problem is that when one of the players locks the phone the app stop to measure the time. This way if the player1 has 10 seconds left to make a move, the player2 still has 50 seconds left. I'm looking for a reliable way to calculate the time even if the player locks the phone or put the app in background.
Edit 2: I think i figured out what the problem is: I think the issue has to do with the fact that the “seconds” are Int, and the Date not and when it gets converted it’s rounded up. I didn't tested it, but when i ahve the solution i'll post the answer. Thanks all for your time!
You're relying on exact timing of notifications that aren't guaranteed to have any exact timing. There's no guarantee about when, exactly, either of those notifications will arrive, and there's nothing you can do about that. Even your two-second fix is, as you say, approximate. It'll probably be different on different models of iPhone or even at different times on the same iPhone, depending how busy iOS is when you check.
What's more, when you go into the background, you can't be certain that you'll stay there. Once in the background, iOS might decide to terminate your app at any time.
I'm not sure what the goal is here but I think you'll need to reconsider what you want to do and see if there's some other approach. Your current two-second hack will, at best, spawn a bunch of other hacks (like the 15 second threshold you mention) without ever being especially accurate. And then it'll probably all break in the next iOS update when some iOS change causes the timing to change.
I would use Date object to track game time.
func gameStart() {
gameStartDate = Date()
}
func timeOnAppActivated() {
let secondsLeft = 60 - abs(gameStartDate?.timeIntervalSinceNow ?? 0)
if secondsLeft > 0 {
timerLbl.text = "time: \(secondsLeft)"
} else {
timerLbl.text = "time: 0"
}
}
Ok, like I mention in the edit 2 of the question:
The first issue was because "seconds" is a Int and then it almost always gains or lose when converting it from Double.
But the main problem was that i had to invalidate the timer when the app enter in background and i didn't.
So now with invalidating the timer when the app gets the notification that will enter background and then starting it when it enter foreground everything works fine.
To test this properly call those methods on button click. It may be coz of delay in releasing some resources in background.
I have an idle player animation and I want to do a smooth transition between some animations. The idle animation is the default one and from that transition I want to be able to switch to another state(let's say fight) and then back to idle. The code for the idle character animation is currently like :
self.addChild(playerAnimation)
playerAnimation.runAction(SKAction.repeatActionForever(
SKAction.animateWithTextures(playerAnimationManager.idleManAnimation.textureArray, timePerFrame: 0.1)))
Now, this is scheduled to go on forever for now, but I would need to intercept it and add a new animation on top of that (which is the same character, in a new state). I was thinking that I should stop the idle animation, switch to the new one and then back to idle when finished but I am not convinced that this is the best way of chaining animations and I haven't really found a good resource explaining how to go about it.
Any suggestions ? Thanks !
Depending on how short your texture array is, you might be able to do this.
I will try to explain without code seeing how I use objective C and you use Swift
First make a property or variable that can be called by any subroutine in this class file. It should be Boolean and should be set to NO. You could call it idleFlag.
Next make a method that changes the animation to fight mode. This change would be by removing the idle animation and replacing it with the fight animation. This method also set's idleFlag to NO. Let's call the method "beginFightAnim"
And last, in your repeatActionForEver idle animation, right after your animateWithTextures animation, add a runBlock animation. In this block define a static variable (one that will be remembered in the calling of the block over and over) and increment it by +1, add an "if statement" that looks something like this -> if (my_static_var == number_of_frames_in_texture_animations && idleFlag). And in the "if statement" set the static variable to 0 and call "beginFightAnim"
After this all you have to do to change the animation is set idleFlag to YES.
Hope it works!
If you have any problems, please comment below.
I want to do a series of examples to make it easier to understand the matter. Suppose you have your node called playerAnimation, a typical atlasc with a plist where you have
player-idle1.png
player-idle2.png
player-idle3.png
...
If you want to intercept in real-time your animation to know what frame is running in that moment you can do:
override func update(currentTime: CFTimeInterval) {
if String(playerAnimation.texture).rangeOfString("player-idle") != nil {
print(String(playerAnimation.texture))
}
}
At this point, after you have assigned a "key" to your action (withKey:"idleAnimation") you could stop your specific idle action when you preefer to start the other next animation.
Another good thing is to build a structure to know everytime your player status and modify this variable each time you launch a new action or animation:
enum PlayerStatus: Int {
case Idle = 1
case Fight = 2
case Run = 3
case Jump = 4
case Die = 5
...
}
var currentStatus: PlayerStatus!