App crashed when using custom presentation transition - ios

I am currently working on a project using custom transition between view controllers. In my storyboard, I control drag a button to another view controller to do a present modally transition, and in the presenting view controller's file I override the prepareForSegue method, the code is:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.destinationViewController is RightEyeViewController {
let rightEyeVC = segue.destinationViewController as RightEyeViewController
rightEyeVC.modalPresentationStyle = .Custom
rightEyeVC.transitioningDelegate = RightEyeTransitioningDelegate()
}
}
And in the UIViewControllerTransitioningDelegate I return the UIViewControllerAnimatedTransitioning that I created, the code is:
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animationController = RightEyeAnimatedTransitioning()
animationController.isPresentation = true
return animationController
}
The problem is every time the app crashes at the line return animationController, but after I add a break point before this line, the transition works without any problems, I don't know what caused this problem, can anyone offer some help!

Related

UINavigationController inside UITabBarController inside UISplitViewController (still) shows detail controller modally instead of pushing

I have what seems to be a very common setup in my universal application, with a root UISplitViewController, using a UITabBarController as a masterViewController, and then I want to:
either push the detail view controller onto the stack if I'm on a vertical iPhone
show the detail controller in the detailViewController of the UISplitViewController on lanscape iPhone 6+ and other larger screens like iPads and such
To that effect, I have exactly the same setup as the ones described in all those discussions that mention a similar issue:
UINavigationController inside a UITabBarController inside a UISplitViewController presented modally on iPhone
iOS8 TabbarController inside a UISplitviewController Master
Adaptive show detail segue transformed to modal instead of push on iPhone when master view controller is a UITabBarController
But none of the solutions mentioned in those questions works. Some of them create an infinite recursive loop and an EXC_BAD_ACCESS. And the latest one I tried simply keeps presenting the detail view controller modally instead of pushing it onto the stack on iPhones. What I did is create a custom UISplitViewController subclass as such:
class RootSplitViewController: UISplitViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.delegate = self
}
}
extension RootSplitViewController: UISplitViewControllerDelegate {
func splitViewController(_ splitViewController: UISplitViewController, showDetail vc: UIViewController, sender: Any?) -> Bool {
if let tabController = splitViewController.viewControllers[0] as? UITabBarController {
if(splitViewController.traitCollection.horizontalSizeClass == .compact) {
tabController.selectedViewController?.show(vc, sender: sender)
} else {
splitViewController.viewControllers = [tabController, vc]
}
}
return true
}
func splitViewController(_ splitViewController: UISplitViewController, separateSecondaryFrom primaryViewController: UIViewController) -> UIViewController? {
if let tabController = splitViewController.viewControllers[0] as? UITabBarController {
if let navController = tabController.selectedViewController as? UINavigationController {
return navController.popViewController(animated: false)
} else {
return nil
}
} else {
return nil
}
}
}
And here is the code in the master view controller to show the detail view controller:
self.performSegue(withIdentifier: "showReference", sender: ["tags": tags, "reference": reference])
Where tags and reference where loaded from Firebase. And of course the "showReference" segue is of the "Show Detail (e.g. Replace)" kind.
The first delegate method is called correctly, as evidenced by the breakpoint that gets hit there when I click an item in the list inside the UITabBarController. And yet the detail view controller still presents modally on iPhone. No problem on iPad though: the detail view controller appears on the right, as expected.
Most of the answers mentioned above are pretty old and some of the solutions are implemented in Objective-C so maybe I did something wrong in the conversion, or something changed in the UISplitViewController implementation since then.
Does anyone have any suggestion?
I figured it out. In fact, it was related to the target view controller I was trying to show. Of the 2 methods I was overriding in UISplitViewControllerDelegate, only the first one was called:
func splitViewController(_ splitViewController: UISplitViewController, showDetail vc: UIViewController, sender: Any?) -> Bool {
if let tabController = splitViewController.viewControllers[0] as? UITabBarController {
if(splitViewController.traitCollection.horizontalSizeClass == .compact) {
tabController.selectedViewController?.show(vc, sender: sender)
} else {
splitViewController.viewControllers = [tabController, vc]
}
}
return true
}
But the view controller I was showing in the first branch of the test was already embedded into a UINavigationController, so I was essentially showing a UINavigationController into another one, and in that case the modal made more sense. So in that case I needed to show the top view controller of the UINavigationController, which I assume was the purpose of the second method I'm overriding in the delegate, but it was never called. So I did it right there with the following implementation:
extension RootSplitViewController: UISplitViewControllerDelegate {
func splitViewController(_ splitViewController: UISplitViewController, showDetail vc: UIViewController, sender: Any?) -> Bool {
if let tabController = splitViewController.viewControllers[0] as? UITabBarController {
if(splitViewController.traitCollection.horizontalSizeClass == .compact) {
if let navController = vc as? UINavigationController, let actualVc = navController.topViewController {
tabController.selectedViewController?.show(actualVc, sender: sender)
navController.popViewController(animated: false)
} else {
tabController.selectedViewController?.show(vc, sender: sender)
}
} else {
splitViewController.viewControllers = [tabController, vc]
}
}
return true
}
}
And that seems to work perfectly, both on iPhones and iPads
Having you tried ShowDetailViewController Method to change the detail view controller in split view controller.
splitViewController.showDetailViewController(vc, sender: self)
If in case your view controller does not contain navigation controller you can also embed it in a navigation controller.
let nav = UINavigationController.init(rootViewController: vc)
splitViewController.showDetailViewController(nav, sender: self)

