i'm fairly new to swift and having some difficulty with unwind segues in a master detail application.
the unwind seems to work fine dismissing my popovers on the iPhone, however when i try the same thing on the iPad, the popover remains.
If i add a dismissViewControllerAnimated to the presenting viewController's unwind handling action, then the iPad version works fine and dismisses the popover, however the iPhone version dismisses the popover and then dismisses the view that presented the popover. i.e. dismisses two views.
I have worked out that the problem is that popover's are automatically dismissed with an unwind when presented modally such as on an iPhone. However when presented as true popovers they don't dismiss with an unwind segue. Could somebody help me figure out how to manage both cases so that only the popover will be dismissed. Thank you very much in advance.
Okay. after a long time working on this i managed to come up with a solution. i used a popoverpresentationcontroller and declared the presenting controller as the delegate. by then adding an additional function forcing the iPhone to use the popover in lieu of the modal presentation, the presentation and dismissal is consistent for the iPhone and iPad. the code is below. I just used a generic UIViewController in the if let vc statement so that I could use this with a popover that's imbedded in a navigation controller also.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let identifier = segue.identifier {
switch identifier {
case "My Segue Identifier" :
if let vc = segue.destinationViewController as? UIViewController {
if let ppc = vc.popoverPresentationController {
ppc.delegate = self
}
}
default: break
}
}
}
additionally you need to add the following function to prevent the modal presentation on the iPhone:
func adaptivePresentationStyleForPresentationController (controller:UIPresentationController)-> UIModalPresentationStyle {
return UIModalPresentationStyle.None
}
Related
I have a UIViewController that implements UITableViewDelegate, UITableViewDataSource and that contains a UITableView as a member variable. When a user click on one of the rows of that table, the app performs a storyboard segue to open the detail view controller. That detail view controller of course has a button in the top left of the screen that is the "back" button to go back up to the UIViewController with the UIViewTable.
So, suppose that I want to programmatically "click" that back button. How exactly would I do that in swift? This is the most recent version of swift (swift 4?) in XCode 10.1.
UPDATE:
So here is how I solved this. As the answers below show, it is possible to use self.navigationController?.popViewController(animated: true) to just return to the previous view controller. What I discovered I also wanted to do, however, was to call a specific method in that view controller so that it executed a certain behavior once it got shown. It turns out that is also possible, but in my case it was a bit tricky, since that prior view controller was actually a UITabBarController. Therefore I had to get the ViewController that I was interested in from the UITabBarController. I did it like this:
let numvc = navigationController!.viewControllers.count
let tvc:UITabBarController = navigationController!.viewControllers[numvc-2] as! UITabBarController
let my_vc: MyCustomVC = tvc.viewControllers![0] as! MyCustomVC
my_vc.some_function()
Here of course MyCustomV is my custom view controller class and some_function() is the method I want to call on that class. Hope this helps someone.
When You run a segue you perform a "pushViewController" method to the next view, so if you want to go back to the previous view programmatically you just have to do is pop the last view like so:
self.navigationController?.popViewController(animated: true)
UPDATE
You just need the if statement if you have multiple segues from that viewController, if not, you can delete and just cast the next view as you wish and set the properties, let the autocomplete write the *prepare(for segue... * method for you, so You don't run into any problems
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "yourSegueName" {
let destinationVC = segue.destination as! CustomViewController
destinationVC.labelExample.text = "Some text I'm sending"
}
}
Are you sure you need to "click" the button?
If all you need is to dismiss details view controller, you can just call navigationController?.popViewController(animated: true)
Or if you want to deal directly with button, you can tell it to send its actions: backButton.sendActions(for: .touchUpInside)
Or if you absolutely need to show button clicking animation, then you will need something like this (you should play and choose suitable delay):
backButton.isHighlighted = true
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) {
backButton.isHighlighted = false
backButton.sendActions(for: .touchUpInside)
}
It's easier to show you a drawing and then explain.
Dashboard Storyboard
I have 2 separate UIViewControllers (i've included just one in the drawing, the other is irrelevant) embedded in container view called ContainerViewController.
Post Storyboard
NewPostViewController shows a UIButton that presents TextPostViewController. As you can see, all of them are embedded in UINavigationControllers. Now, once the completion block of the new post is being called, I have to present the ContainerViewController and it needs to handle it's own logic. The problem is that it's embedded in UINavigationController and once I present it, the UITaBbar is hidden.
I tried to do this:
self.performSegue(withIdentifier: "TextPostToNavContainerVC", sender: nil)
The transition is successful but I'm losing the UITabBar, even though in the DashboardViewController and the ContainerViewController I called:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
tabBarController?.tabBar.isHidden = false
}
What am I doing wrong or is there are better way to do that?
You should instantiate the tab bar controller. not the view controller.
Imagine you're putting a initial view controller ahead of your tab bar controller. Making your tab bar not being pushed
If I undestand it correctly.
You are doing this
Segue connect to a view controller
But you should actually do this Segue connected to a tab bar controller
You can try to add it as a child to control it's frame like this
let textPost = self.storyboard?.instantiateViewController(withIdentifier: "containerID") as! TextPostToNavContainerVC
textPost.view.frame = CGRect(x:20,y:0,width:self.view.frame.width,height:self.view.frame.height-50)
self.view.addSubview(nvc.view)
self.addChildViewController(textPost)
textPost.didMove(toParentViewController: self)
I have a secondary ViewController that appears programmatically via a Storyboard segue:
func actioncall ()
{
performSegue(withIdentifier: "showIgnoreVC", sender: self)
}
)
This function is part of the main ViewController, and is called via an NSNotification from the AppDelegate, which in turn is triggered by a Menu Item click.
However, even though the segue is connected to the main ViewController, the following code does not dismiss the secondary view:
#IBAction func dismiss(_ sender: Any)
{
print("Hello? Gonna close?")
self.presenting?.dismissViewController(self)
}
There are no errors, the function is called upon the correct Dismiss button click, but the secondary view does not dismiss. I have tried every variation of dismissViewController to no avail.
When I use a button on the Main view to activate the same segue, everything works as it is supposed to. I simply do not wish to clutter up the main View with a bunch of buttons.
Any ideas are appreciated, thank you very much.
dismissViewController works only for view controllers, that were presented modally (using Present Modally Segue)
As said in the answer to this SO question, you should dismiss view controllers, presented by Show Segue, like this:
navigationController?.popViewController(animated: true)
dismiss(animated: true, completion: nil)
I'm having a UISplitViewController that contains a UITabBarController as master view.
This UITabBarController contains a UINavigationController. The detail view contains a UINavigationController as well.
On the iPad this works as expected. The show detail segue presents the imageview within the navigation controller on the detail view.
On the iPhone on the other hand I expected that the show detail segue pushes the detail view on the stack of the navigation controller of the master view. But actually it is presented modally over the master view.
When removing the UITabBarController from the storyboard and using the UINavigationController directly in the master view this works.
Has anybody an idea how I could present the detail view on the stack of the master's UINavigationController on an iPhone?
The problem with Peter's solution is that it will fall apart with the iPhone 6 +. How so? With that code, if an iPhone 6 + is in portrait orientation - the detail view pushes onto the navigation stack. All is well, so far. Now, rotate into landscape, and then you'll have the detail view showing as the detail view and the master view.
You'll need the split view controller's delegate to implement two methods:
- (BOOL)splitViewController:(UISplitViewController *)splitViewController showDetailViewController:(UIViewController *)detailVC sender:(id)sender
{
UITabBarController *masterVC = splitViewController.viewControllers[0];
if (splitViewController.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact)
[masterVC.selectedViewController showViewController:detailVC sender:sender];
else
[splitViewController setViewControllers:#[masterVC, detailVC]];
return YES;
}
And now, you'll need to return the top view controller from the selected tab's navigation controller:
- (UIViewController*)splitViewController:(UISplitViewController *)splitViewController separateSecondaryViewControllerFromPrimaryViewController:(UIViewController *)primaryViewController
{
UITabBarController *masterVC = splitViewController.viewControllers[0];
if ([(UINavigationController*)masterVC.selectedViewController viewControllers].count > 1)
return [(UINavigationController*)masterVC.selectedViewController popViewControllerAnimated:NO];
else
return nil; // Use the default implementation
}
With this solution, everything pushes onto the navigation stack when it should and also updates the detail view correctly on the iPad/6+ landscape.
I figured out how to put the detail on to the master's UINavigationController instead of presenting it modally over the UITabBarController.
Using the UISplitViewControllerDelegate method
- splitViewController:showDetailViewController:sender:
In case the UISplitViewController is collapsed get the masters navigation controller and push the detail view onto this navigation controller:
- (BOOL)splitViewController:(UISplitViewController *)splitViewController
showDetailViewController:(UIViewController *)vc
sender:(id)sender {
NSLog(#"UISplitViewController collapsed: %d", splitViewController.collapsed);
// TODO: add introspection
if (splitViewController.collapsed) {
UITabBarController *master = (UITabBarController *) splitViewController.viewControllers[0];
UINavigationController *masterNavigationController = (UINavigationController *)master.selectedViewController;
// push detail view on the navigation controller
//[masterNavigationController pushViewController:vc animated:YES];
// push was not always working (see discussion in answer below), use showViewController instead
[masterNavigationController showViewController:vc sender:sender];
return YES;
}
return NO;
}
The answer of #PeterOettl to his own question put me on the right way and is great for that. So the credit belongs to him.
I have nearly the same storyboard structure as him, but as vc is a navigationController I get a runtime error saying
'Pushing a navigation controller is not supported'
As said, that is because vc is the navigationController of the detail view and not the viewController of the detail view.
Note that I am surprised that #PeterOettl does not get that error in his case also, as the segue given in the storyboard picture, points to the navigation controller of the detail view.
Therefore the code should like that (in Swift) simply adding
let detailViewControllerNavigationController = (vc as UINavigationController).viewControllers[0] as UIViewController
and pushing detailViewControllerNavigationController instead of vc
and the whole code is
func splitViewController(splitViewController: UISplitViewController, showDetailViewController vc: UIViewController, sender: AnyObject?) -> Bool {
println("UISplitViewController collapsed: \(splitViewController.collapsed)")
if (splitViewController.collapsed) {
let master = splitViewController.viewControllers[0] as UITabBarController
let masterNavigationController = master.selectedViewController as UINavigationController
let detailViewControllerNavigationController = (vc as UINavigationController).viewControllers[0] as UIViewController
masterNavigationController.pushViewController(detailViewControllerNavigationController, animated: true)
return true
} else {
return false
}
}
Also note that this code is put in the AppDelegate.swift of the master-detail example of Xcode where a tab bar is added in the master view.
EDIT
In the comments we discussed with #PeterOettl of the difference between .pushViewController and .showViewController.
Apple documentation says :
showViewController:sender:
This method pushes a new view controller
onto the navigation stack in a similar way as the
pushViewController:animated: method. You can call this method directly
if you want but typically this method is called from elsewhere in the
view controller hierarchy when a new view controller needs to be
shown.
Available in iOS 8.0 and later.
I appreciate this discussion thread when I was implementing exactly the same UI structure app, and furthurmore made it adaptive for iPhone 6 Plus rotation and iPad multitasking (Slide Over/Split View, iOS 9 or later).
We have put the full solution (adaptive UISplitViewController with UITabBarController as primary view controller) open sourced on GitHub indievox-inc/TabBarSplitViewController. Thanks!
I implemented #Dreaming In Binary's answer in Swift:
func splitViewController(splitViewController: UISplitViewController, showDetailViewController vc: UIViewController, sender: AnyObject?) -> Bool {
let masterVC = splitViewController.viewControllers[0] as UITabBarController
if splitViewController.traitCollection.horizontalSizeClass == .Compact {
masterVC.selectedViewController?.showViewController(vc, sender: sender)
} else {
splitViewController.viewControllers = [masterVC, vc]
}
return true
}
func splitViewController(splitViewController: UISplitViewController, separateSecondaryViewControllerFromPrimaryViewController primaryViewController: UIViewController!) -> UIViewController? {
let masterVC = splitViewController.viewControllers[0] as UITabBarController
if let navController = masterVC.selectedViewController as? UINavigationController {
if navController.viewControllers.count > 1 {
return navController.popViewControllerAnimated(false)
}
}
return nil
}
How can i implement this popup menu in iphone app like a popover in ipad?
EDIT: This is the best at moment: https://github.com/runway20/PopoverView
iOS 8 and later
Beginning with iOS 8, you can use UIPopoverPresentationController for iPhones in addition to iPads.
Setup
Add a UIBarButtonItem to your main View Controller.
Add another View Controller to the storyboard. Change it to the size that you want the popover to be and add any content that you want it to have. For my example I just added a UILabel. If you want a whole menu, then just add a table view or list of buttons.
Add a segue from the bar button item to the view controller that you will use as the popover. Rather than show, choose Present as Popover.
Select the segue in the storyboard and set the identifier to popoverSegue (or whatever string you called it in the code).
In the Attributes inspector for the popover view controller, check Use Preferred Explicit Size and confirm that it is the size you want it to be.
Code
This is the code for the main view controller that has the bar button item in it.
class ViewController: UIViewController, UIPopoverPresentationControllerDelegate {
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "popoverSegue" {
let popoverViewController = segue.destinationViewController
popoverViewController.modalPresentationStyle = UIModalPresentationStyle.Popover
popoverViewController.popoverPresentationController!.delegate = self
}
}
// MARK: - UIPopoverPresentationControllerDelegate method
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
// Force popover style
return UIModalPresentationStyle.None
}
}
Popover at an arbitrary anchor point
If you want to set the popover to appear somewhere besides a bar button item (on a UIButton for example) then you need to set the sourceView and sourceRect. See this answer for details.
Further reading
The above example comes mostly from the first link.
iPad Style Popovers on the iPhone with Swift
iOS 8 Popover Presentations
UIPopoverPresentationController on iOS 8 iPhone
General overview of popup options in iOS
Have a look at the iPhone UIPopoverController implementation: WEPopover
On iPhone you would generally use a UIActionSheet for a stack of buttons like that. It slides up from the bottom, rather than popping up next to the button, but that's the standard behavior on iPhone.
There is one that is even better than WEPopover. Developed by a company called 50pixels, it is called FPPopover.
You can download FPPopover at https://github.com/50pixels/FPPopover
You would have to manually instantiate a UIView using a custom background image or drawing with transparency, add some UIButtons (or other type of custom view) on top, and also somehow handle all touches outside that view.
Note that is is non-standard UI. An actionsheet would be more HIG compliant.
To get a popover from a right side bar button item on a navigation controller that is part of a tableview controller, the following worked for me for Swift 4 and Xcode 9.
Follow the steps in Suragch answer above (as edited by the Community.)
Do not implement the Segue as shown in the answer above. For some reason, the segue causes the popover to go full screen despite setting the explicit size.
Give your popover view controller a title in Attributes Inspector
Add the following code in the TableView controller where the popup will show.
Modify the string identifier (the one here is referencing a Constant.swift file)
Modify "as! FilterVC" to use the title of the your popover view controller.
/// Shows a filter popover view
#IBAction func filterBtnPressed(_ sender: UIBarButtonItem) {
let popover = storyboard?.instantiateViewController(withIdentifier: FILTER_VC) as! FilterVC
popover.modalPresentationStyle = UIModalPresentationStyle.popover
popover.popoverPresentationController?.backgroundColor = UIColor.green
popover.popoverPresentationController?.delegate = self
popover.popoverPresentationController?.backgroundColor = ColorPalette.Blue.Medium
popover.popoverPresentationController?.sourceView = self.view
popover.popoverPresentationController?.sourceRect = CGRect(x: self.view!.bounds.width, y: 0, width: 0, height: 0)
popover.popoverPresentationController?.permittedArrowDirections = .up
self.present(popover, animated: true)
} }
func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle {
return UIModalPresentationStyle.none
}
You can check WYPopoverController: https://github.com/sammcewan/WYPopoverController
The screenshot above is not a UIActionSheet. It looks like a simple UIView subclass with custom UIButtons on top of it. So go ahead and create the subclass according to your needs and then add it as a subview to your view every time you need it.