YapDatabase icon indicating copy to clipboard operation
YapDatabase copied to clipboard

How to show extra cell with mappings without meddling with fake data objects?

Open johankool opened this issue 10 years ago • 10 comments

How to show an extra cell with mappings without meddling with fake data objects? We need to show an extra cell in our table view to ‘Add new entry’. Ideally we'd somehow tell mappings to take this into account instead of having to fiddle with indices ourselves. Doing it by entering a fake object in the db to represent this sounds also wrong.

A similar use case would be showing a cell for 'No results' when there are otherwise no objects in a group.

johankool avatar Mar 26 '14 14:03 johankool

+1. I didn't find any other way to do this except custom index handling inside - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView and - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section, but it seems sub-optimal.

Using a fake object seems like an interesting idea, especially because it does allow us to forget about indexes. Even better, due to the key-value store, this does not limit potentiality. Maybe a custom class, i.e. LFStaticExtraCell, could be an useful way to add extra cells in a standardized way.

Without the need of those hacks, I thought about extending YapDatabaseViewMappings, with custom implementation of numberOfItemsInSection: and some custom method, like addExtraRowInGroup:(NSString *) group. Next week I'll give a look.

leonardfactory avatar Mar 26 '14 16:03 leonardfactory

I have considered something like this for some time now. I've discussed it with several others, but would like to get your thoughts too.

The request for such a feature usually boils down to one (or more) of the following:

  1. I want to add extra cells at the beginning of my table / section.
  2. I want to add extra cells at the end of my table / section.
  3. I want to do something special when the table / section is empty.

Now one might argue that such things are outside the scope of the database. But mappings provides a lot of app-specific features, and blurs the lines a bit between app & database. So I feel it's worthwhile to discuss & consider.

That being said, it's important to remember that YapDatabaseViewMappings cannot do everything for everybody. At some point we must draw the line. The question is where, exactly? My guiding light, so far, on this question has been:

  • If the functionality is impossible, or insanely difficult, without direct integration into mappings, then it should be integrated into mappings.
  • Otherwise, it probably shouldn't.

So let's look at the issues presented:

  1. I want to add extra cells at the beginning of my table / section.

In order to implement this, you'll need to modify numberOfRowsInSection, and add the number of static cells to the number of rows reported by mappings. And handle the row index properly in cellForRowAtIndexPath. You'll also need to decrement row indexes from the tableView before passing them into mappings. And you'll need to modify the indexPath's reported by mappings when performing the updates via yapDatabaseModified. (indexPath.row + X, where X is the number of static cells)

  1. I want to add extra cells at the end of my table / section.

You'll need to modify numberOfRowsInSection, and add the number of static cells to the number of rows reported by mappings. And handle the row index properly in cellForRowAtIndexPath.

  1. I want to do something special when the table / section is empty.

You only need to handle the special case where the table is empty. This is only further complicated if you wish to handle this by using a special cell, rather than use a special tableHeaderView, or view overlay.

Reviewing the 3 scenarios above, it seems that only scenario 1 presents anything approaching difficulty. Scenarios 2 & 3 are very simple and straight forward. And none of the scenarios require creating fake data objects. (Just a little extra code within your viewController.)

However, I do not anticipate that it would be difficult to add functionality within mappings that could simplify scenario 1. For example, we could add a method such as:

  • (void)setRowOffset:(NSUInteger)offset forGroup:(NSString *)group;

Thus if you were planning on having a single static cell at the beginning of the group/section, you could invoke this with an offset of 1. And then mappings would handle the mapping for you.

This may be a worthwhile endeavor. However, I hesitate to add something like this because then people would think something should be added to mappings that would handle scenario 2 also. And clearly, scenario 2 should not be integrated directly into mappings.

Further, the inclusion of such a method may bring about some confusion... You mentioned interest in scenario 3 (special handling for empty table). So how should mappings act when it has no items, but a row offset is configured to be 1. Should it report numberOfItemsInSection is 1 ? Or does it still say zero ?

And then there are scenarios where people want static cells, but only if there are more than 5 items in the tableView... And mappings won't solve these problems.

So I think my hesitation to add such a method is because it would only be a partial solution. And it may end up being more confusing than it is helpful.

Thoughts?

robbiehanson avatar Mar 27 '14 01:03 robbiehanson

Speaking about those scenarios, what came to my mind is that issues like these could be solved using special tableView (or section) footers&headers.

Even something like "Add new entry" could be shown in this way. Using a cell, as you suggested, would only need little coding.

Only when we're dealing with adding some cell to the top and/or between other rows, we run into trouble. This seems like an unusual scenario, and even with CoreData or any dynamic tableView, there are no easy solutions, at least, no solution I can think of.

Probably some custom class would help here, but I don't think this responsibility should belong to YapDatabaseViewMappings.

leonardfactory avatar Mar 27 '14 10:03 leonardfactory

what came to my mind is that issues like these could be solved using special tableView (or section) footers&headers.

Good point.

even with CoreData or any dynamic tableView, there are no easy solutions

Yup.

Probably some custom class would help here, but I don't think this responsibility should belong to YapDatabaseViewMappings.

I'm in agreement.

