модальні контролери перегляду - як відображати та відхиляти


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

не надсилає повідомлення собі, насправді надсилає повідомлення своєму представницькому ВК, просячи його звільнити. Коли ви представляєте ВК, ви створюєте взаємозв'язок між представленим ВК і представленим. Отже, ви не повинні знищувати презентуючий ВК під час його презентації (представлений ВК не може відправити це повідомлення про відмову назад…). Оскільки ви насправді не враховуєте це, ви залишаєте програму в розгубленому стані. Дивіться мою відповідь Звільнення представленого контролера подання, в якій я рекомендую цей метод чіткіше написаний:

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

У вашому випадку вам потрібно переконатися, що весь контроль здійснюється в mainVC . Вам слід використовувати делегата, щоб відправити правильне повідомлення назад до MainViewController з ViewController1, щоб mainVC міг відхилити VC1, а потім подати VC2.

У VC2 VC1 додайте протокол у файл .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 кнопки. Зверніть увагу, що його викликають із блоку завершення, щоб переконатись, що VC2 не відображається, поки VC1 не буде повністю відхилено.

Зараз ви переходите з VC1-> VCMain-> VC2, тому ви, мабуть, хочете, щоб анімувався лише один із переходів.

оновлення

У своїх коментарях ви висловлюєте здивування складністю, необхідною для досягнення, здавалося б, простої справи. Запевняю вас, цей шаблон делегування є настільки важливим для більшості об’єктивів-C та какао, і цей приклад стосується найпростішого, що ви можете отримати, і вам справді слід докласти зусиль, щоб з ним почуватись комфортно.

У посібнику програмування View Controller від Apple вони мають таке сказати :

Відхилення представленого контролера перегляду

Коли приходить час звільнити представлений контролер подання, найкращим підходом є надання дозволу представницькому контролеру подання його відхилити. Іншими словами, коли це можливо, той самий контролер перегляду, який представив контролер перегляду, також повинен нести відповідальність за його відхилення. Незважаючи на те, що існує декілька методів повідомлення контролера подання представлення про те, що його представлений контролер подання повинен бути відхилений, кращим методом є делегування. Для отримання додаткової інформації див. “Використання делегування для спілкування з іншими контролерами”.

Якщо ви дійсно продумаєте, чого хочете досягти, і як ви це робите, ви зрозумієте, що обмін повідомленнями MainViewController, щоб виконати всю роботу, є єдиним логічним виходом з огляду на те, що ви не хочете використовувати NavigationController. Якщо ви робите використовувати NavController, насправді ви «делегування», навіть якщо не явно, до NavController , щоб зробити всю роботу. Потрібно мати якийсь об’єкт, який відстежує центральне відстеження того, що відбувається з вашою навігацією по ВК, і вам потрібен якийсь спосіб спілкування з ним, що б ви не робили.

На практиці поради 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 щось більше? Ви не знаєте (як і я) ... з делегатом, у вас немає цієї невизначеності.
  • Коли я намагаюся запустити цей метод, він просто зависає без попередження та помилок.

Тож, будь ласка ... знайдіть час, щоб навчитися делегуванню!

оновлення2

У своєму коментарі вам вдалося змусити його працювати, використовуючи це в обробнику кнопок звільнення VC2:

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

Це, звичайно, набагато простіше, але це залишає перед вами низку питань.

Герметичне з'єднання
Ви міцно з'єднуєте свою структуру viewController разом. Наприклад, якщо ви повинні були вставити новий viewController перед mainVC, ваша необхідна поведінка порушиться (ви перейдете до попереднього). У VC1 вам також довелося # імпортувати VC2. Тому у вас досить багато взаємозалежностей, що порушує цілі ООП / MVC.

Використовуючи делегатів, ні VC1, ні VC2 не повинні знати нічого про mainVC або його попередні дані, тому ми зберігаємо все в вільному поєднанні та модулі.

Пам'ять
VC1 не зникла, ви все ще тримаєте на ній два вказівники:

  • mainVC в presentedViewControllerнерухомість
  • presentingViewControllerВластивість VC2

Ви можете перевірити це, увійшовши в журнал, а також просто виконавши це з 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 є представленим ВК. Друге повідомлення насправді не виконується - ну, можливо, трапляються деякі речі, але ви все одно залишаєте два вказівники на об’єкт, від якого ви думали, що позбулися. ( редагувати - я перевірив це, і це не так погано, обидва об'єкти зникають, коли ви повертаєтесь до 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 - Можливо, так, але це твердження було риторичною відповіддю на шматок "уявного" псевдокоду. Я хотів сказати не про те, щоб утримати циклічні пастки, а про те, щоб навчити допитувача, чому делегування є цінним шаблоном дизайну (що, до речі, уникає цієї проблеми). Я думаю, що тут є оманливе твердження - питання стосується модальних ВК, але цінність відповіді полягає головним чином у поясненні шаблону делегування, використовуючи питання та очевидні розлади OP як каталізатора. Дякуємо за інтерес (і ваші зміни) !!
ливарня

12

Приклад у Swift , що зображує пояснення ливарні вище та документацію Apple:

  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 також відхиляються. Apple призначена для модальних контролерів перегляду, щоб вони працювали послідовно - у вашому випадку VC2 представлений VC1. Ви звільняєте VC1, як тільки представляєте VC2 від VC1, тож це суцільний хаос. Щоб досягти того, що ви хочете, buttonPressFromVC1 повинен мати основний VC, присутній VC2 відразу після того, як VC1 звільнить себе. І я думаю, що цього можна досягти без делегатів. Щось на зразок:

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

Зверніть увагу, що self.presentingViewController зберігається в якійсь іншій змінній, оскільки після того, як vc1 відмовляється від себе, вам не слід робити жодних посилань на нього.


1
так просто! Я хотів би, щоб інші прокручували вниз до вашої відповіді, а не зупинялися на верхньому пості.
Ryan Loggerythm

в коді OP, чому не [self dismiss...]трапляється після [self present...] закінчення? Це не відбувається щось асинхронне
Мед

1
Насправді @Honey, під час виклику presentViewController відбувається щось асинхронне - ось чому він має обробник завершення. Але навіть використовуючи це, якщо ви відхиляєте контролер подання подання після того, як він щось представляє, все, що він представляє, також відхиляється. Отож ОП насправді хоче представити контролер перегляду від іншого ведучого насправді, щоб він міг відхилити поточного
Раду Сіміонеску,

Але навіть використовуючи це, якщо ви відхиляєте контролер подання подання після того, як він щось представляє, все, що він представляє, також відхиляється ... Ага, так що компілятор в основному говорить: "те, що ти робиш, є дурним. Ти просто скасував своє попереднє рядок коду (як VC1 я відхилю себе і все, що я представляю). Не робіть це "правильно?
Мед

Компілятор не буде "говорити" про це нічого, і також може бути випадком, коли при виконанні цього не відбудеться збій, просто він буде поводитися так, як цього не очікує програміст
Раду Сіміонеску

5

Раду Сіміонеску - чудова робота! і нижче Ваше рішення для любителів Свіфта:

@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)
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.