Perform animation while performing segue - ios

From my initial View Controller, I want to be able to segue to another view controller. This second view controller, however, takes a few seconds to load, and I want to have my source view controller display an animation while it loads.
Performing the segue blocks the rest of the code from running, but I can't put it in a background thread because it technically updates the UI (Xcode gave me lots of warnings when I tried).
The animation also updates the UI, which means I can't put that in its own separate thread either.
Is there another method I could use to accomplish this?
class LaunchViewController: UIViewController {
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
UIView.animateWithDuration(5, animations: {
//Perform animation that updates the UI
}, completion: nil)
dispatch_async(dispatch_get_main_queue()) {
self.performSegueWithIdentifier("seg", sender: self)
}
}
P.S. When I put performSegueWithIdentifier in a background thread, it worked perfectly. But like I said, I get lots of errors (and Xcode will throw an exception in a coming release).

Related

Can't make custom segue in Xcode 10/iOS 12

I'm having the hardest time implementing a presentation of a drawer sliding partway up on the screen on iPhone.
EDIT: I've discovered that iOS is not respecting the .custom modalTransitionStyle I've set in the Segue. If I set that explicitly in prepareForSegue:, then it calls my delegate to get the UIPresentationController.
I have a custom Segue that is also a UIViewControllerTransitioningDelegate. In the perform() method, I set the destination transitioningDelegate to self:
self.destination.transitioningDelegate = self
and I either call super.perform() (if it’s a Present Modal or Present as Popover Segue), or self.source.present(self.destination, animated: true) (if it’s a Custom Segue, because calling super.perform() throws an exception).
The perform() and animationController(…) methods get called, but never presentationController(forPresented…).
Initially I tried making the Segue in the Storyboard "Present Modally" with my custom Segue class specified, but that kept removing the presenting view controller. I tried "Present as Popover," and I swear it worked once, in that it didn't remove the presenting view controller, but then on subsequent attempts it still did.
So I made it "Custom," and perform() is still being called with a _UIFullscreenPresentationController pre-set on the destination view controller, and my presentationController(forPresented…) method is never called.
Other solutions dealing with this issue always hinge on some mis-written signature for the method. This is mine, verbatim:
public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController?
I've spent the last four days trying to figure out “proper” custom transitions, and it doesn't help that things don’t seem to behave as advertised. What am I missing?
Instead of using a custom presentation segue, you could use a Container View for your drawer. This way, you can use a UIViewController for your Drawer content, while avoiding the issue with the custom segue.
You achieve this in two steps:
First pull a Container View into your main view controller and layout it properly. The storyboard would look like this: (You can see you have two view controllers. One for the main view and one for the drawer)
Second, you create an action that animates the drawer in and out as needed. One simple example could look like this:
#IBAction func toggleDrawer(_ sender: Any) {
let newHeight: CGFloat
if drawerHeightConstraint.constant > 0 {
newHeight = 0
} else {
newHeight = 200
}
UIView.animate(withDuration: 1) {
self.drawerHeightConstraint.constant = newHeight
self.view.layoutIfNeeded()
}
}
Here, I simply change the height constraint of the drawer, to slide it in and out. Of course you could do something more fancy :)
You can find a demo project here.

Delete unused ViewController from memory

