ios Swift 中 UIViewController 的自定义初始化,在情节提要中设置界面

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/35315404/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-31 08:27:58  来源:igfitidea点击:

Custom init for UIViewController in Swift with interface setup in storyboard

iosswiftuiviewcontrollerinitialization

提问by Pigfly

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 并调用超级指定的 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...

视图控制器接口驻留在故事板中,我还将自定义类的接口作为我的视图控制器。即使你没有在这个方法中做任何事情,Swift 也需要调用这个 init 方法。否则编译器会抱怨...

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...

问题是当我尝试调用我的自定义 init 时,MyViewController(meme: meme)它根本没有在我的 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. However these two init method return different selfmemory addresses.

我试图调试,我在我的 viewController 中找到,首先init(coder aDecoder: NSCoder)被调用,然后我的自定义 init 被调用。但是这两个 init 方法返回不同的self内存地址。

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

我怀疑有毛病初始化为我的viewController,它总是会返回selfinit?(coder aDecoder: NSCoder),其中,有没有实现。

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

有谁知道如何正确地为您的 viewController 制作自定义初始化?注意:我的viewController 的界面是在storyboard 中设置的

here is my viewController code:

这是我的 viewController 代码:

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

}

回答by Alexander Grushevoy

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:

正如在上述答案之一中指定的那样,您不能同时使用自定义 init 方法和故事板。但是您仍然可以使用静态方法从故事板实例化 ViewController 并对其执行其他设置。它看起来像这样:

class MemeDetailVC : UIViewController {

    var meme : Meme!

    static func makeMemeDetailVC(meme: Meme) -> MemeDetailVC {
        let newViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("IdentifierOfYouViewController") as! MemeDetailVC

        newViewController.meme = meme

        return newViewController
    }
}

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

不要忘记在故事板中指定 IdentifierOfYouViewController 作为视图控制器标识符。您可能还需要更改上面代码中故事板的名称。

回答by Caleb Kleveter

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.

当您从 Storyboard 初始化时,您不能使用自定义初始化程序,使用init?(coder aDecoder: NSCoder)Apple 设计的 Storyboard 来初始化控制器的方式。但是,有一些方法可以将数据发送到UIViewController.

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

你的视图控制器的名字detail在里面,所以我想你是从不同的控制器到达那里的。在这种情况下,您可以使用该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 Stringinstead of Memefor testing purposes. Also, make sure that you pass in the correct segue identifier ("identifier"was just a placeholder).

我只是使用 type 属性String而不是Meme用于测试目的。另外,请确保您传入正确的 segue 标识符("identifier"只是一个占位符)。

回答by Nitesh Borad

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. I think this is a pretty cool way.

但是,我们可以通过使用从 Storyboard 实例化视图控制器对象并返回视图控制器对象的工厂/类方法来解决这个问题。我认为这是一个非常酷的方式。

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

回答by Ponyboy47

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)

然后你初始化你的 MemeDetailVC let memeDetailVC = MemeDetailVC(theMeme)

Apple's documentation on initializersis pretty good, but my personal favorite is the Ray Wenderlich: Initialization in Depthtutorial 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 answerand cast the view controller into your desired subclass then set the property manually.

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

回答by Wain

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 coderis 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. 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).

因此,在这种情况下,不可能使用自定义参数调用其他一些 init 函数。它应该在准备转场时设置为一个属性,或者您可以放弃转场并直接从故事板加载实例并配置它们(基本上是使用故事板的工厂模式)。

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

在所有情况下,您都使用 SDK 所需的 init 函数并随后传递其他参数。

回答by emrcftci

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

回答by wj2061

UIViewControllerclass conform to NSCodingprotocol which is defined as:

UIViewController类符合NSCoding协议,其定义为:

public protocol NSCoding {

   public func encode(with aCoder: NSCoder)

   public init?(coder aDecoder: NSCoder) // NS_DESIGNATED_INITIALIZER
}    

So UIViewControllerhas 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 UIViewControllerand UIView,There is no room for you to pass parameters.

Storyboradinit?(coder aDecoder: NSCoder)直接调用initUIViewControllerUIView, 没有给你传递参数的余地。

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
}

回答by JAHelia

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

从 iOS 13 开始,您可以使用实例instantiateViewController(identifier:creator:)上的方法初始化驻留在故事板中的视图控制器 UIStoryboard

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

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

回答by Nathan Hosselton

Disclaimer: I do not advocate for this and have not thoroughly tested its resilience, but it is a potentialsolution 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.

从技术上讲,可以通过两次初始化视图控制器来实现自定义初始化,同时保留情节提要配置的界面:第一次通过您的 custom init,第二次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 initmust be stored as optional properties
  • Adds boilerplate which must be maintained as outlets/properties are changed
  • 对象初始化重复,包括任何预初始化属性
  • 传递给自定义的参数init必须存储为可选属性
  • 添加样板,必须在更改出口/属性时维护该样板

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

也许您可以接受这些权衡,但使用风险自负

回答by malhal

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:)和 为包括关系和显示在内的转场对故事板中的默认控制器进行自定义初始化。

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 @IBSegueActionattribute 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 @IBSegueActionmethod 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 destinationViewControllerof the segue object passed to prepareForSegue:sender:. Multiple @IBSegueActionmethods may be defined on a single source view controller, which can alleviate the need to check segue identifier strings in prepareForSegue:sender:. (47091566)

使用新@IBSegueAction属性注释的视图控制器方法可用于在代码中创建 segue 的目标视图控制器,使用具有任何必需值的自定义初始值设定项。这使得在故事板中使用具有非可选初始化要求的视图控制器成为可能。创建从 segue 到@IBSegueAction其源视图控制器上的方法的连接。在支持 Segue Actions 的新操作系统版本上,该方法将被调用,它返回的值将是destinationViewController传递给 的 segue 对象的prepareForSegue:sender:@IBSegueAction可以在单个源视图控制器上定义多个方法,这可以减少在prepareForSegue:sender:. (47091566)

An IBSegueActionmethod 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 NSCodermust 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 nilto 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.

一个IBSegueAction方法最多接受三个参数:编码器、发送者和转场标识符。第一个参数是必需的,如果需要,可以从方法的签名中省略其他参数。的NSCoder必须通过被传递到目标视图控制器的初始化,以确保它与在故事板配置的值定制。该方法返回一个与故事板中定义的目标控制器类型匹配的视图控制器,或者nil使用标准init(coder:)方法初始化目标控制器。如果您知道不需要 return nil,则返回类型可以是非可选的。

In Swift, add the @IBSegueActionattribute:

在 Swift 中,添加@IBSegueAction属性:

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

In Objective-C, add IBSegueActionin 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"];
}