使用故事板中的界面设置在Swift中为UIViewController进行自定义初始化


89

我在为UIViewController的子类编写自定义init时遇到问题,基本上我想将依赖项通过viewController的init方法传递,而不是像 viewControllerB.property = value

所以我为viewController做了一个自定义的初始化,并调用了超级指定的初始化

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中的init属性时...

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

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

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

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

}

您有解决方案吗?
user481610'9

Answers:


49

正如上面答案之一中指定的那样,您不能同时使用和自定义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
    }
}

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


初始化MemeDetailVC后,存在属性“ meme”。然后依赖项注入将值分配给“ meme”。目前,还没有调用loadView()和viewDidLoad。在MemeDetailVC添加/推送以查看层次结构之后,将调用这两种方法。
Jim Yu

最好的答案在这里!
PascalS

仍然需要init方法,并且编译器会说meme存储的属性尚未初始化
Surendra Kumar,

27

从情节提要进行初始化时,不能使用自定义初始化程序,这init?(coder aDecoder: NSCoder)是Apple设计情节提要以初始化控制器的方式。但是,有一些方法可以将数据发送到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"只是一个占位符)。


1
嗨,但是我们如何摆脱指定为可选的问题,我们也不想误认为不分配它。我们只希望严格依赖对于控制器首先有意义就有意义。
Amber K

1
@AmberK您可能想研究对接口进行编程,而不是使用Interface Builder。
卡莱布·克利夫特

好的,即时通讯也正在寻找kickstarter ios项目以供参考,还无法确定他们如何发送视图模型。
Amber K

23

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

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

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

MemeDetailVC类中的Make类方法如下:

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

5
但是仍然存在缺点,该属性必须是可选的。
Amber K

当使用class func INIT(meme: Meme) -> MemeDetailVC的Xcode 10.2混乱并给出错误Incorrect argument label in call (have 'meme:', expected 'coder:')
塞缪尔

21

我执行此操作的一种方法是使用便捷初始化程序。

class MemeDetailVC : UIViewController {

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

然后用以下命令初始化MemeDetailVC let memeDetailVC = MemeDetailVC(theMeme)

Apple的有关初始化程序的文档非常不错,但我个人最喜欢的是Ray Wenderlich:《深度初始化》教程系列,该书应该为您提供有关各种init选项和“正确”操作方式的大量解释/示例。


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

如果您的界面是在情节提要板上设置的,并且您是完全通过编程方式创建控制器的,那么便捷的初始化程序可能是您要尝试执行的操作的最简单方法,因为您不必处理所需的init NSCoder(我仍然不太了解)。

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


1
我不明白,如果我的界面是在情节提要中设置的,并且是通过编程方式创建的,那么我必须使用情节提要调用实例化方法来加载该接口吗?如何将convenience是帮助在这里。
Amber K

swift中的类不能通过调用init该类的另一个函数来初始化(尽管结构可以)。因此,要调用self.init(),您必须将标记init(meme: Meme)convenience初始值设定项。否则,您将必须UIViewController自己在初始化程序中手动设置a的所有必需属性,而我不确定所有这些属性是什么。
Ponyboy47

然后,这不是子类化UIViewController,因为您正在调用self.init()而不是super.init()。您仍然可以使用默认的init初始化MemeDetailVC MemeDetail(),在这种情况下,代码将崩溃
Ali

它仍然被认为是子类化,因为self.init()必须在其实现中调用super.init(),或者self.init()是直接从父级继承的,在这种情况下它们在功能上是等效的。
Ponyboy47

6

最初有几个答案,尽管基本上是正确的,但还是被牛票否决了。答案是,你不能。

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

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

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


4

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

3

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

Storyboradinit?(coder aDecoder: NSCoder)直接调用initUIViewControllerUIView,没有空间传递参数。

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

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

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

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

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

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

  • 复制对象初始化,包括任何预初始化属性
  • 传递给自定义的参数init必须存储为可选属性
  • 添加样板,当更改出口/属性时必须保持样板

也许您可以接受这些折衷,但是使用后果自负


0

尽管我们现在可以使用以下命令对情节提要中的默认控制器进行自定义初始化 instantiateInitialViewController(creator:)和关系(包括关系和演出)为提要中。

Xcode 11中添加了此功能,以下摘录自Xcode 11发行说明

@IBSegueAction使用带有任何必需值的自定义初始化程序,可以使用带有new属性注释的视图控制器方法在代码中创建segue的目标视图控制器。这使得可以在情节提要中使用具有非可选初始化要求的视图控制器。@IBSegueAction在源视图控制器上创建从Segue到方法的连接。在支持Segue Actions的新OS版本上,该方法将被调用,返回的值将是destinationViewController传递给的segue对象的prepareForSegue:sender:@IBSegueAction可以在单个源视图控制器上定义多种方法,这可以减轻检查中的segue标识符字符串的需要prepareForSegue:sender:。(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"];
}

0

正确的流程是,使用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

}

-1

// View控制器位于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)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.