why do we use dequeueReusableCellWithIdentifier if the cells and section fill exactly the size of the screen, or even less than the height of the screen : let's say we have 2 sections with only 1 row for each?
example :
switch (indexPath.section)
{
case kMonitoringSection:
{
cell = [tableView dequeueReusableCellWithIdentifier:kMonitoringCellIdentifier];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kMonitoringCellIdentifier] autorelease];
cell.textLabel.text = NSLocalizedString(#"Monitoring", #"");
UISwitch *switchCtl = [[[UISwitch alloc] initWithFrame:CGRectMake(197, 8, 94, 27)] autorelease];
[switchCtl addTarget:self action:#selector(switchAction:) forControlEvents:UIControlEventValueChanged];
switchCtl.backgroundColor = [UIColor clearColor];
[cell.contentView addSubview:switchCtl];
}
break;
}
case kLevelSection:
{
cell = [tableView dequeueReusableCellWithIdentifier:kLevelCellIdentifier];
UILabel *levelLabel = nil;
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:kLevelCellIdentifier] autorelease];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
cell.textLabel.text = NSLocalizedString(#"Level", #"");
levelLabel = [[[UILabel alloc] initWithFrame:CGRectMake(171, 11, 120, 21)] autorelease];
levelLabel.tag = kLevelTag;
levelLabel.textAlignment = UITextAlignmentRight;
[cell.contentView addSubview:levelLabel];
levelLabel.backgroundColor = [UIColor clearColor];
}
in the sample code : BatteryStatus
if we had 20 rows, i would understand the point, but i'm not sure here...
Thanks
If you only have a static number of cells, say only two (different) cells in your tableView and no cell is repeated, you should instead instanciate your two cells in your XIB (and design them there with your UISwitch for the first one and your custom UILabel for the other) and point an IBOutlet to it.
Much simpler, less code, and totally makes sense when no reuse of the cells is needed.
Do read Apple's Table View Programming Guide which is a really great resource (like quite every Programming Guide in Apple's doc) and explains all this in details. In particular the part "The Technique for Static Row Content" explains this exact use case.
So of course you can use dequeueReusableCellWithIdentifier in such case, it won't hurt (and probably in the BatteryStatus sample they did it because they didn't wonder if it was really useful or not, just as an habit because they do this all the time when they have more rows), but it is not the best way to do it.
Note that sample codes provided by Apple are not always the solution to follow: they are just one way to do sthg, and especially the sample generally tend to focus on the fonctionnality it wants to test (in this case the Battery Status) and does not worry about anything else (especially performance — except if the sample is about performance of course).
You don't have to call it if you don't want to. It isn't a required method or anything like that. The templates put it in for you and many examples use it because it's just there and if you make the table bigger you don't have to add any methods in.
Don't call it if you think you don't need it.
It's needlessly wasteful to instantiate a new cell from scratch every time tableView:cellForRowAtIndexPath: is called, so I would think you'd want to persist your two cells somewhere.
You could do that in any way you want, really, but any general cell caching solution is going to look a lot like dequeueReusableCellWithIdentifier: and any less general solution may get in your way if requirements change. You're not paying a meaningful performance or readability hit for using dequeueReusableCellWithIdentifier: in your example, so why worry about it?
Related
I am trying to create a NSCollectionView programmatically using a NSCollectionViewDataSource.
The code is very simple:
self.collectionView = [[NSCollectionView alloc] init];
// Add collection view to self.view etc.
self.collectionView.dataSource = self;
[self.collectionView registerClass:[NSCollectionViewItem class] forItemWithIdentifier:#"test"]
self.collectionView.collectionViewLayout = gridLayout;
[self.collectionView reloadData]
This leads to the following methods getting called (if I don't set the collectionViewLayout property explicitly these two don't get called either):
- (NSInteger)numberOfSectionsInCollectionView:(NSCollectionView*)collectionView
- (NSInteger)collectionView:(NSCollectionView*)collectionView numberOfItemsInSection:(NSInteger)section
However, collectionView:itemForRepresentedObjectAtIndexPath: is never called. Is there something else that I need to do in order to make sure that the last data source method is called? I have made sure that the two count calls return > 0, so that's not the problem.
So it seems that the problem was actually that I wasn't wrapping the NSCollectionView in a NSScrollView. This probably has to do with the layout being done incorrectly (so the items aren't requested from the data source) if it is not wrapped in a scroll view.
I've been working through different scenario's in the past days, and I dare say that using an NSScrollView, or not, makes practically no difference. With or without scrollView, I've ended up with the same errors and limitations.
What does make a huge difference is the choice between "old school" and the new-fangled collectionView. By "old school" I mean setting the itemPrototype and contents properties, something like this:
NSCollectionView *collectionView = [[NSCollectionView alloc] init];
collectionView.itemPrototype = [TBCollectionViewItem new];
collectionView.content = self.collectionItems;
NSInteger index = 0;
for (NSString *title in _collectionItems) {
NSIndexPath *path = [NSIndexPath indexPathForItem:index inSection:0];
TBCollectionViewItem *item = [collectionView makeItemWithIdentifier:#"Test" forIndexPath:path];
item.representedObject = title;
index++;
}
// Plays well with constraints
New school, something along these lines:
NSCollectionView *collectionView = [[NSCollectionView alloc] init];
collectionView.identifier = TBCollectionViewIdentifier;
[collectionView registerClass:[TBCollectionViewItem class] forItemWithIdentifier:TBCollectionViewItemIdentifier]; //register before makeItemWithIdentifier:forIndexPath: is called.
TBCollectionViewGridLayout *gridLayout = [TBCollectionViewGridLayout collectionViewGridLayout:NSMakeSize(250, 100)]; //getting the contentSize from the scrollView does not help
collectionView.collectionViewLayout = gridLayout;
collectionView.dataSource = self;
Now, you may have noticed the comment that registerClass: must be called before makeItemWithIdentifier:forIndexPath. In practice, that means calling registerClass: before setting .dataSource, whereas in your code you set .dataSource first. The docs state:
Although you can register new items at any time, you must not call the makeItemWithIdentifier:forIndexPath: method until after you register the corresponding item.
I wish I could say that by switching those two lines, all layout problems will be solved. Unfortunately, I've found that the .collectionViewLayout / .dataSource combination is a recipe for (auto)layout disaster. Whether that can be fixed by switching from NSCollectionViewGridLayout to flowLayout, I'm not yet certain.
I've never worked before with NSOutlineView and I'm curious if the cells are getting released and deallocated when the item is collapsing?
I feel like my cells are being stacked on top of each other after every time I expand and collapse the item.
Any kind of help is highly appreciated!
HSObjectViewModel* ovm = item;
HSObjectTableCellView *oCell = [outlineView makeViewWithIdentifier:#"OutlineCell"
owner:self];
oCell.textField.stringValue = ovm.hsObject.name;
NSImage* im = [[NSImage alloc] init];
if(ovm.isVisible) {
im = [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle]
pathForResource:#"on" ofType:#"png"]];
} else {
im = [[NSImage alloc] initWithContentsOfFile:[[NSBundle mainBundle]
pathForResource:#"off" ofType:#"png"]];
}
[oCell.eyeButton setImage:im];
oCell.eyeButton.target = self;
oCell.eyeButton.action = #selector(visibilityButtonClicked:);
[[oCell.eyeButton cell] setHighlightsBy:0];
There are two types of NSOutlineView. One is view-based (the more flexible one to use) and the other is cell-based. Assuming youre using a view-based, the NSTableCellViews in the outline view will not get deallocated when you collapse an item. The cells simply get de-queued i.e. removed from the screen to be used later.
This is done for memory effeciency reasons. The logic is "why allocate lets say 2000+ cellViews if the screen is only able to display 20 at a time?" So the cells will get de-queued (to be used later) and not deallocated generally.
HOWEVER, this behavior is unpredictable. If you set up your code the standard way, then the system will be managing the cells. You cant be 100% sure when a cell will be deallocated. If your users can delete cells from the NSOutlineView, then the chance of cells being deallocated increases.
-Edit-
Based on the comments below, you need to reset the cells after dequeuing them. Assuming your code looks something like this.
- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item {
NSTableCellView *aCellView = [outlineView makeViewWithIdentifier:[tableColumn identifier] owner:self];
// You can reset the cell like so
if (item.status == 0) { // Assuming your item has a property called status (you can make what ever property you want)
aCellView.imageView.image = [NSImage imageNamed:#"redImage"];
} else if (item.status == 1) {
aCellView.imageView.image = [NSImage imageNamed:#"blueImage"];
} else {
aCellView.imageView.image = nil;
}
}
So basically, you observe the properties of the item (properties you should declare to distinguish what item is what) and depending on the property, you reset the cell to show the correct values. In the example above, the cell has a image status 0 = a red image, 1 = blue image, anything else = no image. Had i not reset the cells, when ever i collapse, some cells will have old values of other cells since they are being reused.
Within my app, I bring up a UITableView when prompting the user to select an item. Based on whether the item is in stock, out of stock, on order, etc., I want to display an image on the right-hand side of the UITableViewCell.
I have tried using the accessory view button, but it isn't showing up on the UITableViewCell. I have tried changing the UITableViewCellStyle to UITableViewCellStyleValue2 (the style with the detail closure button), and have also tried setting the accessory type of the cell directly.
static NSString *CellIdentifier = #"MyCellIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
//cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue2 reuseIdentifier:CellIdentifier];
}
[cell.textLabel setText:#"Testing"];
[cell.detailTextLabel setText:#""];
//cell.accessoryType = UITableViewCellAccessoryNone;
//cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
UIImageView *imageView = [[UIImageView alloc] initWithImage:[myImageFilePath stretchableImageWithLeftCapWidth:0 topCapHeight:1.0]];
[imageView setContentMode: UIViewContentModeScaleAspectFit];
[imageView setFrame:CGRectMake(0, 5, cell.frame.size.height - 10, cell.frame.size.height - 5)];
cell.accessoryView = imageView;
return cell;
When the app runs, the UITableViewCells appear with the "Testing" text, but there is no image on the right-hand side of the cells.
Is there another method that needs to be overridden in order to implement a custom UIImage for the detail diclosure button?
The "button" is more of a status indicator, because I don't want the tap of the button to do anything different than tapping anywhere else on the UITableViewCell does (just select the row).
Some additional info:
If I remove
[imageView setFrame:CGRectMake(0, 5, cell.frame.size.height - 10, cell.frame.size.height - 5)];
then the image shows up in the UITableViewCell, but it is too big and does not fit on a single row, so it overlaps with other rows' accessory views.
I was using the setFrame so that it would resize the image to fit with the UIViewContentModeScaleAspectFit so that it would fit on the row, but it seems to prevent the image from appearing at all.
Note that in this example cell.frame.size.height = 44.
If I change the frame to:
[imageView setFrame:CGRectMake(0.0, 0.0, 120, 44)];
then the image appears on the row.
However, since it is 120 pixels wide, it is cutting off the text with the trailing ... earlier that needed. The image could fit about 3 times before the trailing ..., with the width being 120 and height being 44.
How can I get the trailing ... to get closer to the image?
If I reduce the 120 to a lower value, the image moves to the right and eventually runs off the cell.
The width of the UITableView was 804. Updating it to 768 allowed me to use the original code that was posted.
Your question is very similar to this one here:
Using a custom image for a UITableViewCell's accessoryView and having it respond to UITableViewDelegate
You can ignore the "have it respond" bit unless you need to. But, as you can see there, the rect is square for the accessoryView property, so an image that is 120x44 is probably not going to fit correctly, ever.
I would rethink your approach to this problem, and create a custom UITableViewCell subclass that has the necessary items you require (sounds like a UILabel and a UIImageView). This way, you can easily control how large the label is versus how large the image is, how the label will wrap lines (if it wraps at all or maybe truncates) and so on.
Another way is just subclass the UITableviewCell and set frame's in "layoutSubviews" and also make sure to pass correct row height for each cell.
By subclassing you will get full flexibility.
hope this helps
I guess there's an issue with your CGRect X and Y, in this case here 0 and 5. Maybe trying to set them dinamically can fix this. Let me know
I'm having a hard time understanding some of the logic behind the UITableView. I am populating my table from a MPMediaItemCollection as a queue of songs. What I am trying to achieve is to have a now playing indicator as the accessory view of the cell at the index path that matches the currently playing song.
I originally tried this with the following:
if (indexPath.row == [mutArray indexOfObject:[mainViewController.musicPlayer.nowPlayingItem valueForProperty:MPMediaItemPropertyTitle]]) {
UIImageView *playButtonView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"PlayButton.png"]];
[cell setAccessoryView:playButtonView];
}
This works fine for the now playing item, which is always actually objectAtIndex:0. But what I don't understand is why my table seems to define this index once every height of combined visible cells.
Let me try to explain this more clearly. Lets say that my table is 600px tall, and its content height is 1800px. This causes the indicator to be added roughly every 600px down the content height.
Now my first thought was that this was something wrong with the code for judging the index based off the name of the song, so I tried changing it to:
if (indexPath.row == 0)
But this produces the same result!
This screenshot should help explain what I'm talking about.
So, is there anything I can do to make the table treat indexPath0 as only the first cell in reference to the entire table instead of in reference to the currently visible cells?
You have to state explicitly also when the accessory should not be there:
if (indexPath.row==0) {
UIImageView *playButtonView = [[UIImageView alloc] initWithImage:
[UIImage imageNamed:#"PlayButton.png"]];
[cell setAccessoryView:playButtonView];
}
else {
[cell setAccessoryView:nil];
}
The reason is that when cell 0 gets dequeued (i.e. reused) on a different row it still has the accessory view in it.
Actually, this is a nice case study for understanding how dequeueing table view cells actually works ;-).
That's how tableview reuse cells: when cell scrolls out of screen it's added to reuse pool, so once you've added your accessory view to cell and that cell is reused - you'll see it in random places while scrolling.
You can check your cells index in -willDisplayCell:forIndexPath and add (if it's not added), hide (if it's there, but not your desired index) or show (if it's there and it's your index), or add accessory view to all cells and show/hide as needed.
Does anyone have any suggestions for an 'Apple Sanctioned' way to change behaviors (ie, showing specific pictures in an imageView) based upon which segment within a UISegmentedControl is selected? Should a case or switch statement be used?
Forgive the possible obscenely obvious question, but I was instructed that if you need to use a switch or case statement within an object, you probably need to create additinal object, sut I do not think that this rubric applies in this cse, correct?
Thanks for any and all suggestions, and your consideration.
(I'm using Xcode 3.2, iOS 4 SDK)
You can use a switch or if statement if you like. I don't think there's any "Apple sanctioned" way of doing it.
I have no idea what you mean by
I was instructed that if you need to use a switch or case statement within an object you probably need to create additinal object
Never heard that one before.
Anyway, based on your description of what you want to achieve it should be as easy as
UISegmentedControl *seg = [[[UISegmentedControl alloc] initWithItems:[NSArray arrayWithObjects:#"Seg 1", #"Seg2", nil]] autorelease];
UIImageView *imgView = [[[UIImageView alloc] initWithImage:[UIImage imageWithContentsOfFile:path]] autorelease];
switch (seg.selectedSegmentIndex) {
case 0:
imgView.image = [UIImage imageWithContentsOfFile:alt_path];
break;
case 1:
imgView.image = [UIImage imageWithContentsOfFile:differentPath];
break;
default:
break;
}