Absolute positioning over tableView, view hierarchy and redrawing - iphone

on the main screen on my project I have a a TableView, with a navigationBar on top and toolBar on the bottom. The thing is, one of the buttons in the toolbar needs to slide up an UISegmentedControl from the toolbar. I have already implemented this control in a custom UIView, BUT, if i just add it to my rootviewcontroller.view and slide it into place, it will scroll with the rest of the table, which is undesired (I would like it to appear as an extension of the toolbar).
So, what do I do? In rootViewController I do
self.filterView = [[FilterView alloc] initWithTarget:self.tableView reloadDataSelector:#selector(reloadData)];
[[self.view superview] addSubview:self.filterView];
[[self.view superview] bringSubviewToFront:self.filterView];
I add the control view (self.filterView) to my view's superview, and that puts it above the tableview's scroll.
BUT, now the problem. As soon as the tableView goes out of view (I push another view on the navigationController, or specially if the app goes to background) this view gets re layed-out, and my controller view gets moved to (0,0).
The thing is, as far as pushing new views on the navigationController, I can kind of control it by repositioning it in viewWillAppear and viewWillDisappear in my rootViewController. But when the app goes to background and comes back those functions don't get called.
So, is there any way to
(a)prevent my controller view from being moved
(b)detecting when it has been moved unintentionally
(c)detecting coming and going from background from rootViewController
???
I know I can detect passes to background in appDelegate, but I wouldn't feel comfortable dealing with layout issues there.
Thanks!!
EDIT:
To add some info, if I do
NSLog(#"%#",[self.view superview]);
//I get <UIViewControllerWrapperView: 0x61589d0; frame = (0 64; 320 372); autoresize = W+H; layer = <CALayer: 0x615d9c0>>
EDIT2: I guess I could create my own wrapper UIView first, load all my current view hierarchy in it, and put the controller view there. Do you guys think that would be worth the trouble? isn't there a simpler way?
EDIT3: Ended up opting for changing my rootViewController from UITableViewController to UIViewController, and added the tableView programatically, as Phil suggested below. I can now control my main view as I like, and since I am putting my segmentedControl view there I can control how it is positioned, as opposed to before, when I was placing it in an UIViewControllerWrapperView, which I am not too sure who controls or what it does to it's subviews.
SO, just out of curiosity, does anyone know why the UIViewControllerWrapperView that was wrapping my UITableViewController's view was moving my UIView on coming back from background??
To clarify, the setup was like so:
UIViewControllerWrapperView
|
|UITableView
|Custom SegmentedControl UIView

As a side note, you have a pattern in your code that looks like that view is going to leak and addSubview will automatically put the view on the top of the view order.
However, the reason your view is scrolling is because it is being added as a subview of the UITableView, which is a subclass of UIScrollView. When the scroll view scrolls, it will move any subviews up or down by the contentOffset property. As the UIScrollView scrolls it will repeatedly layout its subviews. Since the table view isn't aware of your custom subview, it appears it is just moving it to the 0,0.
I assume you are using UITableViewController. If you plan to have more than just a table view for this view controller, then you should implement a standard view controller instead. This controller would have a normal view that contains the tableview and your other views. UITableViewController is merely for convenience for a very common case.
It's very easy to duplicate UITableViewController's functionality if you are worried about that. It is actually very clearly documented.
When the table view is about to appear
the first time it’s loaded, the
table-view controller reloads the
table view’s data. It also clears its
selection (with or without animation,
depending on the request) every time
the table view is displayed. The
UITableViewController class implements
this in the superclass method
viewWillAppear:. You can disable this
behavior by changing the value in the
clearsSelectionOnViewWillAppear
property.
In your implementation:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if ([self.tableView numberOfSections] == 0) {
[self.tableView reloadData];
} else {
[self.tableView deselectRowAtIndexPath:[self.tableView indexPathForSelectedRow] animated:animated];
}
}
When the table view has appeared, the
controller flashes the table view’s
scroll indicators. The
UITableViewController class implements
this in the superclass method
viewDidAppear:.
In your implementation:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.tableView flashScrollIndicators];
}
It implements the superclass method
setEditing:animated: so that if a user
taps an Edit|Done button in the
navigation bar, the controller toggles
the edit mode of the table.
In your implementation:
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
[super setEditing:editing animated:animated];
[self.tableView setEditing:editing animated:animated];
}

