Arkusz wykrywania został odrzucony na iOS 13

120

Przed iOS 13 prezentowane kontrolery widoku służyły do ​​pokrycia całego ekranu. Po odrzuceniu viewDidAppearfunkcja kontrolera widoku nadrzędnego została wykonana.

Teraz iOS 13 będzie domyślnie przedstawiał kontrolery widoku jako arkusz, co oznacza, że ​​karta częściowo zakryje podstawowy kontroler widoku, co oznacza, że viewDidAppearnie zostanie wywołany, ponieważ nadrzędny kontroler widoku nigdy tak naprawdę nie zniknął.

Czy istnieje sposób na wykrycie, że przedstawiony arkusz kontrolera widoku został odrzucony ? Jakaś inna funkcja, którą mogę zastąpić w nadrzędnym kontrolerze widoku, zamiast używać jakiegoś delegata ?

Marcos Tanaka
źródło
Czy jest więc sposób na odrzucenie wszystkich arkuszy modalnych jednocześnie do katalogu głównego vc?
Weslie
Dlaczego musisz wiedzieć, kiedy została zwolniona? Jeśli chcesz przeładować dane i zaktualizować interfejs użytkownika, Notifications lub KVO mogą być dobrą alternatywą.
martn_st

Odpowiedzi:

61

Czy istnieje sposób na wykrycie, że przedstawiony arkusz kontrolera widoku został odrzucony?

Tak.

Jakaś inna funkcja, którą mogę zastąpić w nadrzędnym kontrolerze widoku, zamiast używać jakiegoś delegata?

Nie. „Jakiś delegat” tak to robisz. Ustaw siebie jako delegata i zastąpienie kontrolera prezentacji presentationControllerDidDismiss(_:).

https://developer.apple.com/documentation/uikit/uiadaptivepresentationcontrollerdelegate/3229889-presentationcontrollerdiddismiss


Brak ogólnego zdarzenia generowanego przez runtime informującego o tym, że prezentowany kontroler widoku, niezależnie od tego, czy został wyświetlony na pełnym ekranie, czy nie, został odrzucony, jest rzeczywiście kłopotliwy; ale to nie jest nowy problem, ponieważ zawsze istniały kontrolery widoku prezentowane na innym ekranie. Tyle, że teraz (w iOS 13) jest ich więcej! Osobne pytanie i odpowiedź poświęcam temu tematowi gdzie indziej: Unified UIViewController "stał się pierwszym" wykrywaniem? .

matowe
źródło
6
to nie wystarczy. Jeśli masz nabbera w prezentowanym VC i niestandardowy przycisk paska, który programowo odrzuca twój widok, kontroler prezentacji, który odrzucił, nie zostanie wywołany.
Irina
15
Cześć @Irina - jeśli odwołać swój pogląd programowo, nie trzeba wywołania zwrotnego, ponieważ oddalił widoku programowo - ty wiesz , że to zrobiłeś, bo ty to zrobił. Metoda delegata jest dostępna tylko w przypadku, gdy robi to użytkownik .
mat.
6
@matt Dzięki za odpowiedź. Kiedy widok jest odrzucany programowo, to nie jest wywoływane (jak mówi Irina) i masz rację, że wiemy, że to zrobiliśmy. Po prostu myślę, że jest niepotrzebna ilość gotowego kodu do napisania tylko po to, aby uzyskać rodzaj „viewWillAppear” z nowym stylem prezentacji modalnej w iOS13. Robi się szczególnie bałagan, gdy zarządzasz routingiem w architekturze, w której routing jest wyodrębniany (na przykład w koordynatorach MVVM + lub typ routera w VIPER)
Adam Waite
3
@AdamWaite Zgadzam się, ale ten problem nie jest nowy. Mamy ten problem od lat, z wyskakującymi okienkami, kontrolerami widoku nie wyświetlanymi na pełnym ekranie, z alertami i tak dalej. Uważam to za poważną lukę w repertuarze „wydarzeń” Apple'a. Mówię tylko, jaka jest rzeczywistość i dlaczego. Bezpośrednio borykam się z problemem tutaj: stackoverflow.com/questions/54602662/…
mat
1
prezentacjaControllerDidDismiss (_ :). nie wywoływane, gdy klikam przycisk Wstecz w Child VC. Każda pomoc?
Krishna Meena
42

