Custom init for UIViewController in Swift with interface setup in storyboard

后端 未结 12 2053
长情又很酷
长情又很酷 2020-11-27 05:08

I\'m having issue for writing custom init for subclass of UIViewController, basically I want to pass the dependency through the init method for viewController rather than se

相关标签:
12条回答
  • 2020-11-27 05:17

    There were originally a couple of answers, which were cow voted and deleted even though they were basically correct. The answer is, you can't.

    When working from a storyboard definition your view controller instances are all archived. So, to init them it's required that init?(coder... be used. The coder is where all the settings / view information comes from.

    So, in this case, it's not possible to also call some other init function with a custom parameter. It should either be set as a property when preparing the segue, or you could ditch segues and load the instances directly from the storyboard and configure them (basically a factory pattern using a storyboard).

    In all cases you use the SDK required init function and pass additional parameters afterwards.

    0 讨论(0)
  • 2020-11-27 05:20

    UIViewController class conform to NSCoding protocol which is defined as:

    public protocol NSCoding {
    
       public func encode(with aCoder: NSCoder)
    
       public init?(coder aDecoder: NSCoder) // NS_DESIGNATED_INITIALIZER
    }    
    

    So UIViewController has two designated initializer init?(coder aDecoder: NSCoder) and init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?).

    Storyborad calls init?(coder aDecoder: NSCoder) directly to init UIViewController and UIView,There is no room for you to pass parameters.

    One cumbersome workaround is to use an temporary cache:

    class TempCache{
       static let sharedInstance = TempCache()
    
       var meme: Meme?
    }
    
    TempCache.sharedInstance.meme = meme // call this before init your ViewController    
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder);
        self.meme = TempCache.sharedInstance.meme
    }
    
    0 讨论(0)
  • 2020-11-27 05:22

    Although we can now do custom init for the default controllers in the storyboard using instantiateInitialViewController(creator:) and for segues including relationship and show.

    This capability was added in Xcode 11 and the following is an excerpt from the Xcode 11 Release Notes:

    A view controller method annotated with the new @IBSegueAction attribute can be used to create a segue’s destination view controller in code, using a custom initializer with any required values. This makes it possible to use view controllers with non-optional initialization requirements in storyboards. Create a connection from a segue to an @IBSegueAction method on its source view controller. On new OS versions that support Segue Actions, that method will be called and the value it returns will be the destinationViewController of the segue object passed to prepareForSegue:sender:. Multiple @IBSegueAction methods may be defined on a single source view controller, which can alleviate the need to check segue identifier strings in prepareForSegue:sender:. (47091566)

    An IBSegueAction method takes up to three parameters: a coder, the sender, and the segue’s identifier. The first parameter is required, and the other parameters can be omitted from your method’s signature if desired. The NSCoder must be passed through to the destination view controller’s initializer, to ensure it’s customized with values configured in storyboard. The method returns a view controller that matches the destination controller type defined in the storyboard, or nil to cause a destination controller to be initialized with the standard init(coder:) method. If you know you don’t need to return nil, the return type can be non-optional.

    In Swift, add the @IBSegueAction attribute:

    @IBSegueAction
    func makeDogController(coder: NSCoder, sender: Any?, segueIdentifier: String?) -> ViewController? {
        PetController(
            coder: coder,
            petName:  self.selectedPetName, type: .dog
        )
    }
    

    In Objective-C, add IBSegueAction in front of the return type:

    - (IBSegueAction ViewController *)makeDogController:(NSCoder *)coder
                   sender:(id)sender
          segueIdentifier:(NSString *)segueIdentifier
    {
       return [PetController initWithCoder:coder
                                   petName:self.selectedPetName
                                      type:@"dog"];
    }
    
    0 讨论(0)
  • 2020-11-27 05:22

    // View controller is in Main.storyboard and it has identifier set

    Class B

    class func customInit(carType:String) -> BViewController 
    
    {
    
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    
    let objClassB = storyboard.instantiateViewController(withIdentifier: "BViewController") as? BViewController
    
        print(carType)
        return objClassB!
    }
    

    Class A

    let objB = customInit(carType:"Any String")
    
     navigationController?.pushViewController(objB,animated: true)
    
    0 讨论(0)
  • 2020-11-27 05:23

    Disclaimer: I do not advocate for this and have not thoroughly tested its resilience, but it is a potential solution I discovered while playing around.

    Technically, custom initialization can be achieved while preserving the storyboard-configured interface by initializing the view controller twice: the first time via your custom init, and the second time inside loadView() where you take the view from storyboard.

    final class CustomViewController: UIViewController {
      @IBOutlet private weak var label: UILabel!
      @IBOutlet private weak var textField: UITextField!
    
      private let foo: Foo!
    
      init(someParameter: Foo) {
        self.foo = someParameter
        super.init(nibName: nil, bundle: nil)
      }
    
      override func loadView() {
        //Only proceed if we are not the storyboard instance
        guard self.nibName == nil else { return super.loadView() }
    
        //Initialize from storyboard
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let storyboardInstance = storyboard.instantiateViewController(withIdentifier: "CustomVC") as! CustomViewController
    
        //Remove view from storyboard instance before assigning to us
        let storyboardView = storyboardInstance.view
        storyboardInstance.view.removeFromSuperview()
        storyboardInstance.view = nil
        self.view = storyboardView
    
        //Receive outlet references from storyboard instance
        self.label = storyboardInstance.label
        self.textField = storyboardInstance.textField
      }
    
      required init?(coder: NSCoder) {
        //Must set all properties intended for custom init to nil here (or make them `var`s)
        self.foo = nil
        //Storyboard initialization requires the super implementation
        super.init(coder: coder)
      }
    }
    

    Now elsewhere in your app you can call your custom initializer like CustomViewController(someParameter: foo) and still receive the view configuration from storyboard.

    I don't consider this a great solution for several reasons:

    • Object initialization is duplicated, including any pre-init properties
    • Parameters passed to the custom init must be stored as optional properties
    • Adds boilerplate which must be maintained as outlets/properties are changed

    Perhaps you can accept these tradeoffs, but use at your own risk.

    0 讨论(0)
  • 2020-11-27 05:27

    As it was specified in one of the answers above you can not use both and custom init method and storyboard.

    But you still can use a static method to instantiate ViewController from a storyboard and perform additional setup on it.

    It will look like this:

    class MemeDetailVC : UIViewController {
        
        var meme : Meme!
        
        static func makeMemeDetailVC(meme: Meme) -> MemeDetailVC {
            let newViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "IdentifierOfYouViewController") as! MemeDetailVC
            
            newViewController.meme = meme
            
            return newViewController
        }
    }
    

    Don't forget to specify IdentifierOfYouViewController as view controller identifier in your storyboard. You may also need to change the name of the storyboard in the code above.

    0 讨论(0)
提交回复
热议问题