使用故事板设置界面的Swift UIViewController的自定义初始化

100

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

因此,我为我的viewController创建了一个自定义的init方法,并调用了超级指定的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(coder aDecoder: NSCoder) 首先被调用,然后才调用我的自定义初始化方法。但是这两个初始化方法返回不同的 self 内存地址。

我怀疑我的视图控制器的初始化方法有问题,并且它将始终使用 init?(coder aDecoder: NSCoder) 返回 self,而该方法没有实现。

有人知道如何正确地为您的视图控制器创建自定义初始化方法吗? 注意:我的视图控制器的界面是在 storyboard 中设置的。

这是我的视图控制器代码:

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
14个回答

57

正如上面的回答所指出的那样,你不能同时使用自定义初始化方法和故事板(storyboard)。

但是你仍然可以使用静态方法从故事板中实例化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
    }
}
不要忘记在你的storyboard中将IdentifierOfYouViewController指定为视图控制器标识符。您还可能需要更改上面代码中的storyboard名称。

在MemeDetailVC被初始化之后,属性“meme”就存在了。然后依赖注入会来分配值给“meme”。此时,loadView()和viewDidLoad还没有被调用。这两个方法将在MemeDetailVC添加/推送到视图层次结构后被调用。 - Jim Yu
最佳答案在这里! - PascalS
1
仍然需要init方法,否则编译器会报错,提示meme存储属性未被初始化。 - Surendra Kumar

32

当你从Storyboard中初始化一个控制器时,无法使用自定义初始化器,使用init?(coder aDecoder: NSCoder)是苹果设计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"
        }
    }
}

为了测试目的,我只是使用了一个字符串类型的属性,而不是 Meme。另外,请确保传递正确的Segue标识符("identifier"只是一个占位符)。


1
嗨,但是我们如何摆脱将其指定为可选项的问题,同时又不想错误地未分配它。我们只想让控制器存在的严格依赖关系有意义。 - Amber K
1
@AmberK 你可能想考虑编程界面,而不是使用Interface Builder。 - Caleb Kleveter
好的,我也在寻找Kickstarter iOS项目作为参考,还不能确定他们是如何发送他们的视图模型的。 - Amber K

26

正如@Caleb Kleveter指出的那样,在从Storyboard初始化时,我们无法使用自定义初始化程序。

但是,我们可以通过使用工厂/类方法来解决这个问题,该方法从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())

6
不过仍有一个缺点,该属性必须是可选的。 - Amber K
1
当使用class func init(meme: Meme) -> MemeDetailVC时,Xcode 10.2会混淆并给出错误提示Incorrect argument label in call (have 'meme:', expected 'coder:') - Samuël
@Amber 这不是一直这样吗? - Eric
超级好的、干净的解决方案!谢谢。 - Mamad Farrahi
1
由于viewDidLoadvc?.meme = meme之前被调用,因此您将无法在viewDidLoad中使用meme变量。 - Petru Lutenco

21

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

class MemeDetailVC : UIViewController {

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

然后,您可以使用let memeDetailVC = MemeDetailVC(theMeme)初始化您的MemeDetailVC。

苹果文档关于初始化器非常好,但我个人最喜欢的是Ray Wenderlich:深入了解初始化教程系列,它应该为您提供关于各种init选项和“正确”做事情的解释/示例。


编辑:虽然你可以在自定义视图控制器上使用便利初始化程序,但每个人都正确地指出,当从故事板或通过故事板segue进行初始化时,您不能使用自定义初始化程序。

如果您的界面是在Storyboard中设置的,并且您完全以编程方式创建控制器,则方便的初始化程序可能是实现您想要的功能最简单的方法,因为您不必处理具有NSCoder的所需init(我仍然不太理解)。

如果您通过故事板获取视图控制器,则需要遵循@Caleb Kleveter's answer并将视图控制器转换为所需的子类,然后手动设置属性。


1
我不明白,如果我的界面是在Storyboard中设置的,然后我又通过编程方式创建它,那么我必须使用Storyboard调用实例化方法来加载界面,对吗?那么“convenience”在这里如何帮助呢? - Amber K
在 Swift 中,类不能通过调用类的另一个 init 函数来进行初始化(但结构体可以)。因此,为了调用 self.init(),您必须将 init(meme: Meme) 标记为 convenience 初始化器。否则,您将不得不手动设置 UIViewController 的所有必需属性,并且我不确定所有这些属性是什么。 - Ponyboy47
这并不是UIViewController的子类,因为你调用的是self.init()而不是super.init()。你仍然可以使用默认的init来初始化MemeDetailVC,例如MemeDetail(),但在这种情况下,代码将会崩溃。 - Ali
它仍被视为子类化,因为self.init()在其实现中必须调用super.init(),或者self.init()直接从父类继承,在这种情况下它们在功能上是等效的。 - Ponyboy47

7
最初有几个回答,它们基本上是正确的,但被投票删除了。答案是,你不能这样做。
当从故事板定义中工作时,您的视图控制器实例都是存档的。因此,要初始化它们,必须使用init?(coder...coder是所有设置/视图信息的来源。
因此,在这种情况下,不可能同时调用某些带有自定义参数的其他init函数。它应该在准备segue时设置为一个属性,或者您可以放弃segues并直接从storyboard加载实例并配置它们(基本上是使用storyboard的工厂模式)。
在所有情况下,您都使用SDK所需的init函数,并在之后传递其他参数。

6

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

4

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

Storyboard直接调用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
}

3

1

这个解决方案展示了一种自定义初始化器的方法,但仍然可以在不使用self.init(nib: nil, bundle: nil)函数的情况下使用Storyboard。

为了实现这一点,让我们首先调整我们的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)
    }
}

然后,您可以通过以下方式实例化并显示视图控制器:

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)

1
XCode 11/iOS13 中,你可以使用 instantiateViewController(identifier:creator:) 来创建视图控制器,而不需要使用 segue。
    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)

你的回答与问题无关。 :D - Mamad Farrahi
我认为是的,问题涉及自定义初始化,根据苹果文档,instantiateViewController方法“从故事板创建指定的视图控制器,并使用您的自定义初始化代码对其进行初始化”。 - MrAn3

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接