MonoTouch UITableViewController and TableFooterView - iphone

I'm trying to develop a small iPhone application using MonoDevelop (v2.8) and MonoTouch (v4.2.2).
My home screen is represented by a UITableViewController which use a UITableView for presentation. I want to fill the UITableView.TableFooterView (which is a UIView object) with some other controls (two labels and two buttons).
To do this I have created a "subview" called SearchView, which is represented by a UIViewController and use a simple UIView for presentation. I assign this view as footer view of the tableview.
public partial class HomeScreen : UITableViewController
{
public override void ViewDidLoad()
{
base.ViewDidLoad();
SearchViewController searchViewController = new SearchViewController();
this.TableView.TableFooterView = searchViewController.View;
}
}
Is this right?
It is right to assume that usaually you create a view (UIView) and consume it by a UIViewController ?

- (void)loadView {
[super loadView];
sBar = [[UISearchBar alloc]initWithFrame:CGRectMake(0,0,320,30)];
sBar.delegate = self;
[self.view addSubview:sBar];
you can add subviews like this ,sbar is the search bar,u can also add any controllers like button ,labels ,textview,textfield in it.

In your UITableViewDataSource you can override public virtual UIView GetViewForFooter(UITableView tableView, int sectionIndex)
public override UIView GetViewForFooter(UITableView tableView, int sectionIndex)
{
// Write a method to get the proper Section via the sectionIndex
var section = GetSection(sectionIndex);
if (section != null)
{
if (section.FooterView == null && !string.IsNullOrEmpty(section.FooterText))
{
// Create your FooterView here
section.FooterView = CreateFooterView(tableView, section.FooterText);
}
return section.FooterView;
}
return null;
}
As of iOS 5 you will also need to override
public virtual float GetHeightForFooter(UITableView tableView, int sectionIndex)
and return the height of the FooterView

Related

UIStatusBarStyle PreferredStatusBarStyle does not work on iOS 7

In my iPhone application built with Xcode 5 for iOS 7 I set UIViewControllerBasedStatusBarAppearance=YES in info.plist, and in my ViewController I have this code:
-(UIStatusBarStyle) preferredStatusBarStyle
{
return UIStatusBarStyleLightContent;
}
But the status bar is still black against the black background.
I know its possible to change this app-wide by setting UIViewControllerBasedStatusBarAppearance=NO in info.plist, but I actually need to alter this on a viewController by viewController basis at runtime.
I discovered that if your ViewController is inside a navigationController then the navigationController’s navigationBar.barStyle determines the statusBarStyle.
Setting your navigationBar’s barStyle to UIBarStyleBlackTranslucent will give white status bar text (ie. UIStatusBarStyleLightContent), and UIBarStyleDefault will give black status bar text (ie. UIStatusBarStyleDefault).
Note that this applies even if you totally change the navigationBar’s color via its barTintColor.
OK, here's the trick. You do have to add the key "View controller-based status bar" and set the value to No.
This is counter to what it appears the meaning of this key is, but even if you set the value to No, you can still change the appearance of the status bar, and whether it shows or not in any view controller. So it acts like "Yes" but set it to "No"!
Now I can get the status bar white or dark.
For preferredStatusBarStyle() to work within UINavigationController and UITabBarController I add the following code, which will get the preferred status bar style from the currently visible view controller.
extension UITabBarController {
public override func childViewControllerForStatusBarStyle() -> UIViewController? {
return selectedViewController
}
}
extension UINavigationController {
public override func childViewControllerForStatusBarStyle() -> UIViewController? {
return visibleViewController
}
}
For Swift 3 those are not methods but properties:
extension UITabBarController {
open override var childViewControllerForStatusBarStyle: UIViewController? {
return selectedViewController
}
}
extension UINavigationController {
open override var childViewControllerForStatusBarStyle: UIViewController? {
return visibleViewController
}
}
The Swift 4.2 properties have been renamed:
extension UITabBarController {
open override var childForStatusBarStyle: UIViewController? {
return selectedViewController
}
}
extension UINavigationController {
open override var childForStatusBarStyle: UIViewController? {
return visibleViewController
}
}
Usage
class ViewController: UIViewController {
// This will be called every time the ViewController appears
// Works great for pushing & popping
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
}
I may be coming to this a bit late, but incase anyone else is looking for a working and verified app wide solution.
#mxcl is correct in describing why this is happening. In order to correct it, we simply create an extension (or category in obj-c) that overrides the preferredSatusBarStyle() method of UINavigationController. Here is an example in Swift:
extension UINavigationController {
public override func preferredStatusBarStyle() -> UIStatusBarStyle {
if let rootViewController = self.viewControllers.first {
return rootViewController.preferredStatusBarStyle()
}
return super.preferredStatusBarStyle()
}
}
This code simply extracts the first view controller (the root view controller) and unwraps it (in obj-c just check that it is not nil). If the unwrap is successful (not nil) then we grab the rootViewControllers preferredStatusBarStyle. Otherwise we just return the default.
Hope this helps anyone who might need it.
To provide more detail into the accepted answer, put the following line in your app delegate's didFinishLaunchingWithOptions: method:
[UIApplication sharedApplication].statusBarStyle = UIStatusBarStyleLightContent;
Then, in your Info.plist, add View controller-based status bar appearance and set it to NO.
I believe that's how it should be done, NOT from the navigation controller, if you want the same status bar color for the entire app. You might have screens that are not necessarily embedded in a UINavigationController, or a different UINavigationController subclass somewhere else, and other things.
EDIT: You can also do it without typing any code: https://stackoverflow.com/a/18732865/855680
In viewDidLoad just write this
[self setNeedsStatusBarAppearanceUpdate];
just do that and it will work
can u please try this
Set UIViewControllerBasedStatusBarAppearance to NO.
Call [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
One more thing i have seen in your question that
you have wrote the method like this
-(void)UIStatusBarStyle PreferredStatusBarStyle ()
{
return UIStatusBarStyle.LightContent;
}
but it should be like this
-(UIStatusBarStyle)preferredStatusBarStyle{
return UIStatusBarStyleLightContent;
}
iOS 13 Solution(s)
The highest-voted answer uses "legacy" code 👎
Setting the barStyle property is now (iOS 13+) considered a "legacy customization." According to Apple,
In iOS 13 and later, customize your navigation bar using the standardAppearance, compactAppearance, and scrollEdgeAppearance properties. You may continue to use these legacy accessors to customize your navigation bar's appearance directly, but you must update the appearance for different bar configurations yourself.
Regarding your attempt - You were on the right track!
UINavigationController is a subclass of UIViewController (who knew 🙃)!
Therefore, when presenting view controllers embedded in navigation controllers, you're not really presenting the embedded view controllers; you're presenting the navigation controllers! UINavigationController, as a subclass of UIViewController, inherits preferredStatusBarStyle and childForStatusBarStyle, which you can set as desired.
Any of the following methods should work:
Override preferredStatusBarStyle within UINavigationController
preferredStatusBarStyle (doc) - The preferred status bar style for the view controller
Subclass or extend UINavigationController
class MyNavigationController: UINavigationController {
override var preferredStatusBarStyle: UIStatusBarStyle {
.lightContent
}
}
OR
extension UINavigationController {
open override var preferredStatusBarStyle: UIStatusBarStyle {
.lightContent
}
}
Override childForStatusBarStyle within UINavigationController
childForStatusBarStyle (doc) - Called when the system needs the view controller to use for determining status bar style
According to Apple's documentation,
"If your container view controller derives its status bar style from one of its child view controllers, [override this property] and return that child view controller. If you return nil or do not override this method, the status bar style for self is used. If the return value from this method changes, call the setNeedsStatusBarAppearanceUpdate() method."
In other words, if you don't implement solution 3 here, the system will fall back to solution 2 above.
Subclass or extend UINavigationController
class MyNavigationController: UINavigationController {
override var childForStatusBarStyle: UIViewController? {
topViewController
}
}
OR
extension UINavigationController {
open override var childForStatusBarStyle: UIViewController? {
topViewController
}
}
You can return any view controller you'd like above. I recommend one of the following:
topViewController (of UINavigationController) (doc) - The view controller at the top of the navigation stack
visibleViewController (of UINavigationController) (doc) - The view controller associated with the currently visible view in the navigation interface (hint: this can include "a view controller that was presented modally on top of the navigation controller itself")
Note: If you decide to subclass UINavigationController, remember to apply that class to your nav controllers through the identity inspector in IB.
P.S. My code uses Swift 5.1 syntax 😎
Here is how I solved it. Usually the navigationController or tabBarController are the ones deciding the appearance of the status bar (hidden, color, etc).
So I ended up subclassing the navigation controller and overriding preferredStatusBarStyle. if the current visible ViewContorller implements StatusBarStyleHandler I ask for the value to be used as the style, if it doesn't I just return a default value.
The way you trigger an update of the status bar appearance is by calling setNeedsStatusBarAppearanceUpdate which triggers preferredStatusBarStyle again and updates UI according to what the method returns
public protocol StatusBarStyleHandler {
var preferredStatusBarStyle: UIStatusBarStyle { get }
}
public class CustomNavigationCotnroller: UINavigationController {
public override var preferredStatusBarStyle: UIStatusBarStyle {
if let statusBarHandler = visibleViewController as? StatusBarStyleHandler {
return statusBarHandler.preferredStatusBarStyle
}
return .default
}
}
Then usage
public class SomeController: UIViewController, StatusBarStyleHandler {
private var statusBarToggle = true
// just a sample for toggling the status bar style each time method is called
private func toggleStatusBarColor() {
statusBarToggle = !statusBarToggle
setNeedsStatusBarAppearanceUpdate()
}
public override var preferredStatusBarStyle: UIStatusBarStyle {
return statusBarToggle ? .lightContent : .default
}
}
Even with all the answers here i still didn't find the exact solution for me, but started with the answer from Daniel. What I ended up with was:
override var preferredStatusBarStyle: UIStatusBarStyle {
return visibleViewController?.preferredStatusBarStyle ?? .lightContent
}
in navigation controllers (similar for tab, just selectedViewController). And then it will respect the:
override var preferredStatusBarStyle: UIStatusBarStyle {
return .lightContent
}
In each view controller unless you set it otherwise. I dont need to call setNeedsStatusBarAppearanceUpdate() anywhere, it just updates when you arrive at each view controller.
1) One setting for whole project:
If available, remove UIViewControllerBasedStatusBarAppearance key-value pair from your info.plist, or set NO without removing it. If it's not available in your info.plist, do nothing. Default is NO for this property.
Add below code to your AppDelegate.m:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
}
2) Different settings for different View Controllers:
Add UIViewControllerBasedStatusBarAppearance key-value pair to your info.plist and set it to YES.
If your View Controller is not embed in to Navigation Controller. Let's say MyViewController. just add code below to your MyViewController.m file. If your View Controller is embed in to Navigation Controller, create a new Cocoa Touch Class and make it subclass of UINavigationController. Let's say MyNC. Select Navigation Controller View on your Storyboard, at right pane; Utilities -> Identity Inspector -> Custom Class -> Class, type "MyNC". After linking Storyboard View with your "MyNC" Cocoa Touch Class, add code below to your MyNC.m:
- (BOOL)prefersStatusBarHidden {
return NO;
}
-(UIStatusBarStyle)preferredStatusBarStyle {
return UIStatusBarStyleLightContent;
}
If in case you wanted to hide the statusBar during splashScreen but wanted to change the style to light content (StatusBarInitiallyHidden on Plist has to be NO to hide statusBar on splash), you can add this to appDelegate's didFinishLaunchingWithOptions method to change to lightContent.
[[UIApplication sharedApplication]setStatusBarHidden:NO withAnimation:UIStatusBarAnimationSlide];
[[UIApplication sharedApplication]setStatusBarStyle:UIStatusBarStyleLightContent];
swift example
in AppDelegate.swift
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {
UIApplication.sharedApplication().statusBarStyle = UIStatusBarStyle.LightContent;
return true
}
in info.plist set View controller-based status bar appearance: NO
If you're using NavigationController, you can subclass NavigationController so that it consults its child view controller
// MyCustomNavigationController
- (NSUInteger)supportedInterfaceOrientations {
UIViewController *viewControllerToAsk = [self findChildVC];
return [viewControllerToAsk supportedInterfaceOrientations];
}
- (BOOL)shouldAutorotate {
UIViewController *viewControllerToAsk = [self findChildVC];
return [viewControllerToAsk shouldAutorotate];
}
- (UIStatusBarStyle)preferredStatusBarStyle {
UIViewController *viewControllerToAsk = [self findChildVC];
return [viewControllerToAsk preferredStatusBarStyle];
}
- (UIViewController *)findChildVC {
return self.viewControllers.firstObject;
}
Swift 4.2
extension UITabBarController {
open override var childForStatusBarStyle: UIViewController? {
return selectedViewController
}
}
extension UINavigationController {
open override var childForStatusBarStyle: UIViewController? {
return visibleViewController
}
}
You can set the status bar style. It will resembles the status bar like IOS 6 and below.
Paste this methods in your view controller
-(UIStatusBarStyle)preferredStatusBarStyle{
return UIStatusBarStyleBlackOpaque;
}
and call this method from view did load like this
if([[[UIDevice currentDevice] systemVersion] floatValue] >= 7.0f)
{
[self setNeedsStatusBarAppearanceUpdate];
}
swift example
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: NSDictionary?) -> Bool {
UIApplication.sharedApplication().statusBarStyle = UIStatusBarStyle.LightContent;
return true
}
in info.plist set View controller-based status bar appearance: NO
I just want to add a note for a specific case I faced. I had another UIWindow in my app to display a chat face to be floating all over my app all the time. Doing this caused none of the solution above to work, and I am not really sure why! All what I have noticed is that my ViewController in the new UIWindow was the reason for that! And if I wanted to change the status bar style I have to do it in that view controller of the new UIWindow.
This note might help others who have a similar structure! So basically you can apply the solutions mentioned above in the ViewController of the new UIWindow.
Again this a specific case.
Thanks
Wanna some tricky? No needs to override status bar style in every view controller
First: Follow #Sahil Kapoor, add 'View controller-based status bar = YES' to plist
Second: Make a subclass of window's root view controller and return StatusBarTrackingController.
final class StatusBarTracker: UIViewController {
override var preferredStatusBarStyle: UIStatusBarStyle {
CustomThemeProvider.currentTheme.asStatusBarStyle
}
}
final class TabBarController: UITabBarController {
private let statusBarTracker = StatusBarTracker()
override var childForStatusBarStyle: UIViewController? {
statusBarTracker
}
}
extension TabBarController: CustomThemeUpdatable {
func applyCustomTheme(_ theme: CustomTheme) {
setNeedsStatusBarAppearanceUpdate()
}
}
// SceneDelegate
window?.rootViewController = TabBarController()
For swift 3, in your UIViewController:
override var preferredStatusBarStyle : UIStatusBarStyle { return UIStatusBarStyle.lightContent }

