Biorąc pod uwagę widok, jak uzyskać jego viewController?

141

Mam wskaźnik do UIView. Jak uzyskać dostęp do jego UIViewController? [self superview]jest inny UIView, ale nie UIViewController, prawda?

mahboudz
źródło
Myślę, że ten wątek zawiera odpowiedzi: dostać się do UIViewController z UIView na iPhonie?
Ushox,
Zasadniczo próbuję wywołać viewWillAppear mojego viewControllera, gdy mój widok jest odrzucany. Widok jest odrzucany przez sam widok, który wykrywa dotknięcie i wywołuje [self removeFromSuperview]; ViewController nie wywołuje samego viewWillAppear / WillDisappear / DidAppear / DidDisappear.
mahboudz
Chodziło mi o to, że próbuję wywołać viewWillDisappear, ponieważ mój widok jest odrzucany.
mahboudz
1
Możliwy duplikat Get to UIViewController z UIView?
Efren,

Odpowiedzi:

42

Tak, superviewjest to widok, który zawiera Twój widok. Twój widok nie powinien wiedzieć, który dokładnie jest jego kontrolerem widoku, ponieważ złamałoby to zasady MVC.

Z drugiej strony kontroler wie, za który widok jest odpowiedzialny ( self.view = myView) i zwykle ten widok deleguje metody / zdarzenia do obsługi do kontrolera.

Zazwyczaj zamiast wskaźnika do widoku powinieneś mieć wskaźnik do kontrolera, który z kolei może albo wykonać jakąś logikę sterującą, albo przekazać coś do swojego widoku.

Dimitar Dimitrov
źródło
24
Nie jestem pewien, czy złamałoby to zasady MVC. W dowolnym momencie widok ma tylko jeden kontroler widoku. Możliwość dotarcia do niego w celu przekazania do niego wiadomości powinna być funkcją automatyczną, a nie taką, nad którą trzeba pracować (dodając właściwość do śledzenia). To samo można powiedzieć o poglądach: dlaczego musisz wiedzieć, kim jesteś dzieckiem? Albo czy istnieją inne poglądy na temat rodzeństwa. Jednak istnieją sposoby na zdobycie tych obiektów.
mahboudz
Masz trochę racji co do widoku, wiedząc o jego rodzicu, nie jest to bardzo jasna decyzja projektowa, ale jest już ustalona, ​​aby wykonać pewne czynności, używając bezpośrednio zmiennej członkowskiej superview (sprawdź typ rodzica, usuń z rodzica itp.). Pracując ostatnio z PureMVC, stałem się trochę bardziej wybredny w kwestii abstrakcji projektu :) Chciałbym zrobić paralelę między klasami UIView i UIViewController iPhone'a a klasami View i Mediator PureMVC - w większości przypadków klasa View nie musi wiedzieć o swojej obsłudze / interfejsie MVC (UIViewController / Mediator).
Dimitar Dimitrov
9
Słowo kluczowe: „większość”.
Glenn Maynard
278

Z UIResponderdokumentacji dla nextResponder:

Klasa UIResponder nie przechowuje ani nie ustawia automatycznie następnego respondera, zamiast tego domyślnie zwraca nil. Podklasy muszą przesłonić tę metodę, aby ustawić następny responder. UIView implementuje tę metodę, zwracając obiekt UIViewController, który nim zarządza (jeśli taki posiada) lub jego superview (jeśli nie) ; UIViewController implementuje metodę, zwracając superview swojego widoku; UIWindow zwraca obiekt aplikacji, a UIApplication zwraca nil.

Tak więc, jeśli powtarzasz widok, nextResponderdopóki nie stanie się on typu UIViewController, masz nadrzędny atrybut viewController dowolnego widoku.

Zauważ, że nadal może nie mieć nadrzędnego kontrolera widoku. Ale tylko wtedy, gdy widok nie jest częścią hierarchii widoków viewControllera.

Rozszerzenie Swift 3 i Swift 4.1 :

extension UIView {
    var parentViewController: UIViewController? {
        var parentResponder: UIResponder? = self
        while parentResponder != nil {
            parentResponder = parentResponder?.next
            if let viewController = parentResponder as? UIViewController {
                return viewController
            }
        }
        return nil
    }
}

Rozszerzenie Swift 2:

extension UIView {
    var parentViewController: UIViewController? {
        var parentResponder: UIResponder? = self
        while parentResponder != nil {
            parentResponder = parentResponder!.nextResponder()
            if let viewController = parentResponder as? UIViewController {
                return viewController
            }
        }
        return nil
    }
}

