使用故事板创建Swift 2.0的通用控制器

25

我正在尝试为我的应用创建一个通用的列表控制器。

我有一个扩展了这个通用控制器并继承自UIViewController的ProductListController。我已经将ProductListController连接到了storyboard,并创建了两个outlet,但我总是收到以下错误:

 Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<UIViewController 0x7c158ca0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key searchBar.'

我对所有插座都收到这个错误,如果我从GenericListController中删除通用的T,则可以正常工作。我猜Storyboard无法加载带有泛型的超级类。我该如何让它工作?

我的代码:

class GenericListController<T> : UIViewController {

    var list : [T] = [T]()
    var filteredlist : [T] = [T]()

    func getData(tableView : UITableView) {
    .....
    }

    func setData(list : [T], tableView : UITableView) {
    .....
    }

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

class ProductListController : GenericListController<ProductModel> {
       @IBOutlet weak var searchBar: UISearchBar!
       @IBOutlet weak var tableView: UITableView!

     override func viewDidLoad() {
       super.viewDidLoad()

       getData(tableView)
     }
}

我发现如果我扩展一个泛型类,并尝试将该类添加到Storyboard中,Xcode不会自动完成类名(可能是因为它无法检测到该类)。

3
为什么需要在UIViewController中添加通用模型? - Hossam Ghareeb
1
因为我的列表控制器逻辑总是相同的,只是对象类型不同。我会有产品列表、销售列表、用户列表等,它们共享相同的方法和逻辑。详情控制器和添加控制器也是如此。如果我不能使用泛型,那么我就必须将我的getData函数复制粘贴到每个新控制器中(其中我有需要类型的dao方法)。 - Renato Probst
@sagits,我在想所有这些是否有用,因为您无法访问通用类型(在本例中为ProductModel)的任何属性。不管是使用您的方法还是我的方法都不行?您是否还创建了一个基本类,使所有T都符合它? - R Menke
所以,只需设计您的自定义UIViewController子类,使其能够处理所有类型的列表。如果需要,您还可以进一步对其进行子类化,为每种类型的列表添加一些自定义内容,同时仍然重用核心方法,而无需将它们“复制粘贴”到每个子子类中。 - ElmerCat
@sagits 如果你不包括IB Outlets,它能运行吗?我猜想它不能。原因如下所述。 - R Menke
@ElmerCat,我需要一个通用类型来解析服务器上的JSON,请查看下面的代码。如果我能摆脱searchView方法,那将非常有用,因为它们对于所有类都是相同的,tableView也是如此。如果您有任何其他方法来实现这一点,请与我们分享。 - Renato Probst
4个回答

16
这是关于为什么不可能在Interface Builder中使用通用类作为自定义视图的答案:使用通用类作为Interface Builder中的自定义视图是不可能的
引用块中提到,Interface Builder通过ObjC运行时与您的代码“交流”。因此,IB只能访问在ObjC运行时中可表示的代码特性。而ObjC不支持泛型。
这暗示了一个可能的解决方法:在Obj-C中使用泛型。也许您可以在Obj-C中创建一个通用的ViewController,然后IB会接受它?
您考虑过使用协议吗?这不会影响Storyboard。稍微更改了一下代码以便于测试。缺点是您不能在协议中有存储属性。因此,您仍需要复制粘贴这些内容。好处是它可以正常工作。
protocol GenericListProtocol {       
    typealias T
    var list : [T] { get set }
    var filteredlist : [T] { get set }
    func setData(list : [T])        
}    
extension GenericListProtocol {        
    func setData(list: [T]) {
        list.forEach { item in print(item) }
    }        
}

class ProductModel {        
    var productID : Int = 0        
    init(id:Int) {
        productID = id
    }        
}    

class ProductListController: UIViewController, GenericListProtocol {

    var list : [ProductModel] = [ProductModel(id: 1),ProductModel(id: 2),ProductModel(id: 3),ProductModel(id: 4)]
    var filteredlist : [ProductModel] = []

    override func viewDidLoad() {            
        super.viewDidLoad()            
        setData(list)            
    }
}

更新: 允许一些属性访问通用类。 将其更改为基本类,以便在 Playground 中轻松测试。UIViewController 的内容在上面的代码中。
class ProductModel {        
    var productID : Int = 0        
    init(id:Int) {
        productID = id
    }        
}

class ProductA : ProductModel {
    var aSpecificStuff : Float = 0
}    

class ProductB : ProductModel {
    var bSpecificStuff : String = ""
}

protocol GenericListProtocol {        
    typealias T = ProductModel
    var list : [T] { get set }
    var filteredlist : [T] { get set }
    func setData(list : [T])        
}

extension GenericListProtocol {        
    func setData(list: [T]) {
        list.forEach { item in
            guard let productItem = item as? ProductModel else {
                return
            }
            print(productItem.productID)
        }
    }        
}


class ProductListController: GenericListProtocol {

