Firebaseui-ios: FirebaseTableViewDataSource not casting custom class with prototypeReuseIdentifier

Created on 10 Mar 2016  Â·  19Comments  Â·  Source: firebase/FirebaseUI-iOS

Hello @mcdonamp

  1. Objective-C
  2. iOS 9
  3. Cocoapods 0.38.0

So I have this code in viewDidLoad of my UITableViewController:

self.dataSource = [[FirebaseTableViewDataSource alloc] initWithRef:self.firebaseRef prototypeReuseIdentifier:@"storyCell" view:self.tableView];

[self.dataSource populateCellWithBlock:^(Story_TVCell *__nonnull cell, FDataSnapshot *__nonnull snap) {
    NSLog(@"%@", [cell class]);
}];

My custom table view cell is like this:

#import <UIKit/UIKit.h>

@interface Story_TVCell : UITableViewCell

@property (strong, nonatomic) IBOutlet UILabel *title;

@end

and the .m file is empty

and my storyboard is like this:

screen shot 2016-03-10 at 8 11 04 pm

screen shot 2016-03-10 at 8 13 53 pm

What my controller is printing is that the class of the cell is UITableViewCell instead of Story_TVCell.
What am I doing wrong?

Thanks for the help.

database docs bug

Most helpful comment

@yougot is right. This doesn't work :

self.dataSource = FirebaseTableViewDataSource(ref: ref_categoryList, prototypeReuseIdentifier: self.IdentifierCategoryTableViewCell, view: categoriesTableView)

But this works :

self.dataSource = FirebaseTableViewDataSource(query: ref_categoryList.queryOrderedByKey(), prototypeReuseIdentifier: self.IdentifierCategoryTableViewCell, view: categoriesTableView)

The difference is precisely this line :
self.hasPrototypeCell = YES;

Present with query argument : https://github.com/firebase/FirebaseUI-iOS/blob/master/FirebaseDatabaseUI/FirebaseTableViewDataSource.m#L135
and absent with ref argument : https://github.com/firebase/FirebaseUI-iOS/blob/master/FirebaseDatabaseUI/FirebaseTableViewDataSource.m#L47

It works with CollectionViews though (everything setup accordingly)
self.dataSource = FirebaseCollectionViewDataSource(ref: ref_categoryList, prototypeReuseIdentifier:self.IdentifierPackTableViewCell, view: self.packCollectionView)

because FirebaseCollectionViewDataSource passes down the identifier as a prototypeReuseIdentifier, not a cell one like FirebaseTableViewDataSource :
https://github.com/firebase/FirebaseUI-iOS/blob/master/FirebaseDatabaseUI/FirebaseCollectionViewDataSource.m#L47

All 19 comments

@jagonzalr Hey, sorry for the delay. I feel like I've encountered this problem, though I don't remember exactly what the resolution was. I think you may actually have to initialze the cell in -initWithFrame for it to work, though I'll have to check my samples.

Meet this problem too. I think you should solve it. Maybe just let me pass the custom cell class into the initializer? Otherwise this data source is simply not useful.

OK, to get the cell in right custom class, we need to call registerClass after created the DataSource:

