Дістатися до UIViewController з UIView?


185

Чи є вбудований спосіб дістатися від точки UIViewдо його UIViewController? Я знаю, що ви можете дістатися UIViewControllerдо його UIViewчерез, [self view]але мені було цікаво, чи є зворотна довідка?

Відповіді:


46

Оскільки ця давно прийнята відповідь, я вважаю, що мені потрібно виправити її кращою відповіддю.

Деякі коментарі щодо необхідності:

  • Ваш погляд не повинен мати прямий доступ до контролера перегляду.
  • Перегляд замість цього повинен бути незалежним від контролера перегляду та мати можливість працювати в різних контекстах.
  • Якщо вам потрібен погляд, щоб взаємодіяти з контролером перегляду, рекомендованим способом, а також те, що Apple робить у компанії Cocoa, - це використовувати шаблон делегата.

Приклад того, як це здійснити:

@protocol MyViewDelegate < NSObject >

- (void)viewActionHappened;

@end

@interface MyView : UIView

@property (nonatomic, assign) MyViewDelegate delegate;

@end

@interface MyViewController < MyViewDelegate >

@end

Перегляд інтерфейсу з його делегатом (як UITableView, наприклад,), і це не байдуже, чи реалізовано його в контролері перегляду або в будь-якому іншому класі, який ви в кінцевому підсумку використовуєте.

Моя оригінальна відповідь випливає: я не рекомендую цього, а також решти відповідей, де досягається прямий доступ до контролера перегляду

Немає вбудованого способу це зробити. Хоча ви можете її обійти, додавши IBOutletна UIViewі підключивши їх у Interface Builder, це не рекомендується. Перегляд не повинен знати про контролер перегляду. Натомість слід зробити так, як пропонує @Phil M і створити протокол, який буде використовуватися як делегат.


26
Це дуже погана порада. Не слід посилатися на контролер перегляду з точки зору
Philippe Leybaert

6
@MattDiPasquale: так, це поганий дизайн.
Філіп Лейбаерт

23
@Phillipe Leybaert Мені цікаво дізнатись ваші думки щодо найкращого дизайну події натискання кнопки, яка повинна викликати певну дію на контролері, без подання посилання на контролер. Дякую
Джонатан Горсман

3
Приклади проектів Apple @PhilippeLeybaert, як правило, демонструють використання певних функцій API. Багато хто з них посилався на те, щоб пожертвувати гарним чи масштабованим дизайном, щоб надати стислу демонстрацію теми. Мені знадобилося багато часу, щоб усвідомити це, і хоча це має сенс, я вважаю це прикро. Я думаю, що багато розробників сприймають ці прагматичні проекти як посібник Apple щодо найкращої дизайнерської практики. Я впевнений, що це не так.
Benjohn

11
Все це на тему "ти не повинен цього робити" - це саме те. Якщо представлення хоче знати про його контролер перегляду, це вирішувати програмісту. періоду.
Даніель Канаан

203

Використовуючи приклад, опублікований Броком, я змінив його так, що він є категорією UIView замість UIViewController, і зробив його рекурсивним, щоб будь-який підрозділ міг (сподіваємось) знайти батьківський UIViewController.

@interface UIView (FindUIViewController)
- (UIViewController *) firstAvailableUIViewController;
- (id) traverseResponderChainForUIViewController;
@end

@implementation UIView (FindUIViewController)
- (UIViewController *) firstAvailableUIViewController {
    // convenience function for casting and to "mask" the recursive function
    return (UIViewController *)[self traverseResponderChainForUIViewController];
}

- (id) traverseResponderChainForUIViewController {
    id nextResponder = [self nextResponder];
    if ([nextResponder isKindOfClass:[UIViewController class]]) {
        return nextResponder;
    } else if ([nextResponder isKindOfClass:[UIView class]]) {
        return [nextResponder traverseResponderChainForUIViewController];
    } else {
        return nil;
    }
}
@end

