2

I am trying to build a generic UITableViewController with a Realm Results<Object> as model.

These are my simplified classes:

Realm Object:

import RealmSwift

class Test: Object {

  dynamic var name = ""

}

TableViewCell:

import UIKit
import RealmSwift

class RealmCell: UITableViewCell {
  typealias Entity = Test // from above

  var object: Entity? {
    didSet {
      if let object = object {
        textLabel?.text = object.name
      }
    }
  }

}

TableViewController:

import UIKit
import RealmSwift

class RealmTableViewController: UITableViewController {

  typealias TableCell = RealmCell // From example above            
  var objects = try! Realm().objects(TableCell.Entity.self) {
    didSet { tableView.reloadData() }
  }

  // MARK: - UITableViewDataSource

  override func numberOfSections(in tableView: UITableView) -> Int {
    return 1
  }

  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return objects.count
  }

  override func tableView(_ tableView: UITableView,
                          cellForRowAt indexPath: IndexPath)
    -> UITableViewCell {

      let cell =
        tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableCell

      cell.object = objects[indexPath.row]

      return cell
  }
}

I can't figure out a way to make the TableCell typealias @IBInspectable. I've been trying with NSClassFromString(_:) without success.

Hope someone can help.

pesch
  • 1,976
  • 2
  • 17
  • 29
  • can't get your question? What is your problem. Why did u create RealmCell if you are not using in TableView – Gagan_iOS May 11 '17 at 13:50
  • I can't subclass this `RealmTableViewController` so I'm forced to repeat my code replacing the typealias. I would like to have a generic class that can be defined in InterfaceBuilder by typing on an @IBInspectable the EntityName – pesch May 11 '17 at 13:55

1 Answers1

3

If I understand you correctly, you basically want to be able to specify the entity name in Interface Builder, yes? I.e. you want to be able to basically select your custom class from the inspector?

If so then that is unfortunately not directly possible. @IBInspectable can only be used with specific types, as is documented here:

You can add the IBInspectable attribute to any property in a class declaration, class extension, or category of type: boolean, integer or floating point number, string, localized string, rectangle, point, size, color, range, and nil.

You can, however, specify a string property as IBInspectable (with a meaningful default) and in your initializers deduct the class from that. This will leave open the possibility of a mistake, i.e. typo, but it can still work.

See also this answer.

Edit after your comment: In this case that won't be possible I'm afraid (at least as far as I know, could be there's some deep level hackery I don't know of, but that would probably ugly as hell). The problem is that what is specified in IB can only be evaluated at runtime, yet the typealias is something defined at compile time.

I think what you basically want is simply a protocol for the functionality every cell class should have (and on which the generic view controller relies). You won't even need a typealias then since a protocol is a type in itself.

The concrete cell classes can then be chosen based on an IBInspectable-ed string, the protocol could even define a method for this.

Depending on your scenario's details you might even write a common superclass for all cells. One that adopts (part of) the protocol already (you could even leave out the protocol in this case but I'd recommend using one for readability).

This obviously assumes you have all needed functionality for the view controller defined for your generic cells, but that's a problem you face in any case (even if you could use a typealias defined during runtime).


2nd edit after I looked at your sample code:

Okay, I looked at it again and can hopefully explain this a bit better now. Unfortunately I can't just add to your repository directly as it doesn't compile (I am missing the Realm framework/pod and even if I added that I'd probably win nothing cause I don't know what exactly you do with it).