self.dataSource = [[FirebaseTableViewDataSource alloc] initWithRef:self.firebaseRef cellReuseIdentifier:@"TOP_POST_CELL" view:self.tableView];
[self.tableView registerClass:[TopPostSummaryCell class] forCellReuseIdentifier:@"TOP_POST_CELL"];`

But then all the UI things on the custom cell would be Nil. Seems the author did not setup them... or not restored from the Storyboard... how come...

all the controls being Nil on the UI is exactly the issue I'm experiencing doing this with swift. is there a way around this?

incidently - the same issue _"all the UI things on the custom cell would be Nil"_ also exists when trying to use custom cells with
_FirebaseCollectionViewDataSource_

Hey guys,

In order to use prototype cells, you should be using the version of the constructor with prototypeReuseIdentifier instead of cellReuseIdentifier. Note that there is also a version of the constructor with a cellClass parameter, which you can pass in [YourCustomCell class] which will do all the reuse identifier magic appropriately, behind the scenes :)

The issue is "all things nil" is not a FirebaseUI issue, but rather a "developer isn't configuring their cell correctly, thus things are nil" issue that FirebaseUI can't really solve--this issue would exist if you did the same thing without FirebaseUI (we basically just forward methods).

With my initial post of the issue I use the prototypeReuseIdentifier not the cellReuseIdentifier so why is that not working?

Also I really don't get the "all things nil"
If I use this method self.dataSource = [[FirebaseTableViewDataSource alloc] initWithRef:self.firebaseRef cellClass:[Story_TVCell class] cellReuseIdentifier:@"storyCell" view:self.tableView];
it returns the Custom Table View cell class but it's always nil

If I use "as your documentation" self.dataSource = [[FirebaseTableViewDataSource alloc] initWithRef:self.firebaseRef prototypeReuseIdentifier:@"storyCell" view:self.tableView];
it returns UITableViewCell instead of the Custom class. The purpose of FirebaseUI is to work as it says in the documentation which doesn't so maybe it would be nice to have an example (in code) with custom table view cell class which uses the prototypeReuseIdentifier @mcdonamp

Can you please clearly define, "not working"? Is the cell not the right type, is the cell the right type but all UI elements are nil, is it something else?

Is your .m file still empty? I think you may need to call initWithFrame to ensure a cell is initialized correctly.

Also, looks like I didn't provide a prototypeReuseIdentifier with a cellClass likely because it's autopopulated from the storyboard (Apple magic) in dequeueCellWithReusableIdentifier: indexPath, which Apple guarantees will return a cell of the correct type.

The problem of all controls are nil: it's not the users of the library. If you restore the cell from the storyboard, the controls are there. It's pointless to return me a cell with all nil controls and say: here is your custom cell! If I have already designed my cell on storyboard why I need to write the initializer again?

Using self.dataSource = [[FirebaseTableViewDataSource alloc] initWithRef:self.firebaseRef prototypeReuseIdentifier:@"storyCell" view:self.tableView]; with empty .m in the custom cell class returns UITableViewCell instead of the CustomCell. The thing is why I need to call the initWithFrame if it is not in the Documentation, and if it is really necessary then please update the Documentation with proper examples. I'm gonna try to do the initWithFrame but it would be awesome to provide a clear example on how to use prototypeReuseIdentifier @mcdonamp

@cslosiu The code passes through exactly what the developers have created (see here), so I'm not really sure how FirebaseUI could be getting in the way, unless we're not wiring up the storyboard section correctly (which we can't do, as that falls squarely in the hands of the developer to use correctly). I'd love a second set of eyes on that to make sure I'm not missing anything.

@jagonzalr that's super strange--per the same snippet of code as above, if the class was registered in the storyboard as your custom cell, the dequeueCellWithReusableIdentifier... method should return a correctly initialized cell. Again, I think it's an issue with the storyboard configuration and/or custom cell initialization, as FirebaseUI does nothing extraordinary when it comes to this initialization. Take a look at this Ray Wenderlich tutorial (near the bottom) for some more color on how to do that--note that the only thing one should have to do is hook up dequeueCellWithReusableIdentifier... to get that automagically working.

Also agree that the docs should be more clear on this point. TBH, I've basically given up on trying to use storyboards given the fact that errors like these are super difficult to debug because Apple hides so much of the configuration. I highly recommend using XIBs and custom cells (which is a minor change from prototype cells--basically you just don't drag the cell in to the tableview and you change the constructor), which ends up in cleaner, more understandable, more sharable code.

Also, long story short, but I think that we need to rethink the public API significantly since those constructors are pretty gnarly, and they don't really promote sectioning. I think I want something like:

FirebaseTableViewDataSource *dataSource = [FirebaseTableViewDataSource dataSourceWithReuseIdenfitifer:@"foo" tableView:self.tableView];
dataSource.isPrototypeCell = YES;
dataSource.modelClass = [YourClass class];

[dataSource addSectionNamed:@"section 1" withReference:myFirebaseReference populateCell:^(Cell *c, Snapshot *s){
  // Populate cells for that section
}];

Maybe separate the add section from the populate cell for each section, but generally we need to make the configuration easier and add support for sectioning--I think it will also clean up the documentation issues (currently they are pretty unintelligible due to trying to handle all the edge cases).

still an issue @jagonzalr how did you eventually solve it?

EDIT:
working solution using XIB:

override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)

    let view = UINib(nibName: "ConversationTableViewCell", bundle: nil).instantiateWithOwner(self, options: nil)[0] as! UIView
    view.translatesAutoresizingMaskIntoConstraints = false
    self.addSubview(view)
    self.addConstraint(NSLayoutConstraint(item: view, attribute: .Top, relatedBy: .Equal, toItem: self, attribute: .Top, multiplier: 1, constant: 0))
    self.addConstraint(NSLayoutConstraint(item: view, attribute: .Bottom, relatedBy: .Equal, toItem: self, attribute: .Bottom, multiplier: 1, constant: 0))
    self.addConstraint(NSLayoutConstraint(item: view, attribute: .Leading, relatedBy: .Equal, toItem: self, attribute: .Leading, multiplier: 1, constant: 0))
    self.addConstraint(NSLayoutConstraint(item: view, attribute: .Trailing, relatedBy: .Equal, toItem: self, attribute: .Trailing, multiplier: 1, constant: 0))

    self.commonInit()
}

make sure you correctly setup the file owner as this class and connect the outlets and actions

I made a FIRDataObject class that can be initialised with a FIRDataSnapshot. The snapshot is set on the object and its values are automatically mapped to the object's keys.

class FIRDataObject: NSObject {

    var snapshot: FIRDataSnapshot
    var ref: FIRDatabaseReference {
        return snapshot.ref
    }
    var key: String {
        return snapshot.key
    }

    required init(_ snapshot: FIRDataSnapshot) {
        self.snapshot = snapshot
        super.init()
        if let values = snapshot.value as? [String:AnyObject] {
            setValuesForKeysWithDictionary(values)
        }
    }
}

I have also made a FIRCollectionViewDataSource as a generic type that takes two arguments, a custom cell and model class.

class FIRCollectionViewDataSource<Cell: UICollectionViewCell, Model: FIRDataObject>: FirebaseCollectionViewDataSource {

    override init(ref: FIRDatabaseReference, cellReuseIdentifier identifier: String, view collectionView: UICollectionView) {
        super.init(ref: ref, modelClass: Model.self, cellClass: Cell.self, cellReuseIdentifier: identifier, view: collectionView)
    }

    func populateCellWithBlock(callback: (Cell, Model) -> Void) {
        self.populateCell = { (cell, object) in
            callback(cell as! Cell, Model(object as! FIRDataSnapshot))
        }
        self.collectionView.dataSource = self
    }

    override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! Cell
        let object = objectAtIndex(UInt(indexPath.row)) as! FIRDataSnapshot

        populateCell(cell, object)

        return cell
    }
}

Here is an example of a custom FIRDataObject and FIRCollectionViewDataSource and how it can be used.

class Object: FIRDataObject {

    var foo: String = ""
    var bar: String = ""
}
class ObjectsDataSource: FIRCollectionViewDataSource<ObjectCell, Object> {
    init(_collectionView: UICollectionView) {
        super.init(
            ref: FIRDatabase.database().referenceWithPath("objects"),
            cellReuseIdentifier: "ObjectCell",
            view: collectionView
        )
    }
}

The populateCell block now provides the cast cell and object you specified in the DataSource's generic arguments. Also once the populateCell block has been set using populateCellWithBlock, the DataSource automatically sets itself as the collectionView's dataSource.

var dataSource: ObjectsDataSource?

override func viewWillAppear(animated: Bool) {

    super.viewWillAppear(animated)

    dataSource = ObjectsDataSource(collectionView)
    dataSource?.populateCellWithBlock { (cell, object) in
        cell.object = object
    }
}

So just to recap, once you have copied the FIRDataObject and FIRCollectionViewDataSource classes and created your custom implementations of them, it's as simple as the above block of code.

@yougot is right. This doesn't work :

self.dataSource = FirebaseTableViewDataSource(ref: ref_categoryList, prototypeReuseIdentifier: self.IdentifierCategoryTableViewCell, view: categoriesTableView)

But this works :

self.dataSource = FirebaseTableViewDataSource(query: ref_categoryList.queryOrderedByKey(), prototypeReuseIdentifier: self.IdentifierCategoryTableViewCell, view: categoriesTableView)

The difference is precisely this line :
self.hasPrototypeCell = YES;

Present with query argument : https://github.com/firebase/FirebaseUI-iOS/blob/master/FirebaseDatabaseUI/FirebaseTableViewDataSource.m#L135
and absent with ref argument : https://github.com/firebase/FirebaseUI-iOS/blob/master/FirebaseDatabaseUI/FirebaseTableViewDataSource.m#L47

It works with CollectionViews though (everything setup accordingly)
self.dataSource = FirebaseCollectionViewDataSource(ref: ref_categoryList, prototypeReuseIdentifier:self.IdentifierPackTableViewCell, view: self.packCollectionView)

because FirebaseCollectionViewDataSource passes down the identifier as a prototypeReuseIdentifier, not a cell one like FirebaseTableViewDataSource :
https://github.com/firebase/FirebaseUI-iOS/blob/master/FirebaseDatabaseUI/FirebaseCollectionViewDataSource.m#L47

In the future I'd like the data sources to know less about the views they populate so that our initializers can be simpler and less confusing (and we can follow the designated init pattern).

For now, feel free to send over a PR with those changes ✨

I totally agree it's a bug and I also have this problem, but seems it does not make sense anymore after @morganchen12 refactor FirebaseTableViewDataSource

Only if you have a plan to release a path version.

We probably won't release a patch version, as these changes are already in master. Expect them to be released in a week or less.

Resolved in the 0.7.0 release.

Was this page helpful?
0 / 5 - 0 ratings