Custom view controller presentation without animation

I have some custom modal presentation and custom controller to present (subclass of UIViewController). It is it's own transitioning delegate and returns some animated transitioning object and presentation controller. I use animated transitioning object to add presented view to container view when presenting and to remove it when dismissing, and of course to animate it. I use presentation controller to add some helper subview.
public final class PopoverPresentationController: UIPresentationController {
private let touchForwardingView = TouchForwardingView()
override public func presentationTransitionWillBegin() {
super.presentationTransitionWillBegin()
self.containerView?.insertSubview(touchForwardingView, atIndex: 0)
}
}
public final class PopoverAnimatedTransitioning: NSObject, UIViewControllerAnimatedTransitioning {
func setupView(containerView: UIView, presentedView: UIView) {
//adds presented view to container view
}
public func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
//1. setup views
//2. animate presentation or dismissal
}
}
public class PopoverViewController: UIViewController, UIViewControllerTransitioningDelegate {
init(...) {
...
modalPresentationStyle = .Custom
transitioningDelegate = self
}
public func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return PopoverAnimatedTransitioning(forPresenting: true, position: position, fromView: fromView)
}
public func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return PopoverAnimatedTransitioning(forPresenting: false, position: position, fromView: fromView)
}
public func presentationControllerForPresentedViewController(presented: UIViewController, presentingViewController presenting: UIViewController?, sourceViewController source: UIViewController) -> UIPresentationController? {
return PopoverPresentationController(presentedViewController: presented, presentingViewController: presenting, position: position, fromView: fromView)
}
}
Everything works fine when I present the controller with presentViewController and pass true in animated property. But when I want to present it without animation and pass false, UIKit only calls presentationControllerForPresentedViewController method, and does not call animationControllerForPresentedController at all. And as far as presented view is added to views hierarchy and positioned in it in animation transitioning object, which is never created, nothing is presented.
What I'm doing is I'm checking in presentation controller if transition is animated and if not I create animated transitioning object manually and make it to setup views.
override public func presentationTransitionWillBegin() {
...
if let transitionCoordinator = presentedViewController.transitionCoordinator() where !transitionCoordinator.isAnimated() {
let transition = PopoverAnimatedTransitioning(forPresenting: true, position: position, fromView: fromView)
transition.setupView(containerView!, presentedView: presentedView()!)
}
}
It works but I'm not sure if that's the best approach.
Documentation says that presentation controller should be responsible only for doing any additional setup or animation during transition and the main work for presentation should be done in animated transitioning object.
Is it ok to always setup views in presentation controller instead and only animate them in animated transitioning object?
Is there any better way to solve that problem?
Solved that by moving all the logic of views setup from animated transitioning to presentation controller.

Delegate methods are not called (transitioningDelegate)

