iOS Timer function heap corruption - ios

So I am new to iOS and am using trying to have a view controller with a timer that periodically updates the UI. The issue that I am seeing is that I am getting heap corruption, more specifically EXC_BAD_ACCESS KERN_INVALID_ADDRESS error that is caused by objc_retain call.
This error is happening in several places but all within my Timer function and higher on the call stack __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
is being called in each case.
I must be missing a reference or not releasing something properly, here is the code
func scheduleTimer() {
timer = Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(self.timerFunc), userInfo: nil, repeats: true)
}
func timerFunc() {
if let gps = sdlService?.getLatestLocation() {
let clCoor = CLLocationCoordinate2D(locStruct: gps)
self.updateLatestDriverIcon(gps: gps, coor: clCoor)
if isRecording {
self.addNextPathPoint(coor: clCoor)
}
latestCoor = clCoor
}
}
func updateLatestDriverIcon(gps: LocationStruct, coor: CLLocationCoordinate2D) {
if latestCoor == nil {
car = MarkerAnnotation(coordinate: coor, title: carMarker)
mapView.addAnnotation(car!)
latestCoor = coor
mapView.centerOnLatestGPS(animated: false)
markerView.rotation = MathUtils.wrap(gps.bearing, min: 0, max: 360)
} else if coor.isDifferent(to: latestCoor!) {
if isMapFollowingCar {
mapView.centerOnLatestGPS(animated: false)
}
car!.coordinate = coor
markerView.rotation = MathUtils.wrap(gps.bearing, min: 0, max: 360)
}
}
Now this timer function is referencing properties of my view controller, as well as a nested function (updateLatestDriverIcon). I have seen crashes on the mapView.centerOnLatestGPS() func, and multiple places within the markerView.rotation call stack all with the same error codes listed above.
What am I missing here?
EDIT:
Here is a stack trace from crashlytics. I am using events triggered over an external accessory so I can be attached to the debugger:
Stack Trace

So after several weeks of tracking this thing down, we found it was due to an animation on a UIView. Not exactly sure why it was throwing errors where it did, if anyone knows why that would be very helpful! Here is some more info on the architecture:
We had a screen updating a UI at about 10HZ and was driven by a timer using the above code. The animation was done on a UIView subclass that was done off of the main thread which was being rendered into a bitmap context. This was being done at ~30Hz.
The animation code:
UIView.animate(
withDuration: self.animationDuration,
animations: { self.currentGearValue = actualGearValue },
completion: { (isComplete) in /* not sure we need this yet */ })
I haven't tested it but it might be because the animation is overlapped if the previous one isn't finished by the time the next animation gets started.

Related

Calling swift animation from completion event?

