Модальные контроллеры View - как отображать и отклонять

Последние недели я ломаю голову над тем, как решить проблему с показом и удалением нескольких контроллеров представления. Я создал пример проекта и вставил код прямо из проекта. У меня есть 3 контроллера представления с соответствующими им файлами.xib. 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 на главном контроллере.

Другая кнопка на контроллере основного вида также должна иметь возможность отображать VC2, напрямую минуя VC1, и должна возвращаться к главному контроллеру при нажатии кнопки на VC2. Там нет долго работающего кода, циклов или каких-либо таймеров. Просто голые звонки для просмотра контроллеров.

6 ответов

Решение

Эта строка:

[self dismissViewControllerAnimated:YES completion:nil];

не отправляет сообщение самому себе, он на самом деле отправляет сообщение своему представляющему VC, прося его выполнить отклонение. Когда вы представляете VC, вы создаете связь между представляющим VC и представленным. Таким образом, вы не должны уничтожать представляющий VC во время его представления (представленный VC не может отправить это сообщение об отклонении назад...). Поскольку вы не принимаете это во внимание, вы оставляете приложение в растерянном состоянии. См. Мой ответ " Отказ от контроллера представленного представления", в котором я рекомендую этот метод, более четко написано:

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

В вашем случае вам необходимо убедиться, что все mainVC, Вы должны использовать делегата для отправки правильного сообщения обратно в MainViewController из ViewController1, чтобы mainVC мог отклонить VC1, а затем представить VC2.

В VC2 VC1 добавьте протокол в ваш файл.h над @interface:

@protocol ViewController1Protocol <NSObject>

    - (void)dismissAndPresentVC2;

@end

и вниз в том же файле в разделе @interface объявите свойство для хранения указателя делегата:

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

В файле VC1.m метод кнопки dismiss должен вызывать метод делегата

- (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 метод. Обратите внимание, что он вызывается из блока завершения, чтобы гарантировать, что VC2 не будет представлен, пока VC1 не будет полностью отклонен.

Теперь вы переходите от VC1->VCMain->VC2, так что вы, вероятно, захотите анимировать только один из переходов.

Обновить

В ваших комментариях вы выражаете удивление сложностью, необходимой для достижения, казалось бы, простой вещи. Уверяю вас, этот шаблон делегирования настолько важен для большей части Objective-C и Какао, и этот пример о самом простом, что вы можете получить, что вы действительно должны приложить усилия, чтобы освоиться с ним.

В Руководстве по программированию Apple View Controller говорится следующее:

Отклонение Представленного Контроллера Представления

Когда приходит время отклонить представленный контроллер представления, предпочтительный подход состоит в том, чтобы позволить представляющему контроллеру представления отклонить его. Другими словами, когда это возможно, тот же контроллер представления, который представлял контроллер представления, также должен нести ответственность за его отклонение. Хотя существует несколько способов уведомления контроллера представления о том, что его представленный контроллер представления должен быть отклонен, предпочтительным методом является делегирование. Для получения дополнительной информации см. "Использование делегирования для связи с другими контроллерами".

Если вы действительно продумываете, чего вы хотите достичь, и как вы собираетесь это сделать, вы поймете, что обмен сообщениями вашего MainViewController для выполнения всей работы является единственным логическим выходом, если вы не хотите использовать NavigationController. Если вы используете 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 вместе. Например, если вы вставите новый viewController перед mainVC, ваше требуемое поведение будет нарушено (вы перейдете к предыдущему). В VC1 вы также должны были #import VC2. Поэтому у вас довольно много взаимозависимостей, что нарушает цели ООП / 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!

Логика ломается, когда вы пытаетесь отклонить представляющий VC, VC2 которого является представленным VC. Второе сообщение на самом деле не выполняется - возможно, что-то происходит, но у вас все еще остаются два указателя на объект, от которого, как вы думали, вы избавились. (изменить - я проверил это, и это не так уж плохо, оба объекта исчезают, когда вы возвращаетесь в mainVC)

Это довольно многословный способ сказать - пожалуйста, используйте делегатов. Если это поможет, я сделал еще одно краткое описание шаблона здесь:
Передача контроллера в construtor всегда плохая практика?

обновление 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, но это лучшее, что вы получите без явного делегирования.

Пример в Swift, изображающий объяснение литейного завода выше и документацию Apple:

  1. Основываясь на документации Apple и приведенном выше объяснении литейного цеха (исправляя некоторые ошибки), представьте версию ViewViewController с использованием шаблона проектирования делегата:

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 {

}

Я думаю, что вы неправильно поняли некоторые основные понятия о контроллерах модального представления iOS. Когда вы отклоняете VC1, все представленные контроллеры представления VC1 также отклоняются. Apple предназначена для потоковой передачи контроллеров модального представления - в вашем случае VC2 представлен VC1. Вы отказываетесь от VC1, как только вы представляете VC2 из VC1, так что это полный беспорядок. Чтобы достичь того, что вы хотите, у buttonPressedFromVC1 должен быть основной VC2, присутствующий в главном VC, сразу после того, как VC1 завершает работу. И я думаю, что это может быть достигнуто без делегатов. Нечто подобное:

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

Обратите внимание, что self.presentingViewController хранится в некоторой другой переменной, потому что после того, как vc1 завершает свою работу, вы не должны делать никаких ссылок на него.

Раду Симионеску - потрясающая работа! и ниже Ваше решение для любителей 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)
    })
}

Я решил проблему с помощью UINavigationController при представлении. В MainVC, при представлении VC1

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

В VC1, когда я хотел бы показать VC2 и одновременно закрыть VC1 (только одна анимация), я могу получить анимацию push

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

А в VC2 при закрытии контроллера представления, как обычно, мы можем использовать:

self.dismiss(animated: true, completion: nil)

Я хотел это:

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];
}
Другие вопросы по тегам