Kategoria celu-C:

@interface UIView (mxcl)
- (UIViewController *)parentViewController;
@end

@implementation UIView (mxcl)
- (UIViewController *)parentViewController {
    UIResponder *responder = self;
    while ([responder isKindOfClass:[UIView class]])
        responder = [responder nextResponder];
    return (UIViewController *)responder;
}
@end

To makro pozwala uniknąć zanieczyszczenia kategorii:

#define UIViewParentController(__view) ({ \
    UIResponder *__responder = __view; \
    while ([__responder isKindOfClass:[UIView class]]) \
        __responder = [__responder nextResponder]; \
    (UIViewController *)__responder; \
})
mxcl
źródło
Tylko jedno: jeśli martwisz się zanieczyszczeniem kategorii, po prostu zdefiniuj je jako funkcję statyczną, a nie makro. Również brutalne typowanie jest niebezpieczne, a ponadto makro może nie być poprawne, ale nie jestem pewien.
mojuba
Makro działa, osobiście używam tylko wersji makra. Rozpoczynam wiele projektów i mam nagłówek, w którym po prostu wpadam wszędzie z kilkoma funkcjami narzędzi makr. Oszczędza mi czas. Jeśli nie lubisz makr, możesz dostosować je do funkcji, ale funkcje statyczne wydają się nudne, ponieważ musisz je umieścić w każdym pliku, którego chcesz użyć. Wygląda na to, że zamiast tego chcesz zadeklarować niestatyczną funkcję w nagłówku i zdefiniowaną w jakimś miejscu w pliku .m?
mxcl
Miło jest uniknąć niektórych delegatów lub powiadomień. Dzięki!
Ferran Maylinch
Powinieneś uczynić to przedłużeniem UIResponder;). Bardzo budujący post.
ScottyBlades
32

@andrey odpowiedź w jednej linii (testowane w Swift 4.1 ):

extension UIResponder {
    public var parentViewController: UIViewController? {
        return next as? UIViewController ?? next?.parentViewController
    }
}

stosowanie:

 let vc: UIViewController = view.parentViewController
Federico Zanetello
źródło
parentViewControllernie można zdefiniować, publicjeśli rozszerzenie znajduje się w tym samym pliku, co Twoje, UIViewmożesz je ustawić fileprivate, kompiluje się, ale nie działa! 😐
Działa dobrze. Użyłem tego do akcji przycisku Wstecz wewnątrz wspólnego pliku nagłówkowego .xib.
McDonal_11
23

Tylko do celów debugowania można wywoływać _viewDelegatewidoki, aby uzyskać ich kontrolery widoku. To jest prywatny interfejs API, więc nie jest bezpieczny dla App Store, ale do debugowania jest przydatny.

Inne przydatne metody:

  • _viewControllerForAncestor- pobierz pierwszy kontroler, który zarządza widokiem w łańcuchu superview. (dzięki n00neimp0rtant)
  • _rootAncestorViewController - pobierz kontroler nadrzędny, którego hierarchia widoków jest aktualnie ustawiona w oknie.
Leo Natan
źródło
Właśnie dlatego doszedłem do tego pytania. Najwyraźniej „nextResponder” robi to samo, ale doceniam wgląd, jaki zapewnia ta odpowiedź. Rozumiem i lubię MVC, ale debugowanie to inna sprawa!
mbm29414
4
Wygląda na to, że działa to tylko na głównym widoku kontrolera widoku, a nie na żadnych jego podglądach podrzędnych. _viewControllerForAncestorprzejdzie w górę nadzorów, aż znajdzie pierwszy, który należy do kontrolera widoku.
n00neimp0rtant
Dzięki @ n00neimp0rtant! Głosuję za tą odpowiedzią, aby ludzie widzieli Twój komentarz.
eyuelt
Zaktualizuj odpowiedź, używając większej liczby metod.
Leo Natan
1
Jest to przydatne podczas debugowania.
evanchin
10

Aby uzyskać odniesienie do UIViewController mającego UIView, możesz utworzyć rozszerzenie UIResponder (która jest superklasą dla UIView i UIViewController), które pozwala przejść w górę przez łańcuch responder i w ten sposób dotrzeć do UIViewController (w przeciwnym razie zwraca nil).

extension UIResponder {
    func getParentViewController() -> UIViewController? {
        if self.nextResponder() is UIViewController {
            return self.nextResponder() as? UIViewController
        } else {
            if self.nextResponder() != nil {
                return (self.nextResponder()!).getParentViewController()
            }
            else {return nil}
        }
    }
}

