Unexpected behaviour when adding subviews to UICollectionViewCell - objective-c

I'm currently facing a strange issue with UICollectionViewCell when adding subviews to it but only during certain situations.
Here is the scenario:
I have a "container" view which conforms to a very specific protocol (ADGControl) with a nested view, typically a UIKit control subclass I.e MyCustomTextField : UITextField for custom controls.
The "container" view exposes a property called "innerControlView" which holds a strong reference to the custom control which is what I'm trying to add as a sub view to the cell's content view.
Here is the code:
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
FormControlCollectionViewCell *cell = [self.formCollectionView dequeueReusableCellWithReuseIdentifier:#"formControlCell" forIndexPath:indexPath];
NSArray *sectionContents = [_controlList objectAtIndex:[indexPath section]];
// This works
//UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 315.0f, 30.0f)];
//textField.borderStyle = UITextBorderStyleLine;
//[cell.controlView addSubview:textField];
// This doesn't (see the behaviour in video clip)
id <ADGControl> control = [sectionContents objectAtIndex:[indexPath row]]; // The container view I'm referring to
[cell.contentView addSubview:(UIView *)[control innerControlView]]; // [control innerControlView] is the typical UIKit control subclass for custom controls. In this example it will be a UITextField
return cell;
}
As you can see in the code comments above, whenever I try to add just a UIKit control (textField) directly, it works just fine. However, as soon as I try to add my custom control ([control innerControlView] I get the unexpected behaviour as seen in the video clip here: http://media.shinywhitebox.com/ryno-burger/ios-simulator-ios-simulator-ipad-ios-a
The above link is just a short 23 seconds video clip to better demonstrate the "unexpected behaviour" that I get.
If anybody can point out what I'm doing wrong of what the issue might be I will be grateful.
Thanks

As you can read in the documentation on UICollectionViewCells, you shouldn't add content subviews to the cell itself, but to it's contentView.
And, like said before in my comment, you shouldn't add subviews in the data source, but in the subclass. You already noted that initWithFrame: wasn't called, use initWithCoder: instead:
-(id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
// Add your subviews here
// self.contentView for content
// self.backgroundView for the cell background
// self.selectedBackgroundView for the selected cell background
}
return self;
}

A view can only be in one superview at once. If its already a subview of your container view, you can't just add it as a subview of another view (your cell).
It's not really clear why you're using a view as part of your model object, but you'll either have to change that or remove the inner view from its current superview before adding it to the cell.

Related

How to properly show the current selection in an NSCollectionView?

I have an NSCollectionView that is showing some images. I have implemented an NSCollectionViewDelegate to tell it which items should be selected and/or highlighted. I'm using a stock NSCollectionViewItem to draw the images and their names. When the user selects an item, my delegate gets the messages about highlight state changes:
- (void)collectionView:(NSCollectionView *)collectionView
didChangeItemsAtIndexPaths:(NSSet<NSIndexPath *> *)indexPaths
toHighlightState:(NSCollectionViewItemHighlightState)highlightState
{
[collectionView reloadItemsAtIndexPaths:indexPaths];
}
I do a similar thing for didSelect/didDeselect:
- (void)collectionView:(NSCollectionView *)collectionView
didSelectItemsAtIndexPaths:(nonnull NSSet<NSIndexPath *> *)indexPaths
{
[collectionView reloadItemsAtIndexPaths:indexPaths];
}
In the NSCollectionViewItems view, I do the following:
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
NSColor* bgColor = [[self window] backgroundColor];
NSColor* highlightColor = [NSColor selectedControlColor];
NSRect frame = [self bounds];
NSCollectionViewItemHighlightState hlState = [collectionViewItem highlightState];
BOOL selected = [collectionViewItem isSelected];
if ((hlState == NSCollectionViewItemHighlightForSelection) || (selected))
{
[highlightColor setFill];
}
else
{
[bgColor setFill];
}
[NSBezierPath fillRect:frame];
}
The problem I'm seeing is that drawing the highlight or selection appears to be random. When it does draw the selection, it's almost always on the items the user has actually selected (though it often leaves off the last item for some reason). Occasionally, it will select a different item the user did not click on or drag over. Often, though, it just doesn't draw.
I've added printing to verify that it is calling -didChangeItemsAtIndexPaths:toHighlightState: and -didSelectItemsAtIndexPaths:. Is there anything I'm doing wrong here?
I've added some logging to the view's -drawRect: method, and it doesn't appear to be getting called on all transitions, even though I'm calling -reloadItemsAtIndexPaths: in the -didChange* methods. Why not?
I've also noticed that the delegate's -should/didDeselectItemsAtIndexPaths: does not seem to get called ever, even though the -should/didSelectItemsAtIndexPaths: does get called. Why is that?
The problem turned out to be calling [collectionView reloadItemsAtIndexPaths:]. When you do that, it removes the existing NSCollectionViewItem and creates a new one (by calling your data source's collectionView:itemForRepresentedObjectAt:). That immediately sets the new collection view item to not selected (or rather it doesn't set it to be selected). When that happens, it won't call your should/didDeselect methods because the existing item doesn't exist anymore, and the new one is not selected.
The real solution turned out to be to subclass NSCollectionViewItem and override -setSelected: to do the following:
- (void)setSelected:(BOOL)selected
{
[super setSelected:selected];
[self.view setNeedsDisplay:YES];
}
When the view's -drawRect: method gets called, it asks the item if it's selected and draws appropriately.
Therefore, I could completely remove all of the should/did/select/Deselect methods from the delegate without any problem, and it all just worked!

