簡體   English   中英

Swift 中 UIViewController 的自定義初始化,在 storyboard 中設置接口

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

我在為 UIViewController 的子類編寫自定義 init 時遇到問題,基本上我想通過 viewController 的 init 方法傳遞依賴關系,而不是像viewControllerB.property = value這樣直接設置屬性

所以我為我的 viewController 做了一個自定義的 init 並調用了 super 指定的 init

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

視圖 controller 接口位於 storyboard 中,我還將自定義 class 的接口作為我的視圖 Z5903CDAZ03F2F9C0304。 並且 Swift 需要調用這個 init 方法,即使你沒有在這個方法中做任何事情。 否則編譯器會抱怨...

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

問題是當我嘗試使用MyViewController(meme: meme)調用我的自定義 init 時,它根本不會在我的 viewController 中初始化屬性......

我試圖調試,我在我的 viewController 中發現,首先調用init(coder aDecoder: NSCoder) ,然后再調用我的自定義 init。 然而,這兩個 init 方法返回不同的self memory 地址。

我懷疑我的 viewController 的 init 有問題,它總是會用init?(coder aDecoder: NSCoder)返回self ,它沒有實現。

有誰知道如何正確地為您的 viewController 進行自定義初始化? 注意:我的viewController的接口設置在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)
    }

}

正如在上述答案之一中指定的那樣,您不能同時使用自定義 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 作為視圖控制器標識符。 您可能還需要更改上面代碼中故事板的名稱。

當您從 Storyboard 初始化時,您不能使用自定義初始化程序,使用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"只是一個占位符)。

正如@Caleb Kleveter 指出的那樣,我們不能在從 Storyboard 初始化時使用自定義初始值設定項。

但是,我們可以通過使用從 Storyboard 實例化視圖控制器對象並返回視圖控制器對象的工廠/類方法來解決這個問題。 我認為這是一個非常酷的方式。

注意:這不是問題的確切答案,而是解決問題的解決方法。

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
}

用法:

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

我這樣做的一種方法是使用便利初始化程序。

class MemeDetailVC : UIViewController {

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

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

Apple 關於初始化程序的文檔非常好,但我個人最喜歡的是Ray Wenderlich:深度初始化教程系列,它應該為您提供有關各種初始化選項和“正確”做事方式的大量解釋/示例。


編輯:雖然您可以在自定義視圖控制器上使用便利初始值設定項,但每個人都正確地指出,從故事板或故事板 segue 初始化時不能使用自定義初始值設定項。

如果您的界面是在情節提要中設置的,並且您完全以編程方式創建控制器,那么便利初始化程序可能是您嘗試做的事情的最簡單方法,因為您不必處理所需的 init NSCoder(我仍然不太明白)。

如果您通過情節提要獲取視圖控制器,那么您需要遵循@Caleb Kleveter 的回答並將視圖控制器轉換為所需的子類,然后手動設置屬性。

原來有幾個答案,雖然基本正確,但還是被牛投票刪掉了。 答案是,你不能。

從故事板定義工作時,您的視圖控制器實例都已存檔。 因此,要初始化它們,需要使用init?(coder...coder是所有設置/視圖信息的來源。

因此,在這種情況下,不可能使用自定義參數調用其他一些 init 函數。 它應該在准備轉場時設置為一個屬性,或者您可以放棄轉場並直接從故事板加載實例並配置它們(基本上是使用故事板的工廠模式)。

在所有情況下,您都使用 SDK 所需的 init 函數並隨后傳遞其他參數。

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
}

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

從 iOS 13 開始,您可以使用UIStoryboard實例上的instantiateViewController(identifier:creator:)方法初始化駐留在故事板中的視圖控制器。

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

盡管我們現在可以使用instantiateInitialViewController(creator:)為故事板中的默認控制器以及包括關系和顯示在內的segue 進行自定義初始化。

此功能已在 Xcode 11 中添加,以下是Xcode 11 發行說明的摘錄:

使用新的@IBSegueAction屬性注釋的視圖控制器方法可用於在代碼中創建 segue 的目標視圖控制器,使用具有任何必需值的自定義初始值設定項。 這使得在故事板中使用具有非可選初始化要求的視圖控制器成為可能。 在其源視圖控制器上創建從 segue 到@IBSegueAction方法的連接。 在支持 Segue Actions 的新操作系統版本上,該方法將被調用,它返回的值將是傳遞給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"];
}

Swift 5-UIViewController子類中的存儲屬性初始化

class SomeClass: UIViewController {

    var storedProperty: String

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

免責聲明:我不提倡這樣做,也沒有徹底測試它的彈性,但這是我在玩耍時發現的一個潛在解決方案。

從技術上講,可以通過兩次初始化視圖控制器來實現自定義初始化,同時保留情節提要配置的界面:第一次通過自定義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)
  }
}

現在在你的應用程序的其他地方你可以調用你的自定義初始值設定項,比如CustomViewController(someParameter: foo)並且仍然從故事板接收視圖配置。

由於以下幾個原因,我認為這不是一個很好的解決方案:

  • 對象初始化重復,包括任何預初始化屬性
  • 傳遞給自定義init參數必須存儲為可選屬性
  • 添加樣板,必須在更改出口/屬性時維護該樣板

也許您可以接受這些權衡,但使用風險自負

正確的流程是,調用指定的初始化程序,在這種情況下是帶有 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

}

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)

此解決方案展示了一種使用自定義初始化程序但仍能夠使用 Storyboard 而不使用self.init(nib: nil, bundle: nil) function 的方法。

為了使它能夠使用它,讓我們首先調整我們的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)
    }
}

然后,您以這種方式實例化並顯示 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)

// 視圖控制器在 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)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM