Custom init for UIViewController in Swift with interface setup in storyboard
Asked Answered
E

14

100

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 setting property directly like viewControllerB.property = value

So I made a custom init for my viewController and call super designated init

init(meme: Meme?) {
        self.meme = meme
        super.init(nibName: nil, bundle: nil)
    }

The view controller interface resides in storyboard, I've also make the interface for custom class to be my view controller. And Swift requires to call this init method even if you are not doing anything within this method. Otherwise the compiler will complain...

required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

The problem is when I try to call my custom init with MyViewController(meme: meme) it doesn't init properties in my viewController at all...

I was trying to debug, I found in my viewController, init(coder aDecoder: NSCoder) get called first, then my custom init get called later. However these two init method return different self memory addresses.

I'm suspecting something wrong with the init for my viewController, and it will always return self with the init?(coder aDecoder: NSCoder), which, has no implementation.

Does anyone know how to make custom init for your viewController correctly ? Note: my viewController's interface is set up in storyboard

here is my viewController code:

class MemeDetailVC : UIViewController {

    var meme : Meme!

    @IBOutlet weak var editedImage: UIImageView!

    // TODO: incorrect init
    init(meme: Meme?) {
        self.meme = meme
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    override func viewDidLoad() {
        /// setup nav title
        title = "Detail Meme"

        super.viewDidLoad()
    }

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        editedImage = UIImageView(image: meme.editedImage)
    }

}
Evident answered 10/2, 2016 at 12:31 Comment(1)
did you get a solution for this?Arcature
G
57

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.

Galliett answered 9/9, 2016 at 13:55 Comment(3)
After MemeDetailVC is initialized, the property "meme" is existed. Then dependency injection comes in to assign the value to "meme". At this moment, loadView() and viewDidLoad haven't got called. Those two methods will get called after MemeDetailVC add/push to view hierarchy.Coquille
Best answer here!Ophiolatry
Still init method is required, and compiler will say meme stored property has not been initialisedHickman
G
32

You can't use a custom initializer when you initialize from a Storyboard, using init?(coder aDecoder: NSCoder) is how Apple designed the storyboard to initialize a controller. However, there are ways to send data to a UIViewController.

Your view controller's name has detail in it, so I suppose that you get there from a different controller. In this case you can use the prepareForSegue method to send data to the detail (This is Swift 3):

override func prepare(for segue: UIStoryboardSegue, sender: AnyObject?) {
    if segue.identifier == "identifier" {
        if let controller = segue.destinationViewController as? MemeDetailVC {
            controller.meme = "Meme"
        }
    }
}

I just used a property of type String instead of Meme for testing purposes. Also, make sure that you pass in the correct segue identifier ("identifier" was just a placeholder).

Glossitis answered 8/9, 2016 at 21:56 Comment(3)
Hi, but how can we get rid of specifiying as an optional and we don't also want to mistake not assigning it. We just want the strict dependency meaningful for the controller to exist in first place.Antecedents
@AmberK You might want to look into programming your interface instead of using Interface Builder.Glossitis
Okay, im also looking kickstarter ios project for reference too, not able to tell yet how they send their view models.Antecedents
T
26

As @Caleb Kleveter has pointed out, we can't use a custom initializer while initialising from a Storyboard.

But, we can solve the problem by using factory/class method which instantiate view controller object from Storyboard and return view controller object. I think this is a pretty cool way.

Note: This is not an exact answer to question rather a workaround to solve the problem.

Make class method, in MemeDetailVC class, as follows:

// Considering your view controller resides in Main.storyboard and it's identifier is set to "MemeDetailVC"
class func `init`(meme: Meme) -> MemeDetailVC? {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let vc = storyboard.instantiateViewController(withIdentifier: "MemeDetailVC") as? MemeDetailVC
    vc?.meme = meme
    return vc
}

Usage:

let memeDetailVC = MemeDetailVC.init(meme: Meme())
Transitive answered 18/12, 2017 at 6:53 Comment(5)
But still the drawback is there, the property has to be optional.Antecedents
when using class func init(meme: Meme) -> MemeDetailVC Xcode 10.2 is confused and gives the error Incorrect argument label in call (have 'meme:', expected 'coder:')Wonky
@Amber isn't it always?Brahe
super nice and clean solution! ThanksOutside
Also you won't be able to use the meme variable inside viewDidLoad since it's called before vc?.meme = memeMerocrine
T
21

One way that I've done this is with a convenience initializer.

class MemeDetailVC : UIViewController {

    convenience init(meme: Meme) {
        self.init()
        self.meme = meme
    }
}

Then you initialize your MemeDetailVC with let memeDetailVC = MemeDetailVC(theMeme)

Apple's documentation on initializers is pretty good, but my personal favorite is the Ray Wenderlich: Initialization in Depth tutorial series which should give you plenty of explanation/examples on your various init options and the "proper" way to do things.