I'm trying to implement a custom transition between two viewControllers. (Swift 3.0)
Between my two viewControllers I have a UISegue with the kind show (animated = true).
So I set the delegate methods of UIViewControllerTransitioningDelegate in the extension of my first view controller :
extension DocumentsViewController : UIViewControllerTransitioningDelegate { ... }
And I also have implemented the required methods by the protocol :
animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
...
}
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
...
}
Now when the segue is perform, in the firstViewController I'm using the delegate method prepareForSegue to finally set the transitioningDelegate to my `secondViewController, see below :
public override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
if let destination = segue.destination as? DocumentDetailViewController {
destination.transitioningDelegate = self
}
}
I check with breakpoints, the delegate is well setted to my firstViewController.
But the delegate methods of transitioningDelegate in my firstViewController are never fired, I don't know why.
Any ideas ?
PS : In my storyboard, my segue have Animated to true, so this should work, but it doesn't.
Thanks.
SOLVED : A mix of MadreDeDios, Matt and Tushar answers.
1 : As I want to keep the navigation in my app, I have to make conform my first viewController to UINavigationControllerDelegate instead of UIViewControllerTransitioningDelegate. (see MadreDeDios answer's).
2 : According to this protocol, I have implemented the following delegate method :
public func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
...
}
3 : I set the delegate earlier at the viewDidLoad() of my firstViewController (see Matt's answer) :
override public func viewDidLoad() {
super.viewDidLoad()
//...
self.navigationController?.delegate = self
}
4 : I'm using a manual push instead of a segue to display my secondViewController (see Tushar's answer).
Now this works, thank you.
The problem is that you are setting the transitioningDelegate too late. Way too late. It needs to be set very early in the lifetime of the view controller. I advise setting this property in the view controller's initializer.
Because you are using a push segue, I assume you are using a navigation controller as well.
When you are using an UINavigationController, it becomes the reference for every transition, animation, and even your app's orientation.
My advice would be to use your navigation controller as the manager for all your animations. All you need to do is to add these few things:
extension MyNavigationController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
//Check for the good animation
return MyAnimation()
}
}
And inside your MyNavigationController class
override func viewDidLoad() {
super.viewDidLoad()
//This is the key
self.delegate = self
//Only if you want to animate the presentation of your navigation controller itself, the first time it appears:
self.transitioningDelegate = self
}
Try removing the transition of storyboard and execute the view presentation code manually and explicitly mention 'true' for the animated parameter :
presentViewController(documentVC, animated: true, completion: nil)

How to perform segue from container view within a view displayed by navigation controller?

I want to segue from a view container within "H" that is presented using the navigation controller connected to the Split View Controller. How can I accomplish this? I have tried regular performSegueWithIdentifier using locally linked storyboard ID's but that removes the top navigation bar. I want to retain the top navigation bar and execute the segue as if it was done using the master navigation controller (rows that select which view controller is being presented in the detail view).
Any help is greatly appreciated!
Here is an example of how to perform a segue from an embedded ViewController.
ViewController.swift
import UIKit
protocol SegueHandler: class {
func segueToNext(identifier: String)
}
class ViewController: UIViewController, SegueHandler {
func segueToNext(identifier: String) {
self.performSegueWithIdentifier(identifier, sender: self)
}
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "EmbedH" {
let dvc = segue.destinationViewController as! HViewController
dvc.delegate = self
}
}
}
HViewController.swift
import UIKit
class HViewController: UIViewController {
weak var delegate: SegueHandler?
#IBAction func pressH(sender: UIButton) {
delegate?.segueToNext("GoToGreen")
}
}
Setup:
Use delegation to have the HViewController tell its embedding viewController to perform the segue.
Create a protocol called SegueHandler which just describes a class that implements the method segueToNext(identifier: String).
protocol SegueHandler: class {
func segueToNext(identifier: String)
}
Make your viewController implement this protocol by adding it to the class declaration line:
class ViewController: UIViewController, SegueHandler {
and by implementing the required function.
Add a delegate property to HViewController:
weak var delegate: SegueHandler?
Click on the embed segue arrow between ViewController and HViewController. Give it the identifier "EmbedH" in the Attributes Inspector.
Create a show segue between ViewController and the GreenViewController by Control dragging from the viewController icon at the top of ViewController to the GreenViewController. Name this segue "GoToGreen" in the Attributes Inspector.
In prepareForSegue for ViewController, when the "EmbedH" segue happens, set the delegate property of HViewController to self (ViewController).
When the user clicks the H button in the HViewController, call delegate?.segueToNext("GoToGreen") to trigger the segue in ViewController.
Here it is running in the simulator:
I was needing exactly what #vacawama proposed here, though I couldn't reproduce that, I tried exactly your steps but self.delegate?.segueToNext("GoToGreen") got called but neither the protocol itself nor the container view controller. After an entire day searching about this approach I realized the problem was with the swift version. Just replace this:
func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "EmbedH" {
let dvc = segue.destination as! HViewController
dvc.delegate = self
}
}
for this:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "EmbedH" {
let dvc = segue.destination as! HViewController
dvc.delegate = self
}
}
Other detail I was missing was about the embedded segue. Be sure to connect the container View to the HViewController, not the View Controller itself, otherwise the Embed option for segue won't appear.