Oto przykład kodu nadrzędnego kontrolera widoku, który jest powiadamiany, gdy podrzędny kontroler widoku, który przedstawia jako arkusz (tj. W domyślny sposób iOS 13), jest odrzucany:

public final class Parent: UIViewController, UIAdaptivePresentationControllerDelegate
{
  // This is assuming that the segue is a storyboard segue; 
  // if you're manually presenting, just see the delegate there.
  public override func prepare(for segue: UIStoryboardSegue, sender: Any?)
  {
    if segue.identifier == "mySegue" {
      segue.destination.presentationController?.delegate = self;
    }
  }

  public func presentationControllerDidDismiss(
    _ presentationController: UIPresentationController)
  {
    // Only called when the sheet is dismissed by DRAGGING.
    // You'll need something extra if you call .dismiss() on the child.
    // (I found that overriding dismiss in the child and calling
    // presentationController.delegate?.presentationControllerDidDismiss
    // works well).
  }
}

Odpowiedź Jerland2 jest zdezorientowana, ponieważ (a) pierwotny pytający chciał uzyskać wywołanie funkcji po odrzuceniu arkusza (podczas gdy zaimplementował prezentację PresentationControllerDidAttemptToDismiss, która jest wywoływana, gdy użytkownik próbuje odrzucić arkusz i nie powiedzie się), oraz (b) ustawienie toModalInPresentation jest całkowicie ortogonalny i faktycznie spowoduje, że prezentowany arkusz będzie niemożliwy do odrzucenia (co jest przeciwieństwem tego, czego chce OP).

Matt
źródło
6
To działa dobrze. Wskazówka, że ​​jeśli używasz kontrolera nawigacyjnego na wywołanym VC, powinieneś przypisać kontroler nawigacyjny jako PresentationController?, Delegate (nie VC, który nav ma jako topViewController).
instAustralia
@instAustralia Czy możesz wyjaśnić dlaczego lub odwołać się do dokumentacji? Dzięki.
Ahmed Osama,
prezentacjaControllerDidDismiss Jak wywołać ją, gdy użytkownik naciśnie przycisk Wstecz?
Krishna Meena
@AhmedOsama - kontroler nawigacji jest kontrolerem prezentacji i dlatego jest delegatem, ponieważ to on będzie odpowiadał na odwołanie. Wypróbowałem VC, który jest również osadzony w kontrolerze nawigacyjnym, ale to jest miejsce, w którym moje przyciski do odrzucenia istnieją i odpowiadają. Nie mogę go znaleźć bezpośrednio w dokumentach Apple, ale odwołuje się do niego tutaj sarunw.com/posts/modality-changes-in-ios13
instAustralia
26

Inna opcja powrotu viewWillAppeari viewDidAppearjest ustawiona

let vc = UIViewController()
vc.modalPresentationStyle = .fullScreen

ta opcja obejmuje pełny ekran i po odrzuceniu wywołuje powyższe metody

PiterPan
źródło
2
Dziękuję PiterPan. To działa. To świetne i najszybsze rozwiązanie.
Erkam KUCET
Dziękujemy za ten szybki i niezawodny sposób przywrócenia poprzedniego domyślnego zachowania. Wspaniale jest móc natychmiast wprowadzić tę poprawkę, a następnie zaplanować przejście do nowego zachowania w racjonalny sposób.
Ian Lovejoy
14
Jest to raczej obejście niż naprawa. Powrót do arkuszy stylów iOS 12 nie jest dobry dla wszystkich. Te z iOS 13 są fajne! :)
Matt
1
zachowaj ostrożność, używając tego na iPadzie, ponieważ iPad domyślnie wyświetla arkusz stronicowy, gdy jest prezentowany modalnie. To zmusi iPada do prezentowania jako fullScreen
wyu
nie działa dla mnie. Otwieram kontroler modalny. zamknij go z odrzuceniem, ale wola nie zostanie wywołana. Czemu? dzięki
neo999
20