Щоб використовувати цей код, додайте його до нового файлу класу (я назвав шахту "UIKitCategories") і видаліть дані класу ... скопіюйте @interface у заголовок, а @implementation - у файл .m. Потім у своєму проекті #import "UIKitCategories.h" та використовуйте в коді UIView:

// from a UIView subclass... returns nil if UIViewController not available
UIViewController * myController = [self firstAvailableUIViewController];

47
І одна з причин, що потрібно дозволити UIView усвідомлювати його UIViewController, - це те, коли у вас є власні підкласи UIView, які потребують натискання модального перегляду / діалогового вікна.
Філ М

Дивовижно, мені довелося отримати доступ до свого ViewController, щоб відобразити користувальницьке спливаюче вікно, яке створюється підпереглядом
aryaxt

2
Хіба це не погана практика для UIView для просування модального перегляду? Я роблю це зараз, але відчуваю, що це не правильно робити.
Van Du Tran

6
Філ, ваш власний вигляд повинен викликати метод делегата, який контролер перегляду слухає, а потім він виштовхує звідти.
мальхал

9
Мені просто подобається, скільки ТАК запитань отримав "мудрець", академічний, прийнятий відповідь з декількома пунктами і друга відповідь, яка практична, брудна і суперечить правилам, у десять разів більше балів :-)
hariseldon78

114

UIViewє підкласом UIResponder. UIResponderвикладає метод -nextResponderіз реалізацією, яка повертається nil. UIViewпереосмислює цей метод, як це зафіксовано у UIResponder(чомусь замість того, щоб UIView), наступному: якщо представлення має контролер перегляду, він повертається -nextResponder. Якщо немає контролера перегляду, метод поверне контрольний вигляд.

Додайте це до свого проекту, і ви готові до участі.

@interface UIView (APIFix)
- (UIViewController *)viewController;
@end

@implementation UIView (APIFix)

- (UIViewController *)viewController {
    if ([self.nextResponder isKindOfClass:UIViewController.class])
        return (UIViewController *)self.nextResponder;
    else
        return nil;
}
@end

Тепер UIViewіснує робочий метод повернення контролера перегляду.


5
Це спрацює лише в тому випадку, якщо між ланцюгом прийому UIViewта прийому немає нічого в ланцюжку відповідей UIViewController. Відповідь Філа М із рекурсією - це шлях.
Олів'є

33

Я б запропонував більш легкий підхід для проходження всього ланцюжка реагування без необхідності додавати категорію в UIView:

@implementation MyUIViewSubclass

- (UIViewController *)viewController {
    UIResponder *responder = self;
    while (![responder isKindOfClass:[UIViewController class]]) {
        responder = [responder nextResponder];
        if (nil == responder) {
            break;
        }
    }
    return (UIViewController *)responder;
}

@end

22

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

@implementation UIView (AppNameAdditions)

- (UIViewController *)appName_viewController {
    /// Finds the view's view controller.

    // Take the view controller class object here and avoid sending the same message iteratively unnecessarily.
    Class vcc = [UIViewController class];

    // Traverse responder chain. Return first found view controller, which will be the view's view controller.
    UIResponder *responder = self;
    while ((responder = [responder nextResponder]))
        if ([responder isKindOfClass: vcc])
            return (UIViewController *)responder;

    // If the view controller isn't found, return nil.
    return nil;
}

@end

Категорія є частиною моєї статичної бібліотеки з підтримкою ARC, яку я надсилаю до кожного створеного додатком. Його перевіряли кілька разів, і я не виявив жодних проблем і протікань.

PS: Вам не потрібно використовувати категорію, як я, якщо відповідний вид є вашим підкласом. В останньому випадку просто покладіть метод у свій підклас, і ви готові йти.


1
Це найкраща відповідь. Не потрібно рекурсії, ця версія добре оптимізована
zeroimpl

12