Drawing + UITableViewCell + UIScrollView subclass?

My (rather complicated) situation is as follows:
TestView is a subclass of UIScrollView which implements -drawRect:, but at some point inside -drawRect: it'll call a method, let's say -drawAnotherPartWithRect:context:. This method is implemented by subclasses of TestView to draw individually a certain part of the context.
There are two subclasses of TestView which implement -drawAnotherPartWithRect:context:, which currently do the same thing inside it: Subclass1 and Subclass2.
As of now, frame size is the only different between the two during initialization.
An instance of Subclass1 is used as a table view's section header, and it works perfectly, yet if Subclass2 is used as a subview of the cell's content view, it'll display, yet not scroll. Its initialization is as follows:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"PortoAppSubjectViewTableViewCell"];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"PortoAppSubjectViewTableViewCell"] autorelease];
Subclass2 *contentView = [[[Subclass2 alloc] initWithFrame:CGRectMake(0.f, 0.f, [tableView bounds].size.width, 32.f)] autorelease];
[contentView setContentSize:CGSizeMake(tableView.bounds.size.width * 3, 32.f)];
[contentView setTag:55];
[[cell contentView] addSubview:contentView];
}
[(SubjectTableViewCellContentView *)[[cell contentView] viewWithTag:55] setContainer:[[[[$container subGradeContainers] objectAtIndex:[indexPath section]] subGradeContainers] objectAtIndex:[indexPath row]]];
return cell;
}
The interesting thing is, the horizontal scroll indicator shows up and shows me that it's scrolling finely, yet the text (drawn with CoreText) doesn't move left/right along with it. That works out-of-the-box with Subclass1. Additionally, if Subclass2 is used instead as the view class of the section header view, it'll work finely.
So, what's up with horizontal scroll views and table view cells? I've checked out other related questions on SO but haven't been able to find any solution.
I solved the issue by instead of drawing directly to a UIScrollView subclass, drawing to a UIView subclass added as a subview of UIScrollView.
I'd still like some follow-ups, therefore leaving the question unanswered – why did it work with the header view and not with the table view cell then?

What are the possible reasons why -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath didn't get called?