I have 3 ViewController.
The first ViewController is checking if the user is logged in.
If yes performSegue to the mainVC and if no performSegue to loginVC.
When I am in loginVC, I log in and performSegue to mainVC.
What I want now is, I want to have all ViewControllers which are unused being "deleted", to save memory.
How is that going to work?
I found here in StackOverflow this piece of code:
class ManualSegue: UIStoryboardSegue {
override func perform() {
sourceViewController.presentViewController(destinationViewController, animated: true) {
self.sourceViewController.navigationController?.popToRootViewControllerAnimated(false)
UIApplication.sharedApplication().delegate?.window??.rootViewController = self.destinationViewController
}
}
}
Is that going to do what I want? It seems like yes because this method is popping the ViewController.
I am using "Show Detail" - segues only, except when using this method I created a custom Segue Segue.
Deletion should be handled by Apple, you (theoretically) shouldn't have to worry about it, so long as you don't create any retain cycles. As a rule, just don't have any strong references to self in blocks. Funny enough, the code you have above, that should dismiss the ViewController (and therefore delete it) also has a retain cycle. Adding [weak self] and strongSelf casts as needed should help:
override func perform() {
sourceViewController.presentViewController(destinationViewController, animated: true) { [weak self] in
guard let strongSelf = self else { return }
strongSelf.sourceViewController.navigationController?.popToRootViewControllerAnimated(false)
UIApplication.sharedApplication().delegate?.window??.rootViewController = strongSelf.destinationViewController
}
}
Memory question
Yes, that is how it works. You do not need to take care of freeing view controllers.
The system will keep track of references to view controller objects. When you do not have references to these anymore then the memory is deallocated. You can read about this more in swift language documentation:
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html
What the code is doing
presentViewController method is showing a view controller modally. The completion closure is performed after presenting the new view controller finishes. Inside closer 2 things happen
popToRootViewControllerRemoves all view controllers inside the sourceViewController object.
rootViewController of the window is set to new value.
This practically changes the root view controller to another one. This seems like a valid action after successful login.
I do not know if step 1 is necessary. That navigation view controller is going to go away anyway so why to pop view controllers inside it?
More about view controllers
You might be also interested in view controller life cycle. UIKit developer documentation contains in-depth details about view controllers:
https://developer.apple.com/documentation/uikit/uiviewcontroller

Swift 3 - Dismissing view controller after async job

I am using Swift 3, Xcode 8.2.
I have a view controller on which there is a button. When the button is pressed, it runs the buttonPressed function below. It kicks off an asynchronous job which takes a few seconds to complete during which a spinning gear activity indicator pops up. Once the job is finished, I want the activity indicator to dismiss and the view controller to dismiss as well to the VC that it originally came from.
func buttonPressed() {
// Set up some stuff here
...
// this code block here is to asynchronously run the processing job
// while the activity indicator gear spins
self.displaySpinningGear()
DispatchQueue.main.async {
self.doSomeJobProcessing()
}
self.dismiss(animated: true, completion: nil)
// also dismiss the camera view
self.presentingViewController?.dismiss(animated: true, completion: nil) // error here
}
However, I get an error on that last statement:
[Assert] Trying to dismiss the presentation controller while
transitioning already.
(<_UIAlertControllerAlertPresentationController: 0x109e3b190>)
2017-03-03 21:37:56.833899 EOB-Reader[27710:6869686] [Assert]
transitionViewForCurrentTransition is not set, presentation controller
was dismissed during the presentation?
(<_UIAlertControllerAlertPresentationController: 0x109e3b190>)
Any help would be greatly appreciated.
Perform anyone transition of these at a time.
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)
}
Two transition operations cannot be performed simultaneously.
In your source code you are dismissing current view controller with animation. Now animation generally takes time around 0.25 second to complete its operation.
Now, in next line you are trying to dismiss a ( ** ) view controller that has presented current view controller. (In your case ( ** ) view controller is also presented by some other view controller.) so, within a fraction of seconds/milliseconds ( ** ) view controller will also try to dismiss it self with animation.
At this time your current view controller is being dismissed and (**) view controller will also start dismiss operation. So, both operations conflict each other on main executing thread. And results into an issue, you are facing.
Also, share your code for block
self.doSomeJobProcessing()
Share here, if you've set any other view/controller transition operations here.
Remember that when you dispatch asynchronous you're typically dispatching to background thread to prevent UI blocking. All on screen (UI) components need be modified on the main thread. So start your progress spinner from the main thread, dispatch your heavy lifting to the background, then redispatch to the main thread that you'd like to. this.dismissviewcontroller:animated
Will update with code shortly.