Related

Add Subview to UITableView's Parent view

I'm trying to add a subview to the parent view of a UITableView in order to have a subview that does not move when the UITableView is scrolled.
When I add the subview to the UITableView it moves with the table on user scrolling action.
So far, my scenario is that I have 3 UINavigationControllers on a UITabBarController.
Each has a UITableViewController.
I would like to add a subview to the screen of the individual controllers. However, I don't want the new subview to be added to the entire application like this:
[[[UIApplication sharedApplication] keyWindow] addSubview:newView];
When I push a new controller to the stack or pop one, I would like the subview to move and disappear with the UITableView it's on top of. Keeping the fact that when the user scrolls the UITableView, the new subview sits at its position without moving.
So that the UITableView and the new subview are on the same level.
Thanks
A couple of approaches:
First, to add a view to a table view that stays when the user scrolls, if your table only has one section, you can add a header for that section. For example, this adds a plain blue UIView as a header to the tableview:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, self.view.bounds.size.width, 50)];
view.backgroundColor = [UIColor blueColor];
return view;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return 50;
}
Second, if you have more than one section, though, that trick doesn't work, and you'll want to use a UIViewController instead of a UITableViewController, add you header to the top of that controller's view and add a tableview to the bottom of that view. You'll want to make sure you set up (a) an IBOutlet for the tableview (generally to a property called tableView, to avoid confusion); and (b) specify the view controller as this tableview's data source and delegate.
Third, if you want this header to appear on every scene in your app, another approach (in iOS5+ apps) is to use view controller containment, create a container view that has your header, and then add your app's first scene as a child view of that view container. It's not hard, but if you're a new developer, I might discourage you from trying that. I've done it and it works well, but it's not for all people.
Fourth, if your app is using a navigation controller, you could customize the navigation bar to render your custom look and feel. It all depends upon what you're trying to accomplish with the header.
Add your subview to the tableview..
Implement the delegate method scrollViewDidScroll.
On scrollViewDidScroll, just set the required center of the subview you want.

Adding UIView subview to single table view controller in navigation stack

I'm working on an app that has three table view controllers in a navigation stack. The root view controller and the second VC have toolbars, but I want to add a subview to the second view controller like this. (The color is just there for visualization.)
I want to add the view programmatically, since I haven't been able to do it with IB without major headaches. Right now, I've been able to kind of get what I want by drawing a UIView in the second view controller like this:
- (void)viewDidLoad {
[super viewDidLoad]
UIView *detailView = [[UIView alloc] initWithFrame:CGRectMake(0, 392, 320, 44)];
detailView = [UIColor redColor];
[self.navigationController.view addSubview:detailView];
[detailView release];
}
The problem with this approach is that once the UIView is loaded in the second view controller, it stays loaded and is drawn in the third and root view controllers. I've tried a variety of methods of removing the UIView, including setting the detailView to nil in viewDidUnload, calling removeFromSuperview in didSelectRowAtIndexPath (which removed the view from the whole stack).
I've also tried adding the subview to self.view, but that pushes it below the visible area of the table view, so I have to scroll up to see it, and it snaps back down when I let go.
Clearly, adding this subview to the navigation controller is not the best way to do what I want, but I'm at a loss as to where to go from here.
As you've already discovered, you definitely should not be reaching up into the navigation controller's view.
You want your SecondViewController to be an UIViewController that implements the UITableViewDelegate and UITableViewDataSource and whose view lays out the UITableView and the UIView you wish to use for your stationary 'footer' in it's own main UIView.
It helps to keep in mind that UITableViewController is ultimately is just a convenience for creating a view controller whose view consists entirely of a UITableView.
Anyway, rather than attempt to put a pile of that code inline in this answer, you can browse it (or svn co) from this read-only svn repo.
EDITED (now that it's not midnight, putting some code/explanation directly in answer):
For the controller to be pushed onto the nav stack that needs the footer create a new UIViewController-based class (do NOT check the 'UITableViewController subclass' box in the template selection dialog).
Add instance variables for the UITableView and the UIView that is to be the extra bottom view.
#interface SecondViewController : UIViewController <UITableViewDelegate, UITableViewDataSource> {
UITableView* tableView;
UIView* customFooterView;
}
#property (nonatomic,retain) IBOutlet UITableView* tableView;
#property (nonatomic,retain) IBOutlet UIView* customFooterView;
#end
In IB add a UITableView and UIView to the existing root view for the controller and lay them out as desired (probably worth altering the auto-resize parameters too if your app can be used in both landscape and portrait). Hook up the two views to the outlets defined for them in the "File's Owner" and also ensure you hook up the UITableView's delegate and dataSource properties to point at the "File's Owner."
Then just implement the UITableViewDelegate and UITableViewDataSource protocols as appropriate for your application.
If you want to lay out the entire 'footer' view in IB then go right ahead. Otherwise you can easily add items programmatically in viewDidLoad (and remember to tear it down in viewDidUnload).
I don't like the approach. You should put your table view inside another view, and put your detail view together in that view.
Despite of that, I think you can remove your view in viewWillDisappear method of your view controller. I also notice that you did not keep your detailView as a private variable, which you should do because you need to reference it when removing it later (I still wonder how you have done it.)
Note that viewDidUnload is called in case of view unloading (i.e. releasing from its controller), so it is not related to navigation.
Not sure which behavior you're looking for but try one of these:
Assign the detailView to the tableFooterView property of the tableview on the second VC.
Reduce the height of the table view and add the detailView to self.view.