//Swift 3
extension UIResponder {
    func getParentViewController() -> UIViewController? {
        if self.next is UIViewController {
            return self.next as? UIViewController
        } else {
            if self.next != nil {
                return (self.next!).getParentViewController()
            }
            else {return nil}
        }
    }
}

let vc = UIViewController()
let view = UIView()
vc.view.addSubview(view)
view.getParentViewController() //provide reference to vc
Andrey
źródło
4

Szybki i ogólny sposób w Swift 3:

extension UIResponder {
    func parentController<T: UIViewController>(of type: T.Type) -> T? {
        guard let next = self.next else {
            return nil
        }
        return (next as? T) ?? next.parentController(of: T.self)
    }
}

//Use:
class MyView: UIView {
    ...
    let parentController = self.parentController(of: MyViewController.self)
}
iTSangar
źródło
2

Jeśli nie znasz kodu i chcesz znaleźć ViewController odpowiadający danemu widokowi, możesz spróbować:

  1. Uruchom aplikację w debugowaniu
  2. Przejdź do ekranu
  3. Uruchom Inspektora widoku
  4. Chwyć widok, który chcesz znaleźć (lub jeszcze lepszy widok podrzędny)
  5. Z prawego okienka pobierz adres (np. 0x7fe523bd3000)
  6. W konsoli debugowania zacznij pisać polecenia:
    po (UIView *) 0x7fe523bd3000
    po [(UIView *) 0x7fe523bd3000 nextResponder]
    po [[(UIView *) 0x7fe523bd3000 nextResponder] nextResponder]
    po [[[(UIView *) 0x7fe523bd3000 nextResponder] nextResponder] nextResponder]
    ...

W większości przypadków otrzymasz UIView, ale od czasu do czasu będzie tam klasa oparta na UIViewController.

m4js7er
źródło
1

Myślę, że możesz przekazać kran do kontrolera widoku i pozwolić mu się tym zająć. To bardziej akceptowalne podejście. Jeśli chodzi o dostęp do kontrolera widoku z jego widoku, powinieneś zachować odwołanie do kontrolera widoku, ponieważ nie ma innego sposobu. Zobacz ten wątek, może pomóc: Dostęp do kontrolera widoku z widoku

Nava Carmon
źródło
Jeśli masz kilka widoków i jeden zamyka wszystkie widoki i musisz wywołać funkcję viewWillDisappear, czy nie byłoby łatwiej, aby ten widok wykrył stuknięcie, niż przekazanie kranu kontrolerowi widoku i sprawdzenie przez kontroler widoku ze wszystkimi widokami, aby zobaczyć, który z nich został podsłuchany?
mahboudz
0

Bardziej bezpieczny kod dla Swift 3.0

extension UIResponder {
    func owningViewController() -> UIViewController? {
        var nextResponser = self
        while let next = nextResponser.next {
            nextResponser = next
            if let vc = nextResponser as? UIViewController {
                return vc
            }
        }
        return nil
    }
}
Denis Krivitski
źródło
0

Niestety, jest to niemożliwe, chyba że podklasujesz widok i nie udostępnisz mu właściwości instancji lub podobnej, która przechowuje odwołanie do kontrolera widoku w nim po dodaniu widoku do sceny ...

W większości przypadków - bardzo łatwo jest obejść pierwotny problem tego postu, ponieważ większość kontrolerów widoku to jednostki dobrze znane programiście, który był odpowiedzialny za dodanie jakichkolwiek podwidoków do widoku ViewControllera ;-) Dlatego domyślam się, że Apple nigdy nie zawracał sobie głowy dodaniem tej własności

Morten Carlsen
źródło
0

Trochę późno, ale tutaj jest rozszerzenie, które umożliwia znalezienie respondera dowolnego typu, w tym ViewController.

extension NSObject{
func findNext(type: AnyClass) -> Any{
    var resp = self as! UIResponder

    while !resp.isKind(of: type.self) && resp.next != nil
    {
        resp = resp.next!
    }

    return resp
  }                       
}
Hackbarth
źródło
-1

Jeśli ustawisz punkt przerwania, możesz wkleić to do debugera, aby wydrukować hierarchię widoków:

po [[UIWindow keyWindow] recursiveDescription]

Gdzieś w tym bałaganie powinieneś znaleźć rodzica swojego widoku :)

Scott McCoy
źródło
recursiveDescriptiondrukuje tylko hierarchię widoków , a nie kontrolery widoku.
Alan Zeino
1
na tej podstawie możesz zidentyfikować kontroler widoku @AlanZeino
Akshay