I've got a UITableView with several different elements added programmatically. The one I'm having trouble with is the UITextView that displays correctly with correct color, size, font, etc... I have a button in one cell that increases the size of the font in the UITextView in another cell. It works fine and has no issues. The numerical value is placed in a Plist, and when you leave the view with the table and come back the size changes perfectly.
I've placed a reloadData in the button which does reload the table and gives the textView new size and resizes it to fit the new content plus resizes the cell perfectly. The issue I'm having is that when the reloadData is called, the old textView remains. So I have two texts, at two different sizes, or three or four and so on. How can I remove the previous textView when it's not set to global?
Everything is set up exactly how one would expect:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
// cell with textView. Everything is instanced and created for just that cell with tags
UITextView *t = [self setSizeAndTextOfTextView];
[cell.contentView addSubview:t];
// cell with button. simple, alloc's and init inside cell. Calls method in same class
cell.contentView addSubview:button];
//method to increases font size
write to Plist the new size
[self.tableView reloadData]; <-- tableView is iboutlet that does reload table
How are you getting the cell in the first place? Are you reusing? If you are you don't want to add the textview as a subview again you want to retrieve the existing one and adjust it
UPDATE:
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
if ([cell.contentView viewWithTag:1]) {
UITextView *t = (UITextView *)[cell.contentView viewWithTag:1];
//This version will take an existing textview and just resize it
[self setSizeAndTextOfTextView:t];
} else {
//This version creates a new text view
UITextView *t = [self setSizeAndTextOfTextView];
t.tag = 1
[cell.contentView addSubview:t];
}
You'll probably need to do something similar with you button as well
The reloadData won't wipe the existing cells, just the data displayed, so you'll get an old one to reuse
You may consider creating a custom subclass of UITableViewCell and associate that with your cell identifier. In your subclass, override the prepareForReuse method to set the cell back to a neutral state. Since cell objects are reused but are only initialized once, prepareForReuse is available to restore an already existing cell to its freshly initialized state.
Related
I'm working on a simple app to display items in a table view. If I return an ordinary UITableViewCell object from tableView:cellForRowAtIndexPath:
static NSString *cellIdentifier = #"EmailCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:
cellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:#"EmailCell"];
}
cell.textLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
... then the interaction with Dynamic Text works as expected; if the user goes to Settings | General | Text Size, changes the slider value and then returns to my app, all of the visible cells immediately update themselves to use the new font size.
If, however, I return a custom subclass of UITableViewCell, where the XIB contains a UILabel that is set to use a Text Style instead of a System Font, then the Dynamic Text does not work properly. Here is the code I'm using to assign the XIB to the table in viewDidLoad:
[self.table registerNib:[UINib nibWithNibName:#"EmailCell"
bundle:[NSBundle mainBundle]]
forCellReuseIdentifier:#"EmailCell"];
and then this code in tableView:cellForRowAtIndexPath:
static NSString *cellIdentifier = #"EmailCell";
EmailCell *cell = (EmailCell *)[tableView
dequeueReusableCellWithIdentifier:cellIdentifier];
When I first run the app, the visible cells appear with a text size that matches the user's selected preferred text size. However, if I then go to settings and change that size and then go back to my app, all of the visible cells remain with the previous text size. If I then scroll up, I will see two cells that show the new (correct) text size but the rest are still the old size. If I stop and restart the app, all cells now appear with the new (correct) text size.
It seems apparent to me that the tableview is keeping the previously-sized cells in the queue and not automatically updating their font size in response to the user's change of preferred text size. But I'm not understanding why the tableview does make this change automatically when the queue contains ordinary non-subclassed UITableViewCell instances. Is there any way I can get this to work without restarting the app (or without recreating the UITableView instance, thereby emptying the queue)? Is there any way to programmatically (and legally) clear this queue?
Edit: in case anyone's interested in this problem, my drop-in fix for this was to write a general utility method that makes a new tableview, copies over all the relevant properties from the original tableview (included registered cell classes) and then swaps the new one for the old one. Since this is a new table, it generates all-new instances of the queued cells which incorporate the new text size.
This is now handled for you in iOS 10.
http://useyourloaf.com/blog/auto-adjusting-fonts-for-dynamic-type/
Set adjustsFontForContentSizeCategory to YES / true on your label and it'll resize itself automatically when the text size preference changes.
Based on what you described, it would seem that you simply want to reload the table anytime the view comes back on screen after the user has backgrounded it. To achieve this the way I think you want, you need to add the following in your init method for your tableView - it will tell your tableView to reload the cells properly whenever the app is about to enter the foreground:
[[NSNotificationCenter defaultCenter] addObserver:self.tableView selector:#selector(reloadData) name:UIApplicationWillEnterForegroundNotification object:nil];
This way, if the user comes back to the view by opening the app after going to the phone's settings, your tableView should reload and the changes (if any were made) should properly be reflected.
You can see a quick video of the result I tested here:
https://www.dropbox.com/s/pvjuiyofydnxnvd/textsize.mov
EDIT:
Like you said in a previous comment, it would seem like it's something wrong with your nib implementation. The only difference is where/how you update the label property. In the custom cell, I created a label property and a font property, and added the label to the cell in init, and in layoutSubviews I overrode the font. Here's the code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
LabelCell *cell = [tableView dequeueReusableCellWithIdentifier:#"LabelCell"];
if (cell == nil) {
cell = [[LabelCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"LabelCell"];
}
cell.myLabel.text = _items[indexPath.row];
cell.myFont = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
return cell;
}
And in the cell itself:
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
self.myLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 0, self.contentView.frame.size.width - 20, 34)];
self.myLabel.backgroundColor = [UIColor greenColor];
[self.contentView addSubview:self.myLabel];
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.myLabel.font = self.myFont;
}
And here is the same result using custom cells with labels:
https://www.dropbox.com/s/ow2zkb6j9yq2c3m/labelcells.mov
Regarding "clearing the queue", the cells don't get queued up until they are juuuust about to be shown on screen, you can see this by logging a counter value right after you dequeue cell with identifier. If there are 10 cells on screen right now, it only dequeues 10 cells. This is the reason why you use the code if (!cell) {do stuff here that's common for all cells} and then you do things that are specific to each cell afterwards, and why this code works even if you were to assume that reloadData didn't "clear the queue", which I'm not convinced it wouldn't anyway after reading through the UITableView docs.
My drop-in fix for this was to write a general utility method that makes a new tableview, copies over all the relevant properties from the original tableview (included registered cell classes) and then swaps the new one for the old one. Since this is a new table, it generates all-new instances of the queued cells which incorporate the new text size.
I'm having some problems implemented dynamic row heights in a UITableView - but it isn't the cells that I'm having a problem with, its the UILabel inside of the cell.
The cell just contains a UILabel to display text. My tableView:heightForRowAtIndexPath: is correctly resizing each cell by calculating the height of the label that will be in it using NSString's sizeWithFont: method.
I have a subclass of UITableViewCell that just holds the UILabel property that is hooked up in storyboard. In storyboard I've set its lines to 0 so it will use as many lines as it needs, and I've set its lineBreak to Word Wrap.
Here is how I'm setting up the cells:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
ExpandCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
SomeObject *object = self.tableObjects[index.row];
cell.myLabel.text = [object cellText];
[cell.myLabel sizeToFit];
return cell;
}
When I build this, I get my table view with the cell's all sized to the correct height for their content, but the labels are all 1 line that just runs off the side of the cells. However, if I scroll the table so cell's leave the screen, and then scroll back to them, their label will be resized correctly and the cell will look how I expected it to initially.
I have also attempted calculating the labels frame with the same method I'm calculating the row height with, and I get the same behavior - it doesn't draw correctly until it scrolls off of the screen and back on again.
I have found two ways to work around this, and neither are acceptable solutions.
First, if in viewDidAppear: I call reloadData on my tableview, the cells and labels draw themselves correctly the first time. This won't work for my situation because I will be adding and removing cells to this table, and I don't want to call reloadData every time a cell is added.
The second workaround seems very strange to me - if I leave the font settings at the default System Font 17 on the UILabel, the cells draw themselves correctly. As soon as I change the font size, it reverts to its behavior of not drawing a label correctly until it leaves the screen and comes back, or gets reloadData called on the tableView.
I'd appreciate any help with this one.
I ended up resolving this by alloc/init'ing the label in cellForRowAtIndexPath. I'm not entirely sure why this is a solution - but it appears the problem I was experiencing has to do with how storyboard (or when, perhaps?) creates the objects within the cell. If I alloc/init the label in the cell in cellForRowAtIndexPath, everything loads and sizes correctly.
So... my current fix is to check if the cell has my custom label in it. If it doesn't, I alloc/init the label and put it in the cell. If it does have one, as in its a cell that's been dequeued, then I just set the text in the label that is already there.
Not sure if its the best solution, but its working for now.
I ended up resolving this by unchecking the AutoSizing checkbox in IB. It is unclear why auto-layout was causing this problem.
I ran over the same problem and I end up solving it by calling [cell layoutIfNeeded] before return the cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
ExpandCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
SomeObject *object = self.tableObjects[index.row];
cell.myLabel.text = [object cellText];
[cell layoutIfNeeded];
return cell; }
I am adding a custom button into my cell.contentView, and I noticed that every time a cell is scrolled off the visible part of the screen and back on, the button gets re-added - the translucent parts of it get more and more solid. What is the correct way to handle it so that it does not keep stacking more objects on top when scrolling through the tableView? Note that custom content is different for each cell, so I cannot put it into the if (cell == nil) {...} block.
The code I have is:
UISegmentedControl *btn = [[UISegmentedControl alloc] initWithItems:[NSArray arrayWithObject:btn_title]];
// set various other properties of btn
...
[cell.contentView addSubview:btn];
Every time the cell is dequeued, you have to remove the old subviews before adding new ones, or else you'll get that stacking effect. You can do this in one of two places:
a) In tableView:cellForRowAtIndexPath:, remove your old views after the dequeueReusableCellWithIdentifier: call and before adding your new ones.
b) If you're using a subclass of UITableViewCell, you can override prepareForReuse to remove unwanted views. prepareForReuse is called every time a cell is dequeued for reuse, so it's a good place to get rid of old views from the last time the cell was configured.
I'll post a sample fix for the code you posted. It can be extended to take care of more views.
The steps are:
Create a method in your CustomCell class that takes care of the whole setup (for example: setupWithItems:)
Once you have a cell in cellForRowAtIndexPath: (after dequeueing it or creating it), you should call setupWithItems: with the new list of items the cell should display.
In your setupWithItems: implementation, make sure you remove the UISegmentedControl from its parent view. You can easily do this it the segmented control is stored as a property of your custom cell.
In your setupWithItems: implementation, create a new UISegmentedControl and add it to the CustomCell's view hierarchy.
Sample code:
-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
CustomCell* cell = [tableView dequeueReusableCellWithIdentifier:kSomeIdentifier];
if (!cell)
{
// Create a new cell
}
NSArray* currentCellItems = [self cellItemsForRow:indexPath.row];
[cell setupWithItems:currentCellItems];
return cell;
}
And in your CustomCell subclass:
- (void)setupWithItems:(NSArray*)items
{
if (self.segmentedControl)
{
[self.segmentedControl removeFromSuperView];
self.segmentedControl = nil;
}
// More setup...
UISegmentedControl *btn = [[UISegmentedControl alloc] initWithItems:[NSArray arrayWithObject:btn_title]];
// set various other properties of btn
[cell.contentView addSubview:btn];
}
I have written a simple contact manager application that uses a UITableView to display the contacts. Each contact is shown as a standard UITableViewCell; custom content is created as UIButtons and UILabels that are added as subviews of the cell's contentView. My table viewController's cellForRowAtIndexPath method includes:
UIButton *emailButton;
UITableViewCell *cell =
[theTableView dequeueReusableCellWithIdentifier:#"My Identifier"];
if (cell == nil)
{
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:identifier] autorelease];
emailButton = [UIButton buttonWithType:UIButtonTypeCustom];
[emailButton setImage:emailImage forState:UIControlStateNormal];
emailButton.tag = EMAIL_BUTTON_TAG;
emailButton.frame = emailButtonFrame;
[cell.contentView addSubview:emailButton];
} else {
emailButton = (UIButton *)[cell viewWithTag:EMAIL_BUTTON_TAG];
}
... set various attributes of the cell, including the content of custom labels
... added as subviews of the contentView exactly as above
This works fine when rendering my table. But I've also added a search bar to my app, set the search bar's controller appropriately, and set the controller's delegate back to this same tableController such that the exact same cellForRowAtIndexPath method is called when performing the searches, and of course I filter the set of cells to be displayed to match the query.
What I see is that when I perform a search, all of the content that I display by setting cell.textLabel.text or cell.imageView.image shows up perfectly in the table, but the emailButton or the labels that I added as subviews of the cell's contentView don't appear. In the debugger, I can clearly see that these controls (the buttons and labels) exist at the time that cellForRowAtIndexPath is called while search filtering is going on. But the controls don't render.
I feel there must be something very subtle in the interactions between table cells and the searchView, but I'm missing it.
Setting the textLabel's text property appears to also bring the textLabel to the front. Even though the text label does not appear to overlap with any of the content view's buttons, this is causing the buttons to disappear. Forcing them to the front after the textLabel is updated makes the problem go away.
It is not clear why this behavior is only appearing in the search case and not in the normal case, but I was able to reproduce it in a simple change to the iOS "TableSearch" example.
you can check if
identifier in *cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:identifier] autorelease];
is equalt to "My Identifier" from [theTableView dequeueReusableCellWithIdentifier:#"My Identifier"]; if not you can get empty cell, which you can use it's cell.imageView and cell.textLabel but does not have the contentView subviews.
I have a UITableview cell that gets a tally from a core data database. The tallyTable is in a view controller inside a UITab view. I have an NSLog statement that prints out the tally value whenever it gets updated. Another tab has a list to change the source (different day) for the tallies. I am using iOS5 with ARC targeting iOS 4.2.
Here's the problem. When I load the application, the correct tallies for whatever the last selected day show up in the table tab. If I then go to the day tab and change the day and return to the tally tab there is no change in the display. However, the viewWillAppear on the tally tab runs and as the table cycles through cellForIndexPath, my NSLog statement prints out all the correct new values. If I then scroll the top label off the screen and back the label updates to the new value.
I've tried setNeedsLayout and setNeedsDisplay on the UILabel, the UITableViewCell, the UITableView and the view controller loading the table. I tried changing the CellReuse identifier so that it would never reuse a cell.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
CollectionItemTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[CollectionItemTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
NSUInteger row = [indexPath row];
cell.textLabel.text = [[self.collectionKeys objectAtIndex:row] valueForKey:#"collectionTitle"];
NSInteger test1 = indexPath.row + 150;
NSLog(#"tag = %i", test1);
cell.tallyButton.tag = test1;
NSNumber * questionID = [[self.collectionKeys objectAtIndex:row] valueForKey:#"answerID"];
cell.tallyLabel.text = [NSString stringWithFormat:#"%i",[self updatePointTotal:questionID]];
NSLog(#"Collection text should be = %#", [NSString stringWithFormat:#"%i",[self updatePointTotal:questionID]]);
[cell setNeedsLayout];
return cell;
}
I've read over a half dozen other similar questions. Got about three hours invested so far in trying to solve this.
EDIT: I thought I fixed it by using the navigation controller to repush the top level view controller on to the view again. I'll admit now this feels like a classically kludgy hack in every way. When the view is PUSHED everything updates and it is seamless. However, in order to have a fixed footer to make selection settings for the table buttons, I used a UIView with two subviews, a UITableView on top and a simple UIView with four buttons below.
The captions on the buttons need to change with the data source. Now when the view controller is pushed onto the view it obscures my fixed footer view. So, I inserted the fixed footer into the UITableview and everything appeared fine until I scrolled the UITableView and the footer scrolled up with it. The table is basically a tally sheet with buttons next to each item and in the footer is four buttons to note the color of the tallied item. Say the next item was a green lego, you would tap "green" in the footer and the button next to "lego" in the table. When I push the view controller with the two subviews the UITableview labels do not update. Thus the tableview needs to be pushed itself (as far as I can tell).
ANSWER: see comment below but ultimately I needed to reload both the visible UITableView data and the delegate UITableView controller data behind it.
I'll give it a shot. First, are you using ARC? If not, you need to add autorelease when you alloc/init a new cell. Otherwise, it's fine as is.
If I'm understanding your question correctly:
The tableView displays the correct data at app launch
You switch away from the tab with the tableView and change the tableView dataSource
You switch back to the tab with the tableView and you see (via NSLog) that the table cells are reloaded with the correct data yet the old data is still visible in the cells
If you scroll a cell off the display and back forcing it to refresh it contains the correct data
Some thoughts:
the tableView will not reload itself automatically when it's view appears. You need to call [tableView reloadData] whenever the dataSource changes. This is independent of whether the tableView is currently displayed or not. My guess is this alone will solve your problem.
You don't need to call setNeedsLayout on the cell unless you want the cell to relayout its subviews based on the data. You also don't need setNeedsDisplay.
I'm assuming there aren't other complicating factors (such as multiple tableViews displaying the same data) that could confuse things.
If you use prepare for reuse method, remember to over the original method with [super prepareForReuse];
Another method if the above way does not work is re setup cell as describe here.
I use the same method i applied for some of my collection view : we should remove/reset your subview where you create/add it to cell 's content. That mean we need set up data each cell completely for each row.
I move the code reset data value from prepare reuse to where i set value and I worked simply !
In my CustomCell.m :
- (void)configCellWith:(id)item At:(NSUInteger)row {
if (_scrollView) {
[[_scrollView subviews]
makeObjectsPerformSelector:#selector(removeFromSuperview)];
_scrollView = nil;
[_scrollView removeFromSuperview];
}
else {
CGFloat y = labelHeight+15;
float scrollHeight = _imgArray.count*200;
_scrollView=[[UIScrollView alloc]initWithFrame:CGRectMake(10, y,SCREEN_WIDTH-20, scrollHeight)];
_scrollView.scrollEnabled=YES;
_scrollView.userInteractionEnabled=YES;
_scrollView.layer.masksToBounds = YES;
[self.contentView addSubview:_scrollView]; } }
Remember to change your data source appropriately too.