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
Related
I have set up UIVideoEditorController as followed
if UIVideoEditorController.canEditVideo(atPath: video.path) {
let editController = UIVideoEditorController()
editController.videoPath = video.path
editController.delegate = self
present(editController, animated:true)
}
It will end up in the cancel delegate call and then dismisses the editor
func videoEditorControllerDidCancel(_ editor: UIVideoEditorController) {
print("in here")
editor.dismiss(animated: true, completion: nil)
}
Does anyone know why this is or if I need additional configurations to get it to work?
I'm working for an app that has users. One of the functionalities is to allow a user to log out and be redirected to the first page. I came across the problem when a user logs out, a toast message "You logged out" should be displayed on the first view of the app and receiving the command from a different page. Basically a toast message that can work with all the views, not only with the current one.
I managed to call a toast function after a user logs out but it won't show the message because the current view is dismissed before to have the chance showing it.
This is the function called:
func showToast(controller: UIViewController, message : String, seconds: Double) {
let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
alert.view.backgroundColor = UIColor.black
alert.view.alpha = 0.6
alert.view.layer.cornerRadius = 15
controller.present(alert, animated: true)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + seconds) {
alert.dismiss(animated: true)
}
}
If you dont know which the current presented VC is you could use this extension here:
extension UIWindow {
func topViewController() -> UIViewController? {
var top = self.rootViewController
while true {
if let presented = top?.presentedViewController {
top = presented
} else if let nav = top as? UINavigationController {
top = nav.visibleViewController
} else if let tab = top as? UITabBarController {
top = tab.selectedViewController
} else {
break
}
}
return top
}
}
Then you can call it it this way:
if let topVC = UIApplication.shared.keyWindow?.topViewController() {
topVC.present(alert, animated: true)
}
Another option is that you pop/dismiss your viewcontroller after you dismissed your alert:
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + seconds) {
alert.dismiss(animated: true)
// popViewController or dismiss here
}
I would like to know if iOS provides us any way to know when the Share Modal will be presented or dismissed.
var completionWithItemsHandler: UIActivityViewController.CompletionWithItemsHandler? { get set }
The completion handler to execute after the activity view controller is dismissed.
let activity = UIActivityViewController(activityItems: [activityItems], applicationActivities: nil)
activity.completionWithItemsHandler = {(activityType: UIActivity.ActivityType?, completed: Bool, returnedItems:[Any]?, error: Error?) in
// dismiss activity
}
self.present(activity, animated: true, completion: nil)
Presents a view controller modally.
completion
self.present(activity, animated: true) {
}
The block to execute after the presentation finishes. This block has no return value and takes no parameters. You may specify nil for this parameter.
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
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