So perhaps what is needed is a good wiki article on this topic. It could provide code samples demonstrating how to solve the scenarios listed above.

robbiehanson avatar Mar 27 '14 18:03 robbiehanson

As I said, next week I'll dig into the problem. I'll post my updates with some code & solutions, like the header&footer one (I've implemented it in my app today, even if code needs a cleanup).

Hopefully I'll get some time to update the wiki too, and I'm looking forward to it: such a common situation needs a nice walkthrough.

leonardfactory avatar Mar 27 '14 18:03 leonardfactory

Agreed. Either the view controller should just do index path adjustments, or the custom tableview as suggested could do that. We went with the latter approach for our project. It didn't seem entirely illogical to have some thing for this in mappings, but yeah, I can see that can get complicated very quickly. Thanks for the feedback!

johankool avatar Mar 28 '14 03:03 johankool

Here's a use case I have right now. We normally show a list of objects in section 0. But under some conditions, we want to show one static cell above this list.

What I'd like to do is say this list appears in section 1, and section 0 has 0 or 1 rows depending on that condition. And I'd rather not manage a fake data object based on that condition to make this work.

What I'd like to do is have a section defined in the mapping that isn't based on groups, but rather statically configured.

Right now I do initWithGroups:@[self.groupName], but this would be great:

initWithSections:@[@{@"type":@"static", @"numberOfRows": @1}, @{@"type":@"group", @"group":self.groupName}]

Apologies if this is already possible and I just haven't figured it out yet. ;)

JimRoepcke avatar Jul 25 '14 21:07 JimRoepcke

See my first comment in this issue. Your use case is actually a perfect example of what I was referring to.

we want to show one static cell above this list

I mentioned 3 scenarios, the first of which was: "I want to add extra cells at the beginning of my table / section."

I then briefly touched on how to solve these scenarios within one's own ViewController. However, your proposed solution is a little different in that you're using a dedicated section. (Certainly as good an approach as what I suggested: tomāto tomäto) So I'd like to go into more detail about how this could be achieved directly within the ViewController, as opposed to within Mappings.

  • In numberOfSections: You can return (1 + [mappings numberOfSections])
  • In numberOfRowsInSection: You can custom code to handle section 0. And for other sections you can return [mappings numberOfRowsInSection:(indexPath.section - 1)]
  • In cellForRowAtIndexPath: You can handle section 0 with a special case. Otherwise, you can set indexPath = [NSIndexPath indexPathWithRow:indexPath.row inSection:(indexPath.section - 1)], and then use your existing code
  • In fact, you might even create a custom method objectAtIndexPath, which does just that. And then you can invoke it from every other place in the code where you need to fetch the database object given the indexPath.
  • In yapDatabaseModified: You'll ask the database for the sectionChanges & rowChanges, given the mappings, and it will spit out an array. Then, when you're enumerating over the array, you simply add 1 to each section. (You might even create a utility function that takes an indexPath, and returns a new indexPath with either +1 or -1 added to the section.)

But under some conditions, we want to show one static cell above this list.

This was one of the main points I made toward the end of my comment: "And then there are scenarios where people want static cells, but only if there are more than 5 items in the tableView... And mappings won't solve these problems."

If "some conditions" evolves, and becomes more complex over time, then it's better that this logic remain within the ViewController. I'd wager that the conditions of some apps will evolve to require a combination of UI state + some database query.

robbiehanson avatar Jul 27 '14 17:07 robbiehanson

Consider scenario that doesn't fit into three suggested ones.

There is a tableView of objects that are loaded and put into database, and there are ads that exist only in memory, but have their own cells and should be displayed in each 10-th row.

What approach do you think is the best in this situation? Data objects and ads are different entities. In fact, ad objects are not even serializable and are received from 3-rd party library on request.

zapko avatar Apr 19 '16 15:04 zapko

there are ads that exist only in memory, but have their own cells and should be displayed in each 10-th row.

Perhaps a method that converts from rowInTheTable to either addIndex or mappingIndex. Something like this:

- (void)getAddIndexPath:(NSIndexPath **)outAddIndexPath mappingIndexPath:(NSIndexPath **)outMappingIndexPath forTableIndexPath:(NSIndexPath *)tableIndexPath
{
    NSIndexPath *addIndexPath = nil;
    NSIndexPath *mappingIndexPath = nil;

    NSUInteger tableRow = tableIndexPath.row;
    if (tableRow > 0 && tableRow %10 == 0)
    {
        addIndexPath = [NSIndexPath indexPathWithRow:(tableRow / 10) inSection:tableIndexPath.section];
    }
    else
    {
        NSUInteger offset = tableRow / 10;
        mappingIndexPath =  [NSIndexPath indexPathWithRow:(tableIndexPath.row - offset) inSection:tableIndexPath.section];
    }

    if (outAddIndexPath) *outAddIndexPath = addIndexPath;
    if (outMappingIndexPath) *outMappingIndexPath = mappingIndexPath;
}

And then you can invoke this method from the various tableView datasource & delegate methods. Which will tell you if that particular row is an add or not. If so, you've got the add index. Otherwise you have an indexPath which you can pass into YapDB to get the underlying object in the database.

robbiehanson avatar Apr 19 '16 18:04 robbiehanson