Use a Generic Class as a Custom View in Interface Builder

Use a generic class as a custom view in Interface Builder

Interface Builder "talks" to your code through the ObjC runtime. As such, IB can can access only features of your code that are representable in the ObjC runtime. ObjC doesn't do generics, so there's not a way for IB to tell the runtime which specialization of your generic class to use. (And a Swift generic class can't be instantiated without a specialization, so you get an error trying to instantiate a MyButton instead of a MyButton<WhateverConcreteType>.)

(You can recognize the ObjC runtime at work in IB when you break other things: Attempting to use a pure Swift class with outlets/actions in a nib/storyboard gives runtime errors about ObjC introspection. Leaving an outlet connected whose corresponding code declaration has changed or gone away gives runtime errors about KVC. Et cetera.)

To ignore the runtime issues and put it in a slightly different way... let's go back to what IB needs to know. Remember that the nib loading system is what instantiates anything in a storyboard at runtime. So even if the parts of your class that take a generic type parameter aren't @IBInspectable, the nib still needs to know what specialization of your generic class to load. So, for IB to let you use generic classes for views, IB would have to have a place for you to identify which specialization of your class the nib should use. It doesn't — but that'd make a great feature request.

In the meantime, if it's still helpful for your MyButton class to involve some generic storage, perhaps you could look into having MyButton reference another class that includes generic storage. (It'd have to do so in such a way that the type of said storage isn't known at compile time, though — otherwise the type parameters of that other class would have to propagate back to MyButton.)

Workaround for loading a UIView subclass with generic constraints from Interface Builder?


But I wonder if there might be a workaround so the view could still be loaded from IB.

No, there isn’t. IB is Objective-C Cocoa. It cannot be made to see or manipulate a Swift generic.

IB knows nothing of the type it needs to pass for the generic constraint to meet the requirement

IB knows nothing of Swift generics at all. The fact that you were able to enter CustomView as your view class is itself a bug.

Link Storyboard to UIViewController with Generic

Storyboards have a problem with having a generic class. The thing is that Interface Builder communicates to the ViewController through the Objective-C runtime. Because of this, InterfaceBuilder is limited to the features that Objective-C provides. In this case, generics are not supported.

A workaround for this is using the .load() method of NSObject.

For example, if you have the mentioned ViewController class:

class APIModelDetailsVC<T where T: APIModel>: UIViewController {...}

You should create a "dummy" ViewController such as:

class StartWasModelDetailsVC: APIModelDetailsVC<StarWarsPerson> {...}

and set this last ViewController in the storyboard. Afterwards, in order to make this work in the Objective-c runtime, you should add the following in your AppDelegate or somewhere before this controller is loaded.

StartWasModelDetailsVC.load()

Hope it helps!

Custom view controller class isn't listed in storyboard's class menu

The answer by @vikingosegundo, while explaining Xcode's complaint and being generally very informative, didn't help me solve my particular problem. My project was started in Xcode 8.3.3 and I already have lots of windows and views in the storyboard so I don't really want to abandon or work around the storyboard/generic issue.

That being said, I did some more research and came to the realization that many people prefer delegation to class inheritance so I decided to explore that approach. I was able to get something working that satisfies my needs.

I present here, a simplified, but functional approach.

First, a protocol that our data models must conform to:

protocol RestModel
{
static var entityName: String { get }
var id: Int { get }
}

Next, a data model:

///
/// A dummy model for testing. It has two properties: an ID and a name.
///
class ModelOne: RestModel
{
static var entityName: String = "ModelOne"
var id: Int
var name: String

init(_ id: Int, _ name: String)
{
self.id = id
self.name = name
}
}

Then, a protocol to which all classes that extend our base class must conform:

///
/// Protocol: ListViewControllerDelegate
///
/// All classes that extend BaseListViewController must conform to this
/// protocol. This allows us to separate all knowledge of the actual data
/// source, record formats, etc. into a view-specific controller.
///
protocol ListViewControllerDelegate: class
{
///
/// The actual table view object. This must be defined in the extending class
/// as @IBOutlet weak var tableView: NSTableView!. The base class saves a weak
/// reference to this variable in one of its local variables and uses that
/// variable to access the actual table view object.
///
weak var tableView: NSTableView! { get }

///
/// This method must perform whatever I/O is required to load the data for the
/// table view. Loading the data is assumed to be asyncronous so the method
/// must accept a closure which must be called after the data has been loaded.
///
func loadRecords()

///
/// This method must simply return the number of rows in the data set.
///
func numberOfRows() -> Int

///
/// This method must return the text that is to be displayed in the specified
/// cell.
/// - parameters:
/// - row: The row number (as supplied in the call to tableView(tableView:viewFor:row:).
/// - col: The column identifier (from tableColumn.identifier).
/// - returns: String
///
func textForCell(row: Int, col: String) -> String

} // ListViewControllerDelegate protocol

Now the actual base class:

class BaseListViewController: NSViewController,  
NSTableViewDataSource,
NSTableViewDelegate
{
//
// The instance of the extending class. Like most delegate variables in Cocoa
// applications, this variable must be set by the delegate (the extending
// class, in this case).
//
weak var delegate: ListViewControllerDelegate?

//
// The extending class' actual table view object.
//
weak var delegateTableView: NSTableView!

//
// Calls super.viewDidLoad()
// Gets a reference to the extending class' table view object.
// Sets the data source and delegate for the table view.
// Calls the delegate's loadRecords() method.
//
override func viewDidLoad()
{
super.viewDidLoad()
delegateTableView = delegate?.tableView
delegateTableView.dataSource = self
delegateTableView.delegate = self
delegate?.loadRecords()
delegateTableView.reloadData()
}


//
// This is called by the extending class' table view object to retreive the
// number of rows in the data set.
//
func numberOfRows(in tableView: NSTableView) -> Int
{
return (delegate?.numberOfRows())!
}


//
// This is called by the extending class' table view to retrieve a view cell
// for each column/row in the table. We call the delegate's textForCell(row:col:)
// method to retrieve the text and then create a view cell with that as its
// contents.
//
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView?
{
if let col = tableColumn?.identifier, let text = delegate?.textForCell(row: row, col: col)
{
if let cell = delegate?.tableView.make(withIdentifier: (tableColumn?.identifier)!, owner: nil) as? NSTableCellView
{
cell.textField?.stringValue = text
return cell
}
}
return nil
}
} // BaseListViewController{}

And, finally, an extending class:

///
/// A concrete example class that extends BaseListViewController{}.
/// It loadRecords() method simply uses a hard-coded list.
/// This is the class that is specified in the IB.
///
class ViewOne: BaseListViewController, ListViewControllerDelegate
{
var records: [ModelOne] = []

//
// The actual table view in our view.
//
@IBOutlet weak var tableView: NSTableView!

override func viewDidLoad()
{
super.delegate = self
super.viewDidLoad()
}

func loadRecords()
{
records =
[
ModelOne(1, "AAA"),
ModelOne(2, "BBB"),
ModelOne(3, "CCC"),
ModelOne(4, "DDD"),
]
}

func numberOfRows() -> Int
{
return records.count
}

func textForCell(row: Int, col: String) -> String
{
switch col
{
case "id":
return "\(records[row].id)"

case "name":
return records[row].name

default:
return ""
}
}
} // ViewOne{}

This is, of course, a simplified prototype. In a real-world implementation, loading the records and updating the table would happen in closures after asynchronously loading the data from a database, web service, or some such.

My full prototype defines two models and two view controllers that extend BaseListViewClass. It works as desired. The production version of the base class will contain numerous other methods (which is why a wanted it to be a base class in the first place :-)

Unknown class in interface builder file when using class which inherits from a generic class in a Storyboard

Little late to the game, but this info might come in handy for anyone bumping into the thread.

Actually, as far as I can tell, there is now, as of Swift 3, some kind of awkward support for generics in storyboards.

I've managed to write a generic base class extending UIViewController, then constraining several subclasses of that generic class and using them in the storyboard.

The actual code has a similar layout to what is specified bellow:

class GenericParent: UIViewController {
// Has a few, non-generic members ...
}

class ActualGenericClass<T>: GenericParent {
// There are more generic members in the actual class
var adapter: Adapter<T> = Adapter()
}

// This class is usable in Interface Builder;
// although auto-complete won't show it, fully writing down
// the class name works
class SpecificInstanceOfActualGenericClass: ActualGenericClass<Int> {
@IBOutlet weak var tableView: UITableView!
}

That works perfectly, to my surprise!

On the other hand, the next example doesn't seem to work:

class GenericCell<T>: UITableViewCell {
var node: T? = nil
}
class SpecificCell: GenericCell<Int> {
@IBOutlet weak var coolTitleLabel: UILabel!
}

As before, explicitly writing the cell's class name on Interface Builder (on the cell's dynamic prototype) sets the class on the table view cell, and outlets are visible.

At runtime, though, when instancing the UIViewController that contains the cell's dynamic prototype, the "unknown class in interface builder file" warning is displayed and the App crashes when dequeueing the cell.

So @Andew Bennet, the statement that:

Storyboards do not support classes which inherit from generic classes

Doesn't seem to be 100% true any more, although you at least WERE right; it's the first time I've managed to pull this one off, albeit only partially!

It bugs me that some things work, others don't, but it's better than nothing.

This is so damn straightforward in Java...

Can ViewController be generic which contains IBOutlets etc?

Simply remove the , Decodable. There is no reason to indicate that the view controller class itself conforms to Decodable.

Your error is gone with:

class AViewController<T> : SuperViewController where T : Decodable {
@IBOutlet weak var leftButton: UIButton!
@IBOutlet weak var rightButton: UIButton!

var pop : T?
}

This is completely independent of why you are also declaring that T must be Decodable. That's a completely separate question I'm not going to address.



Related Topics



Leave a reply



Submit