I'm trying to figure out how to use the different states of a UISegmentedControl to switch views, similar to how Apple does it in the App Store when switiching between 'Top Paid' and 'Top Free'.
The simplest approach is to have two views that you can toggle their visibility to indicate which view has been selected. Here is some sample code on how it can be done, definitely not an optimized way to handle the views but just to demonstrate how you can use the UISegmentControl to toggle the visible view:
- (IBAction)segmentSwitch:(id)sender {
UISegmentedControl *segmentedControl = (UISegmentedControl *) sender;
NSInteger selectedSegment = segmentedControl.selectedSegmentIndex;
if (selectedSegment == 0) {
//toggle the correct view to be visible
[firstView setHidden:NO];
[secondView setHidden:YES];
}
else{
//toggle the correct view to be visible
[firstView setHidden:YES];
[secondView setHidden:NO];
}
}
You can of course further re-factor the code to hide/show the right view.
In my case my views are quite complex and I cannot just change the hidden property of different views because it would take up too much memory.
I've tried several solutions and non of them worked for me, or performed erratically, specially with the titleView of the navBar not always showing the segmentedControl when pushing/popping views.
I found this blog post about the issue that explains how to do it in the proper way. Seems he had the aid of Apple engineers at WWDC'2010 to come up with this solution.
http://redartisan.com/2010/6/27/uisegmented-control-view-switching-revisited
The solution in this link is hands down the best solution I've found about the issue so far. With a little bit of adjustment it also worked fine with a tabBar at the bottom
Or if its a table, you can reload the table and in cellForRowAtIndex, populate the table from different data sources based on the segment option selected.
One idea is to have the view with the segmented controls have a container view that you fill with the different subviews (add as a sole subview of the container view when the segments are toggled). You can even have separate view controllers for those subviews, though you have to forward on important methods like "viewWillAppear" and "viewWillDisappear" if you need them (and they will have to be told what navigation controller they are under).
Generally that works pretty well because you can lay out the main view with container in IB, and the subviews will fill whatever space the container lets them have (make sure your autoresize masks are set up properly).
Try using SNFSegmentedViewController, an open-source component that does exactly what you're looking for with a setup like UITabBarController.
From the answer of #Ronnie Liew, I create this:
//
// ViewController.m
// ResearchSegmentedView
//
// Created by Ta Quoc Viet on 5/1/14.
// Copyright (c) 2014 Ta Quoc Viet. All rights reserved.
//
#define SIZE_OF_SEGMENT 56
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
#synthesize theSegmentControl;
UIView *firstView;
UIView *secondView;
CGRect leftRect;
CGRect centerRect;
CGRect rightRect;
- (void)viewDidLoad
{
[super viewDidLoad];
leftRect = CGRectMake(-self.view.frame.size.width, SIZE_OF_SEGMENT, self.view.frame.size.width, self.view.frame.size.height-SIZE_OF_SEGMENT);
centerRect = CGRectMake(0, SIZE_OF_SEGMENT, self.view.frame.size.width, self.view.frame.size.height-SIZE_OF_SEGMENT);
rightRect = CGRectMake(self.view.frame.size.width, SIZE_OF_SEGMENT, self.view.frame.size.width, self.view.frame.size.height-SIZE_OF_SEGMENT);
firstView = [[UIView alloc] initWithFrame:centerRect];
[firstView setBackgroundColor:[UIColor orangeColor]];
secondView = [[UIView alloc] initWithFrame:rightRect];
[secondView setBackgroundColor:[UIColor greenColor]];
[self.view addSubview:firstView];
[self.view addSubview:secondView];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction)segmentSwitch:(UISegmentedControl*)sender {
NSInteger selectedSegment = sender.selectedSegmentIndex;
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.2];
if (selectedSegment == 0) {
//toggle the correct view to be visible
firstView.frame = centerRect;
secondView.frame = rightRect;
}
else{
//toggle the correct view to be visible
firstView.frame = leftRect;
secondView.frame = centerRect;
}
[UIView commitAnimations];
}
#end
Assign .H in
UISegmentedControl *lblSegChange;
- (IBAction)segValChange:(UISegmentedControl *) sender
Declare .M
- (IBAction)segValChange:(UISegmentedControl *) sender
{
if(sender.selectedSegmentIndex==0)
{
viewcontroller1 *View=[[viewcontroller alloc]init];
[self.navigationController pushViewController:view animated:YES];
}
else
{
viewcontroller2 *View2=[[viewcontroller2 alloc]init];
[self.navigationController pushViewController:view2 animated:YES];
}
}
Swift version:
The parent view controller is responsible for setting the size and position of the view of each child view controller. The view of the child view controller becomes part of the parent view controller's view hierarchy.
Define lazy properties:
private lazy var summaryViewController: SummaryViewController = {
// Load Storyboard
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
// Instantiate View Controller
var viewController = storyboard.instantiateViewController(withIdentifier: "SummaryViewController") as! SummaryViewController
// Add View Controller as Child View Controller
self.add(asChildViewController: viewController)
return viewController
}()
private lazy var sessionsViewController: SessionsViewController = {
// Load Storyboard
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.main)
// Instantiate View Controller
var viewController = storyboard.instantiateViewController(withIdentifier: "SessionsViewController") as! SessionsViewController
// Add View Controller as Child View Controller
self.add(asChildViewController: viewController)
return viewController
}()
Show/Hide Child View Controllers:
private func add(asChildViewController viewController: UIViewController) {
// Add Child View Controller
addChildViewController(viewController)
// Add Child View as Subview
view.addSubview(viewController.view)
// Configure Child View
viewController.view.frame = view.bounds
viewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
// Notify Child View Controller
viewController.didMove(toParentViewController: self)
}
private func remove(asChildViewController viewController: UIViewController) {
// Notify Child View Controller
viewController.willMove(toParentViewController: nil)
// Remove Child View From Superview
viewController.view.removeFromSuperview()
// Notify Child View Controller
viewController.removeFromParentViewController()
}
Manage SegmentedControl tapEvent
private func updateView() {
if segmentedControl.selectedSegmentIndex == 0 {
remove(asChildViewController: sessionsViewController)
add(asChildViewController: summaryViewController)
} else {
remove(asChildViewController: summaryViewController)
add(asChildViewController: sessionsViewController)
}
}
And of course you are able to use within your child view controller classes:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("Summary View Controller Will Appear")
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
print("Summary View Controller Will Disappear")
}
Reference:
https://cocoacasts.com/managing-view-controllers-with-container-view-controllers/
A quick Swift Version:
#IBAction func segmentControlValueChanged(_ sender: UISegmentedControl) {
if segmentControl.selectedSegmentIndex == 0 {
// do something
} else {
// do something else
}
}
Related
The top three answers can solve my questions. It is hard to pick which one is the best. So, I just pick the one who is the first to answer my question. Sorry for amateur and iOSEnthusiatic. Thank you for your help. I appreciate it.
ViewController 1 has a table view.
My question is how to reload the table view only if I click back from view controller 2, and not reload the table view if I click back from view controller 3.
Right now, my code for back button is
#IBAction func backButtonTapped(sender: AnyObject) {
self.dismissViewControllerAnimated(true, completion: nil)
}
In view controller 1. I know that the table view would be reloaded from either view controller 2 or 3
override func viewDidAppear(animated: Bool) {
loadTable()
}
I tried to put loadTable() in viewDidLoad and try to write the below code for back button in view controller 2. But, it doesn't work.
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateViewControllerWithIdentifier("UserHomePageViewController") as! UserHomePageViewController
controller.viewDidLoad()
Any suggestion what I should do? Thank you for your help.
EDIT:
I think this is an easier way to do it, but it still does not work as I thought. I guess it is because the viewDidAppear is executed before the call of reloadTableBool. Correct? Is there any way to fix it? Thank you. You help would be appreciated.
class 2ViewController
#IBAction func backButtonTapped(sender: AnyObject) {
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = storyboard.instantiateViewControllerWithIdentifier("1ViewController") as! 1ViewController
print("viewcontroller2 before call: \(controller.reloadTableBool)")
controller.reloadTableBool = false
print("viewcontroller2 after call: \(controller.reloadTableBool)")
self.dismissViewControllerAnimated(true, completion: nil)
}
class 1ViewController
var reloadTableBool = true
override func viewDidAppear(animated: Bool) {
print("viewcontroller1: \(reloadTableBool)")
if reloadTableBool == true {
loadTable()
}
}
When I click back on view controller 2, it prints
viewcontroller2 before call: true
viewcontroller2 after call: false
viewcontroller1: true
Here is a link to a question I answered a couple days ago. Use the navigation controller delegate to handle the back button. In your second view controller, set the delegate to self and reload the tableview when you press the back button.
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.delegate = self
}
func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
if let controller = viewController as? FirstViewController {
controller.tableView.reloadData()
}
}
NOTE:
I'm assuming you're using the back button of the navigation controller here.
EDIT: Another example using your manually added back button:
#IBAction func backButtonTapped(sender: AnyObject) {
if let viewControllers = app.window?.rootViewController?.childViewControllers {
viewControllers.forEach { ($0 as? FirstViewController)?.tableView.reloadData() }
}
self.dismissViewControllerAnimated(true, completion: nil)
}
Seeing as you are using a navigation controller:
#IBAction func backButtonTapped(sender: AnyObject) {
navigationController?.viewControllers.forEach { ($0 as? FirstViewController)?.tableView.reloadData() }
self.dismissViewControllerAnimated(true, completion: nil)
}
If displaying vc2 is performed by vc1 and is always sure to invalidate the data in vc1, you could do the following:
add a needsReload boolean instance variable to vc1
set it to true whenever you display vc2 (and when instanciating vc1 eg in awakeFromNib if coming from a storyboard)
only perform the content of loadTable if needsReload is true (maybe refactor this logic into a loadTableIfNeeded)
don't forget to set needsReload to false in the end of loadTableIfNeeded
This invalidation pattern is found throughout UIKit, see for example UIView setNeedsLayout/layoutIfNeeded. The advantage is that even if several events cause the data to invalidate, it will only actually get refreshed when you need it.
In your situation it has the additional advantage of keeping the logic contained in vc1 and not creating unnecessary coupling between your VCs, which is always good.
---UPDATE: sample implementation (ObjC but you'll get the idea)
You only need to handle this in VC1, forget about all the back button stuff in VC2. This implementation will mark VC1 for reload as soon as VC2 is presented, but will actually reload only on viewWillAppear, when VC2 is dismissed.
---UPDATE 2: Added a conditional reload based on a delegate callback
Note that _needsReload is now set in the delegate callback, not when VC2 is first presented. Instead we set VC1 as the delegate of VC2. (_needsReload logic is actually unnecessary using this method, kept it for reference)
//VC2: add a delegate to the interface
#class VC2;
#protocol VC2Delegate
- (void) viewController:(VC2*)myVC2 didFinishEditingWithChanges:(BOOL)hasChanges;
#end
#interface VC2
#property (nonatomic, weak) id<VC2Delegate> delegate
#end
#implementation VC2
- (IBAction) finishWithChanges
{
[self.delegate viewController:self didFinishEditingWithChanges:YES];
}
- (IBAction) finishWithoutChanges
{
[self.delegate viewController:self didFinishEditingWithChanges:NO];
}
#end
//VC1: implement the VC2Delegate protocol
#interface VC1 () <VC2Delegate>
#end
#implementation VC1
{
BOOL _needsReload
}
- (void) awakeFromNib
{
//adding this for completeness but the way you did it in Swift (at init) is correct
[super awakeFromNib];
_needsReload = YES;
}
- (void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self reloadTableIfNeeded];
}
- (IBAction) displayVC2
{
VC2* myVC2 = ... //instanciate your VC2 here
myVC2.delegate = self; //set as the delegate of VC2
[self presentViewController:myVC2 animated:YES completion:nil];
}
- (void) viewController:(VC2*)myVC2 didFinishEditingWithChanges:(BOOL)hasChanges
{
_needsReload = hasChanges;
[self reloadTableIfNeeded];
}
- (void) reloadTableIfNeeded
{
if (_needsReload) {
[self.tableView reloadData];
_needsReload = NO;
}
}
#end
You can use notification approach easily for this.
Add observer in your 1st ViewController in viewDidLoad method.
NSNotificationCenter.defaultCenter().addObserver(self, selector: "reloadTable:", name: "reloadTable", object: nil)
func reloadTable(notification : NSNotification){
let isReload : NSNumber = notification.userInfo!["isReload"] as! NSNumber
if (isReload.boolValue) {
self.tableView.reloadData()
}
}
Then post notification like this from your 2nd and 3rd ViewController respectively when you call dismissViewController.
// From 2nd viewcontroller
NSNotificationCenter.defaultCenter().postNotificationName("reloadTable", object: nil, userInfo: ["isReload" : NSNumber(bool: false)])
// From 3rd viewcontroller
NSNotificationCenter.defaultCenter().postNotificationName("reloadTable", object: nil, userInfo: ["isReload" : NSNumber(bool: true)])
I'm using a storyboard segue that presents a view controller as popover. The seque has a custom UIView as its anchor. On pre-iOS9 the popover would correctly point to the centre-bottom of the custom UIView (presented below the UIView). On iOS9 it points to the top-left corner of the UIView.
I did try to trace all selector calls to the custom UIView to find out if there is anything I may need to implement in my custom UIView to provide the 'hotspot' for the popover but couldn't find anything
Any ideas..? Thanks
Thanks to #Igor Camilo his reply - in case it's useful to some, this is how I fixed this in my code:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
UIPopoverPresentationController* possiblePopOver = segue.destinationViewController.popoverPresentationController;
if (possiblePopOver != nil) {
//
// iOS9 -- ensure correct sourceRect
//
possiblePopOver.sourceRect = possiblePopOver.sourceView.bounds;
}
...
}
Example: 'Short' button triggers a popover, the popover points to the top-left corner of 'Sort' control
I had the exact same problem. I just resolved it by setting sourceRect in prepareForSegue:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
switch segue.identifier {
case "Popover Identifier"?:
if #available(iOS 9.0, *) {
segue.destinationViewController?.popoverPresentationController?.sourceRect = anchorView.bounds
}
default:
break
}
}
Had the same issue but my app has a multitude of pop-over so I created a centralized function for the fix (but still had to use it on every view controller that had pop overs).
// Fix for IOS 9 pop-over arrow anchor bug
// ---------------------------------------
// - IOS9 points pop-over arrows on the top left corner of the anchor view
// - It seems that the popover controller's sourceRect is not being set
// so, if it is empty CGRect(0,0,0,0), we simply set it to the source view's bounds
// which produces the same result as the IOS8 behaviour.
// - This method is to be called in the prepareForSegue method override of all
// view controllers that use a PopOver segue
//
// example use:
//
// override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?)
// {
// fixIOS9PopOverAnchor(segue)
// }
//
extension UIViewController
{
func fixIOS9PopOverAnchor(segue:UIStoryboardSegue?)
{
guard #available(iOS 9.0, *) else { return }
if let popOver = segue?.destinationViewController.popoverPresentationController,
let anchor = popOver.sourceView
where popOver.sourceRect == CGRect()
&& segue!.sourceViewController === self
{ popOver.sourceRect = anchor.bounds }
}
}
Here's an example of Igor Camilo's snippet in Objective-C.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
// If the sender is a UIView, we might have to correct the sourceRect of
// a potential popover being presented due to an iOS 9 bug. See:
// https://openradar.appspot.com/22095792 and http://stackoverflow.com/a/32698841/368674
if ([sender isKindOfClass:UIView.class]) {
// Fetch the destination view controller
UIViewController *destinationViewController = [segue destinationViewController];
// If there is indeed a UIPopoverPresentationController involved
if ([destinationViewController respondsToSelector:#selector(popoverPresentationController)]) {
// Fetch the popover presentation controller
UIPopoverPresentationController *popoverPresentationController =
destinationViewController.popoverPresentationController;
// Set the correct sourceRect given the sender's bounds
popoverPresentationController.sourceRect = ((UIView *)sender).bounds;
}
}
}
Here's my solution, inside prepareForSegue:
segue.destinationViewController.popoverPresentationController?.sourceRect = CGRectMake(anchorView.frame.size.width/2, anchorView.frame.size.height, 0, 0)
This will move the pointer to the bottom middle of the anchor view
I also encountered this problem but now it works when I added this to my PrepareForSegue function. Given that my Segue ID contains string Popover
if ([[segue identifier] containsString:#"Popover"]) {
[segue destinationViewController].popoverPresentationController.sourceRect = self.navigationItem.titleView.bounds;
}
If you reference your popover UIViewController in your main UIViewController you can adjust the sourceRect property to offset the popover. For example, given popover popoverVC you can do something like so:
float xOffset = 10.0;
float yOffset = 5.0;
popoverVC.popoverPresentationController.sourceRect = CGMakeRect(xOffset, yOffset, 0.0, 0.0);
Try to set width and height anchor of your source rect (UIView or UIBarButtonItem ) and set it to active .Set it when you are initialising your UIView or UIBarButtonItem.
UIBarButtonItem
[[youruibarbuttonitem.customView.widthAnchor constraintEqualToConstant:youruibarbuttonitem.customView.bounds.size.width] setActive:YES];
[[youruibarbuttonitem.customView.heightAnchor constraintEqualToConstant:youruibarbuttonitem.customView.bounds.size.height] setActive:YES];
UIView
[[uiview.widthAnchor constraintEqualToConstant:uiview.bounds.size.width] setActive:YES];
[[uiview.heightAnchor constraintEqualToConstant:uiview.bounds.size.height] setActive:YES];
A bit of a more updated answer for Swift 3! Pardon all the casting
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "popSegue" {
let popoverViewController = segue.destination
popoverViewController.popoverPresentationController?.delegate = self
segue.destination.popoverPresentationController?.sourceRect = ((sender as? UIButton)?.bounds)!
}
}
just to update to an actual example with working code for any UIView
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
switch segue.identifier {
case "PopoverSegueId"?:
if #available(iOS 9.0, *) {
segue.destination.popoverPresentationController?.sourceRect = (segue.destination.popoverPresentationController?.sourceView?.bounds)!
}
default:
break
}
}
I use the following code to present a viewcontroller.
My problem is: After the animation completes, the transparent main background becomes opaque black.
How can I fix this and make it stay as clearColor?
UIViewController *menuViewController=[[UIViewController alloc]init];
menuViewController.view.backgroundColor=[UIColor clearColor];
menuViewController.view.tintColor=[UIColor clearColor];
menuViewController.view.opaque=NO;
UIView *menuView=[[UIView alloc]initWithFrame:CGRectMake(0,[UIScreen mainScreen].bounds.size.height-200,320,200)];
menuView.backgroundColor=[UIColor redColor];
[menuViewController.view addSubview:menuView];
[self presentViewController:menuViewController animated:YES completion:nil];
update: I am trying to see the contents of "self" (the presenter viewcontroller's view).
Update for iOS 15
Apple has introduced a new API, UISheetPresentationController, which makes it rather trivial to achieve a half-sized (.medium()) sheet presentation. If you are after something more custom, then the original answer is what you need.
let vc = UIViewController()
if let sheet = vc.presentationController as? UISheetPresentationController {
sheet.detents = [.medium()]
}
self.present(vc, animated: true, completion: nil)
Original Answer
You can present a view controller, and still have the original view controller visible underneath, like a form, in iOS 7. To do so, you will need to do two things:
Set the modal presentation style to custom:
viewControllerToPresent.modalPresentationStyle = UIModalPresentationCustom;
Set the transitioning delegate:
viewControllerToPresent.transitioningDelegate = self;
In this case, we have set the delegate to self, but it can be another object. The delegate needs to implement the two required methods of the protocol, possible like so:
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
SemiModalAnimatedTransition *semiModalAnimatedTransition = [[SemiModalAnimatedTransition alloc] init];
semiModalAnimatedTransition.presenting = YES;
return semiModalAnimatedTransition;
}
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
SemiModalAnimatedTransition *semiModalAnimatedTransition = [[SemiModalAnimatedTransition alloc] init];
return semiModalAnimatedTransition;
}
At this point you may be thinking, where did that SemiModalAnimatedTransition class come from. Well, it is a custom implementation adopted from teehan+lax's blog.
Here is the class's header:
#interface SemiModalAnimatedTransition : NSObject <UIViewControllerAnimatedTransitioning>
#property (nonatomic, assign) BOOL presenting;
#end
And the implementation:
#import "SemiModalAnimatedTransition.h"
#implementation SemiModalAnimatedTransition
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
{
return self.presenting ? 0.6 : 0.3;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext
{
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
CGRect endFrame = fromViewController.view.bounds;
if (self.presenting) {
fromViewController.view.userInteractionEnabled = NO;
[transitionContext.containerView addSubview:fromViewController.view];
[transitionContext.containerView addSubview:toViewController.view];
CGRect startFrame = endFrame;
startFrame.origin.y = endFrame.size.height;
toViewController.view.frame = startFrame;
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromViewController.view.tintAdjustmentMode = UIViewTintAdjustmentModeDimmed;
toViewController.view.frame = endFrame;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
else {
toViewController.view.userInteractionEnabled = YES;
[transitionContext.containerView addSubview:toViewController.view];
[transitionContext.containerView addSubview:fromViewController.view];
endFrame.origin.y = endFrame.size.height;
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
toViewController.view.tintAdjustmentMode = UIViewTintAdjustmentModeAutomatic;
fromViewController.view.frame = endFrame;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
}
#end
Not the most straightforward solution, but avoids hacks and works well. The custom transition is required because by default iOS will remove the first view controller at the end of the transition.
Update for iOS 8
For iOS 8, once again the landscape has changed. All you need to do is use the new presentation style .OverCurrentContext, ie:
viewControllerToPresent.modalPresentationStyle = UIModalPresentationOverCurrentContext;
Update
In most cases, you're going to want to follow the guidelines from Ric's answer, below. As he mentions, menuViewController.modalPresentationStyle = .overCurrentContext is the simplest modern way to keep the presenting view controller visible.
I'm preserving this answer because it provided the most direct solution to the OPs problem, where they already had a view managed by the current view controller and were just looking for a way to present it, and because it explains the actual cause problem.
As mentioned in the comments, this isn't a transparency problem (otherwise you would expect the background to become white). When the presentViewController:animated:completion: animation completes, the presenting view controller is actually removed from the visual stack. The black you're seeing is the window's background color.
Since you appear to just be using menuViewController as a host for menuView to simplify the animation, you could consider skipping menuViewController, adding menuView to your existing view controllers view hierarchy, and animate it yourself.
This is a fairly simple problem to solve. Rather than creating a custom view transition, you just need to set the modalPresentationStyle for the view controller being presented.
Also, you should set the background color (and alpha value) for the view controller being presented, in the storyboard / via code.
CustomViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.blackColor().colorWithAlphaComponent(0.6)
}
}
In the IBAction component of presenting view controller -
let vc = storyboard?.instantiateViewControllerWithIdentifier("customViewController") as! CustomViewController
vc.modalPresentationStyle = UIModalPresentationStyle.Custom
presentViewController(vc, animated: true, completion: nil)
You should use a property modalPresentationStyle available since iOS 3.2.
For example:
presenterViewController.modalPresentationStyle = UIModalPresentationCurrentContext;
[presenterViewController presentViewController:loginViewController animated:YES completion:NULL];
Try making top view transparent and add a another view below your desired view and make that view's background color black and set alpha 0.5 or whatever opacity level you like.
It's quite an old post and thanks to Ric's answer, it still works well, but few fixes are required to run it on iOS 14. I suppose it works fine on lower versions of iOS, but I hadn't a chance to test it, since my deployment target is iOS 14.
OK, here is an updated solution in Swift:
final class SemiTransparentPopupAnimator: NSObject, UIViewControllerAnimatedTransitioning {
var presenting = false
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return presenting ? 0.4 : 0.2
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
else {
return
}
var endFrame = fromVC.view.bounds
if presenting {
fromVC.view.isUserInteractionEnabled = false
transitionContext.containerView.addSubview(toVC.view)
var startFrame = endFrame
startFrame.origin.y = endFrame.size.height
toVC.view.frame = startFrame
UIView.animate(withDuration: transitionDuration(using: transitionContext)) {
fromVC.view.tintAdjustmentMode = .dimmed
toVC.view.frame = endFrame
} completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
} else {
toVC.view.isUserInteractionEnabled = true
endFrame.origin.y = endFrame.size.height
UIView.animate(withDuration: transitionDuration(using: transitionContext)) {
toVC.view.tintAdjustmentMode = .automatic
fromVC.view.frame = endFrame
} completion: { _ in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}
}
}
}
I need to perform a Popover segue when user touches a cell in a dynamic TableView. But when I try to do this with this code:
- (void)tableView:(UITableView *)tableview didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[self performSegueWithIdentifier:#"toThePopover" sender:[tableView cellForRowAtIndexPath]];
//...
}
than I get an error:
Illegal Configuration
Popover segue with no anchor
Is there any way to do this (to perform a popover segue from dynamic TableView manually)?
I was faced with this same issue tonight, there a couple workarounds (including presenting the popover the old fashioned way).
For this example, I have an object that is stored in my custom cell class. When the cell is selected I call a function like this to open details in a popOverViewController about the object, and point (anchor) to it's corresponding cell in the table.
- (void)openCustomPopOverForIndexPath:(NSIndexPath *)indexPath{
CustomViewController* customView = [[self storyboard] instantiateViewControllerWithIdentifier:#"CustomViewController"];
self.myPopOver = [[UIPopoverController alloc]
initWithContentViewController:customView];
self.myPopOver.delegate = self;
//Get the cell from your table that presents the popover
MyCell *myCell = (MyCell*)[self.tableView cellForRowAtIndexPath:indexPath];
CGRect displayFrom = CGRectMake(myCell.frame.origin.x + myCell.frame.size.width, myCell.center.y + self.tableView.frame.origin.y - self.tableView.contentOffset.y, 1, 1);
[self.myPopOver presentPopoverFromRect:displayFrom
inView:self.view permittedArrowDirections:UIPopoverArrowDirectionLeft animated:YES];
}
The problem with this method is that we often need the popover view to have a custom initializer. This is problematic if you want your view to be designed in storyboard instead of a xib and have a custom init method that takes your cells associated object as a parameter to use for it's display. You also can't just use a popover segue (at first glance) because you need a dynamic anchor point (and you can't anchor to a cell prototype). So here is what I did:
First, create a hidden 1px X 1px UIButton in your view controllers view. (important to give the button constraints that will allow it to be moved anywhere in the view)
Then make an outlet for the button (I called mine popOverAnchorButton) in your view controller and control drag a segue from the hidden button to the view controller you wish to segue to. Make it a popOver segue.
Now you have a popover segue with a 'legal' anchor. The button is hidden, so no one can touch it accidentally. You are only using this for an anchor point.
Now just call your segue manually in your function like this.
- (void)openCustomPopOverForIndexPath:(NSIndexPath *)indexPath{
//Get the cell from your table that presents the popover
MyCell *myCell = (MyCell*)[self.tableView cellForRowAtIndexPath:indexPath];
//Make the rect you want the popover to point at.
CGRect displayFrom = CGRectMake(myCell.frame.origin.x + myCell.frame.size.width, myCell.center.y + self.tableView.frame.origin.y - self.tableView.contentOffset.y, 1, 1);
//Now move your anchor button to this location (again, make sure you made your constraints allow this)
self.popOverAnchorButton.frame = displayFrom;
[self performSegueWithIdentifier:#"CustomPopoverSegue" sender:myCell];
}
And...... Voila. Now you are using the magic of segues with all their greatness and you have a dynamic anchor point that appears to point to your cell.
now in -(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender you can simply cast the sender to your cell's class (given that you do the proper checks on sender type and which segue is being called) and give the segue's destinationViewController the cell's object.
Let me know if this helps, or anyone has any feedback or improvements.
Just adding this answer as an alternative way to present a popover from a touched cell, though it uses code rather than a segue. It's pretty simple though and has worked for me from iOS 4 through iOS 7:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:NO];
//get the data of the row they clicked from the array
Url* clickedRec = [self.resultsArray objectAtIndex:indexPath.row];
//hide the popover in case it was already opened from a previous touch.
if (self.addNewPopover.popoverVisible) {
[self.addNewPopover dismissPopoverAnimated:YES];
return;
}
//instantiate a view controller from the storyboard
AddUrlViewController *viewControllerForPopover =
[self.storyboard instantiateViewControllerWithIdentifier:#"addUrlPopup"];
//set myself as the delegate so I can respond to the cancel and save touches.
viewControllerForPopover.delegate=self;
//Tell the view controller that this is a record edit, not an add
viewControllerForPopover.addOrEdit = #"Edit";
//Pass the record data to the view controller so it can fill in the controls
viewControllerForPopover.existingUrlRecord = clickedRec;
UIPopoverController *popController = [[UIPopoverController alloc]
initWithContentViewController:viewControllerForPopover];
//keep a reference to the popover since I'm its delegate
self.addNewPopover = popController;
//Get the cell that was clicked in the table. The popover's arrow will point to this cell since it was the one that was touched.
UITableViewCell *clickedCell = [self.tableView cellForRowAtIndexPath:indexPath];
//present the popover from this cell's frame.
[self.addNewPopover presentPopoverFromRect:clickedCell.frame inView:self.myTableView permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}
Swift answer using popoverPresentationController: Using storyboard, set up the new view controller with a Storyboard ID of popoverEdit.
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let fromRect:CGRect = self.tableView.rectForRowAtIndexPath(indexPath)
let popoverVC = storyboard?.instantiateViewControllerWithIdentifier("popoverEdit") as! UIViewController
popoverVC.modalPresentationStyle = .Popover
presentViewController(popoverVC, animated: true, completion: nil)
let popoverController = popoverVC.popoverPresentationController
popoverController!.sourceView = self.view
popoverController!.sourceRect = fromRect
popoverController!.permittedArrowDirections = .Any
}
I have made this in the simplest way:
Make this Popover Presentation Segue in Storyboard as usually but drag from ViewController (not button)
select anchor view as table view
then in table view cell's button touch make:
private func presentCleaningDateDatePicker(from button: UIButton) {
performSegue(withIdentifier: "Date Picker Popover Segue", sender: button)
}
and implement prepare(for segue) method
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let identifier = segue.identifier {
switch identifier {
case "Date Picker Popover Segue":
if let vc = segue.destination as? DatePickerViewController {
if let ppc = vc.popoverPresentationController {
ppc.sourceView = sender as! UIButton
ppc.sourceRect = (sender as! UIButton).frame
ppc.delegate = vc
vc.minimumDate = Date()
vc.maximumDate = Date().addMonth(n: 3)
vc.delegate = self
}
}
default:
break
}
}
}
I have the code below that hides and shows the navigational bar. It is hidden when the first view loads and then hidden when the "children" get called. Trouble is that I cannot find the event/action to trigger it to hide again when they get back to the root view....
I have a "test" button on the root page that manually does the action but it is not pretty and I want it to be automatic.
-(void)hideBar
{
self.navController.navigationBarHidden = YES;
}
-(void)showBar
{
self.navController.navigationBarHidden = NO;
}
The nicest solution I have found is to do the following in the first view controller.
Objective-C
- (void)viewWillAppear:(BOOL)animated {
[self.navigationController setNavigationBarHidden:YES animated:animated];
[super viewWillAppear:animated];
}
- (void)viewWillDisappear:(BOOL)animated {
[self.navigationController setNavigationBarHidden:NO animated:animated];
[super viewWillDisappear:animated];
}
Swift
override func viewWillAppear(_ animated: Bool) {
self.navigationController?.setNavigationBarHidden(true, animated: animated)
super.viewWillAppear(animated)
}
override func viewWillDisappear(_ animated: Bool) {
self.navigationController?.setNavigationBarHidden(false, animated: animated)
super.viewWillDisappear(animated)
}
This will cause the navigation bar to animate in from the left (together with the next view) when you push the next UIViewController on the stack, and animate away to the left (together with the old view), when you press the back button on the UINavigationBar.
Please note also that these are not delegate methods, you are overriding UIViewController's implementation of these methods, and according to the documentation you must call the super's implementation somewhere in your implementation.
Another approach I found is to set a delegate for the NavigationController:
navigationController.delegate = self;
and use setNavigationBarHidden in navigationController:willShowViewController:animated:
- (void)navigationController:(UINavigationController *)navigationController
willShowViewController:(UIViewController *)viewController
animated:(BOOL)animated
{
// Hide the nav bar if going home.
BOOL hide = viewController != homeViewController;
[navigationController setNavigationBarHidden:hide animated:animated];
}
Easy way to customize the behavior for each ViewController all in one place.
One slight tweak I had to make on the other answers is to only unhide the bar in viewWillDisappear if the reason it is disappearing is due to a navigation item being pushed on it. This is because the view can disappear for other reasons.
So I only unhide the bar if this view is no longer the topmost view:
- (void) viewWillDisappear:(BOOL)animated
{
if (self.navigationController.topViewController != self)
{
[self.navigationController setNavigationBarHidden:NO animated:animated];
}
[super viewWillDisappear:animated];
}
I would put the code in the viewWillAppear delegate on each view being shown:
Like this where you need to hide it:
- (void)viewWillAppear:(BOOL)animated
{
[yourObject hideBar];
}
Like this where you need to show it:
- (void)viewWillAppear:(BOOL)animated
{
[yourObject showBar];
}
The currently accepted answer does not match the intended behavior described in the question. The question asks for the navigation bar to be hidden on the root view controller, but visible everywhere else, but the accepted answer hides the navigation bar on a particular view controller. What happens when another instance of the first view controller is pushed onto the stack? It will hide the navigation bar even though we are not looking at the root view controller.
Instead, #Chad M.'s strategy of using the UINavigationControllerDelegate is a good one, and here is a more complete solution. Steps:
Subclass UINavigationController
Implement the -navigationController:willShowViewController:animated method to show or hide the navigation bar based on whether it is showing the root view controller
Override the initialization methods to set the UINavigationController subclass as its own delegate
Complete code for this solution can be found in this Gist. Here's the navigationController:willShowViewController:animated implementation:
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
/* Hide navigation bar if root controller */
if ([viewController isEqual:[self.viewControllers firstObject]]) {
[self setNavigationBarHidden:YES animated:animated];
} else {
[self setNavigationBarHidden:NO animated:animated];
}
}
in Swift 3:
override func viewWillAppear(_ animated: Bool) {
navigationController?.navigationBar.isHidden = true
super.viewWillAppear(animated)
}
override func viewWillDisappear(_ animated: Bool) {
if (navigationController?.topViewController != self) {
navigationController?.navigationBar.isHidden = false
}
super.viewWillDisappear(animated)
}
Give my credit to #chad-m 's answer.
Here is the Swift version:
Create a new file MyNavigationController.swift
import UIKit
class MyNavigationController: UINavigationController, UINavigationControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.delegate = self
}
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
if viewController == self.viewControllers.first {
self.setNavigationBarHidden(true, animated: animated)
} else {
self.setNavigationBarHidden(false, animated: animated)
}
}
}
Set your UINavigationController's class in StoryBoard to MyNavigationController
That's it!
Difference between chad-m's answer and mine:
Inherit from UINavigationController, so you won't pollute your rootViewController.
use self.viewControllers.first rather than homeViewController, so you won't do this 100 times for your 100 UINavigationControllers in 1 StoryBoard.
After multiple trials here is how I got it working for what I wanted.
This is what I was trying.
- I have a view with a image. and I wanted to have the image go full screen.
- I have a navigation controller with a tabBar too. So i need to hide that too.
- Also, my main requirement was not just hiding, but having a fading effect too while showing and hiding.
This is how I got it working.
Step 1 - I have a image and user taps on that image once. I capture that gesture and push it into the new imageViewController, its in the imageViewController, I want to have full screen image.
- (void)handleSingleTap:(UIGestureRecognizer *)gestureRecognizer {
NSLog(#"Single tap");
ImageViewController *imageViewController =
[[ImageViewController alloc] initWithNibName:#"ImageViewController" bundle:nil];
godImageViewController.imgName = // pass the image.
godImageViewController.hidesBottomBarWhenPushed=YES;// This is important to note.
[self.navigationController pushViewController:godImageViewController animated:YES];
// If I remove the line below, then I get this error. [CALayer retain]: message sent to deallocated instance .
// [godImageViewController release];
}
Step 2 - All these steps below are in the ImageViewController
Step 2.1 - In ViewDidLoad, show the navBar
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
NSLog(#"viewDidLoad");
[[self navigationController] setNavigationBarHidden:NO animated:YES];
}
Step 2.2 - In viewDidAppear, set up a timer task with delay ( I have it set for 1 sec delay). And after the delay, add fading effect. I am using alpha to use fading.
- (void)viewDidAppear:(BOOL)animated
{
NSLog(#"viewDidAppear");
myTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(fadeScreen) userInfo:nil repeats:NO];
}
- (void)fadeScreen
{
[UIView beginAnimations:nil context:nil]; // begins animation block
[UIView setAnimationDuration:1.95]; // sets animation duration
self.navigationController.navigationBar.alpha = 0.0; // Fades the alpha channel of this view to "0.0" over the animationDuration of "0.75" seconds
[UIView commitAnimations]; // commits the animation block. This Block is done.
}
step 2.3 - Under viewWillAppear, add singleTap gesture to the image and make the navBar translucent.
- (void) viewWillAppear:(BOOL)animated
{
NSLog(#"viewWillAppear");
NSString *path = [[NSBundle mainBundle] pathForResource:self.imgName ofType:#"png"];
UIImage *theImage = [UIImage imageWithContentsOfFile:path];
self.imgView.image = theImage;
// add tap gestures
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTap:)];
[self.imgView addGestureRecognizer:singleTap];
[singleTap release];
// to make the image go full screen
self.navigationController.navigationBar.translucent=YES;
}
- (void)handleTap:(UIGestureRecognizer *)gestureRecognizer
{
NSLog(#"Handle Single tap");
[self finishedFading];
// fade again. You can choose to skip this can add a bool, if you want to fade again when user taps again.
myTimer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:#selector(fadeScreen) userInfo:nil repeats:NO];
}
Step 3 - Finally in viewWillDisappear, make sure to put all the stuff back
- (void)viewWillDisappear: (BOOL)animated
{
self.hidesBottomBarWhenPushed = NO;
self.navigationController.navigationBar.translucent=NO;
if (self.navigationController.topViewController != self)
{
[self.navigationController setNavigationBarHidden:NO animated:animated];
}
[super viewWillDisappear:animated];
}
In case anyone still having trouble with the fast backswipe cancelled bug as #fabb commented in the accepted answer.
I manage to fix this by overriding viewDidLayoutSubviews, in addition to viewWillAppear/viewWillDisappear as shown below:
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
self.navigationController?.setNavigationBarHidden(false, animated: animated)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
self.navigationController?.setNavigationBarHidden(true, animated: animated)
}
//*** This is required to fix navigation bar forever disappear on fast backswipe bug.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.navigationController?.setNavigationBarHidden(false, animated: false)
}
In my case, I notice that it is because the root view controller (where nav is hidden) and the pushed view controller (nav is shown) has different status bar styles (e.g. dark and light). The moment you start the backswipe to pop the view controller, there will be additional status bar colour animation. If you release your finger in order to cancel the interactive pop, while the status bar animation is not finished, the navigation bar is forever gone!
However, this bug doesn't occur if status bar styles of both view controllers are the same.
If what you want is to hide the navigation bar completely in the controller, a much cleaner solution is to, in the root controller, have something like:
#implementation MainViewController
- (void)viewDidLoad {
self.navigationController.navigationBarHidden=YES;
//...extra code on view load
}
When you push a child view in the controller, the Navigation Bar will remain hidden; if you want to display it just in the child, you'll add the code for displaying it(self.navigationController.navigationBarHidden=NO;) in the viewWillAppear callback, and similarly the code for hiding it on viewWillDisappear
The simplest implementation may be to just have each view controller specify whether its navigation bar is hidden or not in its viewWillAppear:animated: method. The same approach works well for hiding/showing the toolbar as well:
- (void)viewWillAppear:(BOOL)animated {
[self.navigationController setToolbarHidden:YES/NO animated:animated];
[super viewWillAppear:animated];
}
Hiding navigation bar only on first page can be achieved through storyboard as well. On storyboard, goto Navigation Controller Scene->Navigation Bar. And select 'Hidden' property from the Attributes inspector. This will hide navigation bar starting from first viewcontroller until its made visible for the required viewcontroller.
Navigation bar can be set back to visible in ViewController's ViewWillAppear callback.
-(void)viewWillAppear:(BOOL)animated {
[self.navigationController setNavigationBarHidden:YES animated:animated];
[super viewWillAppear:animated];
}
Swift 4:
In the view controller you want to hide the navigation bar from.
override func viewWillAppear(_ animated: Bool) {
self.navigationController?.setNavigationBarHidden(true, animated: animated)
super.viewWillAppear(animated)
}
override func viewWillDisappear(_ animated: Bool) {
self.navigationController?.setNavigationBarHidden(false, animated: animated)
super.viewWillDisappear(animated)
}
By implement this code in your ViewController you can get this effect
Actually the trick is , hide the navigationBar when that Controller is launched
- (void)viewWillAppear:(BOOL)animated {
[self.navigationController setNavigationBarHidden:YES animated:YES];
[super viewWillAppear:animated];
}
and unhide the navigation bar when user leave that page do this is viewWillDisappear
- (void)viewWillDisappear:(BOOL)animated {
[self.navigationController setNavigationBarHidden:NO animated:YES];
[super viewWillDisappear:animated];
}