Search code examples
iosswift3realmtype-aliasibinspectable

@IBInspectable to set a Typealias for a Class


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.


Solution

  • 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).