I got a really strange problem.
My tableView has all the delegate and datasource set up.
Everything is fine.
However, clicking the rows do not activate:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
I used custom cells.
After I click and click and click and click and click, sometimes it goes through.
I wonder what can possibly cause that? It's as if the customCell is "absorbing" the touch event or something or what?
Could that be why? If so, if we want to implement customCell and we want the tableView to handle the touch up event, what should we do?
Additional symptom:
If I remove user interaction enabled from the custom cell then the problem is solved with a catch.
However, clicking the button will somehow erase all the label texts in the customCell.
The implementation of the custom Cell is the following:
- (BGUIBusinessCellForDisplay *) initWithBiz: (Business *) biz
{
if (self.biz == nil) //First time set up
{
self = [super init]; //If use dequeueReusableCellWithIdentifier then I shouldn't change the address self points to right
NSString * className = NSStringFromClass([self class]);
//PO (className);
[[NSBundle mainBundle] loadNibNamed:className owner:self options:nil];
self.frame =self.view.frame;
[self addSubview:self.view]; //What is this for? self.view is of type BGCRBusinessForDisplay2. That view should be self, not one of it's subview Things don't work without it though
}
if (biz==nil)
{
return self;
}
_biz = biz;
self.prominentLabel.text = [NSString stringWithFormat:#"Isi: %#", biz.isiString];
self.Title.text = biz.Title; //Let's set this one thing first
self.Address.text=biz.ShortenedAddress;
//if([self.distance isNotEmpty]){
self.DistanceLabel.text=[NSString stringWithFormat:#"%dm",[biz.Distance intValue]];
self.PinNumber.text =biz.StringPinLineAndNumber;
NSString * URLString=nil;
if(biz.Images.allObjects.count!=0){
//self.boolImage=[NSNumber numberWithBool:true];
Image * image=(biz.Images.allObjects)[0];
URLString = image.URL;
URLString = [NSString stringWithFormat:#"http://54.251.34.144/thumbnailer/Thumbnailer.ashx?imageURL=%#",URLString.UTF8Encode];
//url=[NSURL URLWithString:image.URL];
}else{
float latitude = biz.getCllLocation.coordinate.latitude;
float longitude = biz.getCllLocation.coordinate.longitude;
URLString = [NSString stringWithFormat:#"http://maps.googleapis.com/maps/api/staticmap?&zoom=16&size=160x160&maptype=roadmap&sensor=true&center=%f,%f&markers=size:small|color:blue|%f,%f",latitude,longitude,latitude,longitude];
URLString = URLString.UTF8Encode;
}
//Should add code and add loading indicator here
[BGHPTools doBackground:^{
UIImage * imageBiz = [BGMDImageCacherAndDownloader getImageFromURL:URLString];
[BGHPTools doForeGround:^{
self.Image.image=imageBiz;
[self.Image makeRound];
}];
}];
//self.view=self.view;
/*if (self.tableViewCell == Nil)//Instantiate that tableviewCell
{
PO(self.tableViewCell);
}
self.tableViewCell.business = bis;
self.pinLbl.text = bis.StringPinLineAndNumber;
self.lblTitle.text=bis.Title;
//self.pinLbl.text=bis.pinNumber;*/
//}
/*self.name=[dict objectForKey:#"Title"];
self.address=[dict objectForKey:#"Street"];
CLLocation * cll=[[CLLocation alloc]initWithLatitude:[[dict objectForKey:#"Latitude"] doubleValue] longitude:[[dict objectForKey:#"Longitude"] doubleValue]];
self.distance=[NSNumber numberWithDouble:[cll distanceFromLocation:[cachedProperties currentLocation]]];*/
return self;
Update: I already figure out why the texts are gone. Turns out my background is white. When a row got selected, the text suddenly turn into white. So by setting selected style to blue I sort of get that "fixed".
However, I still do not see where in my code I specify that all label texts should be white if the underlying tableViewCell is selected.
After all, what's selected is the cell, not the label. How the hell the label knows that it has to turn white is beyond me.
If you are using a Storyboard to handle the interface, instead of using:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
Try using
#pragma mark --- Push selectedObject to the detailView ---
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
RRAppDelegate *myAppDelegate = [[UIApplication sharedApplication]delegate];
if ([[segue identifier] isEqualToString:#"PushObjectSegue"]) {
NSIndexPath *selectedRowIndex = [self.tableView indexPathForSelectedRow];
RRObjectViewController *detailViewController = [segue destinationViewController];
detailViewController.selectedObject = [myAppDelegate.goals objectAtIndex:selectedRowIndex.row];
}
}
I was having the same problem with the method you used and instead used this, it started working perfectly. Of course you'd have to adapt the code to your app's viewControllers and data source because I used my AppDelegate as the datasource, and I wasn't using a custom cell.
The most likely thing is that a view in your custom cell is absorbing the touch. Sometimes this is what you want, e.g. a button that does something, rather than selecting the entire cell. Assuming you don't want this, then just set those views' userInteractionEnabled property to NO.
--Additional code for custom NIB loading.
All you have to do is register the NIB in your viewDidLoad routine:
[tableView registerNib: [UINib nibWithNibName:#"yourCellNibName" bundle:nil] forCellReuseIdentifier:#"yourCellTypeID"]
and then in your cellForRowAtIndexPath just call:
newCell = [tableView dequeueReusableCellWithIdentifier #"yourCellTypeID"];
...
return newCell;
And it will load a cell from your XIB (or give you one from the previously used queue).
I just want to update that I think I have figured out what the problem is but still can't solve that quite right yet. And well the update is comprehensive so I think it should be an answer though I hope it's not the answer because some puzzle is still missing.
All the problem is interrelated.
The problem is in this line:
[self addSubview:self.view];
I basically turn that into:
Basically the my custom view cell has a view whose type is also tableViewCell. That view cover the real tableViewCell.
That's why when user interaction is enabled, that view will absorb the user's interaction.
That's also why the label "disappear". What happen is the label doesn't disappear. The label got highlighted and become white. However, what's highlighted is the tableViewCell not the opague view. The white opague self.view is still white while the tableCell itself is tinted with blue. So the label becomes white in the middle of white background and is gone.
I think I should replace [self addSubview:self.view] into self= self.view
However, that would mean changing the value of self. Yes it's in init. But it's still awkward. If anyone has the WAY to implement custom subclass of UI with XIB it'll be great because I haven't found one till now.
Awkward.
I wonder if we can draw a pointer to an XIB and specify that the outlet is self itself.
If that fail, I'll set background of self to white and background of self.view to transparent.
After tons of error and trying I did this:
self.contentView.backgroundColor = [UIColor whiteColor];
//self.view.backgroundColor = [UIColor redColor];
self.frame =self.view.frame;
/*PO(self.view.subviews);
PO(self.subviews);
PO(self.Title.superview);
PO(self.Title);
PO(self.view);
PO(self.Title.superview);
PO(self.view.contentView);*/
//Suck all the subviews from my minions
for (UIView* aSubView in self.view.contentView.subviews) {
[self.contentView addSubview: aSubView];
//[self.contentView add]
}
Basically I "move" all the subViews of my view object to my self object. There is a catch though that when subclassing tableViewCell I should move the subviews of the contentView. Who knows why.
At the end I just set self.view to nil for it's no longer needed and my program works as expected.
Also to set background of your tableViewCell, you need also to set the background of self.contentView rather than self.view.
Another approach you can try is to use story board. Alternatively you can just move the contentView of the self.view to self.
Make sure you'r implementing that method and not
deselectRowAtIndexPath:animated

adding subviews to UITableViewCell's content view makes cell unselectable

I want to add some static text to a UITableViewCell in a UITextView.
UITextView *addressField = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, 300, 75)];
[addressField setBackgroundColor:[UIColor clearColor]];
[addressField setFont:[UIFont fontWithName:#"HelveticaNeue" size:14]];
[addressField setContentInset:UIEdgeInsetsMake(0, 20, 0, 0)];
[addressField setEditable:NO];
[addressField setScrollEnabled:NO];
// change me later
[addressField setText:#"John Doe\n555 Some Street\nSan Francisco, CA, 00000"];
[cell.contentView addSubview:addressField];
[addressField release];
This works great but I this code makes the cell unselectable probably because the UITextView is covering the entire cell.
How can I work around this so that I can have both the UITextView and selectable cells?
btw, I could make the UITextView size a bit smaller but users would still not be able to select the cell if they touch the UITextView.
I think a slightly better way to do it is to create a tap gesture recognizer on the entire table. (For example in your viewDidLoad)
// gesture recognizer to make the entire cell a touch target
UITapGestureRecognizer* tap = [[UITapGestureRecognizer alloc]
initWithTarget:self action:#selector(changeFocus:)];
[tableView addGestureRecognizer:tap];
[tap release];
Then you create a selector (changeFocus: in this case) to do the actual selecting.
- (void)changeFocus:(UITapGestureRecognizer *)tap
{
if (tap.state == UIGestureRecognizerStateEnded)
{
CGPoint tapLocation = [tap locationInView:self.tableView];
NSIndexPath* path = [self.tableView indexPathForRowAtPoint:tapLocation];
[self tableView:self.tableView didSelectRowAtIndexPath:path];
}
}
You can make your changeFocus method more elaborate to prevent selections or give focus to specific subviews of the selected indexPath.
I would adopt the following approach in order to keep interaction enabled with both the UITextView and the UITableViewCell.
Declare your controller class (a UITableViewController I guess ?) as UITexView delegate.
When you declare your UITextView, set the table view controller as it's delegate.
Implement one of the UITextViewDelegate methods (ex : - (void)textViewDidChangeSelection:(UITextView *)textView) in your table view controller .m file.
From within this method you can manipulate the targeted cell either with a custom code or by triggering the tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *) delegate method through selectRowAtIndexPath:animated:scrollPosition:.
Your code might then look like :
In the table view controller .h file :
#interface MyTableViewController : UITableViewController <UITextViewDelegate> { ...
...
}
In the table view controller .m file :
UITextView *addressField = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, 300, 75)];
[addressField setDelegate:self];
...
Then implement this function for example (or any other suitable UITextViewDelegate function) :
- (void)textViewDidChangeSelection:(UITextView *)textView {
// Determine which text view triggered this method in order to target the right cell
...
// You should have obtained an indexPath here
...
// Call the following function to trigger the row selection table view delegate method
[self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionNone]
}
Note that there are other alternatives like subclassing UITextView and deal with it's touch methods. I would recommend to use the possibilites offered by its delegate protocol though.
Note also that it might be handy to have your UITextView declared or at least referenced as an instance variable of the table view controller class. This will help you easily keep track of which addressField was hit and get the right indexPath.
[addressField setUserInteractionEnabled:NO];
I hope this helps you a bit:
[self.view insertSubview:TextView aboveSubview:TableView];
Or vice-versa based on your requirements.

drawRect not being called on added subview

I'm trying to programmatically create a window with custom contentView and one custom NSTextField control, but i'm having trouble getting this hierarchy of window and views to draw themselves.
I create a custom borderless window and override it's setContentView / contentView accessors. This seems to work fine and custom contentView's initWithFrame and drawRect methods get called causing contentView to draw itself correctly.
But as soon as i try programmatically adding custom NSTextField to contentView it doesn't get added or drawn. By saying custom i mean that i override it's designated initializer (initWithFrame:frame - only for custom font setting) and drawRect method which looks like this:
- (void)drawRect:(NSRect)rect {
NSRect bounds = [self bounds];
[super drawRect:bounds];
}
The custom contentView's initializer looks like this:
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self != nil) {
// i want to draw itself to the same
// size as contentView thus i'm using same frame
CustomTextField *textField = [[CustomTextField alloc] initWithFrame:frame];
[self addSubview:textField];
[self setNeedsDisplay:YES];
}
return self;
}
I've been strugling with this for few hours so any pointers are much appreciated. More code is available on request .)
Your -drawRect: override seems wrong to me, why would you deliberately ignore the passed in rect argument? Why is this necessary?
As for why the text field is not appearing, it's most likely because you have not configured it. When you create an NSTextField in code, you don't get the same thing as the default instance that you get when you drag a text field onto a view in IB. You will need to configure the NSTextField and its NSTextFieldCell to get the appearance you desire.
I am using a programmatically added text field that I configure like this:
_textField = [[NSTextField alloc] initWithFrame:textFieldRect];
[[_textField cell] setControlSize:NSSmallControlSize];
[_textField setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
[_textField setBezelStyle:NSTextFieldSquareBezel];
[_textField setDrawsBackground:YES];
[_textField setBordered:YES];
[_textField setImportsGraphics:NO];
[_textField setAllowsEditingTextAttributes:NO];
[_textField setBezeled:YES];
[_textField sizeToFit];
[self addSubview:_textField];
[_textField setFrame:textFieldRect];
[_textField setAutoresizingMask:NSViewMinXMargin];
Thanks for your answers!
I found my own problem. The reason why drawRect wasn't being called is because custom textfield was drawn outside the frame of content view. I guess i forgot to mention crucial detail that i'm drawing window centered on the screen thus it's frame was with x/y offset.
To fill the window with it's content view I init contentView with same frame as window (meaning the same (x;y) offset from window's (0;0) point).
Now i'm just unable to write to custom text field, but that's another problem I think I'm able to handle.