I have a UITableView with custom cells. I need to display Title, Address, Zip, Phone, Email, Website and Description in a Cell. All this information is coming from webserver. I am able to display the contents from the webserver. However, if any of the content is empty, there is a gap where that content should be, and if description is too long I am not able to display all the content. How can I change the height of cell according to the content from server? Please help.
For example : the contents is printing like:
Title
Address
Zip
Phone...
but, if Address is nil then it looks like:
Title
Zip
Phone...
I have the tableview:heightForRowAtIndexPath method but I am not able to update the cell height according to the cell contents.
Sorry for bad question format
Cells resizing can get pretty complicated, so I suggest you simply use a table view framework such as "Sensible TableView", where all the cells are automatically resized to fit contents. I believe they also now have a free version.
you can set the height of every cell with this delegate method
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *text = [yourArray objectAtIndex:indexPath.row];// here just use your data array which you get from server
CGSize mTempSize = [text sizeWithFont:[UIFont fontWithName:fontName size:fontSize] constrainedToSize:constrainedToSize lineBreakMode:UILineBreakModeWordWrap];
return mTempSize.height ;
}
i hope this help you...
Firstly you need to set the size of your labels according to it's text length. There are a number of sizeWithFont methods available to get size for a string. See Apple Developer Documentation
If you have single line labels you can use simplest of them – sizeWithFont:. But if you have multiline labels you should use – sizeWithFont:constrainedToSize:. This method lets you specify the maximum size you want the label be.
Secondly you have to return calculated height for your cell using tableView:heightForRowAtIndexPath:
Hope this helps you.
Your tableView:heightForRowAtIndexPath: call will need to tabe into account the missing data and how all the rest of the cell will be re-laid out to close the gap it leaves. So, your actual call will be non-trivial here.
The new facebook app does a neat trick here, though, to save you calling it every time. It calculates all of the cell heights in the background, just after downloading it, and before rendering the content to the user, storing it in the data store alongside the data itself. This means that the actual table cell rendering is really fast and slick as you don't need to re-layout/recalculate the cell height and layout on each app run/cell reuse.
Related
I'm implementing a search screen in my app using UITableViewDiffableDataSource. Each cell represents a search hit and highlights the search match in the cell title, kind of like Xcode's Open Quickly window highlights portions of its result items. As text is typed into the search field, I update the results list. Results move up and down in the list as their relevance changes.
The trick is that I need to force every cell to re-render every time the search text changes, because a new search string means an update to the highlighted portions of the cell title. But I don't want to animate a deletion and insert, because it's still the same item. How can I tell the data source using the snapshot that it needs to reload cells?
I declare the data source like this:
#property (retain) UITableViewDiffableDataSource<NSString *, SearchHit *> *dataSource;
SearchHit represents one search result; it has properties for a display title and an array of ranges to highlight in the title. And it overrides hash and isEqual: so that every result row is uniquely identified.
My code looks something like this:
-(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
NSArray<SearchHit *> *hits = [self fetchHits:searchText];
NSDiffableDataSourceSnapshot<NSString *, SearchHit *> *snap = [[[NSDiffableDataSourceSnapshot alloc] init] autorelease];
[snap appendSectionsWithIdentifiers:#[#""]];
[snap appendItemsWithIdentifiers:hits];
[snap reloadItemsWithIdentifiers:hits];
[self.dataSource applySnapshot:snap animatingDifferences:YES];
}
At first I didn't have the reloadItemsWithIdentifiers call there, and then no cell would change at all once it was in the result list. Adding the reload call helped, but now most of the cells are constantly one update behind. This smells like a logic error somewhere in my code, but I've verified that the hits passed to the snapshot are correct and the hits passed to the data source's cell creation callback are not.
This article by Donny Wals and this related Twitter thread involving Steve Breen suggests that the way to fix this is to make the item identifier type only represent the properties needed to display the cell. So I updated SearchHit's hash and equality comparison to include the highlighted portions of the title, which they didn't before. Then I got delete and insert animations for all the cells on every update, which I don't want.
This seems like what reloadItemsWithIdentifiers should do...right?
Sample project here on GitHub.
The diffable datasource API may not be the right tool to effect animations on cells themselves. It’s geared towards the animation of the appearance, disappearance and ordering of cells. If your data source has a change that is expressed via Hashable conformance the api will see it as a change and delete/insert etc.
My advice would be to remove the search text from the item identifier and have each cell observe the search text and effect an animation or redraw independently from the datasource.
The proper solution to this is actually in the names of the APIs - the objects you give to the data source should be identifiers, like rowid values from a database. In my case, when the item identifiers don't represent rows in a database that I can look up, I just need to keep the state of the objects in some sort of lookup structure, so that when I call reloadItemsWithIdentifiers, I get the state for each cell from that structure, not from the object that the data source hands to me.
I know that in a view-based table view, the row class NSTableCellView subclasses from NSView. This class contains two properties, an NSTextField and an NSImageView. I am only using the NSTextField without an image view. However, some cells in my table view must contain multiple lines of text, while others may only contain one or two lines. I need to be able to resize individual NSTableCellView views depending on the size of their NSTextField textField property.
Therefore, I needed to do the following:
Get the frameSize of the NSTextField in the table cell view.
Set the frameSize of the NSTableCellView to the frameSize of the NSTextField (the one we got in set one)
However, this approach hasn't been working. I have begun to think that my approach to resize the NSTableCellView is incorrect. Here is the code that I have been using:
[tableCellView setFrameSize:[[tableCellView textField] frame].size];
[tableCellView setNeedsDisplay:YES];
Is there a problem with this approach? I would expect the cell to resize, but it doesn't? What is going wrong?
Thanks.
[edit] I should have started by commenting that the size of the textField has little to do with how large it would need to be to display all of its content.
I use this code to determine the height of a string based on the width of a table cell:
- (CGFloat) displayStringHeightWithWidth:(CGFloat)width
{
CGSize size = NSMakeSize(width,0);
NSRect bounds = [self.displayString boundingRectWithSize:size
options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading];
return bounds.size.height;
}
Ideally you can adapt that to finding the height of the textField.stringValue or textField.attributedStringValue. Not that the above is also from OSX, not iOS, so YMMV on some of the fluff.
So that changes your algorithm to:
Get the width of the table column
Get the height of the required bounding rect for the textfield's text
Tell the tableView that the row height is whatever you found in 2
Now. Regarding #3. I believe that you have to use the tableView:heightOfRow: in NSTableViewDelegate protocol as well as call the table's noteHeightOfRowsWithIndexesChanged: to have row heights change. The tableView's not otherwise aware that the height of your cell has changed. Note the discussion in the documentation. It could be your method would work without the delegate and just telling the table that the row heights for the rows that you are changing are dirty... but I wouldn't really expect it.
I've been trying to work out how to use table views and I'm a little stuck if I'm honest. I wanted to use a tableview with a limited number of rows (say 50 max). It starts of empty, with 0 rows. Then I wanted to do something along the lines of:
[self logMessage:#"Waiting for response"];
Which inserts a new row at the bottom with the above text. If I do another call to this pseudo function:
[self logMessage:#"Server response received"];
It should insert yet another new row below the previous row, and ensure it is visible. Once the above limit of 50 is reached, and a new message is inserted, I wanted the oldest message to be removed. All of this would be scrollable, with the latest being visible by default.
Am I looking at the right thing to do this? Eventually, I was hoping to have this in a nice little drawer below the main window, which I can then toggle from the main menu if needed. But as I said, I can't work out how to use a table view properly, it doesn't seem to be as straight forward as other objects are.
Any example code would be greatly appreciated!
Since log viewer is a read-only application of a UITableView, the way you do it is rather straightforward once you understand the basics. Recall that table views rely on their data models to provide them with the correct information that needs to be displayed.
A data model for "the last fifty lines of log" could be as simple as an NSMutableArray: use insertObject:atIndex: to add lines, and removeLastObject to remove the "overflow" lines, like this:
NSMutableArray *logLines = [NSMutableArray array]; // <<== this goes into the init method
-(void) addLogLine:(NSString*)line {
[logLines insertObject:line atIndex:0];
while (logLines.count > 50) {
[logLines removeLastObject];
}
}
Now you can use logLines as your table's "model": the data provider can tell how many lines there are by looking at logLines.count; the content of each row in the table will be the object at the corresponding index in logLines, and so on. Take a look at the UITableView section of your favorite iOs tutorial for the "boilerplate code" that needs to be written in order to display array elements in a UITableView.
I have an NSImageCell table column whose valuePath is bound to a path supplied by my object through an NSArrayController.
My NSTableViewDelegate implements the -tableView:heightOfRow: method, in order to have variable row height. I need to calculate row height based on the dimensions of the image displayed in the aforementioned column.
Right now, I'm getting horrible performance, though, since I'm calling [[NSImage alloc] initWithContentsOfFile:<path>] for each iteration. Is there any way to load the image representation that each NSImageCell has already retrieved for display?
I'm happy with the performance exhibited by using the valuePath binding, and would rather not cache each image on my own, as there will be many of them, each somewhat large.
I tried the NSTableColumn method -dataCellForRow:, which sounded perfect, except the cell returned returns an objectValue that seems to be the last row for which data was loaded.
Update (solution, unsatisfactory)
I figured out an approximate solution (posted simultaneously below), but it seems clumsy (and I've already seen it fail at random, irreproducible times), and I am still looking for a better solution.
I'm using the -tableView:willDisplayCell:forTableColumn:row: delegate method to populate a mutable dictionary with [[cell objectValue] size] (the image's size) keyed to the represented object's unique ID. Then, in the -tableView:heightOfRow: call I'm looking up the cover from this dictionary. I need to call [tableView noteNumberOfRowsChanged] after data is loaded, otherwise the dictionary isn't filled properly for the initial screen of data.
I tried the NSTableColumn method -dataCellForRow:, which sounded perfect, except the cell returned returns an objectValue that seems to be the last row for which data was loaded.
That's because the NSTableColumn only uses a single data cell for displaying the entire column, swapping out its value as it draws.
If I were you, I would probably try implementing the – tableView:willDisplayCell:forTableColumn:row: method in your NSTableViewDelegate. Then you can intercept the cell before it's about to be drawn, get its value at that moment, and cache just its height (as an NSNumber* or CGFloat). Then you can return that value in -tableView:heightOfRow:.
Then again, it's possible that -tableView:heightOfRow: gets called before – tableView:willDisplayCell:forTableColumn:row:, so that might not work. I'm not sure. But that's where I would start.
I have a tableview. One of the columns in the tableview uses an NSLevelIndicatorCell.
I want to be able to allow the user to edit the warn and critical values for the level indicator such that when they enter a value into a a "warning level" textbox, it changes the warn value of the level indicators being displayed in ALL of the tableview's rows.
I am very much a newbie with Objective-C so all I can figure out so far is that I must need a delegate method to watch the textbox BUT if I succeed in doing that, how on earth do I send the new value to the particular tableview column so that the update happens to ALL of the rows (i.e. how do I send what message to the tableview and target a cell within a column within a tableview)?
Here is the code to the solution I came up with should anyone need it.
- (IBAction)setWarningLevel:(id)sender {
double v;
NSScanner *ns = [NSScanner scannerWithString:[warnLevel stringValue]];
[ns scanDouble:&v];
[levelIndicator setWarningValue:v];
}
This is a textbook case for using Cocoa bindings. Just bind the value of the text field to the NSLevelIndicatorCell in your table view (do that in Interface Builder). The updates should happen automagically.
I think it should apply for all the cells in the table view if you apply the binding to the cell in IB. However if it doesn't, you will need to write a couple lines of code that set up the binding every time a new row in the table is created. That link above will explain everything in detail, but basically you will be setting up a Key-Value Observer relationship in code between the text field and the instance of the level indicator in the row being created.
I think you may have overdone it.
NSTextField subclasses NSControl, so you need to look in the docs for NSControl for a useful function.
Try re-writing it like this; assuming you're taking the value from a warnLevel textfield.
- (IBAction)setWarningLevel:(id)sender {
double v = [warnLevel doubleValue];
[levelIndicator setWarningValue:v];
}
Although this is usually shortened to this;
- (IBAction)setWarningLevel:(id)sender {
[levelIndicator setWarningValue:[warnLevel doubleValue]];
}
You should probably have some validation that the textfield has a valid number. If you're only choosing a couple of numbers have a look at using a stepper control.
Usually, with Cocoa, if you feel like you're jumping through too many hoops, there is sometimes an easier way.
Usually ;-)