Link Storyboard to UIViewController with Generic

烂漫一生 提交于 2020-04-08 10:43:10

问题


I have this view controller, meant to present model object details in a generic way:

class APIModelDetailsVC<T where T: APIModel>: UIViewController {...}

I'd like my storyboard to use this class. I'm able to assign it in Interface Builder:

I do the preparation of this ViewController in a tableview's didSelect method (including specifying the type for the generic placeholder):

override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
  print("didSelectRowAtIndexPath \(indexPath.row)")
  if let vc = UIStoryboard(name: "APIModelDetailsVC", bundle: nil).instantiateInitialViewController() as? APIModelDetailsVC<StarWarsPerson> {
    vc.model = data[indexPath.row]
    self.navigationController?.pushViewController(vc, animated: true)
  }
}

When I attempt to navigate to this view controller, I get the following console error:

Unknown class _TtC14api_collection17APIModelDetailsVC in Interface Builder file.

Is this a documented limitation with Storyboards? Is there a way I can/should specify the generic so the storyboard can link to it?


回答1:


Storyboards have a problem with having a generic class. The thing is that Interface Builder communicates to the ViewController through the Objective-C runtime. Because of this, InterfaceBuilder is limited to the features that Objective-C provides. In this case, generics are not supported.

A workaround for this is using the .load() method of NSObject.

For example, if you have the mentioned ViewController class:

class APIModelDetailsVC<T where T: APIModel>: UIViewController {...}

You should create a "dummy" ViewController such as:

class StartWasModelDetailsVC: APIModelDetailsVC<StarWarsPerson> {...}

and set this last ViewController in the storyboard. Afterwards, in order to make this work in the Objective-c runtime, you should add the following in your AppDelegate or somewhere before this controller is loaded.

StartWasModelDetailsVC.load()

Hope it helps!




回答2:


I just ran into the same problem and came up with another workaround:

I create my generic UIViewController in code only. In IB I then create a UIView in a XIB file that has all the controls I need along with a class called "mainView" that has all the outlets.

In viewDidLoad() of the generic UIViewController I then load the view from the XIB, wire it up and insert it into the view controller's main view like this:

class RSListViewController<T: RSObjectProtocol>: UIViewController, UITableViewDelegate {

override func viewDidLoad() {

    super.viewDidLoad()

    guard let mainView = Bundle.main.loadNibNamed("mainView", owner: self, options: nil)?.first as? MainView else { return }

    self.mainView = mainView
    self.mainView.tableView.delegate = self
    self.mainView.segControl.addTarget(self, action: #selector(RSListViewController.listTypeChanged(_:)), for: .valueChanged)

    self.view.addSubview(mainView)

}

The only caveat that I can see so far is that you cannot address the outlets directly like

self.buttonAdd

but need to go through the view like

self.mainView.buttonAdd

Instead of actions I wire the the events through code with the addTarget like in the code sample above.

It's kind of a mix if you don't want to do "code only" but in my case it seems to work fine. And it does prevent the creation of many dummy ViewControllers which in my view kind of defies the idea of generics. But this is just a matter of personal taste and does not mean to criticize in any way Federico Ojeda's solution.




回答3:


There is another really nice option for this problem.

My solution was inspired by: List all subclasses of one class

Generic UIViewcontrollers work in storyboards provided you register them in the Obj-C runtime before using them. By using a generic base we can make this work. All UIViewControllers are NSObjects, because of this we can retrieve all sublcasses. Therefore we can autoregister all subclasses of UIViewController, even generic ones, and automatically call load() on them in the AppDelegate.

public func address(of object: Any?) -> UnsafeMutableRawPointer {
    return Unmanaged.passUnretained(object as AnyObject).toOpaque()
}

public func subclasses<T>(of theClass: T) -> [T] {
    var count: UInt32 = 0, result: [T] = []
    let allClasses = objc_copyClassList(&count)!
    let classPtr = address(of: theClass)

    for n in 0 ..< count {
        let someClass: AnyClass = allClasses[Int(n)]
        guard let someSuperClass = class_getSuperclass(someClass), address(of: someSuperClass) == classPtr else { continue }
        result.append(someClass as! T)
    }

    return result
}

After this in applicationDidFinishLaunchingWithOptions we can simply do this:

    for subclass in subclasses(of: UIViewController.self) {
        subclass.load()
    }

Say we have the following viewcontrollers:

class BaseViewController<CompletionResult> : UIViewController {
    var onComplete: ((CompletionResult) -> ())?


}

class TestViewController : BaseViewController<String> {


    override func viewDidLoad() {
        super.viewDidLoad()

        self.onComplete = {
            value in
            print(value)
        }

        onComplete?("TEST")
    }


}

We can now safely use TestViewController inside a storyboard!



来源:https://stackoverflow.com/questions/37282965/link-storyboard-to-uiviewcontroller-with-generic

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!