I am trying to call an animation event on a FBSDKLoginButton in swift. I can tell the animation is being call, because the elements that are changing alpha values function correctly. For some reason though, the button will not move in the same call.
If I touch another button, calling the exact same function, the button then moves. Not sure why calling a function from a completion event vs. another location would affect whether or not the object actually moves?!
Under the BTN_Animate, that where the call works. After the
LocalProfile.sharedInstance.updateFromFacebook(updateFromFacebook_Complete:
call you can see the same function, and the argument being passed is assumed true.
#IBAction func BTN_Animate(_ sender: Any) {
animateScreen(loggedIn: true) //Call that works
}
public func loginButton(_ BTN_facebookLogin: FBSDKLoginButton!, didCompleteWith result: FBSDKLoginManagerLoginResult!, error: Error!) {
print("> Attempting Login from Main View Controller")
if ((error) != nil)
{
print("Error: ")
// Process error
print(error)
}
else if result.isCancelled {
// Handle cancellations
print("Request cancelled")
print(result)
}
else {
// If you ask for multiple permissions at once, you
// should check if specific permissions missing
LocalProfile.sharedInstance.updateFromFacebook(updateFromFacebook_Complete: {
self.LBL_Name.text = LocalProfile.sharedInstance.firstName
self.IMG_ProfilePicture.image = LocalProfile.sharedInstance.profilePicture
LocalProfile.sharedInstance.printAllData()
self.animateScreen(loggedIn: LocalProfile.sharedInstance.loggedIn) //Call that doesn't work
})
}
}
public func animateScreen(loggedIn: Bool) {
if(loggedIn) {
//Animating screen objects
print("> Animating screen objects")
UIView.animate(withDuration: 0.7, delay: 1.0, options: UIViewAnimationOptions.curveEaseOut, animations: {
self.BTN_facebookLogin.frame = CGRect(x: self.BTN_facebookLogin.frame.origin.x, y: (self.view.bounds.height - self.BTN_facebookLogin.frame.size.height) - 50, width: self.BTN_facebookLogin.frame.size.width, height: self.BTN_facebookLogin.frame.size.height)
self.IMG_ProfilePicture.alpha = 1
self.LBL_Name.alpha = 1
self.view.layoutIfNeeded()
}, completion: { finished in
print("> Done animating screen objects")
})
}
else {
//Not animating
print("> Not animating screen objects")
}
}
Any help here would be appreciated! Thanks
EDIT: Below code returns that it IS the main thread...
LocalProfile.sharedInstance.updateFromFacebook(updateFromFacebook_Complete: {
self.LBL_Name.text = LocalProfile.sharedInstance.firstName
self.IMG_ProfilePicture.image = LocalProfile.sharedInstance.profilePicture
LocalProfile.sharedInstance.printAllData()
self.animateScreen(loggedIn: LocalProfile.sharedInstance.loggedIn)
let notString = Thread.isMainThread ? "" : "not "
print("This is " + notString + "the main thread")
})
Work around:
Try to update your buttons frame in the main queue. In swift 3.0 you can do as bellow:
DispatchQueue.main.async
{
self.BTN_facebookLogin.frame = CGRect(x: self.BTN_facebookLogin.frame.origin.x, y: (self.view.bounds.height - self.BTN_facebookLogin.frame.size.height) - 50, width: self.BTN_facebookLogin.frame.size.width, height: self.BTN_facebookLogin.frame.size.height)
self.IMG_ProfilePicture.alpha = 1
self.LBL_Name.alpha = 1
self.view.layoutIfNeeded()
}
As others have said, you must make UI calls from the main thread. I'm not familiar with Facebook's APIs, so I don't know if the completion block you're using is called on the main thread or not. You can check with code like this:
let notString = Thread.isMainThread ? "" : "not "
print("This is " + notString + "the main thread")
(That's Swift 3 code)
Put that at the top of your completion block and see what it displays to the console.
My bet is that the completion block is NOT being run on the main thread.
The most common effect of doing UI calls from a background thread is that nothing happens, or it takes a VERY long time to take effect. However, it can also cause other strange effects and even crashes, so it's important not to do it.
You might also be having problems with auto-layout interfering with your frame settings, but based on the symptoms you describe (not working from the completion block but working if you call it directly) it sounds more like a threading problem.
Any time you deal with a completion block you should figure out if the completion block might be called from a background thread. Apple's URLSession class is an example of a class that calls it's completion blocks on background threads. The docs should tell you if a completion block is invoked on a background thread, but the test code I posted above is a good way to be sure.
In contrast, the UIView animateWithDuarion(animations:completion:) family of methods call their completion blocks on the main thread.

Completion block in (animateWithDuration:animations:completion:) is unpredictably delayed

The code is too huge to post it here. My problem is the following. When I call animateWithDuration:animations:completion: (maybe with options) with duration == 0.3 it doesn't mean that the completion block will be called through the same delay. It is called through 2 seconds instead and it is too long for me.
This big delay usually appears before memory warnings but sometimes may work as expected.
Could anybody explain what may cause such a strange behaviour?
Are there any timers involved, like is the animation timer-triggered?
I had a similar problem when my animation was timer-triggered. It turned out the animation was started more than once. animationOngoing flag stopped animation from being started again before finishing.
// Timer function
func timerTextToggle(timer: NSTimer) {
if self.animationOngoing == false {
self.flipAnimation()
}
}
// Animation function
func flipAnimation() {
// important note: it's UIViewAnimationOptions,
// not UIViewAnimationTransition
self.animationOngoing = true
if self.animationToggle == false {
UIView.transitionFromView(self.singleTapLabel!,
toView: self.doubleTapLabel!,
duration: animDuration,
options: UIViewAnimationOptions.TransitionFlipFromBottom,
completion: {
(value: Bool) in
self.animationOngoing = false
})
} else {
UIView.transitionFromView(self.doubleTapLabel!,
toView: self.singleTapLabel!,
duration: animDuration,
options: UIViewAnimationOptions.TransitionFlipFromTop,
completion: {
(value: Bool) in
self.animationOngoing = false
})
}
self.animationToggle = !self.animationToggle
}
I experienced a similar problem to this, although without further information on your scenario, I don't know if this also applies to your issue.
I was calling becomeFirstResponder on a UITextField in the completion block of my animateWithDuration:delay:options:animations:completion. Logging showed the completion block was being called in a timely manner, but the keyboard was taking several seconds to show. This was only occurring on first launch of the keyboard.
This answer helped me solve this... turned out this was somehow linked to the iOS Simulator. This issue did not occur when I wasn't debugging the app... another classic example of chasing a bug for hours in the simulator that didn't actually exist.
The cause of this problem is found out. It is a lot of UIWebView objects rendered in the main thread. And it seems impossible to prevent their loading. Anyways time profiler show that a lot of time is spent to render them even if they are not visible on the screen.
And yes, I can't release them before memory warning event because of requirements

WatchKit InterfaceController vs ViewController

Are awakeWithContext, willActivate, didDeactivate the same as viewDidLoad, viewWillAppear, viewDidAppear in terms of functionality?
I am porting code from a Swift Apple Watch tutorial that was created back when people had to add their own watch AppViewController file to test their watch apps.
The included files and things have changed with the official watch release of Xcode obviously so I’m wondering where to put where.
For example there is some code in the older AppViewController file and so I just copy/pasted it into the new InterfaceController. I put code that was in viewDidLoad, viewWillAppear, viewDidAppear into awakeWithContext, willActivate, didDeactivate respectively.
It seems the methods are different. I got 1 error saying that setText doesn’t exist:
bpmLabel.setText(currentBeatPattern.bpm) = "\(currentBeatPattern.bpm)"
…and 2 errors saying view doesn’t exist:
iconLabel.frame = self.view.bounds
self.view.insertSubview(iconLabel, atIndex: 1)
It’s like WatchKit doesn’t use some of the normal property methods or something.
Error Messages:
http://i.imgur.com/wXMdt3c.png
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
self.view.insertSubview(iconLabel, atIndex: 1) // Xcode error
}
override func willActivate() {
super.willActivate()
iconLabel.frame = self.view.bounds // Xcode error
iconLabel.textAlignment = .Center
iconLabel.font = UIFont.boldSystemFontOfSize(132)
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
newBeat()
NSTimer.scheduledTimerWithTimeInterval(8,
target: self,
selector: Selector("newBeat"),
userInfo: nil,
repeats: true)
beat()
}
func newBeat() {
// 1
if ++currentBeatPatternIndex == beatPatterns.count {
currentBeatPatternIndex = 0
}
// 2
currentBeatPattern = beatPatterns[currentBeatPatternIndex]
// 3
bpmLabel.setText(currentBeatPattern.bpm) = "\(currentBeatPattern.bpm)" // Xcode error
iconLabel.text = currentBeatPattern.icon
}
func beat() {
// 1
UIView.animateWithDuration(currentBeatPattern.duration / 2,
delay: 0.0,
options: .CurveEaseInOut,
animations: {
// 2
self.iconLabel.transform = CGAffineTransformScale(
self.iconLabel.transform, self.shrinkFactor, self.shrinkFactor)
},
completion: { _ in
// 3
UIView.animateWithDuration(self.currentBeatPattern.duration / 2,
delay: 0.0,
options: .CurveEaseInOut,
animations: {
// 4
self.iconLabel.transform = CGAffineTransformScale(
self.iconLabel.transform, self.expandFactor, self.expandFactor)
},
completion: { _ in
// 5
self.beat()
}
)
}
)
}
}
You are right that awakeWithContext, willActivate, and didDeactivate are very similar to the existing UIViewController methods like viewDidLoad, viewWillAppear, and viewDidUnload. The errors you're seeing however are related to the way WatchKit currently works. In order for a watch app to run, all the code is executed on an iPhone but the Apple Watch itself assumes responsibility for the UI elements. What that means is that any views that constitute your watch app MUST be included on your watch app's storyboard. Also, as a direct result, views cannot be instantiated and added to a parent view. All UI elements must be included in your watch app's storyboard and the watch will lay them out based on their arrangement in interface builder. This means you cannot call addSubview and you cannot set an element's frame. You can only adjust its size and set its hidden property to hide or show it on the watch. As far as this method goes –
bpmLabel.setText(currentBeatPattern.bpm) = "\(currentBeatPattern.bpm)"
You are calling the method wrong. In swift the parameters are included in the parentheses. You can call it this way if it's what you mean
bpmLabel.setText("\(currentBeatPattern.bpm)")
but setText is a method that takes a string parameter and cannot be assigned with =
As far as the animations go, I think you're out of luck. Watch apps currently are more like widgets than iOS apps and things like UIView animations and frame math are not available you. You should definitely read
up on WatchKit because there's no way you'll be able to port an iOS app directly to the watch like this.

