モーダルビューコントローラー-表示と非表示の方法


82

複数のViewControllerの表示と非表示に関する問題を解決する方法について、この1週間頭を悩ませています。サンプルプロジェクトを作成し、プロジェクトから直接コードを貼り付けました。3つのViewControllerとそれに対応する.xibファイルがあります。MainViewController、VC1およびVC2。メインビューコントローラーに2つのボタンがあります。

- (IBAction)VC1Pressed:(UIButton *)sender
{
    VC1 *vc1 = [[VC1 alloc] initWithNibName:@"VC1" bundle:nil];
    [vc1 setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal];
    [self presentViewController:vc1 animated:YES completion:nil];
}

これにより、VC1が問題なく開きます。VC1には、VC1を閉じると同時にVC2を開く必要がある別のボタンがあります。

- (IBAction)buttonPressedFromVC1:(UIButton *)sender
{
    VC2 *vc2 = [[VC2 alloc] initWithNibName:@"VC2" bundle:nil];
    [vc2 setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal];
    [self presentViewController:vc2 animated:YES completion:nil];
    [self dismissViewControllerAnimated:YES completion:nil];
} // This shows a warning: Attempt to dismiss from view controller <VC1: 0x715e460> while a presentation or dismiss is in progress!


- (IBAction)buttonPressedFromVC2:(UIButton *)sender
{
    [self dismissViewControllerAnimated:YES completion:nil];
} // This is going back to VC1. 

メインビューコントローラに戻ると同時に、VC1をメモリから完全に削除する必要があります。VC1は、メインコントローラーのVC1ボタンをクリックしたときにのみ表示されます。

メインビューコントローラの他のボタンも、VC1を直接バイパスしてVC2を表示できる必要があり、VC2でボタンがクリックされるとメインコントローラに戻る必要があります。長時間実行されるコード、ループ、またはタイマーはありません。ビューコントローラーへの必要最低限​​の呼び出し。

回答:


189

この行:

[self dismissViewControllerAnimated:YES completion:nil];

はそれ自体にメッセージを送信しているのではなく、実際にメッセージを提示しているVCに送信して、却下を行うように依頼しています。VCを提示するときは、提示しているVCと提示されているVCの間に関係を作成します。したがって、提示中のVCを破棄しないでください(提示されたVCはその却下メッセージを送り返すことはできません…)。あなたはそれを実際に考慮していないので、あなたはアプリを混乱した状態のままにしている。私の答え参照してください。 この方法をより明確に書くことをお勧めする、提示されたビューコントローラの却下

[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];

あなたの場合、すべての制御がで行われるようにする必要がありますmainVC 。mainVCがVC1を閉じてから、VC2を提示できるように、デリゲートを使用して、ViewController1からMainViewControllerに正しいメッセージを送り返す必要があります。

ではVC2 VC1 @interface上記のあなたの.hファイル内のプロトコルを追加します。

@protocol ViewController1Protocol <NSObject>

    - (void)dismissAndPresentVC2;

@end

@interfaceセクションの同じファイルで、デリゲートポインタを保持するプロパティを宣言します。

@property (nonatomic,weak) id <ViewController1Protocol> delegate;

VC1 .mファイルでは、却下ボタンメソッドはデリゲートメソッドを呼び出す必要があります

- (IBAction)buttonPressedFromVC1:(UIButton *)sender {
    [self.delegate dissmissAndPresentVC2]
}

mainVCで、VC1を作成するときにVC1のデリゲートとして設定します。

- (IBAction)present1:(id)sender {
    ViewController1* vc = [[ViewController1 alloc] initWithNibName:@"ViewController1" bundle:nil];
    vc.delegate = self;
    [self present:vc];
}

デリゲートメソッドを実装します。

- (void)dismissAndPresent2 {
    [self dismissViewControllerAnimated:NO completion:^{
        [self present2:nil];
    }];
}

present2:VC2Pressed:ボタンのIBActionメソッドと同じメソッドにすることができます。VC1が完全に閉じられるまで、VC2が提示されないようにするために、完了ブロックから呼び出されることに注意してください。