Dla przyszłych czytelników pełna odpowiedź dotycząca implementacji:

  1. W widoku głównym kontrolery przygotowują się do segue dodaj następujące elementy (zakładając, że twój modal ma kontroler nawigacji)
    // Modal Dismiss iOS 13
    modalNavController.presentationController?.delegate = modalVc
  1. W kontrolerze widoku modalnego dodaj następującą metodę + delegat
// MARK: - iOS 13 Modal (Swipe to Dismiss)

extension ModalViewController: UIAdaptivePresentationControllerDelegate {
    func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {


        print("slide to dismiss stopped")
        self.dismiss(animated: true, completion: nil)
    }
}
  1. Upewnij się w modalnym kontrolerze widoku, że następująca właściwość jest true, aby można było wywołać metodę delegata
    self.isModalInPresentation = true
  1. Zysk
Jerland2
źródło
1
self.isModalInPresentation = true, to przeciągnij odrzuć nie działa. usuń tę metodę delegata linii nadal nazywa się w porządku. Dziękuję Ci.
Yogesh Patel
2
Jest to zdezorientowane, ponieważ (a) pierwotny pytający chciał uzyskać wywołanie funkcji, gdy arkusz jest odrzucany (podczas gdy zaimplementowano PresentationControllerDidAttemptToDismiss, która jest wywoływana, gdy użytkownik próbuje odrzucić arkusz i nie powiedzie się), oraz (b) ustawienie isModalInPresentation jest całkowicie ortogonalny i faktycznie spowoduje, że prezentowany arkusz będzie niemożliwy do odrzucenia (co jest przeciwieństwem tego, czego chce OP).
Matt,
1
Kontynuacja punktu odpowiedzi @Matta (a): Używanie presentationControllerDidDismisspowinno działać
gondo
Niezupełnie poprawne, ponieważ presentationControllerDidAttemptToDismissjest przeznaczone dla przypadków, w których użytkownik próbował odrzucić, ale zostało to zablokowane programowo (przeczytaj uważnie dokumentację dla tej metody). presentationControllerWillDismissMetoda jest jedna wykryć zamiar użytkownika do odwołania lub presentationControllerShouldDismissdo kontrolowania zwalniania lub presentationControllerDidDismissw celu wykrycia faktu bycia odwołany
Vitalii
5

Szybki

Ogólne rozwiązanie do wywołania viewWillAppear w iOS13

class ViewController: UIViewController {

        override func viewWillAppear(_ animated: Bool) {
            super.viewWillAppear(animated)
            print("viewWillAppear")
        }

        //Show new viewController
        @IBAction func show(_ sender: Any) {
            let newViewController = NewViewController()
            //set delegate of UIAdaptivePresentationControllerDelegate to self
            newViewController.presentationController?.delegate = self
            present(newViewController, animated: true, completion: nil)
        }
    }

    extension UIViewController: UIAdaptivePresentationControllerDelegate {
        public func presentationControllerDidDismiss( _ presentationController: UIPresentationController) {
            if #available(iOS 13, *) {
                //Call viewWillAppear only in iOS 13
                viewWillAppear(true)
            }
        }
    }
dimohamdy
źródło
1
To obsługuje tylko odrzucanie przy użyciu slajdu od góry, a nie przez wywołanie funkcji dismiss(_).
Pedro Paulo Amorim
3

FUNC Przeciągnij lub Odrzuć połączenie będzie działać z poniższym kodem.

1) W głównym kontrolerze widoku możesz powiedzieć, który jest jego kontrolerem widoku prezentacji, jak poniżej

 override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "presenterID" {
        let navigationController = segue.destination as! UINavigationController
        if #available(iOS 13.0, *) {
            let controller = navigationController.topViewController as! presentationviewcontroller
            // Modal Dismiss iOS 13
            controller.presentationController?.delegate = self
        } else {
            // Fallback on earlier versions
        }
        navigationController.presentationController?.delegate = self

    }
}

