모달 뷰 컨트롤러-표시 및 해제 방법


82

여러 뷰 컨트롤러를 표시하고 해제하는 문제를 해결하는 방법에 대해 지난 일주일 동안 머리를 빻습니다. 샘플 프로젝트를 만들고 프로젝트에서 직접 코드를 붙여 넣었습니다. 해당 .xib 파일이있는 3 개의 뷰 컨트롤러가 있습니다. MainViewController, VC1 및 VC2. 메인 뷰 컨트롤러에 두 개의 버튼이 있습니다.

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

그러면 문제없이 VC1이 열립니다. VC1에는 VC2를 열면서 동시에 VC1을 닫아야하는 또 다른 버튼이 있습니다.

- (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는 해당 해제 메시지를 다시 보낼 수 없습니다…). 실제로 그것을 고려하지 않기 때문에 앱을 혼란스러운 상태로 두는 것입니다. 이 방법을 권장 하는 내 대답 Dismissing a Presented View Controller 가 더 명확하게 작성되었습니다.

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

귀하의 경우 모든 제어가 mainVC . 델리게이트를 사용하여 ViewController1에서 MainViewController로 올바른 메시지를 다시 보내야 mainVC가 VC1을 해제 한 다음 VC2를 표시 할 수 있습니다.

에서 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에서 이동 중이므로 전환 중 하나만 애니메이션으로 만들 수 있습니다.

최신 정보

당신의 의견에서 당신은 겉보기에 단순한 일을 달성하는 데 필요한 복잡성에 놀라움을 표현합니다. 이 위임 패턴은 Objective-C와 Cocoa의 대부분의 핵심이며,이 예제는 여러분이 얻을 수있는 가장 간단한 것에 대한 것이므로 여러분은 그것에 익숙해지기 위해 노력해야합니다.

애플의에서 보기 컨트롤러 프로그래밍 가이드 그들은이 이 말을 :

제시된보기 컨트롤러 닫기

제시된 뷰 컨트롤러를 닫을 때 선호되는 접근 방식은 제시하는 뷰 컨트롤러가이를 해제하도록하는 것입니다. 즉, 가능할 때마다 뷰 컨트롤러를 표시 한 동일한 뷰 컨트롤러가이를 해제 할 책임이 있습니다. 프리젠 테이션 뷰 컨트롤러에게 제시된 뷰 컨트롤러를 해제해야 함을 알리는 몇 가지 기술이 있지만, 선호하는 기법은 위임입니다. 자세한 내용은 "위임을 사용하여 다른 컨트롤러와 통신"을 참조하십시오.

당신이 달성하고자하는 것과 그것에 대해 어떻게 진행하고 있는지를 정말로 생각한다면, 모든 작업을 수행하기 위해 MainViewController를 메시징하는 것이 NavigationController를 사용하고 싶지 않다는 것을 감안할 때 유일한 논리적 방법이라는 것을 알게 될 것입니다. 당신이 경우에 NavController를 사용, 효과에 당신도없는 경우 명시 적으로 모든 작업을 수행 할 navController에, '위임'이다. 있을 필요가 일부 사용자의 VC 탐색에 무슨 일이 일어나고 있는지의 중심을 추적 객체, 당신은 필요 어떤 무슨 일이 있어도 그것으로 의사 소통의 방법을.

