25

Im trying to create a GenericListController for my app.

I have a ProductListController that extend this generic controller which extends UIViewController. I have connected ProductListController to a storyboard and made 2 outlets, but i always receive this error:

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

I receive this error for all my outlets, if i remove the generic T from GenericListController it works. I guess a storyboard cant load a super with generics. How can i make it work?

My code:

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

--EDIT--

I have found that if i extend an generic class and try to add the class to an storyboard xcode wont autocomplete the class name (probably because it cant detect the class)

6
  • 3
    why do you need to add generic model in UIViewController? Sep 29, 2015 at 6:29
  • 1
    Because my logic for list controller is always the same, it just changes the type of the object. I ll have productList, saleList, userList, and all them share the same methods and logic. Same for the DetailsController, AddController. If i cant use generics i ll have to copy and paste my getData function into each new controller i made (inside it i have dao methods that need a type). Sep 29, 2015 at 14:46
  • @sagits I was wondering how useful all this is since you can't access any of the attribute of the generic type (in this case the ProductModel). Not with your method and not with mine? Are you also creating a basic class that all T will conform to?
    – R Menke
    Oct 2, 2015 at 14:29
  • So just design your custom UIViewController subclass, such that it can handle all types of your lists. You could also subclass that further to add bits of customization for each type of list if needed, while still reusing the core methods without having to "copy-paste" them into each sub-subclass.
    – ElmerCat
    Oct 2, 2015 at 18:40
  • @sagits Does it run when you don't include the IB Outlets? I am guessing it won't. Reasons stated below.
    – R Menke
    Oct 2, 2015 at 23:37

4 Answers 4

16
+50

This answers why it is not possible : use a generic class as a custom view in interface builder

Interface Builder "talks" to your code through the ObjC runtime. As such, IB can can access only features of your code that are representable in the ObjC runtime. ObjC doesn't do generics

This hint at a possible work around : generics in obj-c Maybe you can create a generic ViewController in obj-c and then IB will accept it?

Have you considered using a protocol? This doesn't upset the storyboard. Changed the code a bit to make it easily testable. The downside of this is that you can't have stored properties in a protocol. So you would still need to copy paste those. Upside is that it works.

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

Update: Allow some access to attributes to the generic class. Changed it to a basic class to easily test in a Playground. UIViewController stuff is in the code above.

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()
5
  • Hi, i was trying to implement your answer but i need T to be T where T : MyModel (this class holds the getId method), is it possible? I know this seems strange, but i need to pass the exact classType to my getData, so it can parse json from webservice and return the typed list (which is necessary to implement other generic methods) Oct 5, 2015 at 18:14
  • @sagits I think you are using generics wrong. Generics are awesome when manipulating X that is found in Objects that conform to Y. It is not useful to replace all subclasses. In your case a subclass would still be the way to go. But updated the answer with a way to get some access to attributes that are inherited from a superclass (ProductModel)
    – R Menke
    Oct 5, 2015 at 18:44
  • Thank you, i'll post my code soon, it will make more sense. It worked, but i used typealias T : MyModel. Now i'm trying to update list value from getData, i have tryied updating the protocol list and passing a list from the subclass (getData(myList : [T]) {myList = listFromServer}. But neither of them worked, how should i do that? Oct 5, 2015 at 20:41
  • Thank you again. I'll add it below because i think its still related to generic. There's no much info about swift 2.0 generics, and i think this topic will be very usefull for people in the future. Oct 5, 2015 at 21:12
  • links : on generics, more, some more, even more, seriously, could keep going
    – R Menke
    Oct 5, 2015 at 22:44
8

As @r-menke stated above:

Interface Builder "talks" to your code through the ObjC runtime. As such, IB can can access only features of your code that are representable in the ObjC runtime. ObjC doesn't do generics

This is true,

In my experience, however, we can get around the issue as follows (YMMV).

We can make a contrived example here and see how this fails:

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

print(NSClassFromString("main.D"))

Running example here:

http://swiftstub.com/878703680

You can see that it prints nil

Now lets tweak this slightly and try again:

http://swiftstub.com/346544378

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

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

We get this:

nil Optional(main.D)

Hey-o! It found it AFTER it was initialized the first time.

Lets apply this to storyboards. I am doing this in an application right now (rightly or wrongly)

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

Works as you would expect. In my case my CanvasController is declared as follows:

class CanvasController: MyNSViewController<SomeGeneric1, SomeGeneric2>

Now, I have run into some issues on iOS using this technique with a generic UITableView subclass. I have not tried it under iOS 9 so YMMV. But I am currently doing this under 10.11 for an app I am working on, and have not run into any major issues. That is not to say that I won't run into any issue in the future, or that this is even appropriate to do I cannot claim to know the full ramifications of this. All I can say is that, for now, it appears to get around the issue.

I filed a radr on this back on Aug 4: #22133133 I don't see it in open RADR, but under bugreport.apple.com it's at least listed under my account, whatever that's worth.

3
  • Awesome, using this you was able to reutilize a list ([SomeGeneric2])? And about IBOutlets, was you able to reutilize the ones declared on MyNSViewController? Thanks for your response Nov 12, 2015 at 18:12
  • This works perfectly, and proves wrong the "impossible" claim. Thanks! Mar 31, 2016 at 13:00
  • This basically works, but for some odd reason it breaks when trying to extend the subclasses
    – GetSwifty
    Oct 4, 2016 at 18:44
2

Ill post some code i have archieved with help from user R menke and others. My goal is to have a GenericListProtocol that can handle UISearchBarDelegate, UITableViewDelegate and my getData method (which needs a type class to be able to correctly parse 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()
  }



}

Although i was able to implement most UISearchBarDelegate, UITableViewDelegate methods, i still have to implement 2 of them on my default class:

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 is just a UIViewController with some helper methods

0
2

As a workaround you could just load your ProductListController into the ObjC runtime(i.e. AppDelegate?) before instantiating it with the Storyboard.

ProductListController.load()

Cheers

3
  • Best thing ever!
    – shelll
    Aug 9, 2017 at 11:36
  • Agreed, great answer, thanks :) One caveat: This doesn't work for the initial viewController in the storyboard set as the Main Interface. Looks like iOS tries to load the viewController too early in that case. Workaround: Just create Window and rootViewController manually after calling load().
    – nils
    Sep 24, 2017 at 19:11
  • Actually, I had second thoughts about calling load() since that method is supposed to be invoked automatically. The good news is that we can register the class with the runtime by doing any arbitrary thing with it. Thats why I would just print the class name which has the same effect: print("Loading (ProductListController.self) class")
    – nils
    Sep 24, 2017 at 19:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.