    var list : [ProductA] = [ProductA(id: 1),ProductA(id: 2),ProductA(id: 3),ProductA(id: 4)]
    var filteredlist : [ProductA] = []

    init() {            
        setData(list)            
    }
}

var test = ProductListController()

嗨,我正在尝试实现您的答案,但我需要T是T,其中T:MyModel(此类保存getId方法),这可能吗? 我知道这似乎很奇怪,但我需要将确切的classType传递给我的getData,以便它可以从webservice解析json并返回类型化列表(这对于实现其他泛型方法是必要的)。 - Renato Probst
@sagits 我认为你在错误地使用泛型。当操作符合 Y 的对象中的 X 时,泛型非常棒。但是用它来替换所有子类并不实用。在你的情况下,子类仍然是最好的选择。但我更新了答案,提供了一种获取从超类(ProductModel)继承的属性访问权限的方法。 - R Menke
谢谢,我很快会发布我的代码,这样会更有意义。它起作用了,但我使用了typealias T:MyModel。现在我正在尝试从getData更新列表值,我已经尝试更新协议列表并从子类传递列表(getData(myList:[T]){myList = listFromServer}。但是它们都没有起作用,我该怎么做? - Renato Probst
再次感谢。我会将它添加在下面,因为我认为它仍然与泛型相关。关于Swift 2.0泛型的信息不多,我认为这个主题将对未来的人们非常有用。 - Renato Probst

8

如@r-menke上面所述:

Interface Builder通过ObjC运行时与您的代码“交流”。因此,IB只能访问在ObjC运行时中表示的代码功能。ObjC不使用泛型

这是真的,

然而,在我的经验中,我们可以按照以下方式解决这个问题(YMMV)。

我们可以举一个人为例子来展示它是如何失败的:

class C<T> {}
class D: C<String> {}

print(NSClassFromString("main.D"))

运行示例如下:

http://swiftstub.com/878703680

您可以看到它打印出nil

现在让我们稍微调整一下然后再试一次:

http://swiftstub.com/346544378

class C<T> {}
class D: C<String> {}

print(NSClassFromString("main.D"))
let _ = D()
print(NSClassFromString("main.D"))

我们得到了这个:

nil 可选类型(main.D)

嘿哟!它在第一次初始化后找到了它。
现在让我们将其应用于故事板。我正在一个应用程序中进行此操作(不管对错)。
// do the initial throw away load
let _ = CanvasController(nibName: "", bundle: nil)

// Now lets load the storyboard
let sb = NSStoryboard(name: "Canvas", bundle: nil)
let canvas = sb.instantiateInitialController() as! CanvasController

myView.addSubView(canvas.view)

功能与您预期的一样。在我的情况下,我的CanvasController声明如下:

class CanvasController: MyNSViewController<SomeGeneric1, SomeGeneric2>

现在,我在使用通用的UITableView子类技术时,在iOS上遇到了一些问题。我没有在iOS 9下尝试过,所以可能会有所不同。但是,我目前正在为我正在开发的应用程序在10.11下进行此操作,并且没有遇到任何重大问题。这并不意味着我将来不会遇到任何问题,或者说这样做是否恰当,因为我不能声称知道这样做的全部后果。我只能说,就目前而言,它似乎可以解决问题。
我在8月4日提交了一个radr:#22133133。我没有在open RADR中看到它,但是在bugreport.apple.com下至少列在我的帐户下,无论那是否有价值。

太棒了,使用这个,你能够重用一个列表([SomeGeneric2])吗?至于IBOutlets,你能够重用在MyNSViewController中声明的那些吗?感谢您的回复。 - Renato Probst
这个完美运行了,证明了“不可能”的说法是错误的。谢谢! - hariseldon78
这基本上是能工作的,但由于某些奇怪的原因,在尝试扩展子类时会出现错误。 - GetSwifty

2
我将发布一些代码,这些代码是在用户R menke和其他人的帮助下完成的。我的目标是拥有一个GenericListProtocol,可以处理UISearchBarDelegate、UITableViewDelegate和我的getData方法(需要一个类型类才能正确解析json)。
import Foundation
import UIKit

protocol GenericListProtocol : UISearchBarDelegate, UITableViewDelegate{
    typealias T : MyModel // MyModel is a model i use for getId, getDate...
    var list : [T] { get set }
    var filteredlist : [T] { get set }

    var searchActive : Bool { get set }

    func setData(tableView : UITableView, myList : [T])

    func setData()

    func getData(tableView : UITableView, objectType : T, var myList : [T])

    func filterContentForSearchText(searchText: String)

}

extension GenericListProtocol {

  func setData(atableView : UITableView, myList : [T]) {
    print("reloading tableView data")
    atableView.reloadData()
  }

  func getData(tableView : UITableView, objectType : T, var myList : [T]) {

    let dao: GenericDao<T> = GenericDao<T>()
    let view : UIView = UIView()

    let c: CallListListener<T> = CallListListener<T>(view: view, loadingLabel: "loading", save: true, name: "ProductModel")

    c.onSuccess = { (onSuccess: JsonMessageList<T>) in
        print("status " + onSuccess._meta!.status!) // this is from my ws

        myList = onSuccess.records

        self.setData(tableView, myList: myList)
    }

    c.onFinally = { (any: AnyObject) in
      //  tableView.stopPullToRefresh()
    }

   // my dao saves json list on NSUSER, so we check if its already downloaded 
    let savedList = c.getDefaultList()
    if (savedList == nil) {
        dao.getAll(c);
    }
    else {
         myList = savedList!
        print(String(myList.count))
        self.setData(tableView, myList: myList)

    }


  }

  func searchBarTextDidBeginEditing(searchBar: UISearchBar) {
    searchActive = true;
  }

  func searchBarTextDidEndEditing(searchBar: UISearchBar) {
    searchActive = false;
  }

  func searchBarCancelButtonClicked(searchBar: UISearchBar) {
    searchActive = false;
  }

  func searchBarSearchButtonClicked(searchBar: UISearchBar) {
    searchActive = false;
  }

  func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {
    print("searching")
    self.filterContentForSearchText(searchText)
    if(filteredlist.count == 0){
        searchActive = false;
    } else {
        searchActive = true;
    }
     self.setData()
  }



}

虽然我已经能够实现大部分UISearchBarDelegate和UITableViewDelegate方法,但我仍需要在我的默认类中实现其中的两个:

import Foundation
import UIKit
import EVReflection
import AlamofireJsonToObjects

class ProductListController : GenericListController, GenericListProtocol { 

  @IBOutlet weak var searchBar: UISearchBar!
  @IBOutlet weak var tableView: UITableView!


  var list : [ProductModel] = [ProductModel]()
  var filteredlist : [ProductModel] = [ProductModel]()
  var searchActive : Bool = false


   override func setInit() {
    self.searchBar.delegate = self
    self.listName = "ProductModel"
    self.setTableViewStyle(self.tableView, searchBar4 : self.searchBar)
    getData(self.tableView, objectType: ProductModel(), myList: self.list)
  }

  // this method hasnt worked from extension, so i just pasted it here
  func searchBar(searchBar: UISearchBar, textDidChange searchText: String) {

    self.filterContentForSearchText(searchText)
    if(filteredlist.count == 0){
        searchActive = false;
    } else {
        searchActive = true;
    }
    self.setData(self.tableView, myList: list)
  }

  // this method hasnt worked from extension, so i just pasted it here
  func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

    if searchActive {
        return self.filteredlist.count
    } else {
        return self.list.count
    }
  }

  // self.list = myList hasnt worked from extension, so i just pasted it here
  func setData(atableView: UITableView, myList : [ProductModel]) {
    print(String(myList.count))
    self.list = myList
    print(String(self.list.count))
    self.tableView.reloadData()
  }

  // i decided to implement this method because of the tableView
  func setData() {
    self.tableView.reloadData()
  }


  // this method hasnt worked from extension, so i just pasted it here
  func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

    let cell:GenericListCell = tableView.dequeueReusableCellWithIdentifier("cell") as! GenericListCell


    var object : ProductModel
    if searchActive {
        object = filteredlist[indexPath.row]
    } else {
        object = list[indexPath.row]
    }

    cell.formatData(object.name!, subtitle: object.price ?? "valor", char: object.name!)
    print("returning cell")


    return cell

  }