Незважаючи на те, що це технічно можна вирішити, як рекомендує pgb , IMHO, це є вадою дизайну. Погляд не повинен знати про контролер.


Я просто не впевнений, яким чином viewController може сказати, що один із його поглядів відходить і потрібно викликати один із його методів viewXXXAppear / viewXXXDisappear.
mahboudz

2
Це ідея за схемою спостерігача. Спостережуване (Погляд у цьому випадку) не повинно знати про своїх Спостерігачів безпосередньо. Спостерігач повинен отримувати лише зворотні дзвінки, які їх цікавлять.
Ushox

12

Я змінив де відповідь , щоб я міг пройти будь-який вид, кнопки, етикетки і т.д. , щоб отримати його батьків UIViewController. Ось мій код.

+(UIViewController *)viewController:(id)view {
    UIResponder *responder = view;
    while (![responder isKindOfClass:[UIViewController class]]) {
        responder = [responder nextResponder];
        if (nil == responder) {
            break;
        }
    }
    return (UIViewController *)responder;
}

Редагуйте версію Swift 3

class func viewController(_ view: UIView) -> UIViewController {
        var responder: UIResponder? = view
        while !(responder is UIViewController) {
            responder = responder?.next
            if nil == responder {
                break
            }
        }
        return (responder as? UIViewController)!
    }

Редагування 2: - Швидкий розтяг

extension UIView
{
    //Get Parent View Controller from any view
    func parentViewController() -> UIViewController {
        var responder: UIResponder? = self
        while !(responder is UIViewController) {
            responder = responder?.next
            if nil == responder {
                break
            }
        }
        return (responder as? UIViewController)!
    }
}

7

Не забувайте, що ви можете отримати доступ до кореневого контролера перегляду для вікна, для якого подання є підглядом. Звідти, якщо ви, наприклад, використовуєте контролер перегляду навігації і хочете натиснути на нього новий погляд:

    [[[[self window] rootViewController] navigationController] pushViewController:newController animated:YES];

Однак спочатку потрібно буде правильно встановити властивість rootViewController вікна. Зробіть це під час першого створення контролера, наприклад, у делегата програми:

-(void) applicationDidFinishLaunching:(UIApplication *)application {
    window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    RootViewController *controller = [[YourRootViewController] alloc] init];
    [window setRootViewController: controller];
    navigationController = [[UINavigationController alloc] initWithRootViewController:rootViewController];
    [controller release];
    [window addSubview:[[self navigationController] view]];
    [window makeKeyAndVisible];
}

Мені здається, що згідно з документами Apple, оскільки [[self navigationController] view]це "головний" (під) вигляд вікна, слід встановити rootViewControllerвластивість вікна, яке негайно navigationControllerкерує "основним" поданням.
1111

6

Хоча ці відповіді технічно правильні, включаючи Ushox, я вважаю, що затвердженим способом є реалізація нового протоколу або повторне використання існуючого. Протокол ізолює спостерігача від спостережуваного, подібного до того, як помістити між ними слот пошти. Фактично, це робить Габріель за допомогою виклику методу pushViewController; представлення "знає", що це правильний протокол, щоб ввічливо попросити ваш навігаційний контролер надати перегляд, оскільки viewController відповідає протоколу navigationController. Хоча ви можете створити власний протокол, просто використовуючи приклад Габріеля та повторне використання протоколу UINavigationController, це просто чудово.


6

Я натрапив на ситуацію, коли у мене є невеликий компонент, який я хочу використати повторно, і додав якийсь код у перегляд для багаторазового використання (це дійсно не набагато більше, ніж кнопка, яка відкриває PopoverController ).

Хоча це добре працює в iPad (сам UIPopoverControllerподарунки, для цього не потрібно посилатися на a UIViewController), отримання того ж коду для роботи означає раптово посилання на ваш presentViewControllerз вашого UIViewController. Якийсь непослідовний прав?