2) Ponownie w głównym kontrolerze widoku powiesz, co zrobisz, gdy jego kontroler widoku prezentacji zostanie usunięty

public func presentationControllerDidDismiss(
  _ presentationController: UIPresentationController)
{
    print("presentationControllerDidDismiss")
}

1) W kontrolerze widoku prezentacji, po naciśnięciu przycisku anulowania lub zapisania na tym obrazku. Poniższy kod będzie miał nazwę

self.dismiss(animated: true) {
        self.presentationController?.delegate?.presentationControllerDidDismiss?(self.presentationController!)
    }

wprowadź opis obrazu tutaj

programistów
źródło
1
czy konieczne jest rzutowanie navigationController.topViewController na prezentacjęViewController? Uważam, że tak nie jest
Fitsyu,
Jak mogę ponownie załadować dane w nadrzędnym wirtualnym kapitale po zamknięciu podrzędnego VC przycisku Anuluj?
Krishna Meena
3

Zastąp viewWillDisappear w UIViewController, który jest odrzucany. Zaalarmuje Cię o zwolnieniu za pomocą isBeingDismissedflagi boolowskiej.

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)

    if isBeingDismissed {
        print("user is dismissing the vc")
    }
}

** Jeśli użytkownik jest w połowie przesuwania w dół i przesuwa kartę z powrotem w górę, nadal zostanie zarejestrowana jako odrzucona, nawet jeśli karta nie zostanie odrzucona. Ale to skrajny przypadek, na którym możesz nie przejmować się.

rzemiosło
źródło
A co zself.dismiss(animated: Bool, completion: (() -> Void)?)
iGhost
0

Jeśli chcesz coś zrobić, gdy użytkownik zamyka arkusz modalny z poziomu tego arkusza. Załóżmy, że masz już przycisk Zamknij z @IBActionlogiką i logiką pokazującą alert przed zamknięciem lub zrób coś innego. Chcesz tylko wykryć moment, w którym użytkownik naciska taki kontroler.

Oto jak:

class MyModalSheetViewController: UIViewController {

     override func viewDidLoad() {
        super.viewDidLoad()

        self.presentationController?.delegate = self
     }

     @IBAction func closeAction(_ sender: Any) {
         // your logic to decide to close or not, when to close, etc.
     }

}

extension MyModalSheetViewController: UIAdaptivePresentationControllerDelegate {

    func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
        return false // <-prevents the modal sheet from being closed
    }

    func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
        closeAction(self) // <- called after the modal sheet was prevented from being closed and leads to your own logic
    }
}
Vitalii
źródło
-1

Jeśli ktoś nie ma dostępu do prezentowanym kontrolera widoku, można po prostu zastąpić następującą metodę przedstawiając widok kontroler i zmienić modalPresentationStylesię fullScreenlub można dodać jedną ze strategii wspomniano powyżej, z tym podejściem

 override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
    if let _ = viewControllerToPresent as? TargetVC {
        viewControllerToPresent.modalPresentationStyle = .fullScreen
    }
    super.present(viewControllerToPresent, animated: flag, completion: completion)
}

jeśli przedstawiony kontroler widoku jest kontrolerem nawigacji i chcesz sprawdzić kontroler główny, możesz zmienić powyższy warunek na podobny

if let _ = (viewControllerToPresent as? UINavigationController)?.viewControllers.first as? TargetVC {
   viewControllerToPresent.modalPresentationStyle = .fullScreen
}
Kamran Khan
źródło
-2

Jeśli użyłeś ModalPresentationStyle w FullScreen, zachowanie kontrolera jest normalne.

ConsultarController controllerConsultar = this.Storyboard.InstantiateViewController ("ConsultarController") jako ConsultarController; controllerConsultar.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; this.NavigationController.PushViewController (controllerConsultar, true);

Abelardo del angel Quiroz
źródło
Powtarza istniejące odpowiedzi.
mat.
-3

Z mojego punktu widzenia Apple nie powinno ustawiać pageSheettego domyślniemodalPresentationStyle

