ストーリーボードでインターフェースを設定したSwiftのUIViewControllerのカスタム初期化


89

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

問題は、カスタムinitを呼び出そうとするとMyViewController(meme: meme)、viewControllerのプロパティがまったく初期化されないことです...

私はデバッグしようとしていましたが、viewControllerでinit(coder aDecoder: NSCoder)最初に呼び出され、次にカスタムinitが後で呼び出されました。ただし、これら2つのinitメソッドは異なるselfメモリアドレスを返します。

私は自分のViewControllerのためのinitで何かを間違っを疑っていて、それが常に返されますselfinit?(coder aDecoder: NSCoder)、何の実装を持ちません、。

viewControllerのカスタムinitを正しく作成する方法を知っている人はいますか?注:私の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 2016

回答:


50

上記の回答の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は呼び出されていません。これらの2つのメソッドは、MemeDetailVCを追加/プッシュして階層を表示した後に呼び出されます。
ジム・ユー

ここでのベストアンサー!
PascalS 2018

それでもinitメソッドが必要であり、コンパイラはmemeに保存されたプロパティが初期化されていないと言います
SurendraKumar19年

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

テスト目的StringMemeはなく、タイプのプロパティを使用しました。また、正しいセグエ識別子("identifier"単なるプレースホルダー)を渡すようにしてください。


1
こんにちは。ただし、オプションとして指定することをどのように取り除くことができますか。また、割り当てないことを間違えたくありません。コントローラーにとって意味のある厳密な依存関係が最初に存在することだけが必要です。
アンバー

1
@AmberK Interface Builderを使用する代わりに、インターフェースのプログラミングを検討することをお勧めします。
CalebKleveter19年

さて、私も参照用にキックスターターiosプロジェクトを探していますが、ビューモデルをどのように送信するかはまだわかりません。
アンバー

23

@Caleb Kleveterが指摘しているように、ストーリーボードからの初期化中にカスタム初期化子を使用することはできません。

ただし、ストーリーボードからビューコントローラオブジェクトをインスタンス化し、ビューコントローラオブジェクトを返すファクトリ/クラスメソッドを使用することで、問題を解決できます。これはかなりクールな方法だと思います。

注:これは質問に対する正確な回答ではなく、問題を解決するための回避策です。

次のように、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())

5
しかし、それでも欠点はあります。プロパティはオプションである必要があります。
アンバー

使用しているときclass func にinit (meme: Meme) -> MemeDetailVCXcodeの10.2を混同し、エラーを与えているIncorrect argument label in call (have 'meme:', expected 'coder:')
サミュエル

21

私がこれを行った1つの方法は、便利な初期化子を使用することです。

class MemeDetailVC : UIViewController {

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

次に、MemeDetailVCをで初期化します。 let memeDetailVC = MemeDetailVC(theMeme)

イニシャライザに関するAppleのドキュメントはかなり良いですが、私の個人的なお気に入りはRay Wenderlich:Initialization in Depthチュートリアルシリーズです。これは、さまざまなinitオプションと物事を行うための「適切な」方法に関する多くの説明/例を提供するはずです。


編集:カスタムビューコントローラーで便利なイニシャライザーを使用できますが、ストーリーボードまたはストーリーボードセグエから初期化する場合はカスタムイニシャライザーを使用できないと誰もが正しく述べています。

インターフェイスがストーリーボードに設定されていて、コントローラーを完全にプログラムで作成している場合は、必要なinitを処理する必要がないため、コンビニエンスイニシャライザーがおそらく最も簡単な方法です。 NSCoder(私はまだ本当に理解していません)。

ただし、ストーリーボードを介してView Controllerを取得している場合は、@ Caleb Kleveterの回答に従い、View Controllerを目的のサブクラスにキャストしてから、プロパティを手動で設定する必要があります。


1
インターフェイスがストーリーボードに設定されていて、プログラムで作成している場合は、ストーリーボードを使用してインスタンス化メソッドを呼び出してインターフェイスを正しくロードする必要がありますか?ここでどのようにconvenience役立ちますか。
アンバー

swiftinitのクラスは、クラスの別の関数を呼び出して初期化することはできません(ただし、構造体は初期化できます)。したがって、を呼び出すにはself.init()、を初期化子init(meme: Meme)としてマークする必要がありconvenienceます。そうしUIViewControllerないと、イニシャライザでaの必要なすべてのプロパティを自分で手動で設定する必要があり、それらすべてのプロパティが何であるかわかりません。
Ponyboy 4719年

super.init()ではなくself.init()を呼び出しているため、これはUIViewControllerをサブクラス化するものではありません。デフォルトのinitlikeを使用してMemeDetailVCを初期化することはできますがMemeDetail()、その場合、コードはクラッシュします
Ali

self.init()はその実装でsuper.init()を呼び出す必要があるか、おそらくself.init()が親から直接継承されるため、サブクラス化と見なされます。この場合、機能的に同等です。
Ponyboy 4719

6

もともといくつかの回答がありましたが、基本的には正しいのに、牛が投票して削除しました。答えは、できません。

ストーリーボード定義から作業する場合、ViewControllerインスタンスはすべてアーカイブされます。したがって、それらを初期化するには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、2つの指定された初期化子init?(coder aDecoder: NSCoder)init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)

Storyboradはinit?(coder aDecoder: NSCoder)initUIViewControllerを直接呼び出しUIView、パラメータを渡す余地はありません。

面倒な回避策の1つは、一時キャッシュを使用することです。

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
}


1

正しいフローは、指定された初期化子を呼び出すことです。この場合は、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

}

0

免責事項:私はこれを支持せず、その回復力を徹底的にテストしていませんが、それは私が遊んでいる間に発見した潜在的な解決策です。

技術的には、ビューコントローラを2回初期化することで、ストーリーボードで構成されたインターフェイスを維持しながらカスタム初期化を実現できます。1回init目はカスタム経由で、2回目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:)リレーションシップやショーを含むセグエを使用してカスタムinitを実行できるようになりました。

この機能はXcode11で追加され、以下はXcode11リリースノートからの抜粋です。

新しい@IBSegueAction属性で注釈が付けられたViewControllerメソッドを使用して、必要な値を持つカスタム初期化子を使用して、コードでセグエの宛先ViewControllerを作成できます。これにより、ストーリーボードでオプション以外の初期化要件を持つビューコントローラを使用できるようになります。セグエから@IBSegueActionそのソースViewControllerのメソッドへの接続を作成します。セグエアクションをサポートする新しいOSバージョンでは、そのメソッドが呼び出され、返される値はにdestinationViewController渡されたセグエオブジェクトの値になりますprepareForSegue:sender:@IBSegueAction単一のソースビューコントローラで複数のメソッドを定義できます。これにより、のセグエ識別子文字列をチェックする必要性を軽減できますprepareForSegue:sender:。(47091566)

IBSegueActionコーダ、送信者、およびセグエの識別子:メソッドは、最大3つのパラメータを取ります。最初のパラメーターは必須であり、必要に応じて他のパラメーターをメソッドのシグネチャから省略できます。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-CIBSegueActionで、戻り値の型の前に次を追加します。

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

-1

//ビューコントローラは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)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.