简体   繁体   English

Swift 中 UIViewController 的自定义初始化,在 storyboard 中设置接口

[英]Custom init for UIViewController in Swift with interface setup in storyboard

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我在为 UIViewController 的子类编写自定义 init 时遇到问题,基本上我想通过 viewController 的 init 方法传递依赖关系,而不是像viewControllerB.property = value这样直接设置属性

So I made a custom init for my viewController and call super designated init所以我为我的 viewController 做了一个自定义的 init 并调用了 super 指定的 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.视图 controller 接口位于 storyboard 中,我还将自定义 class 的接口作为我的视图 Z5903CDAZ03F2F9C0304。 And Swift requires to call this init method even if you are not doing anything within this method.并且 Swift 需要调用这个 init 方法,即使你没有在这个方法中做任何事情。 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...问题是当我尝试使用MyViewController(meme: meme)调用我的自定义 init 时,它根本不会在我的 viewController 中初始化属性......

I was trying to debug, I found in my viewController, init(coder aDecoder: NSCoder) get called first, then my custom init get called later.我试图调试,我在我的 viewController 中发现,首先调用init(coder aDecoder: NSCoder) ,然后再调用我的自定义 init。 However these two init method return different self memory addresses.然而,这两个 init 方法返回不同的self memory 地址。

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.我怀疑我的 viewController 的 init 有问题,它总是会用init?(coder aDecoder: NSCoder)返回self ,它没有实现。

Does anyone know how to make custom init for your viewController correctly?有谁知道如何正确地为您的 viewController 进行自定义初始化? Note: my viewController's interface is set up in storyboard注意:我的viewController的接口设置在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)
    }

}

As it was specified in one of the answers above you can not use both and custom init method and storyboard.正如在上述答案之一中指定的那样,您不能同时使用自定义 init 方法和故事板。

But you still can use a static method to instantiate ViewController from a storyboard and perform additional setup on it.但是您仍然可以使用静态方法从故事板实例化ViewController并对其执行其他设置。

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.不要忘记在故事板中指定 IdentifierOfYouViewController 作为视图控制器标识符。 You may also need to change the name of the storyboard in the code above.您可能还需要更改上面代码中故事板的名称。

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.当您从 Storyboard 初始化时,您不能使用自定义初始化程序,使用init?(coder aDecoder: NSCoder)是 Apple 设计故事板以初始化控制器的方式。 However, there are ways to send data to a UIViewController .但是,有一些方法可以将数据发送到UIViewController

Your view controller's name has detail in it, so I suppose that you get there from a different controller.您的视图控制器的名称中有detail ,所以我想您是从不同的控制器到达那里的。 In this case you can use the prepareForSegue method to send data to the detail (This is Swift 3):在这种情况下,您可以使用prepareForSegue方法将数据发送到详细信息(这是 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.我只是使用String类型的属性而不是Meme来进行测试。 Also, make sure that you pass in the correct segue identifier ( "identifier" was just a placeholder).另外,请确保您传入正确的 segue 标识符( "identifier"只是一个占位符)。

As @Caleb Kleveter has pointed out, we can't use a custom initializer while initialising from a Storyboard.正如@Caleb Kleveter 指出的那样,我们不能在从 Storyboard 初始化时使用自定义初始值设定项。

But, we can solve the problem by using factory/class method which instantiate view controller object from Storyboard and return view controller object.但是,我们可以通过使用从 Storyboard 实例化视图控制器对象并返回视图控制器对象的工厂/类方法来解决这个问题。 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: Make类方法,在MemeDetailVC类中,如下:

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

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)然后你用let memeDetailVC = MemeDetailVC(theMeme)初始化你的 MemeDetailVC

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. Apple 关于初始化程序的文档非常好,但我个人最喜欢的是Ray Wenderlich:深度初始化教程系列,它应该为您提供有关各种初始化选项和“正确”做事方式的大量解释/示例。


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.编辑:虽然您可以在自定义视图控制器上使用便利初始值设定项,但每个人都正确地指出,从故事板或故事板 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).如果您的界面是在情节提要中设置的,并且您完全以编程方式创建控制器,那么便利初始化程序可能是您尝试做的事情的最简单方法,因为您不必处理所需的 init NSCoder(我仍然不太明白)。

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.如果您通过情节提要获取视图控制器,那么您需要遵循@Caleb Kleveter 的回答并将视图控制器转换为所需的子类,然后手动设置属性。

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.因此,要初始化它们,需要使用init?(coder...coder是所有设置/视图信息的来源。

So, in this case, it's not possible to also call some other init function with a custom parameter.因此,在这种情况下,不可能使用自定义参数调用其他一些 init 函数。 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.在所有情况下,您都使用 SDK 所需的 init 函数并随后传递其他参数。

UIViewController class conform to NSCoding protocol which is defined as: UIViewController类符合NSCoding协议,其定义为:

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?) .所以UIViewController有两个指定的初始化器init?(coder aDecoder: NSCoder)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. Storyborad 直接调用init?(coder aDecoder: NSCoder)来初始化UIViewControllerUIView ,没有给你传递参数的余地。

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
}

