I have an UITableView (let's call it table1) which contains static rows. The first row contains another UITableView (table2), populated dynamically from an XML file on the internet, which is completed after the cell is being created, because asynchronous. table2 is created and added to cell's contentView only once the request is done (else an error text is prompted as cell's label).
table2 is made of collapsed sections that are expanded when interacted, which thus do change the contentSize/height of the table2.
What I want to achieve is to make the cell of table1 resize dynamically at 2 key points: 1 when the web request is done, which means when the table is created and added as subview, and 2 when a section of table2 is interacted, which makes the height of table1 change; table1 cell should follow and do exactly the same height variation.
I've searched everywhere and tried tons of things, but nothing ever worked, and I've been stuck for days.
Here's what I've tried:
Overriding (estimated)heightForRowAtIndexPath:: useless as it's only called once and before table2 is added
Changing UITableViewAutomaticDimension and estimatedRowHeight: done by default
Using NSLayoutConstraints, within updateConstraints or after table2 creation
Playing with intrinsicContentSize and invalidateIntrinsicContentSize:
Using reloadRowsAtIndexPaths:withRowAnimation:
...but nothing worked, and I feel I've tried everything
About the constraints, I tried something like:
table2.top = contentView.top = cell.top (same for leading & trailing)
cell.bottom = contentView.bottom = table2.bottom (also with height)
EDIT 1:
My code:
// Cell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
self.openSection = -1; // reports which section of table2 is open
SPDSpinnerView *spinner = [[SPDSpinnerView alloc] initWithFrame:self.frame]; // view with UIActivityIndicator & text
spinner.label.text = #"Loading...";
[self addSubview:spinner];
self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
[self loadDataWithCompletion:^(BOOL success) {
// Remove spinner
for (UIView *view in [self subviews]) {
if ([view isKindOfClass:[SPDSpinnerView class]]) [view removeFromSuperview];
}
// Handle result
if (success) {
// Add table view when done
self.table2 = [[UITableView alloc] initWithFrame:self.frame style:UITableViewStylePlain];
self.table2.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
[self.table2 registerClass:[SPDChangelogHeaderView class] forHeaderFooterViewReuseIdentifier:#"headerView"]; // custom tappable header for collapsible sections
self.table2.scrollEnabled = NO;
self.table2.delegate = self;
self.table2.dataSource = self;
self.table2.translatesAutoresizingMaskIntoConstraints = NO;
[UIView transitionWithView:self.contentView duration:.25 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{
[self.contentView addSubview:self.table2];
// FIXME: fit table2 in cell
} completion:nil];
} else {
self.textLabel.text = #"Error loading data";
}
}];
}
return self;
}
// delegates, touch handler for table2, and data loader
(nothing else related to my problem aside from usual code)
EDIT 2:
"Mockups:"
What it looks like currently
What I want it to be at step 1 (after data loading)
What I want it to be when inner sections are collapsed or expanded
For (2) and (3), red lines are the height I want for both table2 and the cell. In this MRE, table2 overlaps the rest of table1 but I want of course to keep the same spacing between cells of table1, table1 must grow as the cell does.
Thank you!
So, with the kind help of DonMag, I've reconsidered my issue. My idea was to embed a whole table in my cell: instead, they proposed to use subsections. A link is worth thousand words:
github.com/DonMag/SubSectionsDemo
If you want further explanation on how it works, let me explain. Basically, the idea is to create 2 kinds of cells you want to "simulate" your embedded table with: (sub)headers & (sub)cells. They will both be cells belonging to the section of your table1, with 2 different looks.
Then, to recreate the collapsing effect, you have to handle cells selection but only for subheader cells in tableView:didSelectRowAtIndexPath:.
Cell collapsing is done by reloading data and then adding to table1 section, during the reloading, only subcells that are not virtually collapsed, and all the subheaders.
Hope it was clear enough to help you understand the code above on GitHub.
Unfortunately I won't be using this as-is because I do use a special subclass of UITableViewController whose behavior is slightly different. I'll try to adapt as much DonMag's code as possible to my use, but the idea by itself will greatly help me.
Related
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.
I have created a subclass of NSTextField that changes its height according to the text it contains. I now want to insert it in another view (an NSTableCellView) and make the view resize according to the height of the text field.
I want to use the -(NSSize)fittingSize method of NSView but unfortunately it doesn't seem to call the fittingSize method of its subviews nor their intrinsicContentSize method.
Here is the code I use for the NSTableCellView subclass:
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self setTranslatesAutoresizingMaskIntoConstraints:NO];
self.expandingTextField = [[JSExpandingTextField alloc] init];
[self addSubview:self.expandingTextField];
[self removeConstraints:self.constraints];
NSDictionary *row = NSDictionaryOfVariableBindings(expandingTextField);
[self addConstraints:[NSLayoutConstraint
constraintsWithVisualFormat:#"V:|-20-[expandingTextField]-28-|"
options:0 metrics:nil views:row]];
}
return self;
}
- (NSSize)fittingSize
{
return [super fittingSize];
}
I override the fittingSize method here only to put a breakpoint or an NSLog.
Here is the code of the table view delegate that provides the height of the table cell:
- (JSDynamicTableCellView *)dummyCell
{
if (!_dummyCell) {
_dummyCell = [[JSDynamicTableCellView alloc] initWithFrame:NSMakeRect(0.0, 0.0, 100, 100)];
}
return _dummyCell;
}
- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row
{
self.dummyCell.expandingTextField.stringValue = #"Test";
NSLog(#"Cell size %#",NSStringFromSize([self.dummyCell fittingSize]));
return [self.dummyCell fittingSize].height;
}
All of this always returns an height for dummyCell of 69 independent of the size of the expanding textfield in the cell.
The question is: how does the 'fittingSize' method figure out the size of its subviews? Should it call their 'fittingSize' or 'ntrinsicContentSize' methods or is it something else?
fittingSize is conceptually simple. It collects all of the constraints that have been added to your view or one of its subviews (recursively), and then it determines the size of the view based on only those constraints. You can think of it as determining the smallest size that is big enough to show the contents of that view hierarchy.
Edit:We need to be quite clear here. fittingSize returns minimum values and will return 0 for any dimension that is not fully specified. For example, if the vertical constraint tying a view to the bottom of its superview is omitted then the fitted height will be 0.
Edit: I just realized what you're probably running into: fittingSize is computing the text field as if it were just one long line, that does not wrap. You can confirm this by giving it a string value with one or more newlines: now the height should be bigger!
So how to fix this? Well, you have a text field, and you give it contents, and you want to know its height. But if the text field wraps, the height is going to depend on the width: make it narrower, and the text field will wrap to more lines, so it will consume more height. So in order to measure the preferred height for a wrapping text field, you have to tell it the width to wrap at.
On OS X Mountain Lion and iOS 6, you can do that with the [NSTextField setPreferredMaxLayoutWidth:] method. For example, if you want to compute the height based on a width of 100, you would call [textField setPreferredMaxLayoutWidth:100]; now fittingSize will report a height for the text field based on it wrapping at a width of 100.
By the way, this is a bad idea:
[self removeConstraints:self.constraints];
Because it removes constraints that other parts of the system have added. You should only ever remove a constraint that you created, either in code or in IB.
Try this (to do this click on the background of the xib or storyboard)
I have 2 tables in one view. Table A lists a bunch of users. Table B lists a users objects. When a row is selected in Table A, Table B is reloaded with the objects that belong to that user.
So when a user selects a row in Table A, the image in the background of the cell changes to the highlighted version of the image.
Here is the normal version of the background image:
Here is the highlighted version of the background image:
As you can see, the highlighted version has a small arrow on the right of it. This arrow is beyond the width of the table cell the table itself. When the row is selected, the image changes as it should, but the image is sized down to fit the whole image into the cell.
What I would like to happen is the image goes outside of the table, or on top of the table for that selected row.
One possible solution I thought was to center the table on the selected row and then overlay that image, but if the user was to try to scroll through the table, the image would need to move and that would be a big pain.
So what I would like to know is it is possible to extend the cell's size beyond the table one it is selected?
Thanks in advance!
EDIT:
The following does not work, just in case anyone was going to try:
[cell setFrame:CGRectMake(cell.frame.origin.x, cell.frame.origin.y, cell.frame.size.width+20, cell.frame.size.height)];
Setting a views clipsToBounds property to NO will allow the view to draw outside of its own frame.
In your UITableViewController subclass:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do what you normally do here, if anything then add...
self.view.clipsToBounds = NO;
}
Doing this has a side effect where you will see full cells be created at the bottom or top of the tableview instead of them scrolling partially into view. Either put the table view into another view that has clipsToBounds set to YES, align the edges of the table view with the edges of the screen, or have views covering over the bottom and top (like a UIToolbar and UINavigationBar normally would).
To get the UITableViewCell's selectedBackgroundView to extend past the table view's frame create a subclass of UITableViewCell and override the layoutSubviews method.
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
// Initialization code
self.backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"YQGyZ"]];
self.selectedBackgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"CQXYh"]];
self.textLabel.backgroundColor = [UIColor clearColor];
self.detailTextLabel.backgroundColor = [UIColor clearColor];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
CGRect frame = self.selectedBackgroundView.frame;
frame.size.width += 13; // where 13 is the size of the arrow overhang from the same images
self.selectedBackgroundView.frame = frame;
// You can also change the location of the default labels (set their background colors to clear to see the background under them)
self.textLabel.frame = CGRectMake(70, 0, 148, 30);
self.detailTextLabel.frame = CGRectMake(70, 30, 148, 30);
}
Good luck.
I would recommend modifying the image resource of the unselected cell background such that it is the same width as the selected background, but just has a transparent rectangle on the side.
I have an NSTableView subclass with a little animation on it that sometimes changes the width of its columns. It works fine, but if one of the cells is being edited at the time, the field editor stays where it is and does not match the new frame of the cell as it is resized. Whenever I animate the columns, I call the following function in the NSTableView subclass:
- (void) updateFieldEditorPosition {
if([self editedRow]!= -1 && [self editedColumn] != -1) {
NSText *fieldEditor = [[self window] fieldEditor:NO forObject:self];
NSRect editedCellFrame = [self frameOfCellAtColumn:[self editedColumn] row:[self editedRow]];
if(!NSEqualRects([fieldEditor frame], editedCellFrame)) {
[fieldEditor setFrame:editedCellFrame];
[fieldEditor setNeedsDisplay:YES];
[self setNeedsDisplay:YES];
}
}
}
Unfortunately, this doesn't seem to do anything. I have checked that I am, indeed, receiving the correct field editor, and through the debugger I can see that the field editor's frame is being set to the right value and changes over time, but it never actually moves on-screen. What am I missing?
EDIT:
So, I've made an interesting discovery. If, at the same time that I set the NSTableColumn's width, I also set it as hidden and then unhide it again, the field editor moves along with it!
[tableColumn setWidth:newWidth];
[tableColumn setHidden:YES];
[tableColumn setHidden:NO];
I don't know what boolean is getting flipped when I set the NSTableColumn as hidden, but it's working and the field editor is updating itself. It's not the NSTableView's needsDisplay, because if I set that instead of hiding the column nothing happens. Can anyone explain this to me so I can do it a little more elegantly?
I'm working on a custom UITableViewCell subclass, where everything is drawn in code rather than using UILabels etc. (This is part learning exercise and partly because drawing in code is much faster. I know that for a couple of labels it wouldn't make a huge difference, but eventually I'll want to generalise this to more complex cells.)
Currently I'm struggling with the delete button animation: how to animate the cell shrinking as the delete button slides in.
Firstly, I am drawing in a custom subview of the cell's contentView. Everything is drawn in that one subview.
I am setting the subview's size by catching layoutSubviews on the cell itself, and doing:
- (void)layoutSubviews
{
[super layoutSubviews];
CGRect b = [self.contentView bounds];
[subcontentView setFrame:b];
}
I'm doing this rather than just setting an autoresizing mask because it seemed more reliable in testing, but I can use the autoresizing mask approach in testing if needed.
Now, the default thing that happens when someone hits the minus is the view gets squished.
I can avoid that by, when setting up my cell, calling
subcontentView.contentMode = UIViewContentModeRedraw;
That gives me the correct end result (my custom view redraws with the new size, and is laid out properly, like the first image I posted), but the animation of the transition is unpleasant: it looks like the cell stretches and shrinks back to size.
I know why the animation is working like that: Core Animation doesn't ask your view to redraw for each frame, it gets it to redraw for the end position of the animation and then interpolates to find the middle bits.
Another solution is to do
subcontentView.contentMode = UIViewContentModeLeft;
That just draws the delete button over my cell, so it covers part of it.
If I also implement
- (void) didTransitionToState:(UITableViewCellStateMask)state
{
[self setNeedsDisplay];
}
then once the delete button has slid in the cell 'jumps' to the correct size. That way there's no nice slidey animation, but at least I get the correct result at the end.
I guess I could run my own animation in parallel with the delete button appearing, temporarily creating another view with a copy of the image of my view in the old size, setting mine to the new size, and fading between them — that way there would be a nice cross fade instead of a sharp jump. Anyone use such a technique?
Now, you might ask why I can't use the contentStretch property and give it a region to resize. The problem with that is I'm making something to be reasonably generic, so it's not always going to be possible. In this particular example it'd work, but a more complex cell may not.
So, my question (after all of this background) is: what do you do in this situation? Does anyone have the animating delete button working for custom drawn cells? If not, what's the best compromise?
This worked for me finally. in subclass of UITableViewCell
subDrawContentView.contentMode = UIViewContentModeLeft;
overide layout subviews method
- (void)layoutSubviews {
CGRect b = [subDrawContentView bounds];
b.size.width = (!self.showingDeleteConfirmation) ? 320 : 300;
[subDrawContentView setFrame:b];
[subDrawContentView setNeedsDisplay];
[super layoutSubviews];
}
So I will paste the code first and then I will explain:
-(void)startCancelAnimation{
[cancelButton setAlpha:0.0f];
[cancelButton setFrame:CGRectMake(320., cancelButton.frame.origin.y, cancelButton.frame.size.width, cancelButton.frame.size.height)];
cancelButton.hidden=NO;
[UIView animateWithDuration:0.4
animations:^(void){
[progressView setFrame:CGRectMake(progressView.frame.origin.x, progressView.frame.origin.y, 159.0, progressView.frame.size.height)];
[text setFrame:CGRectMake(text.frame.origin.x, text.frame.origin.y, 159.0, text.frame.size.height)];
[cancelButton setFrame:CGRectMake(244., cancelButton.frame.origin.y, cancelButton.frame.size.width, cancelButton.frame.size.height)];
[cancelButton setAlpha:1.0f];
} ];
}
-(void)stopCancelAnimation{
[UIView animateWithDuration:0.4
animations:^(void){
[cancelButton setFrame:CGRectMake(320., cancelButton.frame.origin.y, cancelButton.frame.size.width, cancelButton.frame.size.height)];
[cancelButton setAlpha:0.0f];
}completion:^(BOOL completion){
cancelButton.hidden=YES;
[cancelButton setAlpha:1.0f];
[progressView setFrame:CGRectMake(progressView.frame.origin.x, progressView.frame.origin.y, DEFAULT_WIDTH_PROGRESS, progressView.frame.size.height)];
[text setFrame:CGRectMake(text.frame.origin.x, text.frame.origin.y, DEFAULT_WIDTH_TEXT, text.frame.size.height)];
}
];
}
-(void)decideAnimation{
if([cancelButton isHidden]){
[self startCancelAnimation];
}
else{
[self stopCancelAnimation];
}
}
So what I have there is a button that looks like this:
I have an IBOutlet to it. And what I am doing is resizing a UIProgressView and a UITextField (you can resize whatever you want). As for the code is pretty simple, but if you need any help to understand what's going on, please ask. Also, don't forget to add the Swip Gesture to the UITableView... Like this:
UISwipeGestureRecognizer *gesture = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(didSwipe:)];
gesture.numberOfTouchesRequired=1;
gesture.direction = UISwipeGestureRecognizerDirectionRight;
[table addGestureRecognizer:gesture];
[gesture release];
Finally the method that does it all:
-(void)didSwipe:(UIGestureRecognizer *)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
//Get the cell of the swipe...
CGPoint swipeLocation = [gestureRecognizer locationInView:self.table];
NSIndexPath *swipedIndexPath = [self.table indexPathForRowAtPoint:swipeLocation];
UITableViewCell* swipedCell = [self.table cellForRowAtIndexPath:swipedIndexPath];
//Make sure its a cell with content and not an empty one.
if([swipedCell isKindOfClass:[AMUploadsTableViewCell class]]){
// It will start the animation here
[(AMUploadsTableViewCell*)swipedCell decideAnimation];
// Do what you want :)
}
}
}
So as you can see the whole animation is created manually, so you can control exactly what you want. :)