Як згадувалося раніше, не найкращий підхід мати логіку у своєму UIView. Але виглядати по-справжньому марно загортати кілька рядків коду, необхідних в окремий контролер.

У будь-якому випадку, ось швидке рішення, яке додає нову властивість до будь-якого UIView:

extension UIView {

    var viewController: UIViewController? {

        var responder: UIResponder? = self

        while responder != nil {

            if let responder = responder as? UIViewController {
                return responder
            }
            responder = responder?.nextResponder()
        }
        return nil
    }
}

5

Я не думаю, що це "погана" ідея з'ясувати, хто є контролером перегляду в деяких випадках. Що може бути поганою ідеєю - зберегти посилання на цей контролер, оскільки це може змінитися так само, як змінюються нагляди. У моєму випадку у мене є геттер, який проходить ланцюжок реагування.

//.h

@property (nonatomic, readonly) UIViewController * viewController;

//.m

- (UIViewController *)viewController
{
    for (UIResponder * nextResponder = self.nextResponder;
         nextResponder;
         nextResponder = nextResponder.nextResponder)
    {
        if ([nextResponder isKindOfClass:[UIViewController class]])
            return (UIViewController *)nextResponder;
    }

    // Not found
    NSLog(@"%@ doesn't seem to have a viewController". self);
    return nil;
}

4

Найпростіше зробити цикл для пошуку viewController.

-(UIViewController*)viewController
{
    UIResponder *nextResponder =  self;

    do
    {
        nextResponder = [nextResponder nextResponder];

        if ([nextResponder isKindOfClass:[UIViewController class]])
            return (UIViewController*)nextResponder;

    } while (nextResponder != nil);

    return nil;
}

4

Швидкий 4

(більш короткий, ніж інші відповіді)

fileprivate extension UIView {

  var firstViewController: UIViewController? {
    let firstViewController = sequence(first: self, next: { $0.next }).first(where: { $0 is UIViewController })
    return firstViewController as? UIViewController
  }

}

Мій випадок використання, для якого мені потрібно отримати доступ до перегляду спочатку UIViewController: у мене є об'єкт, який обертається навколо AVPlayer/ AVPlayerViewControllerі я хочу надати простий show(in view: UIView)метод, який буде вбудований AVPlayerViewControllerуview . Для цього мені потрібно отримати доступ viewдо UIViewController.


3

Це не відповідає на питання безпосередньо, а скоріше робить припущення про наміри питання.

Якщо у вас є перегляд, і в цьому поданні вам потрібно викликати метод на іншому об'єкті, наприклад скажіть контролер перегляду, замість цього можна використовувати NSNotificationCenter.

Спочатку створіть рядок сповіщень у файлі заголовка

#define SLCopyStringNotification @"ShaoloCopyStringNotification"

На ваш погляд, виклик postNotificationName:

- (IBAction) copyString:(id)sender
{
    [[NSNotificationCenter defaultCenter] postNotificationName:SLCopyStringNotification object:nil];
}

Потім у свій контролер подання ви додаєте спостерігача. Я роблю це в viewDidLoad

- (void)viewDidLoad
{
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(copyString:)
                                                 name:SLCopyStringNotification
                                               object:nil];
}

Тепер (також у тому ж контролері перегляду) реалізуйте свій метод copyString: як зображено в @selector вище.

- (IBAction) copyString:(id)sender
{
    CalculatorResult* result = (CalculatorResult*)[[PercentCalculator sharedInstance].arrayTableDS objectAtIndex:([self.viewTableResults indexPathForSelectedRow].row)];
    UIPasteboard *gpBoard = [UIPasteboard generalPasteboard];
    [gpBoard setString:result.stringResult];
}

Я не кажу, що це правильний спосіб зробити це, це здається чистішим, ніж запуск першого ланцюжка реагування. Я використовував цей код для реалізації UIMenuController на UITableView і передачу події назад до UIViewController, щоб я міг щось зробити з даними.


3