As said in the comment, I'd say you don't need any further IBInspectable property to set a class. This should happen in your storyboard already, i.e. you should set the class value of a given prototype cell to one of the concrete cell classes you have. However, your generic RealmTableViewController doesn't need to know that class. You seem to want to give it knowledge about that if I understand you correctly, probably to have it prepare the cell correctly depending in its concrete characteristics. Don't do that (I'll get to what you want to do in viewDidLoad in a moment). Instead, define a protocol that all cells adopt and that the RealmTableViewController knows about. In your tableView(_:cellForRowAt:) method you use this protocol in the as! ... part when dequeueing the cell. The protocol should define a preparation method that each concrete class should implement and that is then called at this point by the RealmTableViewController.

This way your table view controller stays generic, it doesn't really know anything about the cells it displays, which is how things are intended.

Now to the problem that you (I think) face: You want to have the controller know which kind of prototype cell it uses so that it, too, can prepare specific things (which you could also outsource into a protocol, for example). This is problematic, because you're basically trying to build back a core aspect of a table view controller: It's capability to handle different kinds of cells at the same time. I conclude this from your other code, in viewDidLoad and the objects property, which ultimately seems to also depend on the class of the cell. That's clashing with the design of the table architecture itself, it's not just a syntactical problem. However, there's a solution: Another protocol, plus a "companion" class to the concrete cell classes.

For each cell class you have, define corresponding class that deals with the Realm stuff your view controller(s) need to do. Maybe for some cells you have the same object, then write them accordingly (the new class should have a property that defines which cell it corresponds to). Define a protocol that all of them adopt and that has at least two methods (maybe use better names, it's late here...): doControllerStuff, and getCellIdentifier.

Then your RealmTableViewController finally does get an IBInspectable. If that is set, the controller instantiates the companion object to the cell (which concrete class is used will obviously be depending on the value of the IBInspectable). Should the same companion handle multiple cells, the IBInspectable must also define which cell will be used, i.e. the companion must be configured correctly. You can use some kind of string convention, or even write a factory class that hides the concrete class from the view controller and just gives it back a correct object typed as the protocol.

Anyways, whatever you do in viewDidLoad based on the class so far, change that to the doControllerStuff method. You might even have some super- and subclasses if that can be common amongst cells/companions, that doesn't matter. Lastly, in your `tableView(_:cellForRowAt:) method, you don't use the identifier directly, but instead ask the companion object for the identifier.

There's a slight caveat here though: You must obviously ensure that the interface builder value for the cell's identifier is correctly set, i.e. it matches the IBInspectable that you set in the view controller instance. You can write a catch around this, though and use a default cell, of simply throw an exception.


This has become quite long and I hope it's understandable in spite of that. I don't have the time to make a nice graphic of this or something, so if you still have questions I suggest we take this to chat. :) Just ping me and we will (though I am a bit spare on time starting tomorrow).

Community
  • 1
  • 1
Gero
  • 4,394
  • 20
  • 36
  • Not the entity name but the class for `RealmCell`. I am currently forced to duplicate code. One for each RealmEntity: `RealmEntity1Cell` with a `RealmEntity1TableViewController` for Entity1 and so on. I want to create specific RealmEntityCells and specify their name as String on InterfaceBuilder for a generic `RealmTableViewController`. I understand I would have to provide the name of the RealmCell class as a String in InterfaceBuilder. However, I'm unable to assign the typealias using the class created from the className provided. – pesch May 15 '17 at 17:17
  • Thank you very much @Gero. That's exactly what I'm looking for. But the part that I'm stuck with is the part on your paragraph "The concrete cell classes can then be chosen based on an IBInspectable-ed string, the protocol could even define a method for this." I would appreciate if you could have a look at this code https://github.com/epeschard/RealmGenerics – pesch May 16 '17 at 07:03
  • @pesch I am at work atm so I can't go into too much detail, but I will try to have a look at your code this evening. For now let me say this much: If you're dequeuing the cells like normal prototype cells you will obviously have to provide the correct, concrete kind of cell via IB, that would in a way be "chosing based in an IBInspectable", considering that the identifier is basically the equivalent thing. Your storyboard/xib must have a concrete class as type for the cell, obviously. As mentioned, I will explain this in more detail tonight. :) – Gero May 16 '17 at 11:59
  • Thank you very much for your help. I was using protocols already, but couldn't use the protocol as type since it had an associated type. That's why I was trying to define the class conforming to the protocol in InterfaceBuilder. It all started by using the associated type for this line: var objects = try! Realm().objects(TableCell.Entity.self) { didSet { tableView.reloadData() } } This was handy but can be moved to prepareForSegue for instance. – pesch May 17 '17 at 09:56
  • I'm glad I was able to help. Just for completeness: You can still use a protocol as a type, even if it has an associated type. In your case that didn't work because you were trying to use a concrete part of the conforming object where you couldn't actually know what concrete type it was. That just happened to be linked to the associated type. :) Moving that to e.g. `prepareForSegue` solves this and is a good idea. Happy coding. :) – Gero May 17 '17 at 10:09