Why is 'present as popover' segue covering the whole screen?

In my project I have a button on the bottom right side of the screen and i added another uiviewcontroller to the storyboard, did control-drag to the uiviewcontroller I wanted as the popover, then set that viewcontroller size to (300, 300) and checked 'use preferred explicit size'. When I load the app and click the button, the entire screen gets covered by the "popover". I also tried to go into the popoverViewController's .m file and set the size but that didn't work either.
Any ideas?
Edit: Since it looks like I have to have it be full screen that is fine however I am still running into some other problems I was having earlier. My popup screen will come up and I make the background black and alpha as .5 to make it see through however it'll do the animation, then once the animation is finished the screen will go from .5 opacity to completely black and the only thing I can see is the battery icon thing.
The OP uses Objective-C. This answer presents code in swift. Converting swift to Objective-C should be easy.
In the newly added ViewController, under “Simulated Metrics” change “Size” to “Freeform” and “Status Bar” to “None.”
Under “Simulated Size” change your view’s height and width to the actual size you want your popover’s content to be.
Create a segue to the newly added VC. Use segue type as "Present As Popover" and give a name for the segue, for example "popoverSegue".
In the ViewConroller from which this segue is to be triggered, add the UIPopoverPresentationControllerDelegate protocol.
class ViewController: UIViewController, UIPopoverPresentationControllerDelegate {
}
Override the prepareForSegue function to catch your popover segue. Set the modalPresentationStyle to .Popover to explicitly state that you want a popover and then assign the delegate property of the view’s popoverPresentationController to self:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "popoverSegue" {
let popoverViewController = segue.destinationViewController as! UIViewController
popoverViewController.modalPresentationStyle = UIModalPresentationStyle.Popover
popoverViewController.popoverPresentationController!.delegate = self
}
}
Implement the adaptivePresentationStyleForPresentationController function to tell your app that you really want that popover presentation and will accept no substitutions:
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.None
}
Following these, I could get a popup on iPhone which is not full screen but the size set for the ViewController.
Source: iPad Style Popovers on the iPhone with Swift
Thanks to Bharat for the great answer, I personally use a UIStoryboardSegue that does pretty much the same thing. That way, I can change the class of the segue in the storyboard, have what I want, and not pollute my controllers:
class AlwaysPopupSegue : UIStoryboardSegue, UIPopoverPresentationControllerDelegate
{
override init(identifier: String?, source: UIViewController, destination: UIViewController)
{
super.init(identifier: identifier, source: source, destination: destination)
destination.modalPresentationStyle = UIModalPresentationStyle.popover
destination.popoverPresentationController!.delegate = self
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.none
}
}
Swift 3-5 version
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "SEGUE_IDENTIFIER" {
let popoverViewController = segue.destination as! YourViewController
popoverViewController.modalPresentationStyle = UIModalPresentationStyle.popover
popoverViewController.popoverPresentationController!.delegate = self
}
}
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.none
}
Swift 4 Version
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "SegueIdentifier" {
let popoverViewController = segue.destination
popoverViewController.modalPresentationStyle = .popover
popoverViewController.presentationController?.delegate = self
}
}
Don't forget to add
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.none
}
On iPhone you can creat a custom view controller that can manage all the popovers. Since each view controller has its own navigation controller, you can add a new view controller to the app.window.rootviewcontroller as a du view and bring all to front.
If you didn't want to write your own, you can use something like this for instance: http://cocoapods.org/pods/FPPopover
This is Swift 5 code, some/most of the above mentioned solutions are all valid. This is an effort to present whole solution. This example supposes you are using a xib for popover view controller but this would work otherwise as well, say, in prepare for segue. Here's a complete code:
Presenting ViewController:
let popoverVC = PopoverVC(nibName: "popoverVC", bundle: nil)
popoverVC.completionHandler = { [unowned self] (itemIndex : Int?) in
if let itemIndex = itemIndex
{
// Do completion handling
}
}
popoverVC.preferredContentSize = CGSize(width: 200, height: 60)
popoverVC.modalPresentationStyle = .popover
if let pvc = popoverVC.popoverPresentationController {
pvc.permittedArrowDirections = [.down]
pvc.delegate = self
pvc.sourceRect = button.frame
pvc.sourceView = button // Button popover is presented from
present(popoverVC, animated: true, completion: nil)
}
This is important:
extension ViewController: UIPopoverPresentationControllerDelegate {
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return .none
}
}

Resources