Swift 5斯威夫特 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
    }
}

As of iOS 13 you can initialize the view controller that resides in a storyboard using: instantiateViewController(identifier:creator:) method on the UIStoryboard instance.从 iOS 13 开始,您可以使用UIStoryboard实例上的instantiateViewController(identifier:creator:)方法初始化驻留在故事板中的视图控制器。

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

Although we can now do custom init for the default controllers in the storyboard using instantiateInitialViewController(creator:) and for segues including relationship and show.尽管我们现在可以使用instantiateInitialViewController(creator:)为故事板中的默认控制器以及包括关系和显示在内的segue 进行自定义初始化。

This capability was added in Xcode 11 and the following is an excerpt from the Xcode 11 Release Notes :此功能已在 Xcode 11 中添加,以下是Xcode 11 发行说明的摘录:

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.使用新的@IBSegueAction属性注释的视图控制器方法可用于在代码中创建 segue 的目标视图控制器,使用具有任何必需值的自定义初始值设定项。 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.在其源视图控制器上创建从 segue 到@IBSegueAction方法的连接。 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: .在支持 Segue Actions 的新操作系统版本上,该方法将被调用,它返回的值将是传递给prepareForSegue:sender:的 segue 对象的destinationViewController 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: .可以在单个源视图控制器上定义多个@IBSegueAction方法,这可以减少在prepareForSegue:sender:检查 segue 标识符字符串的需要。 (47091566) (47091566)

An IBSegueAction method takes up to three parameters: a coder, the sender, and the segue's identifier. IBSegueAction方法最多采用三个参数:编码器、发送者和 segue 的标识符。 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. NSCoder必须传递到目标视图控制器的初始值设定项,以确保使用故事板中配置的值对其进行自定义。 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.该方法返回一个与故事板中定义的目标控制器类型匹配的视图控制器,或者返回nil以使用标准的init(coder:)方法初始化目标控制器。 If you know you don't need to return nil , the return type can be non-optional.如果您知道不需要返回nil ,则返回类型可以是非可选的。

In Swift, add the @IBSegueAction attribute:在 Swift 中,添加@IBSegueAction属性:

@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:在Objective-C中,在返回类型前添加IBSegueAction

- (IBSegueAction ViewController *)makeDogController:(NSCoder *)coder
               sender:(id)sender
      segueIdentifier:(NSString *)segueIdentifier
{
   return [PetController initWithCoder:coder
                               petName:self.selectedPetName
                                  type:@"dog"];
}

Swift 5 - Stored Property Initialization in UIViewController Subclass Swift 5-UIViewController子类中的存储属性初始化

class SomeClass: UIViewController {

    var storedProperty: String

     required init?(coder aDecoder: NSCoder) {
        self.storedProperty = "Stored Prop"
        super.init(coder: aDecoder)
     }
}

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.从技术上讲,可以通过两次初始化视图控制器来实现自定义初始化,同时保留情节提要配置的界面:第一次通过自定义init ,第二次在loadView()中从情节loadView()中获取视图。

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.现在在你的应用程序的其他地方你可以调用你的自定义初始值设定项,比如CustomViewController(someParameter: foo)并且仍然从故事板接收视图配置。

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传递给自定义init参数必须存储为可选属性
  • Adds boilerplate which must be maintained as outlets/properties are changed添加样板,必须在更改出口/属性时维护该样板

Perhaps you can accept these tradeoffs, but use at your own risk .也许您可以接受这些权衡,但使用风险自负

Correct flow is, call the designated initializer which in this case is the init with nibName,正确的流程是,调用指定的初始化程序,在这种情况下是带有 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

}

In XCode 11/iOS13 , you can use instantiateViewController(identifier:creator:) also without segues:XCode 11/iOS13 中,您也可以使用instantiateViewController(identifier:creator:)而不使用 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)

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.此解决方案展示了一种使用自定义初始化程序但仍能够使用 Storyboard 而不使用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:为了使它能够使用它,让我们首先调整我们的MemeDetailsVC以接受一个 NSCoder 实例作为其自定义初始化程序的一部分,然后将该初始化程序委托给super.init(coder:) ,而不是它的 nibName 等效项:

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:然后,您以这种方式实例化并显示 View Controller:

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)

// View controller is in Main.storyboard and it has identifier set // 视图控制器在 Main.storyboard 中,它有标识符集

Class B 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 A级

let objB = customInit(carType:"Any String")

 navigationController?.pushViewController(objB,animated: true)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM