自定义视图控制器类未列在storyboard的类菜单中

时间:2017-08-29 20:38:20

标签: cocoa swift3 xcode-storyboard xcode8

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

第一个类是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 出现在该菜单中。

我不知所措。

2 个答案:

答案 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的模型和两个视图控制器。它按预期工作。基类的生产版本将包含许多其他方法(这就是为什么它希望它首先成为基类: - )