Game sometimes crashes when removing CADisplayLink from run loop

This doesn't happen every time you play the game, maybe once for every 5 or 10 plays. When the game ends, I remove my CADisplayLink (which I use to animate the playing area, a bit like the pipes in Flappy Bird) from the run loop. However, on the few occasions, it crashes on that line. Next to the line, it has:
Thread 1: EXC_BAD_ACCESS (code=1, address=0x10)
This is the code:
func endGame(r : String) {
UIView.animateWithDuration(0.4, delay: 0.2, options: .CurveLinear, animations: {
self.scoreLabel.alpha = 0
}, completion: {
(finished: Bool) in
self.scoreLabel.removeFromSuperview()
});
self.view.userInteractionEnabled = false
reason = r
println("Game Over!!!")
//Crashes on this line
blockUpdateDisplayLink.removeFromRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
shiftDisplayLink.removeFromRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes)
scoreTimer.invalidate()
UIView.animateWithDuration(0.0001, delay: 0.7, options: .CurveLinear, animations: {
}, completion: {
(finished: Bool) in
self.performSegueWithIdentifier("Game Over", sender: self)
});
}
if I comment out the first CADisplayLink part, it will just crash on the second anyway.
This is the stacktrace:
Which has the same "Thread 1" error as above.
What is going on??
You must invalidate display links (rather than just removing them from the run loop) because if they are in the middle of running when they activate they may still try to use their associated run loop.
Which run loop are you adding the CADisplayLink to? You might need to be using NSRunLoop.mainRunLoop() instead.
Also, if your CADisplayLink is only being added on one NSRunLoop, you could try calling blockUpdateDisplayLink.invalidate() instead of removing it.
If you post the code where you create the CADisplayLink objects, it will make it easier to track down the issue.
I will really suggest you to debug you're code with Instruments and NSZombies and find out your memory issue problem.
Instruments
NSZombies

