我的应用程序具有用于创建自定义视图控制器的类层次结构。
第一个类是AppViewController。它扩展了NSViewController,并包含所有视图控制器通用的方法,如显示警报,从数据库中检索数据等。它没有定义任何变量。
class AppViewController: NSViewController
{
...
}
下一个类是ListViewController,并且对于我的所有" list"是通用的。观点。这些视图包含单个NSTableView,其中包含关联数据库表中所有记录的列表。它扩展了AppViewController并符合通常的协议。
请注意,此类是通用的,因此它可以正确处理不同的视图和数据模型。
class ListViewController<Model: RestModel>: AppViewController,
NSWindowDelegate,
NSTableViewDataSource,
NSTableViewDelegate
{
...
}
ListViewController定义了许多变量,包括NSTableView的IBOutlet。该插座没有连接到故事板中的任何内容。计划是在运行时设置它。
ListViewController还定义了各种函数,包括viewDidLoad(),viewWillAppear(),一些特定于应用程序的函数等。
最后一个类特定于数据库模型和视图,在本例中是Customers视图。它扩展了ListViewController。
class Clv: ListViewController<CustomerMaster>
{
...
}
CustomerMaster是一个符合RestModel协议的具体类。
问题:
奇怪的是,最后一个类Clv没有显示在故事板的自定义类:类下拉菜单中,这意味着我无法将其指定为我的视图的自定义类。
我尝试输入它,但这会导致运行时错误
Interface Builder文件中的未知类_TtC9Inventory3Cl ...
如果我删除&lt; Model:RestModel&gt;从ListViewController类定义中删除&lt; CustomerMaster&gt;从Clv类定义中,Clv类出现在Class菜单中(当然,这并不是真的有帮助,只是一个观察)。
AppViewController和ListViewController do 出现在该菜单中。
我不知所措。
答案 0 :(得分:1)
今年早些时候,我为一个应用程序创建了一个类似的架构,我必须告诉你:它不能与故事板一起工作,因为那些在实例化过程中对泛型一无所知。
尽管如此,使用笔尖仍然可以自己初始化视图控制器。
一个例子:
import UIKit
class ViewController<Model: Any>: UIViewController {
var model:Model?
}
您可以像
一样实例化此视图控制器let vc = ViewController<ListItem>(nibName: "ListViewController", bundle: nil)
或将其子类化
class ListViewController: ViewController<ListItem> {
}
并将其实例化为
let vc = ListViewController(nibName: "ListViewController", bundle: nil)
现在它编译并运行,但你还没有获得太多,因为你无法用通用属性连接你的笔尖。
但你可以做的是在一个非通用的基础视图控制器中有一个UIView类型的IBOutlet,用一个通用的视图控制器为它创建一个通用的两个通用契约:一个用于模型,一个用于视图,屁股你最有可能希望这适合您的模型。但是现在您必须拥有一些知道如何在视图上显示模型的代码。我称之为渲染器,但你也会发现许多例子都被称为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")
}
}
您现在可以继承ContentRenderer并覆盖render方法以在视图上显示您的内容。
<强> TL;博士强>
通过使用我刚才说明的方法,您可以将任何通用视图控制器与不同的模型,渲染器和视图相结合。您获得了令人难以置信的灵活性 - 但您无法使用故事板。
答案 1 :(得分:0)
@vikingosegundo的答案,在解释Xcode的抱怨并且通常非常有用时,并没有帮助我解决我的特定问题。我的项目是在Xcode 8.3.3中开始的,我在故事板中已经有很多窗口和视图,所以我真的不想放弃或解决故事板/泛型问题。
话虽这么说,我做了一些更多的研究,并意识到许多人更喜欢授权继承阶级,所以我决定探索这种方法。我能够得到满足我需求的工作。
我在这里介绍一种简化但功能性的方法。
首先,我们的数据模型必须遵守的协议:
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
}
}
然后,扩展我们的基类的所有类必须符合的协议:
///
/// 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 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{}
最后,一个扩展类:
///
/// 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{}
这当然是一个简化的原型。在实际实现中,在从数据库,Web服务或其他类似的数据异步加载数据后,加载记录和更新表将发生在闭包中。
我的完整原型定义了两个扩展BaseListViewClass的模型和两个视图控制器。它按预期工作。基类的生产版本将包含许多其他方法(这就是为什么它希望它首先成为基类: - )