【问题标题】:Custom init for UIViewController in Swift with interface setup in storyboardSwift 中 UIViewController 的自定义初始化,并在情节提要中设置界面
【发布时间】:2016-05-20 19:36:08
【问题描述】:

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

所以我为我的 viewController 做了一个自定义的 init 并调用了 super 指定的 init

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

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

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

问题是当我尝试使用 MyViewController(meme: meme) 调用我的自定义 init 时,它根本不会在我的 viewController 中初始化属性...

我正在尝试调试,我在我的 viewController 中发现,init(coder aDecoder: NSCoder) 首先被调用,然后我的自定义 init 稍后被调用。然而这两个 init 方法返回不同的self 内存地址。

我怀疑我的 viewController 的 init 有问题,它总是会返回 selfinit?(coder aDecoder: NSCoder),它没有实现。

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

这是我的 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)
    }

}

【问题讨论】:

  • 您找到解决方案了吗?

标签: ios swift uiviewcontroller initialization


【解决方案1】:

正如在上述答案之一中指定的那样,您不能同时使用自定义 init 方法和情节提要。

但您仍然可以使用静态方法从情节提要中实例化 ViewController 并对其执行其他设置。

看起来像这样:

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

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

【讨论】:

  • MemeDetailVC初始化后,属性“meme”就存在了。然后依赖注入将值分配给“meme”。此时,还没有调用 loadView() 和 viewDidLoad。这两个方法将在 MemeDetailVC 添加/推送到视图层次结构后被调用。
  • 最佳答案在这里!
  • 仍然需要 init 方法,编译器会说 meme 存储的属性尚未初始化
【解决方案2】:

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

您的视图控制器的名称中包含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"
        }
    }
}

我只是使用String 类型的属性而不是Meme 来进行测试。另外,请确保您传递了正确的 segue 标识符("identifier" 只是一个占位符)。

【讨论】:

  • 嗨,但是我们怎样才能摆脱指定为可选的,我们也不想误认为没有分配它。我们只希望首先存在对控制器有意义的严格依赖。
  • @AmberK 你可能想研究一下你的界面编程而不是使用 Interface Builder。
  • 好的,我也在寻找kickstarter ios 项目以供参考,但尚不知道他们如何发送他们的视图模型。
【解决方案3】:

正如@Caleb Kleveter 所指出的,在从情节提要进行初始化时,我们不能使用自定义初始化程序。

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

注意:这不是问题的确切答案,而是解决问题的解决方法。

制作类方法,在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
}

用法:

let memeDetailVC = MemeDetailVC.init(meme: Meme())

【讨论】:

  • 但缺点仍然存在,该属性必须是可选的。
  • 当使用class func init(meme: Meme) -> MemeDetailVC Xcode 10.2 被混淆并给出错误Incorrect argument label in call (have 'meme:', expected 'coder:')
  • @Amber 不是一直这样吗?
【解决方案4】:

我做到这一点的一种方法是使用便利初始化程序。

class MemeDetailVC : UIViewController {

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

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

Apple's documentation on initializers 相当不错,但我个人最喜欢的是Ray Wenderlich: Initialization in Depth 教程系列,它应该为您提供有关各种初始化选项和“正确”做事方式的大量解释/示例。


编辑:虽然您可以在自定义视图控制器上使用便捷初始化程序,但每个人都正确地指出在从情节提要或通过情节提要 segue 初始化时不能使用自定义初始化程序。

如果您的界面是在情节提要中设置的,并且您完全以编程方式创建控制器,那么便利初始化程序可能是执行您想做的事情的最简单方法,因为您不必处理需要使用 NSCoder 进行初始化(我仍然不太了解)。

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

【讨论】:

