Problem: Scrolling to top again to tableView changes content offset and it hides first tableView cell.
I know there are lot of post on scrolling issue with tableView's scrollView where tableView content offsets changes after scrolling but there solution didn't helped me.
I tried to set content offset to 0 in -(void)layoutSubviews method but doesn't help. Also, this doesn't help.
self.automaticallyAdjustsScrollViewInsets = NO;
self.parentViewController.automaticallyAdjustsScrollViewInsets = NO;
In Detail:
When tableView loads, I see content offset set to {0, 0}
Printing description of $6:
<UITableView: 0x7fca15828400; frame = (0 0; 1920 1080); autoresize = RM+BM; gestureRecognizers = <NSArray: 0x60800024cb40>; layer = <CALayer: 0x60800022c780>; contentOffset: {0, 0}; contentSize: {1920, 1600}>
when I scroll down, and top to again, I see offset content changed.
Printing description of $10:
<UITableView: 0x7fca14880400; frame = (0 0; 1920 1080); autoresize = RM+BM; gestureRecognizers = <NSArray: 0x618000056da0>; layer = <CALayer: 0x618000037920>; contentOffset: {0, 108}; contentSize: {1920, 3754}>
However, I set canFocusRowAtIndexPath to YES, I don't see problem with scrollView but I don't want tableViewCell to focused.
- (BOOL)tableView:(UITableView *)tableView canFocusRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
Any help please?
This issue was because UICollectionViewFlowLayout which I was setting programmatically and messing with content offset every time when tableView's cellForRowIndexpath is getting called.
The question and the screen shots pretty much say it all. The cells are being created from a nib that has been registered with the tables.
Update: I decided to dig in with lldb:
(lldb) po servicesTable.tableHeaderView
nil
(lldb) po characteristicsTable.tableHeaderView
nil
The header views are nil! Does this mean that I am being fooled by what I see on my storyboard? That I have not actually created table headers?
Let's keep digging:
`(lldb) po characteristicsTable!.subviews
▿ 4 elements
- 0 : <UITableViewWrapperView: 0x7f89f4840a00; frame = (0 0; 343 270.5); gestureRecognizers = <NSArray: 0x610000051070>; layer = <CALayer: 0x61000003df00>; contentOffset: {0, 0}; contentSize: {343, 270.5}>
- 1 : <UIImageView: 0x7f89f2d1c6a0; frame = (3 265; 337 2.5); alpha = 0; opaque = NO; autoresize = TM; userInteractionEnabled = NO; layer = <CALayer: 0x600000224840>>
- 2 : <UIView: 0x7f89f50162a0; frame = (0 100; 343 44); autoresize = RM+BM; layer = <CALayer: 0x618000039e80>>
- 3 : <UIImageView: 0x7f89f2c1c370; frame = (337.5 179.5; 2.5 88); alpha = 0; opaque = NO; autoresize = LM; userInteractionEnabled = NO; layer = <CALayer: 0x6080004241e0>>`
(lldb) po characteristicsTable!.subviews[2].subviews
▿ 2 elements
- 0 : <UILabel: 0x7f89f5016570; frame = (8 8; 327 28); text = 'Characteristics'; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x61800008cbc0>>
- 1 : <UIButton: 0x7f89f2d06f40; frame = (313 11; 22 22); opaque = NO; autoresize = RM+BM; layer = <CALayer: 0x600000223300>>
Alright, the characteristics table sub view at index 2 is the one that we see labeled as "Header" in the storyboard's document outline (i.e. a view containing a label with the text "Characteristics" and a button).
Lets try this:
(lldb) expression characteristicsTable.tableHeaderView = characteristicsTable.subviews[2]
Voila! The characteristics table's header is positioned properly.
From all of this I am concluding that I do not know how to drag a view onto a table and have it become the table's header. Can anyone advise? (I will continue to experiment)
In order to use the Interface Builder to add a table header to a UITableView, drag a view onto the table view's entry in the document outline. DO NOT drag the view onto the table view's rendering in the storyboard. Jeez! These type of subtleties are darned exasperating.
(lldb) po servicesTable.tableHeaderView
▿ Optional<UIView>
Sometimes I want to use UIViewController as truly "controller" rather than a view with controller. That is, the UIViewController will gain control of an existing UIView.
In a nested UIViewcontroller, my UIViewController's view is a subview of some other view.
In some cases, there is a possibility that UIViewController would control the same view but not at the same time.
So during delegate assignment I would ensure that the previous UIViewController stop registering the view as it's view.
UITableView * tvDelegated = (UITableView *)self.delegateForTableController.tvDelegated; //Make sure tvDelegated is not deallocated
UIViewController * vcPreviousViewController = [tvDelegated vcImmediateUIViewController];
assert(vcPreviousViewController.view==tvDelegated);
PO(vcPreviousViewController.view);
PO(vcPreviousViewController.view.superview);
PO(tvDelegated);
PO(tvDelegated.superview);
vcPreviousViewController.view=nil;//if the table view used to have a different tableViewController then disassociate that
PO(vcPreviousViewController.view);
PO(vcPreviousViewController.view.superview);
PO(tvDelegated);
PO(tvDelegated.superview);
Basically I store a strong pointer to the previous ViewController's view in a variable called tvDelegated. This ensure that the view does not disappear.
Result, first everything is normal:
self.BGtableController.view: <UITableView: 0x1dc0d600; frame = (0 0; 320 548); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x1e485850>; layer = <CALayer: 0x1e4e35b0>; contentOffset: {3, 10}>
self.BGtableController.view.superview: <UIView: 0x1e4bd910; frame = (0 0; 320 548); autoresize = W+H; layer = <CALayer: 0x1e4bd970>>
vcPreviousViewController.view: <UITableView: 0x1dc0d600; frame = (0 0; 320 548); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x1e485850>; layer = <CALayer: 0x1e4e35b0>; contentOffset: {3, 10}>
vcPreviousViewController.view.superview: <UIView: 0x1e4bd910; frame = (0 0; 320 548); autoresize = W+H; layer = <CALayer: 0x1e4bd970>>
After vcPreviousViewController.view=nil is executed the tvDelegated is not gone as expected, but the superview is gone.
vcPreviousViewController.view: <UITableView: 0x1dc0b000; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x1e4cd330>; layer = <CALayer: 0x1e4cddd0>; contentOffset: {3, 10}>
vcPreviousViewController.view.superview: (null)
tvDelegated: <UITableView: 0x1dc0d600; frame = (0 0; 320 548); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x1e485850>; layer = <CALayer: 0x1e4e35b0>; contentOffset: {3, 10}>
tvDelegated.superview: (null)
This is another amazing thing. The vcPreviousViewController.view does not become nil. What becomes nil is vcPreviousViewController.view.superview even though I clearly set vcPreviousViewController.view to nil.
There is no reason to use UIViewController than. You can inherite you class from NSObject, but it doesn't matter.
It is a bad practice to use delegates so tricky. You'll recieve more problems like this.
What do I do in the same case:
1) One screen - one view controller. Don't make your logic more
complicated. Set delegate of all views to standard ViewController.
Regardless you need:
2a) Create custom class, possibly singlton. Looks like you need something like manager.
2b) Use Notifactions mechanism, in case you don't need something like manager. It helps you avoid headache with delegate links.
As Apple docs on UIViewController's view property say:
"Each view controller object is the sole owner of its view. You must not associate the same view object with multiple view controller objects. The only exception to this rule is that a container view controller implementation may add this view as a subview in its own view hierarchy. Before adding the subview, the container must first call its addChildViewController: method to create a parent-child relationship between the two view controller objects.
If you access this property and its value is currently nil, the view controller automatically calls the loadView method and returns the resulting view."
I suppose that you get this behaviour because you don't call proper methods for UIViewController's parent/child relationship. So you might get cached version of your view which was pulled out of view hierarchy and therefore has no superview.
Let's examine this line of codes (only 1 actually change anything)
PO(self.containerForFormerHeader);
PO(self.containerForFormerHeader.subviews);
[self.containerForFormerHeader sizeToFit];
PO(self.containerForFormerHeader);
Result:
self.containerForFormerHeader: <UIView: 0x8b669c0; frame = (0 75; 320 0); autoresize = W+H; layer = <CALayer: 0x8b5eb30>>
self.containerForFormerHeader.subviews: (
"<UIImageView: 0xd572210; frame = (0 0; 320 10); autoresize = LM+RM+TM; userInteractionEnabled = NO; layer = <CALayer: 0xd572250>> - shading-top-Table.png"
)
self.containerForFormerHeader: <UIView: 0x8b669c0; frame = (0 75; 320 0); autoresize = W+H; layer = <CALayer: 0x8b5eb30>>
From the result it's obvious that:
The view has a height of 0.
The view has a subview whose height is 10
After sizetofit, nothing change. How come?
I can compute the frame directly however, this bothers me.
When you call the sizeToFit method on a view, it ends up calling the sizeThatFits: method on the view. The default implementation of sizeThatFits: returns the view's current size.
So unless your custom view explicitly overrides the sizeThatFits: method to return an appropriate size to contain its subviews, nothing will change size when you call sizeToFit.
This is all spelled out in the docs for UIView sizeToFit and UIView sizeThatFits:.
Classes like UILabel do implement sizeThatFits: to return an appropriate size.
In my app (code very similar to Apple's PhotoScroller demo from WWDC10), I have a UIScrollView. Within the scroll view I have overwridden layoutSubviews with code like:
- (void) layoutSubviews {
[super layoutSubviews];
// center the image as it becomes smaller than the size of the screen
CGSize boundsSize = self.bounds.size;
...
}
In iOS 4.x if the application starts up on landscape mode (user is holding it landscape), then layoutSubviews gets called twice (very quickly). The first time its called, boundsSize has dimensions that indicate its in portrait but immediately afterwards it gets called again and self.bounds.size returns dimensions that indicate the device is in landsacpe and my layout calculations work correctly.
In iOS 5.x, layoutSubviews only gets called once with bounds.size returning dimensions indicating portrait and it doesn't get the second call, so all my calculation code is messed up.
If the user physcially rotates the device then layoutSubviews gets called and works correctly -- so a user starting the app in landscape (draws incorrectly), rotates to portrait (draws correcty) and then rotates back to landscape (now draws correctly).
Its that second "automatic" call of layoutSubviews that I'm missing.
Anybody else notice this or have any advice?
Update:
I did find this in the UIKit release notes for iOS 5, but I'm not sure if it's relevent or even what the impact is of this change.
Rotation callbacks in iOS 5 are not applied to view controllers that are presented over a full screen. What this means is that if your code presents a view controller over another view controller, and then the user subsequently rotates the device to a different orientation, upon dismissal, the underlying controller (i.e. presenting controller) will not receive any rotation callbacks. Note however that the presenting controller will receive aviewWillLayoutSubviews call when it is redisplayed, and the interfaceOrientation property can be queried from this method and used to lay out the controller correctly.
Further updates:
Using [UIView recursiveDescription], to dump the view hierarchies, I got the following output:
iOS 4 (which is working correctly):
Portrait:
2011-12-19 15:57:06.400 stroom[10889:11603] <0x5e7f9b0 stroomViewController.m:(402)> <UIView: 0x6a6f2f0; frame = (0 0; 768 1024); autoresize = W+H; layer = <CALayer: 0x6a659d0>>
| <UIScrollView: 0x5e911e0; frame = (-20 0; 808 1024); clipsToBounds = YES; autoresize = LM+W+RM+TM+H+BM; layer = <CALayer: 0x5e91370>; contentOffset: {0, 0}>
| | <stroomFullScreenPhotoScrollView: 0x5ea1010; baseClass = UIScrollView; frame = (20 0; 768 1024); clipsToBounds = YES; layer = <CALayer: 0x5ea0940>; contentOffset: {0, 0}>
| | | <UIImageView: 0x5ea2600; frame = (0 224; 768 576); transform = [0.75, 0, 0, 0.75, 0, 0]; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x5ea0890>>
Landscape:
2011-12-19 15:57:34.522 stroom[10889:11603] <0x5e7f9b0 stroomViewController.m:(402)> <UIView: 0x6d96c30; frame = (0 0; 768 1024); transform = [0, 1, -1, 0, 0, 0]; autoresize = W+H; layer = <CALayer: 0x6d66440>>
| <UIScrollView: 0x5e9eb70; frame = (-27 0; 1078 768); clipsToBounds = YES; autoresize = LM+W+RM+TM+H+BM; layer = <CALayer: 0x5eadde0>; contentOffset: {0, 0}>
| | <stroomFullScreenPhotoScrollView: 0x6dab850; baseClass = UIScrollView; frame = (20 0; 1038 768); clipsToBounds = YES; layer = <CALayer: 0x6dab2e0>; contentOffset: {0, 0}>
| | | <UIImageView: 0x5e97f70; frame = (0 224; 1024 768); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x5e97fa0>>
iOS 5 (which is working incorrectly):
Portrait:
2011-12-19 15:55:59.530 stroom[10850:16103] <0x848b520 stroomViewController.m:(402)> <UIView: 0xa884710; frame = (0 0; 768 1024); autoresize = W+H; layer = <CALayer: 0xa8849a0>>
| <UIScrollView: 0xa883820; frame = (-20 0; 808 1024); clipsToBounds = YES; autoresize = LM+W+RM+TM+H+BM; layer = <CALayer: 0xa883a80>; contentOffset: {0, 0}>
| | <stroomFullScreenPhotoScrollView: 0x8699630; baseClass = UIScrollView; frame = (20 0; 768 1024); clipsToBounds = YES; layer = <CALayer: 0x8699360>; contentOffset: {0, 0}>
| | | <UIImageView: 0x869a7c0; frame = (0 0; 768 576); transform = [0.75, 0, 0, 0.75, 0, 0]; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x869a800>>
Landscape:
2011-12-19 15:56:32.521 stroom[10850:16103] <0x848b520 stroomViewController.m:(402)> <UIView: 0x8498530; frame = (0 0; 768 1024); transform = [0, 1, -1, 0, 0, 0]; autoresize = W+H; layer = <CALayer: 0x8498560>>
| <UIScrollView: 0x849ead0; frame = (-27 0; 1077 768); clipsToBounds = YES; autoresize = LM+W+RM+TM+H+BM; layer = <CALayer: 0x848f390>; contentOffset: {808, 0}>
| | <stroomFullScreenPhotoScrollView: 0x81e4b80; baseClass = UIScrollView; frame = (828 0; 768 1024); clipsToBounds = YES; layer = <CALayer: 0x81e7dc0>; contentOffset: {0, 0}>
| | | <UIImageView: 0x81e5090; frame = (0 0; 768 576); transform = [0.75, 0, 0, 0.75, 0, 0]; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x81e5010>>
From these I can see that the contentOffset of the UIScrollView in iOS 5 looks incorrect and that the landscape frame have incorrect dimensions in iOS 5 where they appear to have correct dimensions in iOS 4.
Eventually, I stumbled upon the solution to this problem for me. I'll provide the answer here as it maybe helpful for others.
The solution for me was to implement the - (void)viewWillAppear:(BOOL)animated method on my view controller. I didn't have one and I had all my setup logic in - (void) viewDidLoad method.
In particular, I implemented the following:
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
CGRect bounds = pagingScrollView.bounds; // <=== this was the key
// ... use the bounds in my view setup logic
}
It appears to me, that there has been a change in iOS 5 (from previous versions of the OS). When a view is loaded when a device is in landscape, the bounds property on a view does not reflect the orientation transformation until viewWillAppear. Previously, the bounds value was correct for me in viewDidLoad but moving the calculations and reading of the bounds property out of there and into viewWillAppear did the trick.
All works fine in iOS4 as well, when I did my testing.
Perhaps, I should always have been reading the bounds property in viewWillAppear and it may well be the more correct behavior. However, something has changed in between iOS4 and iOS 5 that I have not been able to find documentation on.
`
I was having similar issue. Somehow, I solved it.
In iOS 5.x, When you create new XIB file, by default, it Orientation is set to Portrait. So, your layoutSubviews only getting called once with bounds.size returning dimensions indicating portrait and it doesn't get the second call.
Just, Do one thing, set your XIB orientation to Landscape in XIB design. So, your code will check orientation for the first time, it will get it as Landscape & your all layouts will be set accordingly.
I'm having a similar issue. I got around it by hardcoding the bounds rect depending on the view controller's interface orientation and possibly flagging the scrollview for setNeedsLayout on viewDidLoad and viewWillAnimateToInterfaceOrientation.