How do I disable drag and drop on NSTextView?

I have a NSWindowController that contains several NSViewControllers. I would like to universally accept drag and drop events with the NSWindowController class and not be intercepted by other views such as NSTextView (contained in a NSViewController)
How can I tell NSTextView to ignore the drag & drop event?
I found out that there were two things needed to skip past NSTextView's interception of the drag and drop event.
In the NSViewController containing your NSTextView:
- (void)awakeFromNib
{
[self noDragInView:self.view];
}
- (void)noDragInView:(NSView *)view
{
for (NSView *subview in view.subviews)
{
[subview unregisterDraggedTypes];
if (subview.subviews.count) [self noDragInView:subview];
}
}
Now subclass your NSTextView and add this method:
- (NSArray *)acceptableDragTypes
{
return nil;
}
The NSTextView should now properly ignore the drag and drop event and leave it to be handled by the NSWindow.
It is sufficient to subclass the NSTextView and override the getter for its acceptableDragTypes property, no need to unregisterDraggedTypes. In Swift:
override var acceptableDragTypes : [String] {
return [String]()
}
Swift 5
import Cocoa
class NSTextViewNoDrop: NSTextView {
override var acceptableDragTypes: [NSPasteboard.PasteboardType] { return [] }
}
Slight update.
import Cocoa
class MyTextView : NSTextView {
// don't accept any drag types into the text view
override var acceptableDragTypes : [NSPasteboard.PasteboardType] {
return [NSPasteboard.PasteboardType]()
}
}

