I've just started programming in Swift, what I'm trying to accomplish is a very simple app with an initial UIViewController, a UIPageViewController that shows some book pages and a destination UIViewController.
My approach so far is this:
The UIViewController1 is loaded and has a showPage button that simply shows UIPageViewController
present(walkthroughViewController, animated: true, completion: nil)
When the user reaches the last page of the UIPageViewController, I show the destination UIViewController2, addressing the segue from the start UIViewController
override func onUIPageViewControllerRigthClosing(){
let pvc = self.presentingViewController as! StartPageController
dismiss(animated: true){
pvc.performSegue(withIdentifier: "startTest", sender: nil)
}
}
Everything works correctly, but the problem is that when UIPageViewController is dismissed, the Starting UIViewController is showed and then is showed the second with the animated segue.
What I am trying to achieve is to directly display the target UiViewController to the user on the dismiss of the UIPageViewController, without showing the transition with animation from start View to the destination View.
I'm completely wrong approaching or there is a way to do the segue before dismissing the UIPageViewController?
Here I created a gif that shows the problem, when I close the UIPageViewController I see the previous view in transition: GIF demo
I suggest you using this approach: for these screens transitions use childViewControllers instead of presenting them modally and dismissing with default UIKit functions.
You have problems with naming, so let me rename view controllers.
Say, you have:
RootViewController (the first screen, user see after
app launch).
OnboardingViewController (your pageViewController or other container)
AppContentViewController (actually app main screen)
I suggest you using this approach: for screens transitions on RootViewController use childViewControllers instead of presenting them modally and dismissing with default UIKit functions.
Here is sample code that works with childViewControllers
extension UIViewController {
func displayChildController(_ content: UIViewController, duration: TimeInterval = 0.4, animation: (() -> ())? = nil, completion: #escaping () -> () = {}) {
content.view.frame = CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height)
view.addSubview(content.view)
addChildViewController(content)
UIView.animate(withDuration: animation != nil ? duration : 0, animations: {() -> Void in
animation?()
}, completion: {(_ finished: Bool) -> Void in
content.didMove(toParentViewController: self)
completion()
})
}
func hideChildController(_ content: UIViewController, duration: TimeInterval = 0.4, animation: (() -> ())? = nil, completion: #escaping () -> () = {}) {
UIView.animate(withDuration: animation != nil ? duration : 0, animations: {() -> Void in
animation?()
}, completion: {(_ finished: Bool) -> Void in
content.willMove(toParentViewController: nil)
content.view.removeFromSuperview()
content.removeFromParentViewController()
completion()
})
}
}
Here is "algorithm":
I assuming that you are using single storyboard with all these view controllers.
On OnBoardingViewController declare onDoneCallback:
class OnBoardingViewController: ... {
var onDoneCallback = {}
...
}
On RootViewController when you need present OnboardingViewController:
func presentOnboardingScreen() {
let onboardingVC = self.storyboard?.instantiateViewController(withIdentifier: "OnboardingViewController") as! OnboardingViewController
onboardingVC.transform = .init(translationX: 0, y: self.view.frame.height)
onboardingVC.onDoneCallback = {
self.presentAppContentAfterOnboarding() // see below
}
displayChildController(onboardingVC, duration: 0.3, animation: {
vc.view.transform = .identity
})
}
When you need call onDoneCallback closure on OnboardingViewController
presentAppContentAfterOnboarding method on RootViewController could look like:
func presentAppContentAfterOnboarding() {
let onboardingVC = self.childViewControllers.last as! OnboardingViewController
let appContentVC = self.storyboard?.instantiateViewController(withIdentifier: "AppContentViewController") as! AppContentViewController
displayChildController(appContentVC)
view.insertSubview(appContentVC.view, belowSubview: onboardingVC.view)
hideChildController(childVC, duration: duration, animation: {
onboardingVC.view.transform = .init(translationX: 0, y: self.view.frame.height)
})
}
Note. Don't forget to set Storyboard ID of OnboardingViewController and AppContentViewController in your storyboard.
Here is the sample project
Related
I am transitioning Child ViewControllers, from a ViewController to a TabBarController.
I am using this method to do so;
private func transitionController(to new: UIViewController, completion: (() -> Void)? = nil) {
current.willMove(toParent: nil)
addChild(new)
transition(from: current, to: new, duration: 6, options: [], animations: {
}) { completed in
self.current.removeFromParent()
new.didMove(toParent: self)
self.current = new
completion?()
}
}
However, when presenting the TabBarController it animates the TabBar up from outside the window.
How do I stop this? - I Just want a simple cross-dissolve animation such as presenting a controller modally.
Current Animation
I'm trying to present a view controller with custom animation, so I came up with this code. When I tried to present the view controller, I got no error but the app crashes. Can someone tell me why my app crashes?
My code looks like this
func presentLeftToRight<T>(viewName: String, class: T.Type) where T: DefaultView {
let presentView = self.storyboard?.instantiateViewController(withIdentifier: viewName) as! T
presentView.loadViewIfNeeded()
presentView.topContainer.alpha = 0
presentView.bgMenuView.alpha = 0
presentView.mainButton.alpha = 0
self.view.insertSubview(presentView.view, belowSubview: scrollView)
self.view.layoutIfNeeded()
UIView.animate(withDuration: 0.5, animations: {
self.scrollView.frame.origin.x = self.view.frame.width
}, completion: { _ in
self.present(presentView, animated: false, completion: nil)
})
}
So I have 2 View Controllers and a Navigation Controller. When the screen is clicked, the 1st VC segues to the 2nd VC, and there is a back button for the segue to unwind to go back to the 1st VC.
I did not like the vertical animation of the segues, so (with some help) I created custom, horizontal animations.
The 1st VC works great with the animation sliding from right to left. But once that is done, the 2nd VC does not want to unwind (2nd VC should go from left to right).
I do get this Warning..
Warning: Attempt to present UINavigationController: 0x7fce2082b000 on TestApp.HomeViewController: 0x7fce20410030 whose view is not in the window hierarchy!
Also, if I take the script from the 1st VC segue, then I am able go unwind from the 2nd VC w/ the proper animation.
Here's the code for the segues:
1st VC
#IBAction func performSegue(_ sender: Any) {
if shouldPerformSegue(withIdentifier: "segue", sender: Any?.self) {
performSegue(withIdentifier: "segue", sender: nil)
}
}
#IBAction func unwindToHomeView(segue: UIStoryboardSegue) {
}
override func unwind(for unwindSegue: UIStoryboardSegue, towardsViewController subsequentVC: UIViewController) {
let segue = SegueFromLeft(identifier: unwindSegue.identifier, source: unwindSegue.source, destination: unwindSegue.destination)
segue.perform()
}
Right to Left Animation
let dst = self.destination
let src = self.source
let containerView = src.view.superview
dst.view.transform = CGAffineTransform(translationX: src.view.frame.size.width, y: 0)
containerView?.addSubview(dst.view)
UIView.animate(withDuration: 0.25, delay: 0.0, options: UIViewAnimationOptions.curveEaseInOut, animations: {
dst.view.transform = CGAffineTransform(translationX: 0, y: 0)
}, completion: { success in
src.present(dst, animated: false, completion: nil)
})
Left to Right Animation
let dst = self.destination
let src = self.source
src.view.superview?.insertSubview(dst.view, at: 0)
UIView.animate(withDuration: 0.25, delay: 0.0, options: UIViewAnimationOptions.curveEaseInOut, animations: {
src.view.transform = CGAffineTransform(translationX: -src.view.frame.size.width, y: 0)
}, completion: { success in
src.dismiss(animated: false, completion: nil)
})
Any help would be greatly appreciated.
You can simply push viewcontroller to navigation controllers where you don't need to explicitly handle left right animation. In VC1 navigationController?.pushViewController(VC2, animated: true) will simply animate VC2 from right to left with handy back button which can be used to animate VC2 left to right displaying VC1.
For my solution I actually did away with any "custom" animations, and fixed my issues mostly just by moving the navigation controller as the initial controller, attached to the VC1.
From there I just pushed to the next view (VC2), and used unwind segue to go back (to VC1).
Basic code for pushing:
let vcName = "Main"
let viewController = storyboard?.instantiateViewController(withIdentifier: vcName)
self.navigationController?.pushViewController(viewController!, animated: true)
I have run into an issue when using a custom segue. I have two tableviews that I'm an trying to switch back and forth from. When I click on a cell in tableview1 it should take me to tableview2. I have a button on tableview2 that connects to the exit of the storyboard. From there it should take me back to tableview1 but whenever I press the button, the application crashes with a BAD_ACCESS error.
Here is my custom segue class:
class TableViewSegue: UIStoryboardSegue {
override func perform() {
scale()
}
func scale () {
let toViewcontroller = self.destination
let fromViewcontroller = self.source
let containerView = fromViewcontroller.view.superview
let originalCenter = fromViewcontroller.view.center
toViewcontroller.view.transform = CGAffineTransform(scaleX: 0.05, y: 0.05)
toViewcontroller.view.center = originalCenter
containerView?.addSubview(toViewcontroller.view)
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: {
toViewcontroller.view.transform = CGAffineTransform.identity
}, completion: { success in
fromViewcontroller.present(toViewcontroller, animated: false, completion: nil) //The application crashes and highlights this line as the error.
})
}
}
I have implemented this method in my tableViewController1:
#IBAction func prepareForUnwind(segue: UIStoryboardSegue) {
}
Not sure why the tableview2 does not dismiss.
EDIT: The issue had to do with needing a navigation controller.
The problem is that you are presenting the toViewcontroller each time a segue is performed. So the app presents table2 over table1, and then tries again to present table1 over table2 on the unwind.
Modify your custom segue to check - essentially - which direction you're going:
class TableViewSegue: UIStoryboardSegue {
override func perform() {
scale()
}
func scale () {
let toViewcontroller = self.destination
let fromViewcontroller = self.source
let containerView = fromViewcontroller.view.superview
let originalCenter = fromViewcontroller.view.center
toViewcontroller.view.transform = CGAffineTransform(scaleX: 0.05, y: 0.05)
toViewcontroller.view.center = originalCenter
containerView?.addSubview(toViewcontroller.view)
let fromP = fromViewcontroller.presentingViewController
UIView.animate(withDuration: 0.5, delay: 0, options: .curveEaseInOut, animations: {
toViewcontroller.view.transform = CGAffineTransform.identity
}, completion: { success in
// if nil, we are presenting a new VC
if fromP == nil {
fromViewcontroller.present(toViewcontroller, animated: false, completion: nil)
} else {
fromViewcontroller.dismiss(animated: false, completion: nil)
}
})
}
}
Note: This is assuming:
you are not trying to push/pop within a UINavigationController ... you'd need to add some other checks to handle that.
you are only going one-level-in, that is, you are not presenting, presenting, presenting, etc. and then trying to unwind.
I've created a custom segue which is a reversed version of a vertical segue, here is my perform function:
var sourceViewController = self.sourceViewController as UIViewController!
var destinationViewController = self.destinationViewController as UIViewController!
sourceViewController.view.addSubview(destinationViewController.view)
destinationViewController.view.frame = sourceViewController.view.frame
destinationViewController.view.transform = CGAffineTransformMakeTranslation(0, -sourceViewController.view.frame.size.height)
destinationViewController.view.alpha = 1.0
UIView.animateWithDuration(0.5, delay: 0.0, options: UIViewAnimationOptions.CurveEaseOut, animations: { () -> Void in
destinationViewController.view.transform = CGAffineTransformMakeTranslation(0, 0)
}) { (finished: Bool) -> Void in
destinationViewController.view.removeFromSuperview()
sourceViewController.presentViewController(destinationViewController, animated: false, completion: nil)
}
When I perform it in my app it works and the animation is exactly what I want but I have this warning in the console:
Unbalanced calls to begin/end appearance transitions for <Custom_Segues.ViewController: 0x7a3f9950>.
I read many posts concerning this problem on Stack Overflow but I didn't find one linked to my situation, does someone know what is the problem? I tried many things on my code and I know the problem is in the two last lines but I don't know what to change.
EDIT/ANSWER:
After reading the answers I found a solution: changing the view then applying the old VC on the new one and do the animation. The code is safe and there is no flash of the old VC at the end of the animation.
var sourceViewController = self.sourceViewController as UIViewController!
var destinationViewController = self.destinationViewController as UIViewController!
var duplicatedSourceView: UIView = sourceViewController.view.snapshotViewAfterScreenUpdates(false) // Screenshot of the old view.
destinationViewController.view.addSubview(duplicatedSourceView) // We add a screenshot of the old view (Bottom) above the new one (Top), it looks like nothing changed.
sourceViewController.presentViewController(destinationViewController, animated: false, completion: {
destinationViewController.view.addSubview(duplicatedSourceView) // We add the old view (Bottom) above the new one (Top), it looks like nothing changed.
UIView.animateWithDuration(0.33, delay: 0.0, options: UIViewAnimationOptions.CurveEaseOut, animations: { () -> Void in
duplicatedSourceView.transform = CGAffineTransformMakeTranslation(0, sourceViewController.view.frame.size.height) // We slide the old view at the bottom of the screen
}) { (finished: Bool) -> Void in
duplicatedSourceView.removeFromSuperview()
}
})
}
This looks runloop timing related.
destinationViewController.view.removeFromSuperview()
sourceViewController.presentViewController(destinationViewController, animated: false, completion: nil)
These two lines should not execute within the same runloop.
destinationViewController.view.removeFromSuperview()
// Force presentViewController() into a different runloop.
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(0.001 * Double(NSEC_PER_SEC)))
dispatch_after(time, dispatch_get_main_queue()) {
sourceViewController.presentViewController(destinationViewController, animated: false, completion: nil)
}
I added this as a comment, but I think it good enough to make a second solution. After looking at How to custom Modal View Controller presenting animation?
The solution converted to swift:
var transition = CATransition()
transition.duration = 1
transition.type = kCATransitionFade
transition.subtype = kCATransitionFromBottom
sourceViewController.view.window?.layer.addAnimation(transition, forKey: kCATransition)
sourceViewController.presentViewController(destinationViewController, animated:false, completion:nil)
You can adjust this to match your needs.