自定义视图控制器 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 的原因:-)
我的应用程序有一个 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 定义中删除
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 的原因:-)