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?


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

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.


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; \
})

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

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.

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

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)
}

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.


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


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

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


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

-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 :)


recursiveDescriptiondrukuje tylko hierarchię widoków , a nie kontrolery widoku.
Alan Zeino

1
na tej podstawie możesz zidentyfikować kontroler widoku @AlanZeino
Akshay
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.