실제로 애플의 조언은 당신이 신뢰할 수있는, 조금 극단적가 ... 정상적인 경우에, 당신은 전용 위임 및 방법을 할 필요가 없습니다입니다 [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은 사라지지 않았으며 여전히 두 개의 포인터를 보유하고 있습니다.

  • 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 를 해제 하려고 할 때 로직이 무너집니다 . 두 번째 메시지는 실제로 실행되지 않습니다. 아마도 몇 가지 일이 발생하지만 제거했다고 생각한 객체에 대한 두 개의 포인터가 남아 있습니다. ( 편집-나는 이것을 확인했고 그렇게 나쁘지 않습니다. 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을 해제합니다.
Hema

VC2에는 버튼이 있으며 클릭하면 VC2가 메모리에서 해제되고 컨트롤은 VC1로 이동하지 않고 주 컨트롤러로 돌아 가야합니다.
Hema

@Hema, 나는 완벽하게 귀하의 요구 사항을 이해하고,이 당신을 확신 한 것입니다 그것을 할 올바른 방법. 나는 조금 더 많은 정보로 내 대답을 업데이트했습니다. 내 접근 방식을 시도했지만 문제가 발생한 경우 제대로 작동하지 않는 항목을 정확히 보여주는 새 질문을 제기하여 도움을 받으십시오. 명확성을 위해이 질문에 다시 링크 할 수도 있습니다.
파운드리

안녕하세요 그는 : 귀하의 통찰력에 감사드립니다. 나는 또한 다른 스레드 (원래 스레드)에 대해 이야기하고 있으며 거기에 언급 된 제안에서 스 니펫을 게시했습니다. 이 문제를 해결하기 위해 모든 전문가 답변을 시도하고 있습니다. URL은 여기에 있습니다 : stackoverflow.com/questions/14840318/…
Hema

1
@Honey-아마도 그럴 수도 있지만, 그 진술은 '상상 된'의사 코드 조각에 대한 수사 학적 대답이었습니다. 내가 말하고 싶었던 요점은 유지주기 트랩에 관한 것이 아니라 왜 위임이 중요한 디자인 패턴인지 질문자에게 교육하는 것입니다. 나는 그것이 오해의 소지가있는 논쟁이라고 생각합니다. 질문은 모달 VC에 관한 것이지만 대답의 가치는 대부분 질문을 사용하는 델리게이트 패턴의 설명과 촉매로서 OP의 명백한 좌절감에 있습니다. 관심과 수정에 감사드립니다 !!
파운드리

12

위의 파운드리에 대한 설명과 Apple의 문서를 묘사 한 Swift 예제 :

  1. 온 내놓고 애플의 문서 위임 디자인 패턴을 사용하여, 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가 맞습니까?
Honey

10

iOS 모달 뷰 컨트롤러에 대한 몇 가지 핵심 개념을 오해했다고 생각합니다. VC1을 닫으면 VC1에 의해 제시된 모든 뷰 컨트롤러도 닫힙니다. 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...]

1
@Honey 실제로 presentViewController를 호출 할 때 비동기가 발생합니다. 이것이 완료 핸들러가있는 이유입니다. 그러나 그것을 사용하더라도 프리젠 테이션 뷰 컨트롤러가 무언가를 제시 한 후에 닫으면 제시하는 모든 것도 닫힙니다. 영업 이익은 실제로는 현재의 기각 할 수 있도록, 실제로 다른 발표자에서의 ViewController을 제시하고자 그래서
라두 Simionescu

하지만 그것을 사용하더라도, 제시하는 뷰 컨트롤러가 무언가를 제시 한 후에 닫으면 제시하는 모든 것이 사라집니다 . 아하, 그래서 컴파일러는 기본적으로 "당신이하는 일이 어리 석다. 한 줄의 코드 (VC1로서 나는 나 자신과 내가 제시하는 모든 것을 무시할 것입니다.)하지 마십시오 "맞습니까?
Honey

컴파일러는 그것에 대해 아무 말도하지 않을 것이며, 이것을 실행할 때 충돌하지 않는 경우 일 수도 있습니다. 단지 프로그래머가 예상하지 못한 방식으로 동작 할 것입니다
Radu Simionescu

5

Radu Simionescu-멋진 작품입니다! Swift 애호가를위한 솔루션 아래 :

@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"를 캡처하지 않고 강력한 참조가 필요한 이유를 이해하지 못합니다. 즉 "var presentingVC".. 어쨌든 작동합니다. thx
emdog4 dec.

1

나는 이것을 원했다 :

MapVC는 전체 화면의지도입니다.

버튼을 누르면지도 위에 PopupVC (전체 화면이 아님)가 열립니다.

PopupVC에서 버튼을 누르면 MapVC로 돌아가서 viewDidAppear를 실행하고 싶습니다.

내가 했어:

MapVC.m : 버튼 작업에서 프로그래밍 방식으로 segue 및 대리자 설정

- (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을 닫고 싶을 때 (하나의 애니메이션 만) 푸시 애니메이션을 만들 수 있습니다.

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

그리고 VC2에서 뷰 컨트롤러를 닫을 때 평소와 같이 사용할 수 있습니다.

self.dismiss(animated: true, completion: nil)
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.