How to change height of the popoverController when one of its viewControllers gets popped?

I have a SplitViewController based app. It uses a rootViewController inside a popoverController. The rootViewController sets the height of the popover by specifying (in viewDidLoad)
self.contentSizeForViewInPopover = CGSizeMake(320.0, 573.0);
When you select a row in the rootViewController, it pushes a secondViewController. The secondViewController makes the popover taller by specifying (in viewDidLoad):
self.contentSizeForViewInPopover = CGSizeMake(320.0, 900.0);
When the user taps on the back button to pop the secondViewController, the height of the popover stays taller. I would like to adjust the height back to the original size. I tried setting contentSizeForViewInPopover in viewWillAppear and also in navigationController's willShowViewController delegate methods. But these did not have any effect.
FWIW, I worked around this problem by manually resizing the popoverController in my view's viewWillAppear method. In other words, I set self.contentSizeForViewInPopover in -[viewDidLoad] and set popoverController.popoverContentSize in -[viewWillAppear:]. Of course, this requires that you save a pointer to the popoverController.
A better way to do this is to change the navigation controller's contentSizeForViewInPopover property. This way you don't need a pointer to the popover controller. Here's how I implemented it in my view controller (in viewDidAppear):
self.contentSizeForViewInPopover = someSize;
if (self.navigationController)
self.navigationController.contentSizeForViewInPopover = someSize;
This implementation also takes care of the case where the view controller doesn't have a navigation controller. If you change the navigation controller's property without also changing the view controller's (self), it won't work. Also, it didn't work for me in the viewWillAppear method.
We've found that the selected solution is best, except that it's probably a bad design idea to have a reference to the popover in the view. Instead, set the UINavigationController's delegate and handle it in navigationController:didShowViewController:animated:. In our case, this is best handled in the place that the popover is displayed, thus already having access to the popoverController.
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
[popoverController setPopoverContentSize:viewController.contentSizeForViewInPopover];
}

How do I reduce the height of a TableView when it is constructed in IB?