EDIT: While you can use a convenience initializer on custom view controllers, everyone is correct in stating that you cannot use custom initializers when initializing from the storyboard or through a storyboard segue.

If your interface is set up in the storyboard and you're creating the controller completely programmatically, then a convenience initializer is probably the easiest way to do what you're trying to do since you don't have to deal with the required init with the NSCoder (which I still don't really understand).

If you're getting your view controller via the storyboard though, then you will need to follow @Caleb Kleveter's answer and cast the view controller into your desired subclass then set the property manually.

Transubstantiate answered 8/9, 2016 at 22:33 Comment(4)
I don't get it, if my interface is setup in the storyboard and yea im creating it programmatically, then I must call the instantiate method using storyboard to load the interface right? How will the convenience is help here.Antecedents
Classes in swift cannot be initialized by calling another init function of the class (structs can though). So in order to call self.init(), you must mark the init(meme: Meme) as a convenience initializer. Otherwise, you would have to manually set all the required properties of a UIViewController in the initializer yourself, and I'm not sure what all those properties are.Transubstantiate
this is not subclassing UIViewController then, because you are calling self.init() and not super.init(). you can still initialize the MemeDetailVC using default init like MemeDetail() and in that case the code will crashLeflore
It is still considered subclassing because self.init() would have to call super.init() in its implementation or perhaps self.init() is directly inherited from the parent in which case they are functionally equivalent.Transubstantiate
U
7

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.

Ul answered 3/9, 2016 at 8:23 Comment(0)
A
6

Swift 5

You can write custom initializer like this ->

class MyFooClass: UIViewController {

    var foo: Foo?

    init(with foo: Foo) {
        self.foo = foo
        super.init(nibName: nil, bundle: nil)
    }

    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        self.foo = nil
    }
}
Agonic answered 22/9, 2019 at 16:27 Comment(0)
C
4

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
}
Crippen answered 3/9, 2016 at 12:56 Comment(0)
M
3

As of iOS 13 you can initialize the view controller that resides in a storyboard using: instantiateViewController(identifier:creator:) method on the UIStoryboard instance.

tutorial: https://sarunw.com/posts/better-dependency-injection-for-storyboards-in-ios13/

Megagamete answered 10/12, 2019 at 8:39 Comment(0)
C
1

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"];
}
Crystallite answered 9/12, 2019 at 13:59 Comment(0)
C
1

In XCode 11/iOS13, you can use instantiateViewController(identifier:creator:) also without segues:

    let vc = UIStoryboard(name: "StoryBoardName", bundle: nil).instantiateViewController(identifier: "YourViewControllerIdentifier", creator: {
        (coder) -> YourViewController? in
        return YourViewController(coder: coder, customParameter: "whatever")
    })
    present(vc, animated: true, completion: nil)
Coadjutant answered 3/12, 2021 at 16:7 Comment(2)
Your answer doesn't related to the question. :DOutside
I think it does, the question is about custom init, and according to apple documentation, instantiateViewController method "creates the specified view controller from the storyboard and initializes it using your custom initialization code".Coadjutant
L
1

This solution shows a way to have custom initializers but still be able to use Storyboard WITHOUT using the self.init(nib: nil, bundle: nil) function.

To make it possible to use that, let’s first tweak our MemeDetailsVC to also accept an NSCoder instance as part of its custom initializer, and to then delegate that initializer to super.init(coder:), rather than its nibName equivalent:

class MemeDetailVC : UIViewController {
    var meme : Meme!
    @IBOutlet weak var editedImage: UIImageView!

    init?(meme: Meme, coder: NSCoder) {
        self.meme = meme
        super.init(coder: aDecoder)
    }
    @available(*, unavailable, renamed: "init(product:coder:)")
        required init?(coder: NSCoder) {
            fatalError("Invalid way of decoding this class")
        }

    override func viewDidLoad() {
        title = "Detail Meme"
        super.viewDidLoad()
    }

    override func viewWillAppear(animated: Bool) {
        super.viewWillAppear(animated)
        editedImage = UIImageView(image: meme.editedImage)
    }
}

And then, you instantiate & show the View Controller this way:

guard let viewController = storyboard?.instantiateViewController(
        identifier: "MemeDetailVC",
        creator: { coder in
            MemeDetailVC(meme: meme, coder: coder)
        }
    ) else {
        fatalError("Failed to create Product Details VC")
    }
//Then you do what you want with the view controller.
    present(viewController, sender: self)
Lone answered 19/8, 2022 at 2:23 Comment(0)
C
0

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.

Colchester answered 22/11, 2019 at 18:19 Comment(0)
C
0

Correct flow is, call the designated initializer which in this case is the init with nibName,

init(tap: UITapGestureRecognizer)
{
    // Initialise the variables here


    // Call the designated init of ViewController
    super.init(nibName: nil, bundle: nil)

    // Call your Viewcontroller custom methods here

}
Cioffred answered 7/6, 2020 at 7:34 Comment(0)
C
-1

// 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)
Codee answered 1/10, 2019 at 5:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.