  • 我不明白,如果我的界面是在情节提要中设置的并且是我以编程方式创建的,那么我必须使用情节提要调用实例化方法来加载界面对吗? convenience 将如何提供帮助。
  • swift 中的类不能通过调用该类的另一个 init 函数来初始化(虽然结构可以)。所以为了调用self.init(),你必须将init(meme: Meme)标记为convenience初始化器。否则,您必须自己在初始化程序中手动设置 UIViewController 的所有必需属性,我不确定所有这些属性是什么。
  • 这不是 UIViewController 的子类,因为您调用的是 self.init() 而不是 super.init()。你仍然可以使用MemeDetail() 之类的默认初始化来初始化 MemeDetailVC,在这种情况下代码会崩溃
  • 它仍然被认为是子类化,因为 self.init() 必须在其实现中调用 super.init() 或者 self.init() 可能直接从父级继承,在这种情况下它们是功能性的等价的。
【解决方案5】:

原来有几个答案,虽然基本正确,但还是被牛投票删除了。答案是,你不能。

从情节提要定义工作时,您的视图控制器实例都已存档。因此,要初始化它们,需要使用 init?(coder...coder 是所有设置/视图信息的来源。

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

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

【讨论】:

    【解决方案6】:

    斯威夫特 5

    您可以像这样编写自定义初始化程序 ->

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

    【讨论】:

      【解决方案7】:

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

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

      所以UIViewController 有两个指定的初始化器init?(coder aDecoder: NSCoder)init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)

      Storyborad 直接调用init?(coder aDecoder: NSCoder) 初始化UIViewControllerUIView,没有给你传参数的余地。

      一个麻烦的解决方法是使用临时缓存:

      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
      }
      

      【讨论】:

        【解决方案8】:

        从 iOS 13 开始,您可以使用以下方法初始化位于情节提要中的视图控制器: UIStoryboard 实例上的 instantiateViewController(identifier:creator:) 方法。

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

        【讨论】:

          【解决方案9】:

          虽然我们现在可以使用 instantiateInitialViewController(creator:) 为情节提要中的默认控制器以及包括关系和显示在内的 segues 进行自定义初始化。

          此功能是在 Xcode 11 中添加的,以下是Xcode 11 Release Notes 的摘录:

          使用新的 @IBSegueAction 属性注释的视图控制器方法可用于在代码中创建 segue 的目标视图控制器,使用具有任何所需值的自定义初始化程序。这使得在情节提要中使用具有非可选初始化要求的视图控制器成为可能。在源视图控制器上创建从 segue 到 @IBSegueAction 方法的连接。在支持 Segue Actions 的新 OS 版本上,将调用该方法并且它返回的值将是传递给 prepareForSegue:sender: 的 segue 对象的 destinationViewController。可以在单个源视图控制器上定义多个@IBSegueAction 方法,这可以减少检查prepareForSegue:sender: 中的segue 标识符字符串的需要。 (47091566)

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

          在 Swift 中,添加 @IBSegueAction 属性:

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

          在Objective-C中,在返回类型前添加IBSegueAction

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

          【讨论】:

            【解决方案10】:

            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)
            

            【讨论】:

              【解决方案11】:

              免责声明:我不提倡这样做,也没有彻底测试它的弹性,但这是我在玩耍时发现的潜在解决方案。

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

              现在,您可以在应用程序的其他地方调用您的自定义初始化程序,例如 CustomViewController(someParameter: foo),并且仍然可以从情节提要接收视图配置。

              我不认为这是一个很好的解决方案,原因如下:

              • 对象初始化重复,包括任何预初始化属性
              • 传递给自定义 init 的参数必须存储为可选属性
              • 添加了样板文件,当网点/属性发生变化时必须对其进行维护

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

              【讨论】:

                【解决方案12】:

                正确的流程是,调用指定的初始化器,在这种情况下是带有 nibName 的 init,

                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
                
                }
                

                【讨论】:

                  【解决方案13】:

                  // 视图控制器在 Main.storyboard 中并设置了标识符

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

                  A类

                  let objB = customInit(carType:"Any String")
                  
                   navigationController?.pushViewController(objB,animated: true)
                  

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2012-04-03
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2012-08-09
                    • 1970-01-01
                    相关资源
                    最近更新 更多