Swift - slowing down "too fast" animation (UIImage updates) -- aka is NSTimer the only option here?

Newbie to IOS programming - learning through Swift. I'm writing a simple "slot machine / dice game".
I'm trying to show the user a flashing sequence of rolls before the "actual" roll appears.
func doFancyDiceRoll() {
for x in 1...100 {
Die1.image = PipsImg[RollOne()]
Die2.image = PipsImg[RollOne()]
Die3.image = PipsImg[RollOne()]
}
}
Die1, etc., are defined as generic UIImage views.
In any case, I'm not seeing the 100x iterations, just the images at the end of the loop. I'm assuming that either it redraws "too fast" or that IOS is trying to be smart, and only draws the last images so as to conserve resources.
I'll wildly guess that I need to either implement some kind of delay here, or, IOS needs to be told to explicitly draw out my images, and not try to outthink my intent.
For the delay, I've seen something about NSTimer, but nothing I saw seems to simply say something like "pause for .05" second, and the whole construct was unclear as they were ObjC examples/conversions.
(Note: I've simplified things here --- normally, I would store the value of RollOne() so I can use it later. I also would like to make an array (or collection?) like Die[1].image, but that is another question.)
========== Edit =======
OK, so I'm following up with more of my original code, merged in with that of #doctordoder so we can discuss a bit better. Hopefully that is kosher. (If this appended question is too long, please advise me on the best way to post a lengthy follow-up directly.)
import UIKit
class ViewController: UIViewController {
//( stripping out stuff unneeded for discussion )
// refers to same label below - works but kosher??
#IBOutlet var btnRoll_x: UIView
#IBAction func btnRoll(sender: AnyObject) {
triggerRoll()
}
var timer : NSTimer? = nil
var rolls : Int = 0
func triggerRoll() {
//hiding is bad UI, but until i know how to "disable & dim"
btnRoll_x.hidden = true
timer = NSTimer.scheduledTimerWithTimeInterval(0.10, target: self, selector: "doFancyDiceRoll", userInfo: nil, repeats: true);
}
func doFancyDiceRoll() {
Die1.image = PipsImg[randomInt(6)]
Die2.image = PipsImg[randomInt(6)]
Die3.image = PipsImg[randomInt(6)]
if (++rolls > 10)
{
timer?.invalidate()
timer = nil
rolls = 0 // DCB added this presumed missing line
btnRoll_x.hidden = false //again, need to do this better
}
}
}
Hopefully, my implementation of the code is what would have been intended. I made some minor adjustments for (hopeful) clarity.
Good news is I have working code. I have just enough understanding to get in place, but I'm fuzzy on some details.
Here is what I (think I) know...
We declare an NSTImer object, and a roll counter at the main level of the class. I note that in my original version, I had the roll counter scoped within the rolling function itself. Took me a while to understand why it could not live in the DiceRoll loop itself, but now I do. I'm going to express it poorly, but since the timer is going to call DiceRoll multiple instances, it needs to live outside the function.
The button btnRoll gets touched, and invokes triggerRoll().
To prevent the user from touching the button while we are in progress, which put us into a state where the roll counter never got to zero, I hide the button. (I'll figure how to properly put in in disabled state later.)
The timer is set. It fires every .1 second (within limits), and is set to repeat. (until .invalidate ). And it "calls" the function doFancyDiceRoll via the selector: attribute.
So, the big change as previously noted is that doFancy..Roll() no longer loops. It excites a single instance up updating the images. It checks the counter, and if we reach the limit, we kill the timer, which stops the timer (invalidate). (And I unhide the button, making it available again.)
So, a few things I am concerned/wondering about: I get the value of timers for other things that need to happen periodically (take health away every second, check a GPS position every 10 seconds, etc.). It's seems a odd construct to force a screen refresh.
Frankly, I would have expected to see see something like this:
func doFancyDiceRoll() {
for x in 1...100 {
Die1.image = PipsImg[RollOne()] // and 2 and 3 of course.....
VIewController.forceRedraw <<=== something like this, or maybe
ViewController.wait(.05) <<== this?? I dunno ;-)
}
}
instead we end up with about 20 extra lines or so. I'd be interested in knowing if there other approaches that could work keeping the loop intact.
Anyway, assuming this is the one true way to go, I guess my followup to this is how do I pass parameters, since this is not a "real" function call. Trying
selector: "doFancyDiceRoll(40)"
was not objected to by the IDE, but failed in execution.
I had exactly same problem back in days, entire loop is finished before the view is refreshed as #doctordoder mentioned. Solved with using NSTimer
var rollCount = 0
let arrayOfImages = ["image01", "image02", "image03"]
var timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: Selector("doFancyDiceRoll"), userInfo: nil, repeats: true)
func doFancyDiceRoll() {
if rollCount == 100 {
timer.invalidate
rollCount = 0
{
else {
//get images from array arrayOfImages[rollCount]
rollCount++
}
}
there could be typos, since I have no Xcode right now.
I have basically the same answer as above :(, but I thought I'd post it anyway.
var timer : NSTimer? = nil
var rolls : Int = 0
func doFancyDiceRoll() {
timer = NSTimer.scheduledTimerWithTimeInterval(0.10, target: self, selector: "roll", userInfo: nil, repeats: true);
}
func roll() {
println("rolling")
Die1.image = PipsImg[RollOne()]
Die2.image = PipsImg[RollOne()]
Die3.image = PipsImg[RollOne()]
if (++rolls > 100)
{
timer?.invalidate()
timer = nil
}
}
Rather than NSTimer and invalidating, you can use dispatch_after to do the work for you.
func rollDice(howManyTimes: Int) {
die1.image = PipsImg[RollOne()]
die2.image = PipsImg[RollOne()]
die3.image = PipsImg[RollOne()]
if howManyTimes > 0 {
let delayTime = dispatch_time(DISPATCH_TIME_NOW, Int64(Double(NSEC_PER_SEC) / 10.0))
dispatch_after(delayTime, dispatch_get_main_queue()) {
self.rollDice(howManyTimes - 1)
}
}
}
This will run the code for the number of times specified, delaying each time by 0.1 seconds. It works like this: First it sets the images on each die, then, if there are more iterations, it does a dispatch_after to call itself with rollDice(howManyTimes - 1)
With this, you don't need to maintain a NSTimer and it is pretty self contained.

Resources