контроллеры модального представления - как отображать и закрывать

82

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

Хема
источник

Ответы:

189

Эта строка:

[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;

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

- (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 Guide они это , чтобы сказать :

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

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

Если вы действительно продумаете, чего хотите достичь и как вы это делаете, вы поймете, что обмен сообщениями с вашим 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 никуда не делась, у вас все еще есть два указателя на нее:

  • presentedViewControllerсобственность mainVC
  • 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 )

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

update 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
кажется сложным. Я пытался следовать и копировать до точки, но потерялся в середине. Есть ли другой способ добиться этого? Я также хотел добавить, что в делегате приложения главный контроллер установлен как корневой контроллер представления. Я не хочу использовать контроллеры навигации, но мне интересно, почему это должно быть так сложно достичь. Подводя итог, при запуске приложения я показываю контроллер основного представления с двумя кнопками. При нажатии на первую кнопку загружается VC1. На VC1 есть кнопка, и при ее нажатии должна загружаться VC2 без ошибок или предупреждений, в то же время удаляя VC1 из памяти.
Hema
На VC2 у меня есть кнопка, и нажатие на нее должно удалить VC2 из памяти, а управление должно вернуться к основному контроллеру, а не к VC1.
Hema
@Hema, я прекрасно понимал ваши требования, и уверяю вас , это является правильным способом сделать это. Я обновил свой ответ, добавив немного дополнительной информации, надеюсь, что это поможет. Если вы попробовали мой подход и застряли, задайте новый вопрос с указанием, что именно не работает, чтобы мы могли помочь. Вы также можете дать ссылку на этот вопрос для ясности.
литейный завод
Привет, он был: Спасибо за понимание. Я также говорю о другом потоке (исходном потоке) и просто опубликовал фрагмент из упомянутых там предложений. Я пробую все ответы экспертов, чтобы решить эту проблему. URL-адрес находится здесь: stackoverflow.com/questions/14840318/…
Hema
1
@Honey - Возможно, но это утверждение было риторическим ответом на часть «воображаемого» псевдокода. Я хотел сказать не о ловушках сохранения циклов, а о том, чтобы объяснить задающему вопрос, почему делегирование является ценным шаблоном проектирования (который, кстати, позволяет избежать этой проблемы). Я думаю, что это вводящее в заблуждение утверждение здесь - вопрос касается модальных VC, но ценность ответа в основном заключается в его объяснении шаблона делегата с использованием вопроса и очевидного разочарования OP в качестве катализатора. Спасибо за ваш интерес (и ваши правки) !!
литейный цех
12

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

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

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 должен иметь mainVC, представляющий VC2 сразу после того, как VC1 закрывается. И я думаю, что этого можно добиться без делегатов. Что-то вроде:

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

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

Раду Симионеску
источник
1
так просто! Я бы хотел, чтобы другие прокручивали вниз до вашего ответа, а не останавливались на верхнем посте.
Райан Логгеритм
в коде OP, почему не [self dismiss...]происходит после [self present...] завершения? Дело не в том, что происходит что-то асинхронное
Дорогая,
1
@Honey на самом деле, при вызове presentViewController происходит что-то асинхронное - поэтому у него есть обработчик завершения. Но даже используя это, если вы отклоняете представляющий контроллер представления после того, как он что-то представляет, все, что он представляет, также отклоняется. Итак, OP на самом деле хочет представить контроллер представления от другого докладчика, чтобы он мог отклонить текущий
Раду Симионеску
Но даже используя это, если вы отклоняете представляющий контроллер представления после того, как он что-то представляет, все, что он представляет, также отклоняется ... Ага, значит, компилятор в основном говорит: «То, что вы делаете, глупо. Вы только что отменили предыдущее. строка кода (как VC1 я отпущу себя и все, что я представляю). Не делайте этого ", верно?
мед
Компилятор ничего не «скажет» об этом, и может также случиться так, что при выполнении этого не произойдет сбой, просто он будет вести себя так, как не ожидает программист.
Раду
5

Раду Симионеску - классная работа! и ниже Ваше решение для любителей 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
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 (только одна анимация), я могу использовать анимацию push с помощью

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

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

self.dismiss(animated: true, completion: nil)
Дуонг Нго
источник