I send an async request and I present a view controller when I receive success. It works.
But I'm getting an issue when my app is in background when I receive success and that I pass it in foreground. The view controller is not always displayed.
I think it's about the main thread but I'm not sure.
How can I fix it ?
EDIT:
Here is the function that I call after the success:
func showPopup(on viewController: UIViewController) {
let viewControllerToPresent = MyPopupViewController(nibName: "Popup", bundle: nil)
let popup = PopupDialog(viewController: viewControllerToPresent, buttonAlignment: .horizontal, transitionStyle: .zoomIn, gestureDismissal: false)
let button = DefaultButton(title: "Ok") {
popup.dismiss(animated: true, completion: nil)
}
popup.addButtons([button])
viewController.present(popup, animated: true, completion: nil)
}
When your application is in the background, i.e. suspended, Apple doesn't allow you to make any changes substantive changes to the user interface. In this case your best approach is probably to save that you want to do something on return and check in your App Delegates applicationDidBecomeActive method.
If you call UI functions from the background the result is unpredictable. Just explicitly present on the main thread. Here is a simplified example:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.doBackgroundStuff()
}
func getThread() -> String {
return Thread.isMainThread ? "UI" : "BG"
}
func doBackgroundStuff() {
// force to background
DispatchQueue.global().async {
print("doBackgroundStuff: this is the \(self.getThread()) thread")
self.showPopup(on: self)
}
}
func showPopup(on viewController: UIViewController) {
print("showPopup OUTER: this is the \(self.getThread()) thread")
// force to foreground
DispatchQueue.main.sync {
print("showPopup INNER: this is the \(self.getThread()) thread")
let popup = PresentingViewController(nibName: "PresentingViewController", bundle: nil)
viewController.present(popup, animated: true, completion: nil)
}
}
}
The UI is shown and the output on the console is:
doBackgroundStuff: this is the BG thread
showPopup OUTER: this is the BG thread
showPopup INNER: this is the UI thread
Related
Can we customize SwiftEventBus Library to only trigger in the current active ViewController.
I'm trying to trigger an action when ever a notification occurs, so i'm using swift event bus to trigger when ever a push notification comes but it is triggering in all the places it is registered. Can we make so that it will only trigger the action in the active view. If not is there any other library I can use?
Wouldn't it be enough to deregister inactive ViewControllers as mentioned in the SwiftEventBus readme?
//Perhaps on viewDidDisappear depending on your needs
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
SwiftEventBus.unregister(self)
}
Modify library(or subclass SwiftEventBus) like below:
#discardableResult
open class func on(_ target: AnyObject, name: String, sender: Any? = nil, queue: OperationQueue?, handler: #escaping ((Notification?) -> Void)) -> NSObjectProtocol {
let id = UInt(bitPattern: ObjectIdentifier(target))
//modification start
let handlerIner:((Notification?) -> Void) = { [weak target] n in
if let vc = target as? UIViewController, vc.view?.window != nil {
handler(n)
}
}
let observer = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: name), object: sender, queue: queue, using: handlerIner)
// modification end
let namedObserver = NamedObserver(observer: observer, name: name)
Static.queue.sync {
if let namedObservers = Static.instance.cache[id] {
Static.instance.cache[id] = namedObservers + [namedObserver]
} else {
Static.instance.cache[id] = [namedObserver]
}
}
return observer
}
I am using Swift 3.
The behavior I am trying to do is: the user clicks on a button, a spinning gear alert controller displays while it kicks off a long-running function. Once that function is done executing, the spinning gear goes away and the view controller dismisses.
The code below kicks off the doProcessing function but doesn't display the spinning gear until about a second before the view dismisses. So this isn't quite right.
func displaySpinningGear() {
print("display spinning gear")
// show the alert window box
let activityAlertController = UIAlertController(title: "Processing", message: "Please wait while the photo is being processed.", preferredStyle: .alert)
//create an activity indicator
let indicator = UIActivityIndicatorView(frame: activityAlertController.view.bounds)
indicator.autoresizingMask = [.flexibleWidth, .flexibleHeight]
indicator.hidesWhenStopped = true
indicator.activityIndicatorViewStyle = UIActivityIndicatorViewStyle.gray
//add the activity indicator as a subview of the alert controller's view
activityAlertController.view.addSubview(indicator)
indicator.isUserInteractionEnabled = false // required otherwise if there buttons in the UIAlertController you will not be able to press them
indicator.startAnimating()
print("start animating")
self.present(activityAlertController, animated: true, completion: nil)
}
func onButtonClick() {
self.displaySpinningGear()
DispatchQueue.main.async {
self.doProcessing() // long running function
}
if let viewController = presentingViewController {
// This block will dismiss both current and a view controller presenting current
viewController.dismiss(animated: true, completion: nil)
}
else {
// This block will dismiss only current view controller
self.dismiss(animated: true, completion: nil)
}
}
The code below kicks off the doProcessing function but the view dismisses immediately and I can tell from the console that my doProcessing function is still running. This is not right either.
function onButtonClick() {
DispatchQueue.global(qos: .background).async {
print("Processing")
self.doProcessing() // run in background
DispatchQueue.main.async {
self.displaySpinningGear()
}
}
if let viewController = presentingViewController {
// This block will dismiss both current and a view controller presenting current
viewController.dismiss(animated: true, completion: nil)
}
else {
// This block will dismiss only current view controller
self.dismiss(animated: true, completion: nil)
}
}
How do I get the background function to kick off while displaying a spinning gear and dismiss the view and alert controller when the background function is done running (not before)?
EDIT
Tried moving the code to spin the gear outside the background block as per #Honey's suggestion in the comment but to no avail. The view immediately dismisses while the process function is still processing (I can tell through print statements).
func onButtonClick() {
DispatchQueue.main.async {
self.displaySpinningGear()
}
DispatchQueue.global(qos: .background).async {
print("Processing")
self.doProcessing() // run in background
}
if let viewController = presentingViewController {
// This block will dismiss both current and a view controller presenting current
viewController.dismiss(animated: true, completion: nil)
}
else {
// This block will dismiss only current view controller
self.dismiss(animated: true, completion: nil)
}
}
Make a Callback from long running function so when it ends returns a value and catch it to disappear the alert.
Try it:
typealias DoProcessingCallback = (_ finished: Bool) -> Void
func onButtonClick() {
self.displaySpinningGear()
self.doProcessing(callback: { (finished) in
if finished {
// Here you DismissViewController
// Here you DismissAlert
}
}) // long running function
}
func doProcessing(callback: DoProcessingCallback) {
// YOUR LONG CODE....
// When you know it already finished
callback(true)
}
Hope it helps you
I had the same issue and tried a bunch of different things and this is what worked:
activityView.alpha = 0.8
DispatchQueue.global(qos: .default).async(execute: {
DispatchQueue.main.async(execute: {
self.performSegue(withIdentifier: "cropToProcessed", sender: self)
})
})
Basically I set the alpha for activity indicator to 0.0 initially and when the button is pressed I set it to 0.8 and I set it back to 0.0 in viewWillDisappear and it works
I have a need to implement a spinning wheel during a backend job. I have my backend-job in a separate class.
class ViewControllerA: UITableViewController {
// Code
var GetBackendRecordObj = GetBackendRecord(initparam:param);
// CODE TO START ANIMATION (SPINNING WHEEL)
self.view.addSubview(self.activityIndicator)
self.activityIndicator.bringSubview(toFront: self.view)
self.activityIndicator.startAnimating()
// CODE TO CALL THE BACKEND IS IN ANOTHER CLASS
GetBackendRecordObj.fetch_record()
}
class GetBackendRecord{
var transaction_id: String = ""
var current_email_id: String = ""
init(initparam: String) {
self.initparam = initparam
}
func fetch_record (){
do{
DispatchQueue.global(qos: .userInitiated).async {
//code
DispatchQueue.main.async { () -> Void in
//code to process response from backend
// NEED CODE TO STOP ANIMATION (SPINNING WHEEL)THAT WAS STARTED IN VIEWCONTROLLERA
})
}
}
}
How can I access the UITableViewcontroller after the backend call is done, so I can stop the animation. OR If there is a better way to start / stop animation when executing a backend-job (in a separate class) please let me know.
Add a completion handler to fetch_record:
func fetch_record(_ completionHandler: #escaping () -> Swift.Void) {
do{
DispatchQueue.global(qos: .userInitiated).async {
//code
DispatchQueue.main.async { () -> Void in
//code to process response from backend
completionHandler()
})
}
}
}
When calling it in your ViewController, you can specify what to do after completion:
GetBackendRecordObj.fetch_record() {
self.activityIndicator.stopAnimating()
}
If you need to know when the response returns so that you can stop the loader, you need to add a completion handler to the method that makes your internet call. You should generally have completion handlers on most methods that make internet calls, especially if you need UI things to happen only once you have gotten a response.
So for the fetchRecord function:
I have added a completion handler to this call. Preferably you would hand something off here just after #escaping(something like a dictionary or an array if it is a JSON response) and then process that response in another method. But if you want the code to process the response in this method with the threading that you've set up here, then I've written it accordingly.
func fetch_record(withCompletion comp: #escaping () ->()){
do{
DispatchQueue.global(qos: .userInitiated).async {
//code
DispatchQueue.main.async { () -> Void in
//code to process response from backend
//this tells whatever called this method that it is done
comp()
})
}
}
}
Then in your view controller when you call GetBackendRecordObj.fetch_record()
GetBackendRecordObj.fetch_record(withCompletion: { [weak self] () in
//when it hits this point, the process is done
self?.activityIndicator.stopAnimating()
}
Instead of activityIndicator better to use MBProgressHUD.
https://github.com/jdg/MBProgressHUD
To show MBProgressHUD
let loadingNotification = MBProgressHUD.showHUDAddedTo(self.view, animated: true)
loadingNotification.mode = MBProgressHUDMode.Indeterminate
loadingNotification.labelText = "Loading"
To hide MBProgressHUD
DispatchQueue.main.async { () -> Void in
MBProgressHUD.hideAllHUDsForView(self.view, animated: true)
})
You can implement show MBProgressHUD in seperate class from where you initiate back end job and hide code once your back end process finish.
I've an XCode project programmed in swift with a registration/connection process. When you're connected, even if the app restarted you don't repeat this process until you click on "log out" button.
I've noticed that if you follow the registration/connection process when you start the app, memory is about 500Mb whereas if the app started with a connected user, memory is about 140Mb.
Registration process is composed of 3 view controllers connected with custom segue. Access to logged in view is made by following code :
func switchRootViewController(rootViewController: UIViewController, animated: Bool, completion: (() -> Void)?)
{
if animated
{
UIView.transitionWithView(window!, duration: 0.5, options: .TransitionCrossDissolve, animations:
{
let oldState: Bool = UIView.areAnimationsEnabled()
UIView.setAnimationsEnabled(false)
self.window!.rootViewController = rootViewController
UIView.setAnimationsEnabled(oldState)
}, completion: { (finished: Bool) -> () in
if (completion != nil)
{
completion!()
}
})
}
else
{
window!.rootViewController = rootViewController
}
}
Where rootViewController is replace by the logged in view I want to load as you can see here :
func goToNextView()
{
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let homeViewController = storyboard.instantiateViewControllerWithIdentifier("conversationViewController") as!ConversationViewController
homeViewController.currentUser = self.currentUser
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.switchRootViewController(home, animated: true, completion: nil)
}
However it doesn't clean the memory.
Here a screen of memory allocation :
So my question is : is it possible to "kill" some view controller in order to clean the memory ?
Here for example it can be obvious to "kill" registration process views when you're connected.
Thank you for answers.
I've tried a whole bunch of ways to get Game Center working in my SpriteKit game. Unfortunately the way I've done it in the past using ObjC and ViewControllers don't work because I'm using SKScene/ a GameScene.
This is the swift version of the code (I think):
// MARK: Game Center Integration
//login and make available
func authenticateLocalPlayer(){
let localPlayer = GKLocalPlayer()
print(localPlayer)
localPlayer.authenticateHandler = {(viewController, error) -> Void in
if ((viewController) != nil) {
self.presentViewController(viewController!, animated: true, completion: nil)
}else{
print((GKLocalPlayer.localPlayer().authenticated))
}
}
}
//submit a score to leaderboard
func reportScoreToLeaderboard(thisScore:Int){
if GKLocalPlayer.localPlayer().authenticated {
let scoreReporter = GKScore(leaderboardIdentifier: "LeaderboardID")
scoreReporter.value = Int64(thisScore)
let scoreArray: [GKScore] = [scoreReporter]
GKScore.reportScores(scoreArray, withCompletionHandler: { (error: NSError?) -> Void in
if error != nil {
print(error!.localizedDescription)
} else {
print("Score submitted")
}
})
}
}
//show leaderboard (call from button or touch)
func showLeaderboard() {
let vc = self.view?.window?.rootViewController
let gc = GKGameCenterViewController()
gc.gameCenterDelegate = self
vc?.presentViewController(gc, animated: true, completion: nil)
}
//hides view when finished
func gameCenterViewControllerDidFinish(gameCenterViewController: GKGameCenterViewController){
gameCenterViewController.dismissViewControllerAnimated(true, completion: nil)
}
Unfortunetely, no matter what I try, I either get this error:
Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior
... or it just crashes.
I've read that NSNotifications can be used? But how?
I'm guessing the best way is to set it all up in the GameViewController.swift and use NSNotifications to communicate with the RootViewController from the GameScene? I can't seem to find a tutorial or example though.
Use delegation when a view controller needs to change views, do not have the view itself change views, this could cause the view trying to deallocating while the new view is being presented, thus the error you are getting