現在、VC1-> VCMain-> VC2から移動しているため、トランジションの1つだけをアニメーション化する必要があります。

更新

あなたのコメントでは、一見単純なことを達成するために必要な複雑さに驚きを表明しています。確かに、この委任パターンは、Objective-CとCocoaの多くの中心であり、この例は、入手できる最も単純なものであるため、実際に慣れるために努力する必要があります。

AppleのViewControllerプログラミングガイドでは、次のように述べています

提示されたViewControllerを閉じる

提示されたビューコントローラを却下するときが来たとき、好ましいアプローチは、提示されたビューコントローラにそれを却下させることです。言い換えると、可能な場合は常に、ViewControllerを提示したのと同じViewControllerがそれを却下する責任を負う必要があります。提示されたビューコントローラを却下する必要があることを提示するビューコントローラに通知するためのいくつかの手法がありますが、推奨される手法は委任です。詳細については、「委任を使用した他のコントローラーとの通信」を参照してください。

何を達成したいのか、そしてそれをどのように進めているのかを本当に考えれば、NavigationControllerを使用したくない場合は、MainViewControllerにメッセージを送信してすべての作業を実行することが唯一の論理的な方法であることがわかります。あなたがいる場合やるからNavControllerを使用し、実際にあなたも明示されていない場合、すべての作業を行うためにからNavControllerに、「委任」です。そこにする必要があるいくつかのあなたのVCのナビゲーションで何が起こっているの中心を追跡し、オブジェクト、あなたが必要とするいくつかの何をするにしても、それと通信する方法を。

実際には、Appleのアドバイスは少し極端です...通常の場合、専用のデリゲートとメソッドを作成する必要はありません。信頼でき[self presentingViewController] dismissViewControllerAnimated:ます。あなたのような場合に、却下してリモートに他の影響を与えたい場合です。あなたが世話をする必要があるオブジェクト。

これは、すべての代理人の手間をかけずに作業することを想像できるものです...

- (IBAction)dismiss:(id)sender {
    [[self presentingViewController] dismissViewControllerAnimated:YES 
                                                        completion:^{
        [self.presentingViewController performSelector:@selector(presentVC2:) 
                                            withObject:nil];
    }];

}

提示コントローラーに却下するように要求した後、presentingViewControllerのメソッドを呼び出してVC2を呼び出す完了ブロックがあります。デリゲートは必要ありません。(ブロックの大きなセールスポイントは、これらの状況でデリゲートの必要性が減ることです)。ただし、この場合、邪魔になることがいくつかあります...

  • VC1では、mainVCがメソッドを実装していることを知らないためpresent2デバッグが困難なエラーやクラッシュが発生する可能性があります。代表者はこれを回避するのに役立ちます。
  • VC1が却下されると、完了ブロックを実行することは実際にはありません...それともそうですか?self.presentingViewControllerはもう何か意味がありますか?あなたは知りません(私も知りません)...代理人と一緒に、あなたはこの不確実性を持っていません。
  • このメソッドを実行しようとすると、警告やエラーなしでハングします。

だからお願いします...代表団を学ぶために時間をかけてください!

update2

あなたのコメントでは、VC2の却下ボタンハンドラーでこれを使用することにより、それを機能させることができました:

 [self.view.window.rootViewController dismissViewControllerAnimated:YES completion:nil]; 

これは確かにはるかに簡単ですが、多くの問題が残ります。

密結合
viewController構造を一緒にハードワイヤリングしています。たとえば、mainVCの前に新しいviewControllerを挿入すると、必要な動作が中断されます(前の動作に移動します)。VC1では、VC2を#importする必要もあります。したがって、相互依存関係が非常に多く、OOP / MVCの目標が達成されません。

デリゲートを使用すると、VC1もVC2も、mainVCまたはその前身について何も知る必要がないため、すべてを疎結合でモジュール化した状態に保ちます。