  override func viewDidLoad() {
   //         searchFuckinBar.delegate = self
    super.viewDidLoad()


    // Do any additional setup after loading the view, typically from a nib.
  }


   func filterContentForSearchText(searchText: String) {
    // Filter the array using the filter method
    self.filteredlist = self.list.filter({( object: ProductModel) -> Bool in
        // let categoryMatch = (scope == "All") || (object.category == scope)
        let stringMatch = object.name!.lowercaseString.rangeOfString(searchText.lowercaseString)
        return  (stringMatch != nil)
    })
  }

    func formatCell(cell : GenericListCell, object : ProductModel) {
    cell.formatData(object.name!, subtitle: object.price ?? "valor", char: object.name!)
  }

  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
    // Dispose of any resources that can be recreated.
  }
} 

*GenericListController只是一个带有一些辅助方法的UIViewController。

2
作为一种解决方法,您可以在使用故事板实例化 ProductListController 之前将其加载到 ObjC 运行时(即 AppDelegate?)中。
ProductListController.load()

干杯


最好的事情! - shelll
同意,非常好的答案,谢谢 :) 但有一个注意事项:这对于在故事板中设置为主界面的初始viewController不起作用。看起来iOS会在这种情况下尝试过早地加载viewController。解决方法:只需在调用load()后手动创建Window和rootViewController即可。 - nils
其实,我对调用load()方法有所犹豫,因为该方法应该自动调用。好消息是,我们可以通过对类进行任意操作来将其注册到运行时中。这就是为什么我只会打印具有相同效果的类名:print("Loading (ProductListController.self) class")。 - nils

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接