Detecting when the 'back' button is pressed on a navbar

I need to perform some actions when the back button(return to previous screen, return to parent-view) button is pressed on a Navbar.
Is there some method I can implement to catch the event and fire off some actions to pause and save data before the screen disappears?
UPDATE: According to some comments, the solution in the original answer does not seem to work under certain scenarios in iOS 8+. I can't verify that that is actually the case without further details.
For those of you however in that situation there's an alternative. Detecting when a view controller is being popped is possible by overriding willMove(toParentViewController:). The basic idea is that a view controller is being popped when parent is nil.
Check out "Implementing a Container View Controller" for further details.
Since iOS 5 I've found that the easiest way of dealing with this situation is using the new method - (BOOL)isMovingFromParentViewController:
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (self.isMovingFromParentViewController) {
// Do your stuff here
}
}
- (BOOL)isMovingFromParentViewController makes sense when you are pushing and popping controllers in a navigation stack.
However, if you are presenting modal view controllers you should use - (BOOL)isBeingDismissed instead:
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (self.isBeingDismissed) {
// Do your stuff here
}
}
As noted in this question, you could combine both properties:
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if (self.isMovingFromParentViewController || self.isBeingDismissed) {
// Do your stuff here
}
}
Other solutions rely on the existence of a UINavigationBar. Instead like my approach more because it decouples the required tasks to perform from the action that triggered the event, i.e. pressing a back button.
While viewWillAppear() and viewDidDisappear() are called when the back button is tapped, they are also called at other times. See end of answer for more on that.
Using UIViewController.parent
Detecting the back button is better done when the VC is removed from its parent (the NavigationController) with the help of willMoveToParentViewController(_:) OR didMoveToParentViewController()
If parent is nil, the view controller is being popped off the navigation stack and dismissed. If parent is not nil, it is being added to the stack and presented.
// Objective-C
-(void)willMoveToParentViewController:(UIViewController *)parent {
[super willMoveToParentViewController:parent];
if (!parent){
// The back button was pressed or interactive gesture used
}
}
// Swift
override func willMove(toParent parent: UIViewController?) {
super.willMove(toParent: parent)
if parent == nil {
// The back button was pressed or interactive gesture used
}
}
Swap out willMove for didMove and check self.parent to do work after the view controller is dismissed.
Stopping the dismiss
Do note, checking the parent doesn't allow you to "pause" the transition if you need to do some sort of async save. To do that you could implement the following. Only downside here is you lose the fancy iOS styled/animated back button. Also be careful here with the interactive swipe gesture. Use the following to handle this case.
var backButton : UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
// Disable the swipe to make sure you get your chance to save
self.navigationController?.interactivePopGestureRecognizer.enabled = false
// Replace the default back button
self.navigationItem.setHidesBackButton(true, animated: false)
self.backButton = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.Plain, target: self, action: "goBack")
self.navigationItem.leftBarButtonItem = backButton
}
// Then handle the button selection
func goBack() {
// Here we just remove the back button, you could also disabled it or better yet show an activityIndicator
self.navigationItem.leftBarButtonItem = nil
someData.saveInBackground { (success, error) -> Void in
if success {
self.navigationController?.popViewControllerAnimated(true)
// Don't forget to re-enable the interactive gesture
self.navigationController?.interactivePopGestureRecognizer.enabled = true
}
else {
self.navigationItem.leftBarButtonItem = self.backButton
// Handle the error
}
}
}
### More on view will/did appear
If you didn't get the `viewWillAppear` `viewDidDisappear` issue, Let's run through an example. Say you have three view controllers:
ListVC: A table view of things
DetailVC: Details about a thing
SettingsVC: Some options for a thing
Lets follow the calls on the detailVC as you go from the listVC to settingsVC and back to listVC
List > Detail (push detailVC) Detail.viewDidAppear <- appear
Detail > Settings (push settingsVC) Detail.viewDidDisappear <- disappear
And as we go back...
Settings > Detail (pop settingsVC) Detail.viewDidAppear <- appear
Detail > List (pop detailVC) Detail.viewDidDisappear <- disappear
Notice that viewDidDisappear is called multiple times, not only when going back, but also when going forward. For a quick operation that may be desired, but for a more complex operation like a network call to save, it may not.
Those who claim that this doesn't work are mistaken:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if self.isMovingFromParent {
print("we are being popped")
}
}
That works fine. So what is causing the widespread myth that it doesn’t?
The problem seems to be due to an incorrect implementation of a different method, namely that the implementation of willMove(toParent:) forgot to call super.
If you implement willMove(toParent:) without calling super, then self.isMovingFromParent will be false and the use of viewWillDisappear will appear to fail. It didn't fail; you broke it.
NOTE: The real problem is usually the second view controller detecting that the first view controller was popped. Please see also the more general discussion here: Unified UIViewController "became frontmost" detection?
EDIT A comment suggests that this should be viewDidDisappear rather than viewWillDisappear.
First Method
- (void)didMoveToParentViewController:(UIViewController *)parent
{
if (![parent isEqual:self.parentViewController]) {
NSLog(#"Back pressed");
}
}
Second Method
-(void) viewWillDisappear:(BOOL)animated {
if ([self.navigationController.viewControllers indexOfObject:self]==NSNotFound) {
// back button was pressed. We know this is true because self is no longer
// in the navigation stack.
}
[super viewWillDisappear:animated];
}
I've playing (or fighting) with this problem for two days. IMO the best approach is just to create an extension class and a protocol, like this:
#protocol UINavigationControllerBackButtonDelegate <NSObject>
/**
* Indicates that the back button was pressed.
* If this message is implemented the pop logic must be manually handled.
*/
- (void)backButtonPressed;
#end
#interface UINavigationController(BackButtonHandler)
#end
#implementation UINavigationController(BackButtonHandler)
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item
{
UIViewController *topViewController = self.topViewController;
BOOL wasBackButtonClicked = topViewController.navigationItem == item;
SEL backButtonPressedSel = #selector(backButtonPressed);
if (wasBackButtonClicked && [topViewController respondsToSelector:backButtonPressedSel]) {
[topViewController performSelector:backButtonPressedSel];
return NO;
}
else {
[self popViewControllerAnimated:YES];
return YES;
}
}
#end
This works because UINavigationController will receive a call to navigationBar:shouldPopItem: every time a view controller is popped. There we detect if back was pressed or not (any other button).
The only thing you have to do is implement the protocol in the view controller where back is pressed.
Remember to manually pop the view controller inside backButtonPressedSel, if everything is ok.
If you already have subclassed UINavigationViewController and implemented navigationBar:shouldPopItem: don't worry, this won't interfere with it.
You may also be interested in disable the back gesture.
if ([self.navigationController respondsToSelector:#selector(interactivePopGestureRecognizer)]) {
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}
This works for me in iOS 9.3.x with Swift:
override func didMoveToParentViewController(parent: UIViewController?) {
super.didMoveToParentViewController(parent)
if parent == self.navigationController?.parentViewController {
print("Back tapped")
}
}
Unlike other solutions here, this doesn't seem to trigger unexpectedly.
You can use the back button callback, like this:
- (BOOL) navigationShouldPopOnBackButton
{
[self backAction];
return NO;
}
- (void) backAction {
// your code goes here
// show confirmation alert, for example
// ...
}
for swift version you can do something like in global scope
extension UIViewController {
#objc func navigationShouldPopOnBackButton() -> Bool {
return true
}
}
extension UINavigationController: UINavigationBarDelegate {
public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
return self.topViewController?.navigationShouldPopOnBackButton() ?? true
}
}
Below one you put in the viewcontroller where you want to control back button action:
override func navigationShouldPopOnBackButton() -> Bool {
self.backAction()//Your action you want to perform.
return true
}
For the record, I think this is more of what he was looking for…
UIBarButtonItem *l_backButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRewind target:self action:#selector(backToRootView:)];
self.navigationItem.leftBarButtonItem = l_backButton;
- (void) backToRootView:(id)sender {
// Perform some custom code
[self.navigationController popToRootViewControllerAnimated:YES];
}
The best way is to use the UINavigationController delegate methods
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
Using this you can know what controller is showing the UINavigationController.
if ([viewController isKindOfClass:[HomeController class]]) {
NSLog(#"Show home controller");
}
For Swift with a UINavigationController:
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
if self.navigationController?.topViewController != self {
print("back button tapped")
}
}
You should check out the UINavigationBarDelegate Protocol.
In this case you might want to use the navigationBar:shouldPopItem: method.
As Coli88 said, you should check the UINavigationBarDelegate protocol.
In a more general way, you can also use the - (void)viewWillDisapear:(BOOL)animated to perform custom work when the view retained by the currently visible view controller is about to disappear. Unfortunately, this would cover bother the push and the pop cases.
As purrrminator says, the answer by elitalon is not completely right, since your stuff would be executed even when popping the controller programmatically.
The solution I have found so far is not very nice, but it works for me. Besides what elitalon said, I also check whether I'm popping programmatically or not:
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if ((self.isMovingFromParentViewController || self.isBeingDismissed)
&& !self.isPoppingProgrammatically) {
// Do your stuff here
}
}
You have to add that property to your controller and set it to YES before popping programmatically:
self.isPoppingProgrammatically = YES;
[self.navigationController popViewControllerAnimated:YES];
Thanks for your help!
I have solved this problem by adding a UIControl to the navigationBar on the left side .
UIControl *leftBarItemControl = [[UIControl alloc] initWithFrame:CGRectMake(0, 0, 90, 44)];
[leftBarItemControl addTarget:self action:#selector(onLeftItemClick:) forControlEvents:UIControlEventTouchUpInside];
self.leftItemControl = leftBarItemControl;
[self.navigationController.navigationBar addSubview:leftBarItemControl];
[self.navigationController.navigationBar bringSubviewToFront:leftBarItemControl];
And you need to remember to remove it when view will disappear:
- (void) viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
if (self.leftItemControl) {
[self.leftItemControl removeFromSuperview];
}
}
That's all!
7ynk3r's answer was really close to what I did use in the end but it needed some tweaks:
- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item {
UIViewController *topViewController = self.topViewController;
BOOL wasBackButtonClicked = topViewController.navigationItem == item;
if (wasBackButtonClicked) {
if ([topViewController respondsToSelector:#selector(navBackButtonPressed)]) {
// if user did press back on the view controller where you handle the navBackButtonPressed
[topViewController performSelector:#selector(navBackButtonPressed)];
return NO;
} else {
// if user did press back but you are not on the view controller that can handle the navBackButtonPressed
[self popViewControllerAnimated:YES];
return YES;
}
} else {
// when you call popViewController programmatically you do not want to pop it twice
return YES;
}
}
I used Pedro Magalhães solution, except navigationBar:shouldPop was not called when I used it in an extension like this:
extension UINavigationController: UINavigationBarDelegate {
public func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
return self.topViewController?.navigationShouldPopOnBackButton() ?? true
}
But the same thing in a UINavigationController subclass worked fine.
class NavigationController: UINavigationController, UINavigationBarDelegate {
func navigationBar(_ navigationBar: UINavigationBar, shouldPop item: UINavigationItem) -> Bool {
return self.topViewController?.navigationShouldPopOnBackButton() ?? true
}
I see some other questions reporting this method not being called (but the other delegate methods being called as expected), from iOS 13?
iOS 13 and UINavigationBarDelegate::shouldPop()
self.navigationController.isMovingFromParentViewController is not working anymore on iOS8 and 9 I use :
-(void) viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
if (self.navigationController.topViewController != self)
{
// Is Popping
}
}
(SWIFT)
finaly found solution.. method we were looking for is "willShowViewController" which is delegate method of UINavigationController
//IMPORT UINavigationControllerDelegate !!
class PushedController: UIViewController, UINavigationControllerDelegate {
override func viewDidLoad() {
//set delegate to current class (self)
navigationController?.delegate = self
}
func navigationController(navigationController: UINavigationController, willShowViewController viewController: UIViewController, animated: Bool) {
//MyViewController shoud be the name of your parent Class
if var myViewController = viewController as? MyViewController {
//YOUR STUFF
}
}
}

Switching to a TabBar tab view programmatically?

Let's say I have a UIButton in one tab view in my iPhone app, and I want to have it open a different tab in the tab bar of the TabBarController. How would I write the code to do this?
I'm assuming I unload the existing view and load a specific tab view, but I'm not sure how to write the code exactly.
Try this code in Swift or Objective-C
Swift
self.tabBarController.selectedIndex = 1
Objective-C
[self.tabBarController setSelectedIndex:1];
Note that the tabs are indexed starting from 0. So the following code snippet works
tabBarController = [[UITabBarController alloc] init];
.
.
.
tabBarController.selectedViewController = [tabBarController.viewControllers objectAtIndex:4];
goes to the fifth tab in the bar.
My opinion is that selectedIndex or using objectAtIndex is not necessarily the best way to switch the tab. If you reorder your tabs, a hard coded index selection might mess with your former app behavior.
If you have the object reference of the view controller you want to switch to, you can do:
tabBarController.selectedViewController = myViewController
Of course you must make sure, that myViewController really is in the list of tabBarController.viewControllers.
You can simply just set the selectedIndex property on the UITabBarController to the appropriate index and the view will be changed just like the user tapped the tab button.
I tried what Disco S2 suggested, it was close but this is what ended up working for me. This was called after completing an action inside another tab.
for (UINavigationController *controller in self.tabBarController.viewControllers)
{
if ([controller isKindOfClass:[MyViewController class]])
{
[self.tabBarController setSelectedViewController:controller];
break;
}
}
Like Stuart Clark's solution but for Swift 3:
func setTab<T>(_ myClass: T.Type) {
var i: Int = 0
if let controllers = self.tabBarController?.viewControllers {
for controller in controllers {
if let nav = controller as? UINavigationController, nav.topViewController is T {
break
}
i = i+1
}
}
self.tabBarController?.selectedIndex = i
}
Use it like this:
setTab(MyViewController.self)
Please note that my tabController links to viewControllers behind navigationControllers. Without navigationControllers it would look like this:
if let controller is T {
For cases where you may be moving the tabs, here is some code.
for ( UINavigationController *controller in self.tabBarController.viewControllers ) {
if ( [[controller.childViewControllers objectAtIndex:0] isKindOfClass:[MyViewController class]]) {
[self.tabBarController setSelectedViewController:controller];
break;
}
}
My issue is a little different, I need to switch from one childViewController in 1st tabBar to home viewController of 2nd tabBar. I simply use the solution provided in the upstairs:
tabBarController.selectedIndex = 2
However when it switched to the home page of 2nd tabBar, the content is invisible. And when I debug, viewDidAppear, viewWillAppear, viewDidLoad, none of them is called.
My solutions is to add the following code in the UITabBarController:
override var shouldAutomaticallyForwardAppearanceMethods: Bool
{
return true
}
I wanted to be able to specify which tab was shown by class rather than index as I thought it made for a robust solution that was less dependant on how you wire up IB. I didn't find either Disco's or Joped's solutions to work so i created this method:
-(void)setTab:(Class)class{
int i = 0;
for (UINavigationController *controller in self.tabBarContontroller.viewControllers){
if ([controller isKindOfClass:class]){
break;
}
i++;
}
self.tabBarContontroller.selectedIndex = i;
}
you call it like this:
[self setTab:[YourClass class]];
Hope this is helpful to someone
import UIKit
class TabbarViewController: UITabBarController,UITabBarControllerDelegate {
//MARK:- View Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
//Tabbar delegate method
override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
let yourView = self.viewControllers![self.selectedIndex] as! UINavigationController
yourView.popToRootViewController(animated:false)
}
}
Use in AppDelegate.m file:
(void)tabBarController:(UITabBarController *)tabBarController
didSelectViewController:(UIViewController *)viewController
{
NSLog(#"Selected index: %d", tabBarController.selectedIndex);
if (viewController == tabBarController.moreNavigationController)
{
tabBarController.moreNavigationController.delegate = self;
}
NSUInteger selectedIndex = tabBarController.selectedIndex;
switch (selectedIndex) {
case 0:
NSLog(#"click me %u",self.tabBarController.selectedIndex);
break;
case 1:
NSLog(#"click me again!! %u",self.tabBarController.selectedIndex);
break;
default:
break;
}
}
Like Stuart Clark's solution but for Swift 3 and using restoration identifier to find correct tab:
private func setTabById(id: String) {
var i: Int = 0
if let controllers = self.tabBarController?.viewControllers {
for controller in controllers {
if let nav = controller as? UINavigationController, nav.topViewController?.restorationIdentifier == id {
break
}
i = i+1
}
}
self.tabBarController?.selectedIndex = i
}
Use it like this ("Humans" and "Robots" must also be set in storyboard for specific viewController and it's Restoration ID, or use Storyboard ID and check "use storyboard ID" as restoration ID):
struct Tabs {
static let Humans = "Humans"
static let Robots = "Robots"
}
setTabById(id: Tabs.Robots)
Please note that my tabController links to viewControllers behind navigationControllers. Without navigationControllers it would look like this:
if controller.restorationIdentifier == id {

Monotouch open document - UIDocumentInterationController

I want to open a document on my iphone app written in monotouch
- i.e. launch a PDF file in the default PDF viewer.
I think I should be using UIDocumentInterationController?
Anyone have any Ideas on this..
I have put together the following on a viewcontroller ( with a toolbar)
but it doesnt work :-( It does nothing!!
string s = string.Format("{0}",strFilePath);
NSUrl ns = NSUrl.FromFilename (s);
UIDocumentInteractionController PreviewController =
UIDocumentInteractionController.FromUrl(ns);
PreviewController.Delegate = new UIDocumentInteractionControllerDelegateClass();
PreviewController.PresentOpenInMenu(btnOpen,true);
public class UIDocumentInteractionControllerDelegateClass : UIDocumentInteractionControllerDelegate
{
public UIViewController FileViewController = new UIViewController();
public UIDocumentInteractionControllerDelegateClass ()
{
}
public override UIViewController ViewControllerForPreview (UIDocumentInteractionController controller)
{
return FileViewController;
}
public override UIView ViewForPreview (UIDocumentInteractionController controller)
{
return FileViewController.View;
}
}
First thing I would try, is ensuring when you present the options menu, that it is taking place on the main thread:
InvokeOnMainThread(delegate{
PreviewController.PresentOpenInMenu(btnOpen,true);
});
If that alone doesn't work, another thing I noticed is that you're creating a new view controller in the delegate class. It doesn't appear to be added to the stack anywhere in your code, so maybe thats why it's not showing. Code I've used is as follows:
PreviewController.Delegate = new UIDocumentInteractionControllerDelegateClass(this);
...
...
public class UIDocumentInteractionControllerDelegateClass : UIDocumentInteractionControllerDelegate
{
UIViewController viewC;
public UIDocumentInteractionControllerDelegateClass(UIViewController controller)
{
viewC = controller;
}
public override UIViewController ViewControllerForPreview (UIDocumentInteractionController controller)
{
return viewC;
}
public override UIView ViewForPreview (UIDocumentInteractionController controller)
{
return viewC.View;
}
public override RectangleF RectangleForPreview (UIDocumentInteractionController controller)
{
return viewC.View.Frame;
}
}
This will then use the current viewcontroller to present the preview in. The only other change I can think of using is rather than presenting from a UIBarButtonItem try:
PreviewController.PresentOpenInMenu(new RectangleF(320,320,0,500), this.View, true);
I hope this helps!