My problem is very complicated to explain so I will do my best for explain it.
I'm doing a swift application with TableView. In this TableView, I have some data which are store in local (Dictionnary, Arrays, var, ...).
So, in my TableView I'm refreshing this datas every 0.01 second. Then, when I scroll my TableView this refresh is stopped and I don't want it. I Want a "continue refresh".
Someone can explain how I can do it? I search on StackOverflow and the most answer is : the Thread.
I understood Thread in C but it's very vague for me in Swift.
If you have an exercise for train multithreading in Swift you can post it !
Thanks for your time.
P.S: I can post some code but I don't think it's really necessary for my question.
EDIT:
There is the code for the timer and update
override func viewDidLoad() {
timer = Timer.scheduledTimer(timeInterval: 0.01, target: self,
selector:#selector(ViewController.updateTimer), userInfo: nil,
repeats: true)
}
func updateTimer () {
var j = 0
for _ in rows {
if (rows[j]["Activ"] as! Bool == true ) {
rows[j]["timer"] = (rows[j]["timer"] as! Double + 0.01) as AnyObject
}
j += 1
}
myTableView.reloadData()
}
So, it happens because the Timer works in this same DispatchQueue as scrolling in the UITableView. You can solve this problem by using, for example, AsyncTimer.
Related
I built an extension to the UIButton class to do fadeOut. When I use this I get memory leak warning in the profiler. I am using Swift 4 and Xcode 9.3.
Thanks in advance for any help.
extension UIButton {
func fadeOut() {
let fadeOut = CABasicAnimation(keyPath: "opacity")
fadeOut.duration = 0.35
fadeOut.fromValue = 1
fadeOut.toValue = 0.0
fadeOut.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
fadeOut.autoreverses = false
fadeOut.repeatCount = 0
fadeOut.isRemovedOnCompletion = true
self.layer.add(fadeOut, forKey: nil)
}
}
The calling function is given below. Also please note: new, level and card are UIButtons. When I comment out button.fadeout() in the function below the memory leak goes away as per the Xcode profiler. Hope this gives more context. If any other information is required to help analyze, I happy to provide the info.
private func menu_fadeout(){
func menu_fadeout_helper(_ button:UIButton){
button.fadeOut()
button.isHidden = true
button.isEnabled = false
}
menu_fadeout_helper(hint)
menu_fadeout_helper(new)
menu_fadeout_helper(level)
menu_fadeout_helper(card)
}
After staring at the code for a couple minutes, I see the issue. In your function. . .
private func menu_fadeout(){
func menu_fadeout_helper(_ button:UIButton){
button.fadeOut()
button.isHidden = true
button.isEnabled = false
}
menu_fadeout_helper(hint)
menu_fadeout_helper(new)
menu_fadeout_helper(level)
menu_fadeout_helper(card)
}
. . .you never directly reference the UIButtons hint, new, level, and card. Eventually, after pressing the buttons a ton of times, the memory will fill up with nothing and your app will crash. (or worse)
Change the function to this to (supposedly) remove the memory leak.
private func menu_fadeout(){
func menu_fadeout_helper(_ button: UIButton) -> UIButton {
button.fadeOut()
button.isHidden = true
button.isEnabled = false
return button
}
menu_fadeout_helper(self.hint)
menu_fadeout_helper(self.new)
menu_fadeout_helper(self.level)
menu_fadeout_helper(self.card)
}
After lots of poking around it turns out the animation layers cause leaks for various reasons - most have guesses but no precise answers.
To solve my problem I reimplemented the fadeOut function without using the CABasicAnimation and using UIView.animate and made NO other change to the code. The profiler has no issues now - all is good. Thanks!
fyi there seems to inadvertent leaks whenever using stings in the context of buttons etc. If anyone has any pointers or suggestions on that topic would appreciate it.
look for reference cycle (use weak/unowned self in capture lists)
remove all animations from layers
invalidate timer if there is one
I have an NSTimer that calls a function. The function flashes a different button each time it is called. i have the time interval set to one second and repeats is true, it stops repeating when the variable pcChoice == 10, but for some reason when i run the program it is delaying for a second and then calling the function a number of times right after each other. To the user, it looks like one second passes, then a number of buttons are flashed at once. What I need instead is for each button to flash one after another, with one second in between each flash.
override func viewDidLoad() {
var lit = [b0o,b1o,b2o,b3o,b4o,b5o,b6o,b7o,b8o]
var litIndex = 0
super.viewDidLoad()
for i in 1...10{
print(randomIndex)
print(computerChoices)
var buttonChoice = lit[randomIndex]
randomIndex = Int(arc4random_uniform(UInt32(lit.count)))
computerChoices[i] = randomIndex
print("yoyoyo")
self.view.setNeedsDisplay()
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("flashingButtons"), userInfo: nil, repeats: true)
}
}
func flashingButtons(){
var lit = [b0o,b1o,b2o,b3o,b4o,b5o,b6o,b7o,b8o]
one = computerChoices[pcChoice]
lit[one].setImage(UIImage(named: "redb.png"), forState: UIControlState.Normal)
pcChoice += 1
if pcChoice == 10{
timer.invalidate()
}
}
This line
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("flashingButtons"), userInfo: nil, repeats: true)
is executed 10 times because of your for loop. That gives you 10 different timers, each of which will invoke flashingButtons in about 1 second from now. Move that call out of the loop. You also don't need 10 calls to setNeedsDisplay in viewDidLoad.
Since you don't modify lit, make it a let instead of a var. I notice also that you have 9 elements in lit but you're looping through 10 times. The lit array in viewDidLoad is never used, and is different from the one in flashingButtons.
You could invoke arc4random_uniform inside flashingButtons each time the time fires. Or if you want the lights truly shuffled, each one being hit once, try something like this using GameplayKit:
let lit = [b0o,b1o,b2o,b3o,b4o,b5o,b6o,b7o,b8o]
let shuffledLit : [<lightobjects>] // whatever type of b0o, etc is
override func viewDidLoad() {
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: Selector("flashingButtons"), userInfo: nil, repeats: true)
shuffledLit = GKRandomSource.sharedRandom().arrayByShufflingObjectsInArray(lit)
super.viewDidLoad()
self.view.setNeedsDisplay()
}
I'm trying to figure how to call an action when an AVAudioPlayer hits specific second by using NSTimer.
Code:
var audioFile = try! AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: NSBundle.mainBundle().pathForResource("FileName", ofType: "mp3")!))
override func viewDidLoad() {
super.viewDidLoad()
NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: Selector("checkPlaybackTime:"), userInfo: nil, repeats: true)
}
func checkPlaybackTime(timer:NSTimer){
let seconds : NSTimeInterval = audioFile.currentTime
if (seconds == 20.0){
btnPausePlay.setTitle("Play", forState: UIControlState.Normal)
}
}
The action that I wanted to call is that the btnPausePlay button text sets to "Play" which it never does as you see as I've called it in the if statement.
Edit: It now works. I changed from if (seconds == 20.0 to if (seconds >= 20.0)
Although this answer does not make explicit use of NSTimer, it does leverage the method addPeriodicTimeObserverForInterval of the AVPlayer class to achieve the same purpose:
After initialising your player, do this:
let observerInterval = CMTimeMakeWithSeconds(0.5, 100) //this sets the interval at every half second
timeObserverToken = audioPlayer.addPeriodicTimeObserverForInterval(observerInterval, queue: nil, usingBlock: self.getPlaybackTimer)
Function getPlaybackTimer looks like this:
func getPlaybackTimer(time: CMTime){
let currentTime = audioPlayer.currentTime()
/* *** */
}
As nhgrif points out, your logic is flawed. You are comparing floating point values in a way that you should not. You need to take little variations into account, almost always when comparing floating point numbers.
In this case, however, this wouldn't suffice alone, as your comparison might take place every so little outside your checking interval. You are better of by using inequality here, i.e. >= 20.0. You might want to disable the timer then.
I have an NSTimer() and an AI logic for a board game. The AI logic takes long time to process about 3 to 5 seconds(which is ok). When the program is executing the AI logic, the NSTimer doesn't fire until the AI logic finished it's execution.
This is how i started the timer during the initial stage of the game.
public var timer = NSTimer()
...
let timeSelector:Selector = "timerFired"
if self.useTime {
timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: timeSelector, userInfo: nil, repeats: true)
}
I added the AI logic in Update of an SKScene so that when it detects it is his turn, he will start checking for available moves
override func update(currentTime: CFTimeInterval) {
if gameStatus.playerTurn == "AI" && !aiThiking {
aiThiking = true
aIPlayer.aiCheckAvaialbleMoves()
// executeMove
// change Turn
aiThiking = false
}
My question is, is it possible to let the AI logic execute and let the timer still running at the same time?
Your problem seems to be that aIPlayer.aiCheckAvailableMoves() freezes the app as long as it takes to execute, so NSTimer does not work as it should. You can use different threads to avoid this:
dispatch_async(dispatch_get_global_queue(priority, 0)) {
aIPlayer.aiCheckAvaialbleMoves()
}
This code runs this function in a new thread and when the code ended, the thread is closed automatically.
You could use completion blocks to know exactly when the function ended. Here is an example:
func aiCheckAvailableMoves(completion : (result : BOOL) -> Void) {
dispatch_async(dispatch_get_global_queue(priority, 0)) {
//Do your code normally
//When you have finished, call the completion block (like a return)
completion(true)
}
}
And then, from your update: you will get when the function ended (please note here there is no dispatch_async):
aIPlayer.aiCheckAvailableMoves(){
//Finished checking moves, maybe schedule again?
}
Be careful with threads, as they can cause unwanted behaviour or/and crashes!
I have an app using an NSTimer at centisecond (0.01 second) update intervals to display a running stopwatch in String Format as 00:00.00 (mm:ss.SS). (Basically cloning the iOS built-in stopwatch to integrate into realtime sports timing math problems, possibly needing millisecond accuracy in the future)
I use (misuse?) the NSTimer to force-update the UILabel. If the user presses Start, this is the NSTimer code used to start repeating the function:
displayOnlyTimer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: Selector("display"), userInfo: nil, repeats: true)
And here is the function that is executed by the above NSTimer:
func display() {
let currentTime = CACurrentMediaTime() - timerStarted + elapsedTime
if currentTime < 60 {
timeDisplay.text = String(format: "%.2f", currentTime)
}else if currentTime < 3600 {
var minutes = String(format: "%00d", Int(currentTime/60))
var seconds = String(format: "%05.2f", currentTime % 60)
timeDisplay.text = minutes + ":" + seconds
}else {
var hours = String(format: "%00d", Int(currentTime/3600))
var minutes = String(format: "%02d", (Int(currentTime/60)-(Int(currentTime/3600)*60)))
var seconds = String(format: "%05.2f", currentTime % 60)
timeDisplay.text = hours + ":" + minutes + ":" + seconds
}
}
There will be at least 2 display links running at the same time. Will this method be too inefficient once all other elements are in play?
The display is then updated without using NSTimer when the user presses stop/pause/reset. I didn't find anything that directly translated into Swift. I'm fairly certain I'm using an inefficient method to force update the text UILabel quickly in the UIView.
More Details:
I'm working on less messy code for the running timer format (mm:ss.SS). I will update this once more when I've finished that.
UPDATE: Thanks to Rob and jtbandes for answering both of my questions (formatting method and display update method).
It was easy to replace the NSTimer (see above) with CADisplayLink():
displayLink = CADisplayLink(target: self, selector: Selector("display"))
displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
And then replace all instances in code of
displayOnlyTimer.invalidate()
with
displayLink.paused = true
(this will pause the display link from updating)
For rapid UI updates you should use a CADisplayLink. Anything faster than the display refresh rate is a waste of processing power since it physically cannot be displayed. It also provides a timestamp of the previous frame so you can try to predict when the next frame will be.
You're calculating CACurrentMediaTime() - timerStarted + elapsedTime multiple times. I would recommend doing it only once and saving it in a local variable.
Consider using NSDateComponentsFormatter. Try to reuse one instance of the formatter rather than creating a new one each time (which is usually the most expensive part). Overall, the less string manipulation you can do, the better.
You can check CACurrentMediaTime at the beginning and end of your display method to see how long it takes. Ideally it should be much less than 16.6ms. Keep an eye on the CPU usage (and general power consumption) in the Xcode debug navigator.
I was solving the same problem today and found this answer. The Rob's and jtbandes' advices are helped a lot and i was able to assemble the clean and working solution from around the internet. Thanks you guys. And thanks to mothy for the question.
I've decided to use CADisplayLink because there is no point in firing timer's callback more often than the screen updates:
class Stopwatch: NSObject {
private var displayLink: CADisplayLink!
//...
override init() {
super.init()
self.displayLink = CADisplayLink(target: self, selector: "tick:")
displayLink.paused = true
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
//...
}
//...
}
I'm tracking time by incrementing the elapsedTime variable by displayLink.duration each tick:
var elapsedTime: CFTimeInterval!
override init() {
//...
self.elapsedTime = 0.0
//...
}
func tick(sender: CADisplayLink) {
elapsedTime = elapsedTime + displayLink.duration
//...
}
Time-formatting is done through NSDateFormatter:
private let formatter = NSDateFormatter()
override init() {
//...
formatter.dateFormat = "mm:ss,SS"
}
func elapsedTimeAsString() -> String {
return formatter.stringFromDate(NSDate(timeIntervalSinceReferenceDate: elapsedTime))
}
The UI can be updated in the callback closure which Stopwatch calls on every tick:
var callback: (() -> Void)?
func tick(sender: CADisplayLink) {
elapsedTime = elapsedTime + displayLink.duration
// Calling the callback function if available
callback?()
}
And that's all you need to do in the ViewController to utilize the Stopwatch:
let stopwatch = Stopwatch()
stopwatch.callback = self.tick
func tick() {
elapsedTimeLabel.text = stopwatch.elapsedTimeAsString()
}
Here is the gist with the full code of Stopwatch and usage guide:
https://gist.github.com/Flar49/06b8c9894458a3ff1b14
I hope that this explanation and gist will help others who will stumble upon this thread in the future with the same problem :)