Why Are The UITableView's Cells Being Inserted Above The Table Header? - xcode8

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>

Related

How in the earth [cell.contentView setUserInteractionEnabled: NO]; solves this?

I have the exact same problem
Custom buttons in XIB used as Custom UITableViewCell don't respond to taps (ios7)
Basically buttons in UITableViewCell no longer works.
It used to work fine in IOS 6. Not anymore in IOS 7.
I saw 2 people with the same solution
[cell.contentView setUserInteractionEnabled: NO]
However, the solution doesn't make sense at all.
The buttons are obviously a subview of cell.contentView. If we set userinteractionenabled to no, it should disable user interaction to all the subviews of cell.contentView. So how the hell this work at all?
Actually what have changed between IOS 7 to IOS 6 so that this problem exist in the first place.
Note: It doesn't really solve my problem.
As I suspected, the buttons inside UITableViewCell is indeed a subview of contentView
2013-10-11 13:07:04.946 [15131:a0b] self: <BGCatalogTableCellForCatalog: 0x12e12d20; baseClass = UITableViewCell; frame = (0 0; 320 220); layer = <CALayer: 0x12e12c70>>
2013-10-11 13:07:04.947 [15131:a0b] self.contentView: <UITableViewCellContentView: 0x12e12600; frame = (0 0; 320 220); gestureRecognizers = <NSArray: 0x12e12440>; layer = <CALayer: 0x12e125d0>>
2013-10-11 13:07:04.947 [15131:a0b] self.btnBrochureButton: <UIButton: 0x12e100b0; frame = (0 0; 160 219); opaque = NO; autoresize = RM+TM+BM; layer = <CALayer: 0x12e102c0>>
2013-10-11 13:07:04.948 [15131:a0b] self.btnBrochureButton.superview: <UITableViewCellContentView: 0x12e12600; frame = (0 0; 320 220); gestureRecognizers = <NSArray: 0x12e12440>; layer = <CALayer: 0x12e125d0>>
2013-10-11 13:07:04.948 [15131:a0b] self.btnBrochureButton.superview.superview: <UITableViewCellScrollView: 0x12e12920; frame = (0 0; 320 220); clipsToBounds = YES; autoresize = W+H; gestureRecognizers = <NSArray: 0x12e12840>; layer = <CALayer: 0x12e128f0>; contentOffset: {0, 0}>
2013-10-11 13:07:04.949 [15131:a0b] self.btnBrochureButton.superview.superview.superview: <BGCatalogTableCellForCatalog: 0x12e12d20; baseClass = UITableViewCell; frame = (0 0; 320 220); layer = <CALayer: 0x12e12c70>>
2013-10-11 13:07:04.949 [15131:a0b] <UIButton: 0x12e100b0; frame = (0 0; 160 219); opaque = NO; autoresize = RM+TM+BM; layer = <CALayer: 0x12e102c0>>
So both the btnBrochureButton.superview and self.contentView is 0x12e12600
I got the problem fixed. Starting in IOS 7, contentView is not a direct subview of UITableViewCell. This causes complication.
I'm not sure if this will help you, but I found that my issue was related to the issues described in the posts you referenced, but I also had one more piece of the puzzle. The contentView.userInterActionEnabled = NO didn't work for me, for the same reason you described.
In my app, I'm dynamically resizing the table view cells, changing their heights at runtime based on the content in the cell. This created a separate issue for me where the cell was being resized, but the contentView, for some reason, was not being resized. So my button was visible, but was outside the bounds of the contentView and therefore not available to user interaction.
The way I finally discovered it was by setting the background color of my contentView to a different color, where I finally saw that it was too short. I solved the issue by changing the height for the contentView after I calculated the total height for the parent cell.
I hope this can help you or at least give you something new to check for.

How in the earth setting a UIViewController view to nil make the view lost its superview?

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.

Why size to fit doesn't work at all and do nothing in this obvious scenario

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.

Why the tableHeaderView got removed when I added that to another subView and replace the tableHeaderView?

This is the full code of what I want to do (only 2 lines)
[self.headerViewofWholeTable addSubview:self.delegate.tableView.tableHeaderView];
self.delegate.tableView.tableHeaderView= self.headerViewofWholeTable;
Things doesn't work, so I started adding print information
[self.headerViewofWholeTable addSubview:self.delegate.tableView.tableHeaderView];
PO(self.headerViewofWholeTable.subviews);
self.delegate.tableView.tableHeaderView= self.headerViewofWholeTable;
PO(self.headerViewofWholeTable.subviews);
Simple 4 line of codes :D
Result:
self.headerViewofWholeTable.subviews: (
"<UILabel: 0x8bf9770; frame = (50 15; 250 21); text = 'Pull Down to Refresh'; clipsToBounds = YES; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <CALayer: 0x8bf97e0>>",
"<UILabel: 0x8b63aa0; frame = (50 35; 257 20); text = 'last updated'; clipsToBounds = YES; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <CALayer: 0x8b63b10>>",
"<UIView: 0x8b63b80; frame = (20 11; 22 54); autoresize = RM+BM; layer = <CALayer: 0x8bf6c40>>",
"<UIImageView: 0x8be9ca0; frame = (0 0; 320 10); autoresize = LM+RM+BM; userInteractionEnabled = NO; layer = <CALayer: 0x8be9ce0>> - shading-top-Table.png"
)
self.headerViewofWholeTable.subviews: (
"<UILabel: 0x8bf9770; frame = (50 15; 250 21); text = 'Pull Down to Refresh'; clipsToBounds = YES; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <CALayer: 0x8bf97e0>>",
"<UILabel: 0x8b63aa0; frame = (50 35; 257 20); text = 'last updated'; clipsToBounds = YES; opaque = NO; autoresize = RM+BM; userInteractionEnabled = NO; layer = <CALayer: 0x8b63b10>>",
"<UIView: 0x8b63b80; frame = (20 11; 22 54); autoresize = RM+BM; layer = <CALayer: 0x8bf6c40>>"
)
So you see,
that self.delegate.tableView.tableHeaderView= self.headerViewofWholeTable; remove the view that's pointed to by self.delegate.tableView.tableHeaderView. However, the view
"<UIImageView: 0x8be9ca0; frame = (0 0; 320 10); autoresize = LM+RM+BM; userInteractionEnabled = NO; layer = <CALayer: 0x8be9ce0>> - shading-top-Table.png"
should not go away because it's retained by self.headerViewofWholeTable
So can anyone explain what happened?
I can easily circumvent the problem, but I want to know more of what's actually going on.
Most likely the implementation of the UITableView setTableHeaderView method (which you call in the second line) assumes that the current header view is still a subview of the table. So when you try to assign a new header view, the table view first removes the current header from its superview (not knowing it has already been moved to your other view). The table view isn't expecting that its header view has been moved to a new view. So it ends up removing the header view from your headerViewofWholeTable view.

iOS5, UIScrollView and layoutSubviews behaviour

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.