自定义视图控制器 class 未在情节提要的 class 菜单中列出

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

我的应用程序有一个 classes 层次结构用于创建自定义视图控制器。

第一个class是AppViewController。它扩展了 NSViewController 并包含了我所有视图控制器通用的方法,比如显示警报、从数据库中检索数据等等。它没有定义任何变量。

class AppViewController: NSViewController
{
    ...
}

下一个 class 是 ListViewController,它对我的​​所有 "list" 视图都是通用的。这些视图包含单个 NSTableView,其中包含关联数据库 table 中所有记录的列表。它扩展了 AppViewController 并符合通常的协议。

请注意,此 class 是通用的,因此它可以正确处理不同的视图和数据模型。

class ListViewController<Model: RestModel>: AppViewController,
                                            NSWindowDelegate,
                                            NSTableViewDataSource,
                                            NSTableViewDelegate
{
    ...
}

ListViewController 定义了许多变量,包括 NSTableView 的 IBOutlet。该插座未连接到情节提要中的任何内容。计划是将其设置在 运行 时间。

ListViewController 还定义了各种函数,包括 viewDidLoad()、viewWillAppear()、许多特定于应用程序的函数等等。

最后一个 class 特定于数据库模型和视图,在本例中为 Customers 视图。它扩展了 ListViewController。

class Clv: ListViewController<CustomerMaster>
{
    ...
}

CustomerMaster 是一个具体的class,符合 RestModel 协议。

问题:
奇怪的是,最后一个class,Clv,并没有出现在storyboard的Custom Class:Class下拉菜单中,也就是说我不能指定为custom class 我的观点。

我试着直接输入它,但这会导致 运行 时间错误

Unknown class _TtC9Inventory3Clv in Interface Builder file ...

如果我从 ListViewController class 定义中删除 并从 Clv class 定义中删除 ,则 Clv class 会出现在Class 菜单(当然这并没有什么帮助,只是一个观察)。

AppViewController 和 ListViewController 都 出现在该菜单中。

我很茫然

今年早些时候,我为一个应用程序创建了一个类似的架构,我必须告诉你:它不能与情节提要一起工作,因为情节提要在实例化期间对泛型一无所知。

尽管使用 nib 是可行的,因为您仍然可以自己初始化视图控制器。

一个例子:

import UIKit

class ViewController<Model: Any>: UIViewController {
    var model:Model?
}

你可以像这样实例化这个视图控制器

let vc = ViewController<ListItem>(nibName: "ListViewController", bundle: nil)

或子class它

class ListViewController: ViewController<ListItem> {
}

并像

一样实例化它
let vc = ListViewController(nibName: "ListViewController", bundle: nil)

现在它可以编译和运行了,但是您还没有得到太多,因为您不能将您的 nib 与通用属性联系起来。

但是您可以做的是在非通用基础视图控制器中使用 UIView 类型的 IBOutlet,将其与具有两个通用合同的通用视图控制器子class:一个用于模型,一个用于视图,你很可能希望它适合你的模型。但是现在您必须有一些代码知道如何将您的模型显示在视图中。我称之为渲染器,但您还会发现很多这样的示例 class 被称为 Presenter。

视图控制器:

class BaseRenderViewController: UIViewController {
    var renderer: RenderType?
    @IBOutlet private weak var privateRenderView: UIView!

    var renderView: UIView! {
        get { return privateRenderView }
        set { privateRenderView = newValue }
    }
}


class RenderedContentViewController<Content, View: UIView>: BaseRenderViewController {

    var contentRenderer: ContentRenderer<Content, View>? {
        return renderer as? ContentRenderer<Content, View>
    }

    open
    override func viewDidLoad() {
        super.viewDidLoad()

        guard let renderer = contentRenderer, let view = self.renderView as? View else {
            return
        }
        do {
            try renderer.render(on: view)

        } catch (let error) {
            print(error)
        }
    }
}

渲染器:

protocol RenderType {}

class Renderer<View: UIView>: RenderType {
    func render(on view: View) throws {
        throw RendererError.methodNotOverridden("\(#function) must be overridden")
    }
}

class ContentRenderer<Content, View: UIView>: Renderer<View> {
    init(contents: [Content]) {
        self.contents = contents
    }
    let contents: [Content]

    override func render(on view: View) throws {
        throw RendererError.methodNotOverridden("\(#function) must be overridden")
    }
}

您现在可以子class ContentRenderer 并覆盖渲染方法以在视图上显示您的内容。

tl;dr

通过使用我刚才说明的方法,您可以将任何通用视图控制器与不同的模型、渲染器和视图结合起来。您获得了难以置信的灵活性 — 但您将无法使用它来使用故事板。

@vikingosegundo 的回答虽然解释了 Xcode 的投诉并且通常非常有用,但并没有帮助我解决我的特定问题。我的项目是在 Xcode 8.3.3 开始的,我已经在情节提要中有很多 windows 和视图,所以我真的不想放弃或解决 storyboard/generic 问题。

话虽这么说,但我做了一些更多的研究,并意识到许多人更喜欢委托而不是 class 继承,所以我决定探索这种方法。我能够得到满足我需求的东西。

我在这里介绍一种简化但实用的方法。

首先,我们的数据模型必须遵守的一个协议:

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

接下来是数据模型:

///
/// 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
  }
}

然后,所有扩展我们基础 class 的 classes 必须遵守的协议:

///
/// 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

现在实际基数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{}

最后,扩展 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{}

这当然是一个简化的原型。在现实世界的实现中,加载记录和更新 table 将在从数据库、Web 服务或类似的异步加载数据后发生在闭包中。

我的完整原型定义了两个模型和两个扩展 BaseListViewClass 的视图控制器。它按需要工作。基础 class 的生产版本将包含许多其他方法(这就是为什么首先希望它成为基础 class 的原因:-)