I wanted to add a view to the bottom of my screen. The controller is a UITableViewController, how do I shrink the tableView and add a extra view at the bottom of the tableview?
I've tried setting the frame of self.tableView in different places (viewDidLoad, viewWillAppear etc) but nothing happens. The tableView is created by IB and not programtically.
I've tried added a footer to my table view but that's not what I want, because the footer actually scrolls up, I want a static non moving View at the bottom of the screen.
I'm not saying you can't do it otherwise, but you may not want a UITableViewController for this situation. You can still have your view controller implement UITableViewDelegate and UITableViewDataSource, but place a vanilla UIView in your nib, into which you place a UITableView. Then just make sure to set the view outlet to the UIView containing your table. This has the effect of allowing you to create your additional view within IB. I just tried this and it appeared to work.
I'm guessing you're using a UINavigationController. When you push a controller onto your navigation stack, UINavigationController resizes its view to full screen, ignoring the geometry and autoresizing behavior you've defined in IB.
This resizing seems to happen after viewWillAppear:. In the past I've had some success resizing a table view and adding a sibling view in viewDidAppear:, after calling [super viewDidAppear:]. This is a bit risky though, since Apple could break it by changing how UINavigationController works behind the scenes.
A safer option is to push a view controller onto your navigation stack that controls a wrapper view. Then add your UITableView and its sibling as subviews of that wrapper view. The annoying thing about this option is that you'll probably want to use a nested UITableViewController to manage your non-full screen table view, but the documentation for UIViewController says it's designed to manage full screen views only. If you decide to ignore this admonition and nest your view controllers anyway, you'll find that viewWill/DidAppear/Disappear don't get called on the nested controller, so you'll have to manually delegate those methods from your wrapper view controller. This lack of support for nested controllers is one of my biggest pet peeves about UIKit, and I've gone to great lengths to engineer around it.
If you want to toe the line and use view controllers only for full screen views, you can push a normal view controller that controls your full screen wrapper view, manually implement all the UITableViewDataSource and UITableViewDelegate methods in your view controller, and set it as the delegate for your table view.
you want to change the -loadView method. Not viewDidLoad or viewWillAppear. This will allow you to make additional configurations with your tableview even if it is created in IB.
- (void)loadView {
[super loadView];
CGRect titleRect = CGRectMake(0, 0, 300, 40);
UILabel *tableTitle = [[UILabel alloc] initWithFrame:titleRect];
tableTitle.textColor = [UIColor blueColor];
tableTitle.backgroundColor = [self.tableView backgroundColor];
tableTitle.opaque = YES;
tableTitle.font = [UIFont boldSystemFontOfSize:18];
tableTitle.text = [curTrail objectForKey:#"Name"];
self.tableView.tableHeaderView = tableTitle;
[self.tableView reloadData];
[tableTitle release];
}
I don't know how to do it in IB but the way to do it in code is with this:
- (void) loadView
{
UITableView *tv = [[UITableView alloc] initWithFrame: rect
style: UITableViewStyleGrouped];
// finishg configuring table view
self.view = tv;
[tv release];
}
Trying to do it in two stages -- style first and then frame or frame first and then style -- neither of them works.

How to scroll to tableview row?

I have a UIViewController (named VC) that inherits from UITableViewDelegate and UIScrollViewDelegate. The previous UIViewController loads VC like this:
[self.view addSubview:VC.view];
which means viewWillAppear doesn't fire. I can add that method just after the above line:
[VC viewWillAppear];
but then it will fire before cellForRowAtIndexPath, which results in an empty tableview reference when I try to scroll.
I would like to scroll to a particular row in the table on load of VC. But because I don't know when the tableview's cellForRowAtIndexPath has completed (lack of viewWillAppear), I don't have any place to put the scroll code. I already keep a reference to the tableview and can use it for scrolling. But where can I place the scroll code?
Do you mean that your UIViewController conforms to the UITableViewDelegate and UIScrollViewDelegate protocols rather than 'inherits'?
You could invoke [VC viewWillAppear:] directly from your parent view controller. If the view handled by VC is always a sub-view of another view controller's view then do you really need a full UIViewController instance to back your UITableView? You could just provide a delegate and datasource implementation for the table view.
You could then instantiate your delegate and datasource either in the NIB that defines the table view (by adding an object in Interface Builder) or in the viewDidLoad method of the view controller that is currently the parent to VC.
Surely when the new view controller's view is added and becomes visible, it will call the viewDidAppear rather than the viewWillAppear. I'd stick the scroll code in there.
If that still doesn't work then try a delay in your init method. Something like this:
[self performSelector:#selector(myScrollingFunction) withObject:nil afterDelay:2.0];
You could play around with the delay time to get something that suits, although your view controller's view may not always appear after the same amount of time.