web-dev-qa-db-ja.com

ストーリーボードを使用したSwift 2.0のジェネリックコントローラー

アプリのGenericListControllerを作成しようとしています。

UIViewControllerを拡張するこの汎用コントローラーを拡張するProductListControllerがあります。 ProductListControllerをストーリーボードに接続して2つのアウトレットを作成しましたが、常に次のエラーが発生します。

 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を削除すると、すべてのアウトレットでこのエラーが発生します。ストーリーボードはジェネリックでスーパーをロードできないと思います。どうすればそれを機能させることができますか?

私のコード:

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

-編集-

ジェネリッククラスを拡張してストーリーボードにクラスを追加しようとすると、xcodeがクラス名をオートコンプリートしないことがわかりました(おそらくクラスを検出できないため)

25
sagits

これは、それが不可能な理由に答えます: インターフェイスビルダーのカスタムビューとしてジェネリッククラスを使用します

Interface Builderは、ObjCランタイムを介してコードと「対話」します。そのため、IBは、ObjCランタイムで表現可能なコードの機能にのみアクセスできます。 ObjCはジェネリックを行いません

考えられる回避策のこのヒント: obj-cのジェネリック obj-cでジェネリックViewControllerを作成すると、IBがそれを受け入れますか?

プロトコルの使用を検討しましたか?これはストーリーボードを混乱させることはありません。簡単にテストできるようにコードを少し変更しました。これの欠点は、プロトコルにプロパティを保存できないことです。したがって、それらをコピーして貼り付ける必要があります。利点は、それが機能することです。

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()
15
R Menke

@ r-menkeが上で述べたように:

Interface Builderは、ObjCランタイムを介してコードと「対話」します。そのため、IBは、ObjCランタイムで表現可能なコードの機能にのみアクセスできます。 ObjCはジェネリックを行いません

これは本当です、

しかし、私の経験では、次のように問題を回避できます(YMMV)。

ここで不自然な例を作成し、これがどのように失敗するかを確認できます。

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

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

ここで実行例:

http://swiftstub.com/87870368

nilが出力されることがわかります

これを少し微調整して、もう一度試してみましょう。

http://swiftstub.com/346544378

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

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

これを取得します:

nil Optional(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でいくつかの問題に遭遇しました。私はiOS9で試したことがないので、YMMVです。しかし、私は現在、作業中のアプリに対して10.11でこれを行っており、大きな問題は発生していません。それは、私が将来問題にぶつからないということではなく、これが適切であるということでもありません。私はこれの完全な影響を知っていると主張することはできません。私が言えるのは、今のところ、問題を回避しているように見えるということだけです。

私は8月4日にこれにradrを提出しました:#22133133オープンRADRには表示されませんが、bugreport.Apple.comの下では、少なくとも私のアカウントの下にリストされています。

8
Adam Venturella

ユーザーRmenkeなどの助けを借りてアーカイブしたコードを投稿します。私の目標は、UISearchBarDelegate、UITableViewDelegate、およびgetDataメソッド(jsonを正しく解析できるようにするために型クラスが必要)を処理できるGenericListProtocolを用意することです。

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メソッドを実装できましたが、デフォルトのクラスに2つ実装する必要があります。

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
sagits

回避策として、ストーリーボードでインスタンス化する前に、ProductListControllerをObjCランタイム(つまり、AppDelegate?)にロードすることができます。

ProductListController.load()

乾杯

2
pera