Chciałbym przywrócić fullScreendomyślny styl za pomocąswizzling

Lubię to:

private func _swizzling(forClass: AnyClass, originalSelector: Selector, swizzledSelector: Selector) {
    if let originalMethod = class_getInstanceMethod(forClass, originalSelector),
       let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector) {
        method_exchangeImplementations(originalMethod, swizzledMethod)
    }
}

extension UIViewController {

    static func preventPageSheetPresentationStyle () {
        UIViewController.preventPageSheetPresentation
    }

    static let preventPageSheetPresentation: Void = {
        if #available(iOS 13, *) {
            _swizzling(forClass: UIViewController.self,
                       originalSelector: #selector(present(_: animated: completion:)),
                       swizzledSelector: #selector(_swizzledPresent(_: animated: completion:)))
        }
    }()

    @available(iOS 13.0, *)
    private func _swizzledPresent(_ viewControllerToPresent: UIViewController,
                                        animated flag: Bool,
                                        completion: (() -> Void)? = nil) {
        if viewControllerToPresent.modalPresentationStyle == .pageSheet
                   || viewControllerToPresent.modalPresentationStyle == .automatic {
            viewControllerToPresent.modalPresentationStyle = .fullScreen
        }
        _swizzledPresent(viewControllerToPresent, animated: flag, completion: completion)
    }
}

A następnie umieść tę linię w swoim AppDelegate

UIViewController.preventPageSheetPresentationStyle()
Jakub
źródło
1
To genialne, ale nie mogę się z tym zgodzić. Jest to hacky i, co więcej, jest sprzeczne z iOS 13. W iOS 13 powinno się używać prezentacji „kartowych”. Odpowiedź, jakiej Apple od nas oczekuje, to nie „obejście tego”; to „pogódź się z tym”.
mat.
Zgadzam się z twoim punktem, to rozwiązanie nie pomaga w używaniu stylu prezentacji kart, jak zachęca nas Apple. Jednak ustawienie go jako stylu domyślnego spowoduje, że istniejące wiersze kodu presentingViewControllerbędą gdzieś błędne, ponieważ nie zostaną viewWillAppear
wyzwolone
1
Tak, ale jak już powiedziałem w mojej własnej odpowiedzi, zawsze był to problem w przypadku prezentacji nie pełnoekranowych (takich jak popovers i arkusz strony / formularzy na iPadzie), więc to nic nowego. Po prostu teraz jest tego więcej. Poleganie na tym viewWillAppearbyło w pewnym sensie zawsze złe. Oczywiście nie podoba mi się, że Apple przychodzi i wycina mi podłogę. Ale jak mówię, musimy po prostu z tym żyć i robić rzeczy w nowy sposób.
mat.
W moim projekcie jest kilka scenariuszy, w których nie wiem, gdzie presentedControllerjest prezentowany kontroler widoku (wywoływany ) i nie wiem, co to jest presentingViewController. Na przykład: w niektórych przypadkach muszę użyć, UIViewController.topMostViewController()który zwraca mi najwyższy kontroler widoku w bieżącym oknie. Dlatego chciałbym zrobić swizzling, aby zachować aktualne zachowanie, aby robić właściwe rzeczy (odświeżanie danych, interfejs użytkownika) w viewWillAppearmoich kontrolerach widoku. Jeśli masz jakieś pomysły na rozwiązanie tego problemu, pomóż.
jacob
Cóż, uważam, że rozwiązanie, do którego odsyłam na końcu mojej odpowiedzi, rozwiązuje ten problem. Konfiguracja w czasie prezentacji wymaga trochę pracy, ale w zasadzie gwarantuje, że każdy prezenter (w tym prezenter alertów) usłyszy, kiedy przedstawiony kontroler widoku zostanie zwolniony.
mat.
-6

czy nie byłoby łatwo wywołać metodę PresentViewController.viewWillAppear? przed zwolnieniem?

self.presentingViewController?.viewWillAppear(false)
self.dismiss(animated: true, completion: nil)
Mikesch8764
źródło
4
Nie do ciebie dzwonisz.
mat.