Segue is not working, and i can't figure out why

I have an if...then statement in the ViewDidLoad method for the view that acts as my storyboard entry point.
Basically, I am doing a check to see if there is any data in core data, to indicate that they've completed a small "setup form".
If it is found that the core data is empty or that the app has not been properly set up, I want it to automatically kick them over to the settings view with a segue.
My ViewDidLoad method looks like this:
override func viewDidLoad() {
super.viewDidLoad()
//Get any entries from the App Settings Entity
getAppSettings()
//If any entries are found, check to see if the setup has been completed
if (appSettings.count > 0) {
print("We found entries in the database for App Settings")
if (appSettings[0].setupComplete == false) {
print("Setup has not been completed")
self.performSegue(withIdentifier: "toAppSettings", sender: self)
} else {
print("Setup is completed")
//Load the settings into global variables
preferredRegion = appSettings[0].region!
usersName = appSettings[0].usersName!
}
} else {
print("We found no entries in the database for App Settings")
self.performSegue(withIdentifier: "toAppSettings", sender: self)
}
}
I made sure that the segue does exist, and that the identifier for the segue is exactly as I have it in the quotes (I even copied & pasted all instances of it to make sure that they are all consistent).
I also went the extra mile and put a checker in the "prepare for segue" method, to print whether it was getting called, and who the sender was:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
print("We're getting ready to segue!")
print(segue.identifier)
}
Both of those items get printed to the log, which tells me that the segue is being recognized and that the app is attempting to fire it. But - for some reason that I can't figure out - it simply isn't firing.
Any ideas what I am missing here?
I have an if...then statement in the ViewDidLoad
But that's the problem. viewDidLoad is way too early for this. Remember, all viewDidLoad means is that your view controller has a view. That's all it means. That view is not yet in the interface, and the view controller itself may not even be in the view hierarchy! It's just sitting out there in object space. You cannot segue from here; there is nothing to segue from.
Try waiting until viewDidAppear. If that makes it work, you might try moving it back to viewWillAppear, but I don't guarantee anything. Keep in mind that these methods can be called multiple times, so you might also need a flag to make sure that this segue fires just the once.

How should a custom segue support an unwind to a viewcontroller earlier in the call chain?

I have a storyboarded app using segues without a navigation controller. We have a simple custom segue like the code below that works fine for normal cases. However we have a situation where after invoking a chain of view controllers A->B->C we’d sometimes like C to unwind directly back to A. This works fine with the built-in transitions as UIKit seems to dismiss the intervening controller without any trouble, properly showing a transition from C back to A.
However in my custom segue I am not certain if or how I should dismiss the intervening view controllers. Assume that the code below knows both that it is unwinding and that the destination view controller is not the one presenting the source view controller. Here is what I’ve tried:
1) If I simply leave the code as is seems to work fine! However I get errors “Unbalanced calls to begin/end appearance transitions” for view controller C.
2) If I change the code to dismiss view controller B first I see the transition to B. I don’t see any way to suppress this.
From what I've read the unbalanced calls error comes up often when you have overlapping calls to transitions. However I don't see how that could be the case here since UIKit is dismissing them (presumably). I've also tried dispatching the completion work to the main thread (to try to get it into the next run loop) but no change.
class MyCustomSegue: UIStoryboardSegue {
override func perform()
{
let sourceView = sourceViewController.view
let destView = destinationViewController.view
let unwinding = // …
// Put the destination view on top
sourceView.superview?.insertSubview(destView, aboveSubview: sourceView)
// Do the animation
UIView.animateWithDuration(1.0, animations: { complete in
} ) { (Finished) -> Void in
if unwinding {
// calling this on presenter or presentee is equivalent
self.sourceViewController.dismissViewControllerAnimated(false, completion: nil)
} else {
self.sourceViewController.presentViewController(self.destinationViewController, animated: false, completion: nil)
}
}
}
}

Resources