Модальные контроллеры 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 на основном контроллере.

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

Ответ 1

Эта строка:

[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 метод кнопки-приглашения должен вызвать метод делегата

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

Теперь вы переходите из VC1- > VCMain- > VC2, поэтому вам, возможно, понадобится только один из переходов для анимации.

обновление

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

В Руководстве по программированию контроллера Apple View есть чтобы сказать:

Отклонение контроллера представления данных

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

Если вы действительно продумаете то, что хотите достичь, и как вы собираетесь это делать, вы поймете, что обмен сообщениями с вашим MainViewController для выполнения всей работы - это единственный логический выход из-за того, что вы не хотите использовать навигационный контроллер. Если вы используете NavController, вы фактически делегируете, даже если не явно, на navController для выполнения всей работы. Там должен быть какой-то объект, который держит центральную часть того, что происходит с вашей навигацией по VC, и вам нужен какой-то способ общения с ним, что бы вы ни делали.

На практике советы Apple немного экстремальны... в обычных случаях вам не нужно делать выделенный делегат и метод, вы можете положиться на [self presentingViewController] dismissViewControllerAnimated: - это, когда в таких случаях, как вы, хотите, чтобы вы отклонили иметь другие эффекты на удаленных объектах, которые вам нужно позаботиться.

Вот что вы можете себе представить, чтобы работать без всякой проблемы с делегатом...

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

}

После запроса диспетчера представления отклонить нас, у нас есть блок завершения, который вызывает метод в представленииViewController для вызова VC2. Никакой делегат не нужен. (Большая точка продажи блоков заключается в том, что они уменьшают потребность в делегатах в этих обстоятельствах). Однако в этом случае есть несколько вещей, мешающих...

  • в VC1 вы не знаете, что mainVC реализует метод present2 - вы можете в конечном итоге с трудными для отладки ошибок или сбоев. Делегаты помогут вам избежать этого.
  • после того, как VC1 будет уволен, на самом деле это не так, чтобы выполнить блок завершения... или это так? Может ли self.presentingViewController что-нибудь значить? Вы не знаете (я тоже)... с делегатом, у вас нет этой неопределенности.
  • Когда я пытаюсь запустить этот метод, он просто зависает без предупреждения или ошибок.

Так что, пожалуйста... найдите время, чтобы изучить делегацию!

Update2

В вашем комментарии вам удалось заставить его работать, используя это в обработчике кнопки отклонения VC2:

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

Это, конечно, намного проще, но это оставляет вам ряд проблем.

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

Логика ломается, поскольку вы пытаетесь отклонить представленный VC, VC2 которого является представленным VC. Второе сообщение действительно не выполняется. Возможно, что-то происходит, но вы все еще остаетесь с двумя указателями на объект, о котором вы думали, что избавились. (edit - я проверил это, и это не так уж плохо, оба объекта исчезают, когда вы возвращаетесь к 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, но это лучшее, что вы получите без частичного делегирования.

Ответ 2

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

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

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

Ответ 3

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

  • Основываясь на документации 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 {

}

Ответ 4

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

Ответ 5

Я хотел этого:

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];
}

Ответ 6

Я решил проблему, используя 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)