Це, звичайно, погана ідея і неправильний дизайн, але я впевнений, що ми всі зможемо насолодитися швидким рішенням найкращої відповіді, запропонованого @Phil_M:

static func firstAvailableUIViewController(fromResponder responder: UIResponder) -> UIViewController? {
    func traverseResponderChainForUIViewController(responder: UIResponder) -> UIViewController? {
        if let nextResponder = responder.nextResponder() {
            if let nextResp = nextResponder as? UIViewController {
                return nextResp
            } else {
                return traverseResponderChainForUIViewController(nextResponder)
            }
        }
        return nil
    }

    return traverseResponderChainForUIViewController(responder)
}

Якщо ви маєте намір зробити прості речі, такі як показ модального діалогового вікна або даних відстеження, це не виправдовує використання протоколу. Я особисто зберігаю цю функцію в об’єкті утиліти, ви можете використовувати її з усього, що реалізує протокол UIResponder, як:

if let viewController = MyUtilityClass.firstAvailableUIViewController(self) {}

Увесь кредит на @Phil_M


3

Можливо, я тут запізнююся. Але в цій ситуації мені не подобається категорія (забруднення). Я люблю так:

#define UIViewParentController(__view) ({ \
UIResponder *__responder = __view; \
while ([__responder isKindOfClass:[UIView class]]) \
__responder = [__responder nextResponder]; \
(UIViewController *)__responder; \
})

3

Швидше рішення

extension UIView {
    var parentViewController: UIViewController? {
        for responder in sequence(first: self, next: { $0.next }) {
            if let viewController = responder as? UIViewController {
                return viewController
            }
        }
        return nil
    }
}

Мені подобається ця відповідь. Я не впевнений, що це швидше. Свіфт - це ще одне - не можу визначитися - яке з декількох парадигм - воно хоче - бути одруженим - мовами. У цьому випадку у вас є хороша демонстрація більш функціонального підходу ( sequenceбіт). Тож якщо "стрімкий" означає "більш функціональний", то, мабуть, він швидший.
Тревіс Гріггс

2

Оновлена ​​версія для swift 4: Дякую @Phil_M та @ paul-slm

static func firstAvailableUIViewController(fromResponder responder: UIResponder) -> UIViewController? {
    func traverseResponderChainForUIViewController(responder: UIResponder) -> UIViewController? {
        if let nextResponder = responder.next {
            if let nextResp = nextResponder as? UIViewController {
                return nextResp
            } else {
                return traverseResponderChainForUIViewController(responder: nextResponder)
            }
        }
        return nil
    }

    return traverseResponderChainForUIViewController(responder: responder)
}

2

Версія Swift 4

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
}

Приклад використання

 if let parent = self.view.parentViewController{

 }

2

Два рішення станом на Swift 5.2 :

  • Детальніше про функціональну сторону
  • Немає необхідності для returnключового слова зараз 🤓

Рішення 1:

extension UIView {
    var parentViewController: UIViewController? {
        sequence(first: self) { $0.next }
            .first(where: { $0 is UIViewController })
            .flatMap { $0 as? UIViewController }
    }
}

Рішення 2:

extension UIView {
    var parentViewController: UIViewController? {
        sequence(first: self) { $0.next }
            .compactMap{ $0 as? UIViewController }
            .first
    }
}
  • Це рішення вимагає ітерації спочатку через кожного респондента, тому може бути не найефективнішим.

1

На відповідь Філа:

У рядку: id nextResponder = [self nextResponder]; якщо self (UIView) не є підглядом представлення ViewController, якщо ви знаєте ієрархію себе (UIView), ви можете також використовувати: id nextResponder = [[self superview] nextResponder];...


0

Моє рішення, ймовірно, вважатиметься невірним, але у мене була подібна ситуація, як і майонез (я хотів переключити погляди у відповідь на жест в EAGLView), і я отримав контролер подання EAGL таким чином:

