Calling swift animation from completion event? - ios

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.

Related

How to break out of a loop that uses async DispatchQueue inside

I am using a for loop coupled with a DispatchQueue with async to incrementally increase playback volume over the course of a 5 or 10-minute duration.
How I am currently implementing it is:
for i in (0...(numberOfSecondsToFadeOut*timesChangePerSecond)) {
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i)/Double(timesChangePerSecond)) {
if self.activityHasEnded {
NSLog("Activity has ended") //This will keep on printing
} else {
let volumeSetTo = originalVolume - (reductionAmount)*Float(i)
self.setVolume(volumeSetTo)
}
}
if self.activityHasEnded {
break
}
}
My goal is to have activityHasEnded to act as the breaker. The issue as noted in the comment is that despite using break, the NSLog will keep on printing over every period. What would be the better way to fully break out of this for loop that uses DispatchQueue.main.asyncAfter?
Updated: As noted by Rob, it makes more sense to use a Timer. Here is what I did:
self.fadeOutTimer = Timer.scheduledTimer(withTimeInterval: timerFrequency, repeats: true) { (timer) in
let currentVolume = self.getCurrentVolume()
if currentVolume > destinationVolume {
let volumeSetTo = currentVolume - reductionAmount
self.setVolume(volumeSetTo)
print ("Lowered volume to \(volumeSetTo)")
}
}
When the timer is no longer needed, I call self.fadeOutTimer?.invalidate()
You don’t want to use asyncAfter: While you could use DispatchWorkItem rendition (which is cancelable), you will end up with a mess trying to keep track of all of the individual work items. Worse, a series of individually dispatch items are going to be subject to “timer coalescing”, where latter tasks will start to clump together, no longer firing off at the desired interval.
The simple solution is to use a repeating Timer, which avoids coalescing and is easily invalidated when you want to stop it.
You can utilise DispatchWorkItem, which can be dispatch to a DispatchQueue asynchronously and can also be cancelled even after it was dispatched.
for i in (0...(numberOfSecondsToFadeOut*timesChangePerSecond)) {
let work = DispatchWorkItem {
if self.activityHasEnded {
NSLog("Activity has ended") //This will keep on printing
} else {
let volumeSetTo = originalVolume - (reductionAmount)*Float(i)
self.setVolume(volumeSetTo)
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + Double(i)/Double(timesChangePerSecond), execute: work)
if self.activityHasEnded {
work.cancel() // cancel the async work
break // exit the loop
}
}

Notifying main UI thread from background thread in Swift 4

I'm doing some communication work on a background thread, which I start like so:
self.thread = Thread(target: self, selector: #selector(threadMain), object: nil)
self.thread?.start()
...
func threadMain() {
...
}
threadMain is invoked correctly and the processing rolls as it should. The last code sequence on threadMain is the notification of the main thread via externally provided callback "onComplete", which I wanted to do like so:
print("leaving thread")
DispatchQueue.main.async {
self.onComplete?(CommunicationCallbackParams(errorCode:
self.errorCode, commState: self.commState, redirectUrl: self.redirectUrl))
}
However, the code inside the closure is never called. If I remove the "DispatchQueue.main.async" wrap it works, but I notify on non-UI-thread level. What might go wrong here?
The same principle is working fine in Objective C...
The most obvious thing is that you're calling self.onComplete with optional chaining. If onComplete is nil, that code won't get called. However, you say that it does get called if you don't wrap it in a call to DispatchQueue.main.async.
Oh, found the solution. I was calling the class from XCTest using a semaphore in order to wait for the result:
let semaphore = DispatchSemaphore.init(value:0)
let _ = Communication(url: "<some url>") { (result) in
print("Current thread is main thread: \(Thread.isMainThread)")
print(result)
semaphore.signal()
}
semaphore.wait()
That semaphore pattern must have blocked the notification. Once I changed it to expectations, it worked fine.
let exp = expectation(description: "\(#function)\(#line)")
let _ = Communication(url: "<some url>") { (result) in
print("Current thread is main thread: \(Thread.isMainThread)")
print(result)
exp.fulfill()
}
waitForExpectations(timeout: 60, handler: nil)

Performing an unknown amount of animations sequentially

I am creating a game where the user can move a SKShapeNode around. Now, I am trying to write a utility function that will perform back-to-back animations sequentially.
Description of what I'm Trying
I first dispatch_async to a serial thread. This thread then calls a dispatch_sync on the main thread to perform an animation. Now, to make the animations run sequentially, I would like to block the GlobalSerialAnimationQueue until the animation is completed on the main thread. By doing this, I (theoretically) would be able to run animations sequentially. My code is pasted below for more description
func moveToCurrentPosition() {
let action = SKAction.moveTo(self.getPositionForCurrRowCol(), duration: 1.0)
dispatch_async(GlobalSerialAnimateQueue) {
//this just creates an action to move to a point
dispatch_sync(GlobalMainQueue, {
self.userNode!.runAction(action) {
//inside the completion block now want to continue
//WOULD WANT TO TRIGGER THREAD TO CONTINUE HERE
}
})
//WOULD LIKE TO PAUSE HERE, THIS BLOCK FINISHING ONLY WHEN THE ANIMATION IS COMPLETE
}
}
So, my question is, how would I write a function that can take in requests for animations, and then perform them sequentially? What grand-central-dispatch tools should I use, or should I be trying a completely different approach?
I figured out how to do this using a grand-central-dispatch semaphore. My updated code is here.
func moveToCurrentPosition() {
let action = SKAction.moveTo(self.getPositionForCurrRowCol(), duration: Animation.USER_MOVE_DURATION)
dispatch_async(GlobalSerialAnimateQueue) {
let semaphore = dispatch_semaphore_create(0)
dispatch_sync(GlobalMainQueue, {
self.userNode!.runAction(action) {
//signal done
dispatch_semaphore_signal(semaphore)
}
})
//wait here...
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
}
}
SpriteKit provides a much simpler and intuitive API for running actions in sequence. Have a look at the documentation here.
You can simply perform actions as a sequence of events, with blocks in between or as completion:
let action = SKAction.moveTo(self.getPositionForCurrRowCol(), duration: Animation.USER_MOVE_DURATION)
let otherAction = SKAction.runBlock({
//Perform completion here.
})
self.userNode!.runAction(SKAction.sequence([action, otherAction]))

Why is popViewControllerAnimated taking so long to run?

I have a secondary viewController that allows me to delete images from the camera roll. The problem is, the completionHandler fires like it's suppose to, but the popViewController doesn't actually seem to run for about 8 seconds. It definitely fires, because I can see the optional output. And I checked just doing the pop, and it runs correctly. I checked the viewWillDisapear event, and it fires late as well, which I expected considering the nav controller hadn't popped the view current viewController yet.
PHPhotoLibrary.sharedPhotoLibrary().performChanges({
PHAssetChangeRequest.deleteAssets(assetsToDelete)
return
}, completionHandler: { success, error in
if success {
println("success")
println(navigationController.popViewControllerAnimated(true))
println("so slow")
}
if let error = error {
println(error)
}
return
})
This is what the documentation says:
Photos executes both the change block and the completion handler block
on an arbitrary serial queue. To update your app’s UI as a result of a
change, dispatch that work to the main queue.
The navigation controller needs to be executed from the main thread, so you need to wrap the call to something like
dispatch_async(dispatch_get_main_queue()) {
navigationController.popViewControllerAnimated(true)
}
For Swift 3
DispatchQueue.main.async() {
self.navigationController?.popViewController(animated: true)
}

Setting a subview.hidden = false locks up UI for many seconds

I'm using a button to populate a UIPickerView on a hidden UIVisualEffectView. The user clicks the button, the VisualEffectView blurs everything else, and the PickerView displays all the names in their contact list (I'm using SwiftAddressBook to do this.)
This works fine except when the user clicks the button, the UI locks up for about 5-10 seconds. I can't find any evidence of heavy CPU or memory usage. If I just print the sorted array to the console, it happens almost immediately. So something about showing the window is causing this bug.
#IBAction func getBffContacts(sender: AnyObject) {
swiftAddressBook?.requestAccessWithCompletion({ (success, error) -> Void in
if success {
if let people = swiftAddressBook?.allPeople {
self.pickerDataSource = [String]()
for person in people {
if (person.firstName != nil && person.lastName != nil) {
//println("\(person.firstName!) \(person.lastName!)")
self.pickerDataSource.append(person.firstName!)
}
}
//println(self.pickerDataSource)
println("done")
self.sortedNames = self.pickerDataSource.sorted { $0.localizedCaseInsensitiveCompare($1) == NSComparisonResult.OrderedAscending }
self.pickerView.reloadAllComponents()
self.blurView.hidden = false
}
}
else {
//no success, access denied. Optionally evaluate error
}
})
}
You have a threading issue. Read. The. Docs!
requestAccessWithCompletion is merely a wrapper for ABAddressBookRequestAccessWithCompletion. And what do we find there?
The completion handler is called on an arbitrary queue
So your code is running in the background. And you must never, never, never attempt to interact with the user interface on a background thread. All of your code is wrong. You need to step out to the main thread immediately at the start of the completion handler. If you don't, disaster awaits.

Resources