メモリ
VC1は消えていません、あなたはまだそれへの2つのポインタを持っています:

  • mainVCのpresentedViewControllerプロパティ
  • VC2のpresentingViewControllerプロパティ

これは、ロギングによってテストできます。また、VC2からこれを実行するだけでもテストできます。

[self dismissViewControllerAnimated:YES completion:nil]; 

それはまだ機能し、VC1に戻ります。

それはメモリリークのように私には思えます。

これの手がかりはあなたがここに来ている警告にあります:

[self presentViewController:vc2 animated:YES completion:nil];
[self dismissViewControllerAnimated:YES completion:nil];
 // Attempt to dismiss from view controller <VC1: 0x715e460>
 // while a presentation or dismiss is in progress!

VC2が提示されたVCである提示VCを却下しようとすると、ロジックが機能しなくなります。2番目のメッセージは実際には実行されません-おそらくいくつかのことが起こりますが、それでも、削除したと思ったオブジェクトへの2つのポインターが残っています。(編集-私はこれをチェックしましたが、それほど悪くはありません。mainVCに戻ると、両方のオブジェクトが消えます

これはかなり長い言い方です。デリゲートを使用してください。それが役立つ場合は、ここでパターンについて別の簡単な説明をし
ました。コンストラクターでコントローラーを渡すことは常に悪い習慣ですか?

更新3
本当にデリゲートを避けたい場合は、これが最善の方法です。

VC1の場合:

[self presentViewController:VC2
                   animated:YES
                 completion:nil];

しかし、何も却下しないでください...私たちが確認したように、それはとにかく実際には起こりません。

VC2の場合:

[self.presentingViewController.presentingViewController 
    dismissViewControllerAnimated:YES
                       completion:nil];

VC1を却下していないので、VC1介しMainVCに戻ることができます。MainVCはVC1を却下します。VC1がなくなったため、VC2も一緒に表示され、クリーンな状態でMainVCに戻ります。

VC1はVC2について知る必要があり、VC2はMainVC-> VC1経由で到着したことを知る必要があるため、依然として高度に結合されていますが、明示的な委任を少し行わずに取得するのが最善です。


1
複雑なようです。フォローしてドットにコピーしようとしましたが、途中で迷子になりました。これを達成する他の方法はありますか?また、アプリデリゲートで、メインコントローラーがルートビューコントローラーとして設定されていることを追加したいと思います。ナビゲーションコントローラーを使いたくないのですが、なぜこれを実現するのがそれほど複雑なのか疑問に思います。要約すると、アプリが起動すると、2つのボタンが付いたメインビューコントローラーが表示されます。最初のボタンをクリックすると、VC1がロードされます。VC1にはボタンがあり、クリックするとエラーや警告なしでVC2がロードされ、同時にVC1がメモリから削除されます。
ヘマ2013

VC2にはボタンがあり、それをクリックするとVC2がメモリから削除され、コントロールはVC1ではなくメインコントローラーに戻るはずです。
ヘマ2013

@ヘマ、私はあなたの要件を完全に理解しました、そしてこれがそれをする正しい方法であることをあなたに保証します。私はもう少し情報で私の答えを更新しました、それが役立つことを願っています。私のアプローチを試しても行き詰まった場合は、何が機能していないかを正確に示す新しい質問を提起してください。わかりやすくするために、この質問にリンクして戻ることもできます。
ファウンドリ

こんにちは彼は:あなたの洞察力をありがとう。私は別のスレッド(元のスレッド)についても話していて、そこに記載されている提案の抜粋を投稿しました。私はこの問題を突き止めるためにすべての専門家の答えを試しています。URLはこちら:stackoverflow.com/questions/14840318/...
ヘマ

1
@ Honey-おそらくそうかもしれませんが、このステートメントは、「想像上の」擬似コードの一部に対する修辞的な答えでした。私が言いたかったのは、サイクルトラップを保持することではなく、委任が価値のあるデザインパターンである理由(偶然にもその問題を回避する)について質問者を教育することです。これは誤解を招く論争だと思います。質問はモーダルVCに関するものですが、回答の価値は主に、質問を使用したデリゲートパターンの説明と、OPの明らかなフラストレーションを触媒として使用することにあります。あなたの興味(そしてあなたの編集)に感謝します!!
ファウンドリ

12

上記のファウンドリの説明とAppleのドキュメントを描いたSwiftの例:

  1. 基づかAppleのドキュメントと(いくつかのエラーを修正)上記のファウンドリの説明で、デリゲートのデザインパターンを使用してpresentViewControllerバージョン:

ViewController.swift

import UIKit

protocol ViewControllerProtocol {
    func dismissViewController1AndPresentViewController2()
}

class ViewController: UIViewController, ViewControllerProtocol {

    @IBAction func goToViewController1BtnPressed(sender: UIButton) {
        let vc1: ViewController1 = self.storyboard?.instantiateViewControllerWithIdentifier("VC1") as ViewController1
        vc1.delegate = self
        vc1.modalTransitionStyle = UIModalTransitionStyle.FlipHorizontal
        self.presentViewController(vc1, animated: true, completion: nil)
    }

    func dismissViewController1AndPresentViewController2() {
        self.dismissViewControllerAnimated(false, completion: { () -> Void in
            let vc2: ViewController2 = self.storyboard?.instantiateViewControllerWithIdentifier("VC2") as ViewController2
            self.presentViewController(vc2, animated: true, completion: nil)
        })
    }

}

ViewController1.swift

import UIKit

class ViewController1: UIViewController {

    var delegate: protocol<ViewControllerProtocol>!

    @IBAction func goToViewController2(sender: UIButton) {
        self.delegate.dismissViewController1AndPresentViewController2()
    }

}

ViewController2.swift

import UIKit

class ViewController2: UIViewController {

}
  1. 上記のファウンドリの説明(いくつかのエラーの修正)に基づいて、デリゲートデザインパターンを使用したpushViewControllerバージョン:

ViewController.swift

import UIKit

protocol ViewControllerProtocol {
    func popViewController1AndPushViewController2()
}

class ViewController: UIViewController, ViewControllerProtocol {

    @IBAction func goToViewController1BtnPressed(sender: UIButton) {
        let vc1: ViewController1 = self.storyboard?.instantiateViewControllerWithIdentifier("VC1") as ViewController1
        vc1.delegate = self
        self.navigationController?.pushViewController(vc1, animated: true)
    }

    func popViewController1AndPushViewController2() {
        self.navigationController?.popViewControllerAnimated(false)
        let vc2: ViewController2 = self.storyboard?.instantiateViewControllerWithIdentifier("VC2") as ViewController2
        self.navigationController?.pushViewController(vc2, animated: true)
    }

}

ViewController1.swift

import UIKit

class ViewController1: UIViewController {

    var delegate: protocol<ViewControllerProtocol>!

    @IBAction func goToViewController2(sender: UIButton) {
        self.delegate.popViewController1AndPushViewController2()
    }

}

ViewController2.swift

import UIKit

class ViewController2: UIViewController {

}

あなたの例のViewControllerクラスではmainVCですよね?
ハニー

10

iOSモーダルビューコントローラーに関するいくつかのコアコンセプトを誤解していると思います。VC1を閉じると、VC1によって提示されたViewControllerもすべて閉じられます。Appleは、モーダルビューコントローラがスタック方式で流れることを目的としていました。あなたの場合、VC2はVC1によって提示されます。VC1からVC2を提示するとすぐに、VC1を却下するため、完全に混乱します。あなたが望むことを達成するために、buttonPressedFromVC1は、VC1がそれ自体を却下した直後にmainVCがVC2を提示する必要があります。そして、これは代表者なしで達成できると思います。線に沿った何か:

UIViewController presentingVC = [self presentingViewController];
[self dismissViewControllerAnimated:YES completion:
 ^{
    [presentingVC presentViewController:vc2 animated:YES completion:nil];
 }];

self.presentingViewControllerは他の変数に格納されていることに注意してください。これは、vc1がそれ自体を閉じた後は、それを参照しないようにするためです。


1
とても簡単!他の人が一番上の投稿で止まるのではなく、あなたの答えまでスクロールダウンしてくれることを願っています。
Ryan Loggerythm 2015

OPのコードでは、終了後になぜ[self dismiss...]発生しないのですか?非同期で何かが起こっているわけではありません [self present...]
Honey

1
@Honey実際、presentViewControllerを呼び出すと、非同期で何かが発生します。そのため、完了ハンドラーがあります。しかし、それを使用しても、何かを提示した後に提示しているView Controllerを閉じると、それが提示するすべてのものも同様に却下されます。したがって、OPは実際に別のプレゼンターからビューコントローラーを提示して、現在のビューコントローラーを却下できるようにします
Radu Simionescu

しかし、それを使用しても、何かを提示した後に提示しているView Controllerを閉じると、それが提示するものもすべて却下されます...ああ、コンパイラは基本的に「あなたがしていることは愚かです。コードの行(VC1として、私は自分自身と私が提示しているものは何でも却下します)。
ハニー

コンパイラーはそれについて何も「言いません」。これを実行してもクラッシュしない場合もあります。プログラマーが
予期

5

Radu Simionescu-素晴らしい仕事です!スイフト愛好家のためのあなたのソリューションの下:

@IBAction func showSecondControlerAndCloseCurrentOne(sender: UIButton) {
    let secondViewController = storyboard?.instantiateViewControllerWithIdentifier("ConrollerStoryboardID") as UIViewControllerClass // change it as You need it
    var presentingVC = self.presentingViewController
    self.dismissViewControllerAnimated(false, completion: { () -> Void   in
        presentingVC!.presentViewController(secondViewController, animated: true, completion: nil)
    })
}

これはある意味で、実際に機能することに不満を感じます。ブロックが「self.presentingViewController」をキャプチャせず、強力な参照、つまり「varpresentingVC」が必要な理由がわかりません。とにかく、これは機能します。thx
emdog4 2015

1

私はこれが欲しかった:

MapVCは、全画面表示のマップです。

ボタンを押すと、地図の上にPopupVC(全画面表示ではありません)が開きます。

PopupVCのボタンを押すと、MapVCに戻り、viewDidAppearを実行したいと思います。

これは私がしました:

MapVC.m:ボタンアクションで、プログラムでセグエし、デリゲートを設定します

- (void) buttonMapAction{
   PopupVC *popvc = [self.storyboard instantiateViewControllerWithIdentifier:@"popup"];
   popvc.delegate = self;
   [self presentViewController:popvc animated:YES completion:nil];
}

- (void)dismissAndPresentMap {
  [self dismissViewControllerAnimated:NO completion:^{
    NSLog(@"dismissAndPresentMap");
    //When returns of the other view I call viewDidAppear but you can call to other functions
    [self viewDidAppear:YES];
  }];
}

PopupVC.h:@interfaceの前に、プロトコルを追加します

@protocol PopupVCProtocol <NSObject>
- (void)dismissAndPresentMap;
@end

@interfaceの後、新しいプロパティ

@property (nonatomic,weak) id <PopupVCProtocol> delegate;

PopupVC.m:

- (void) buttonPopupAction{
  //jump to dismissAndPresentMap on Map view
  [self.delegate dismissAndPresentMap];
}

1

プレゼンテーション時にUINavigationControllerを使用して問題を解決しました。MainVCで、VC1を提示する場合

let vc1 = VC1()
let navigationVC = UINavigationController(rootViewController: vc1)
self.present(navigationVC, animated: true, completion: nil)

VC1では、VC2を表示すると同時にVC1を閉じる(1つのアニメーションのみ)場合、次の方法でプッシュアニメーションを作成できます。

let vc2 = VC2()
self.navigationController?.setViewControllers([vc2], animated: true)

また、VC2では、View Controllerを閉じると、通常どおり次のコマンドを使用できます。

self.dismiss(animated: true, completion: nil)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.