EAGLViewController *vc = ((EAGLAppDelegate*)[[UIApplication sharedApplication] delegate]).viewController;

2
Будь ласка , перепишіть код наступним чином : EAGLViewController *vc = [(EAGLAppDelegate *)[UIApplication sharedApplication].delegate viewController];.
Джонатан Стерлінг

1
Проблема не в синтаксисі крапок, а в типах. У Цілі C оголосити записаний вами об’єкт ClassName *object- зірочкою.
1111

На жаль, це насправді те, що у мене було, але HTML-віджет StackOverflow виглядає так, ніби зірочка означала курсив ... Я змінив його на блок коду, тепер він відображається правильно. Дякую!
gulchrider

0

Я думаю, що є випадок, коли спостережуваному потрібно повідомити спостерігача.

Я бачу подібну проблему, коли UIView в UIViewController реагує на ситуацію, і йому потрібно спочатку сказати своєму контролеру батьківського перегляду приховати кнопку назад, а потім після завершення сказати контролеру батьківського перегляду, що йому потрібно вискакувати зі стека.

Я намагався це зробити з делегатами, не маючи успіху.

Я не розумію, чому це має бути поганою ідеєю?


0

Ще один простий спосіб - мати власний клас перегляду та додати властивість контролера перегляду у клас перегляду. Зазвичай контролер перегляду створює подання, і саме там контролер може встановити властивість. В основному це замість того, щоб обшукати (з невеликим злому) контролера, мати контролер, щоб налаштувати його на вигляд - це просто, але має сенс, оскільки саме контролер "контролює" погляд.


0

Якщо ви не збираєтесь завантажувати це в App Store, ви також можете використовувати приватний метод UIView.

@interface UIView(Private)
- (UIViewController *)_viewControllerForAncestor;
@end

// Later in the code
UIViewController *vc = [myView _viewControllerForAncestor];

0
var parentViewController: UIViewController? {
    let s = sequence(first: self) { $0.next }
    return s.compactMap { $0 as? UIViewController }.first
}

Хоча цей код може відповісти на питання, хороша відповідь також повинна пояснити, що робить код і як він вирішує проблему.
BDL

0

Щоб отримати контролер заданого виду, можна використовувати ланцюг UIFirstResponder.

customView.target(forAction: Selector("viewDidLoad"), withSender: nil)

-1

Якщо ваш rootViewController - це UINavigationViewController, який був створений у класі AppDelegate, то

    + (UIViewController *) getNearestViewController:(Class) c {
NSArray *arrVc = [[[[UIApplication sharedApplication] keyWindow] rootViewController] childViewControllers];

for (UIViewController *v in arrVc)
{
    if ([v isKindOfClass:c])
    {
        return v;
    }
}

return nil;}

Там, де вимагається клас контролерів перегляду.

ВИКОРИСТАННЯ:

     RequiredViewController* rvc = [Utilities getNearestViewController:[RequiredViewController class]];

-5

Немає способу.

Те, що я роблю, - передати покажчик UIViewController на UIView (або відповідну спадщину). Вибачте, що не можу допомогти з підходом до цієї проблеми, оскільки не вірю в ІБ.

Щоб відповісти першому коментатору: іноді вам потрібно знати, хто вас зателефонував, оскільки це визначає, що ви можете зробити. Наприклад, у базі даних, можливо, ви мали лише доступ для читання або читання / запис ...


10
Що це означає - "я не вірю в IB"? Я його запустив, він, безумовно, існує.
marcc

5
Вам потрібно краще зрозуміти розваги та абстрагування, особливо щодо англійської мови. Це означає, що мені це не подобається.
Джон Сміт

4
Я не люблю авокадо. Але я думаю, що я можу допомогти комусь зробити гуакамоле. Це можна зробити в ІБ, тому очевидно, що ваша відповідь "немає способу" невірна. Вам подобається IB чи ні, не має значення.
Злочивий кіт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.