Добраться до UIViewController из UIView?


185

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

Ответы:


46

Так как это был принятый ответ в течение долгого времени, я чувствую, что должен исправить это с лучшим ответом.

Некоторые комментарии о необходимости:

  • Ваше представление не должно иметь прямого доступа к контроллеру представления.
  • Вместо этого представление должно быть независимым от контроллера представления и иметь возможность работать в разных контекстах.
  • Если вам необходимо, чтобы представление взаимодействовало с контроллером представления, рекомендуемый способ и то, что Apple делает через Какао, - это использование шаблона делегата.

Пример того, как это реализовать:

@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
Это очень плохой совет. Вы не должны ссылаться на контроллер вида из вида
Филипп Лейберт

6
@MattDiPasquale: да, это плохой дизайн.
Филипп Лейберт

23
@Phillipe Leybaert Мне любопытно узнать ваши мысли о лучшем дизайне события нажатия кнопки, которое должно вызвать какое-то действие на контроллере, без представления, содержащего ссылку на контроллер. Спасибо
Джонатан Хорсман

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

11
Все это бс о "ты не должен этого делать" это просто так. Если представление хочет знать о своем контроллере представления, это должен решить программист. период.
Даниэль Канаан

203

Используя пример, опубликованный Brock, я изменил его так, чтобы он был категорией 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, чтобы отобразить пользовательское всплывающее окно, которое создается
подпредставлением

2
Разве это не плохая практика для UIView выдвигать модальное представление? Я делаю это прямо сейчас, но я чувствую, что это неправильно,
Ван Ду Тран

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

9
Мне просто нравится, как много вопросов SO получил «мудрец», академик, принял ответ с несколькими баллами и вторым ответом, который практичен, грязен и противоречит правилам, в десять раз больше баллов :-)
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 , ИМХО, это недостаток дизайна. Представление не должно знать о контроллере.


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

2
Это идея шаблона Observer. Наблюдаемый (вид в данном случае) не должен знать о своих наблюдателях напрямую. Наблюдатель должен получать только те
ответные

12

Я изменил de answer, чтобы я мог передать любое представление, кнопку, метку и т. Д., Чтобы получить его родитель 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 docs, так [[self navigationController] view]как это «основной» (под) вид окна, необходимо установить rootViewControllerсвойство окна, которое немедленно navigationControllerконтролирует «основной» вид.
апреля

6

Хотя эти ответы являются технически правильными, включая Ushox, я думаю, что одобренный способ состоит в том, чтобы внедрить новый протокол или повторно использовать существующий. Протокол изолирует наблюдателя от наблюдаемого, что-то вроде вставки почтового отсека между ними. По сути, это то, что делает Габриэль через вызов метода pushViewController; представление «знает», что это правильный протокол, чтобы вежливо попросить ваш navigationController выдвинуть представление, поскольку viewController соответствует протоколу navigationController. Хотя вы можете создать свой собственный протокол, просто используйте пример Габриэля и повторно используйте протокол UINavigationController.


6

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

В то время как это работает отлично в IPad (в UIPopoverControllerсамых подарках, для них не нуждается указания на 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

Я не думаю, что это «плохая» идея выяснить, кто является контроллером представления в некоторых случаях. Что может быть плохой идеей, так это сохранить ссылку на этот контроллер, поскольку он может меняться так же, как меняются суперпредставления. В моем случае у меня есть геттер, который пересекает цепочку респондента.

//.час

@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

Простейший цикл do while для поиска viewController.

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

    do
    {
        nextResponder = [nextResponder nextResponder];

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

    } while (nextResponder != nil);

    return nil;
}

4

Swift 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«S 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

Это, конечно, плохая идея и неправильный дизайн, но я уверен, что нам всем понравится решение Swift с лучшим ответом, предложенным @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немного). Так что, если «swifty» означает «более функциональный», то я думаю, что это более «swifty».
Трэвис Григгс

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Теперь не нужно ключевое слово now

Решение 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, если вы знаете иерархию self (UIView), вы также можете использовать: id nextResponder = [[self superview] nextResponder];...


0

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

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

2
Пожалуйста , перепишите код следующим образом : EAGLViewController *vc = [(EAGLAppDelegate *)[UIApplication sharedApplication].delegate viewController];.
Джонатан Стерлинг

1
Проблема не в точечном синтаксисе, а в типах. В Objective C объявить объект, который вы пишете ClassName *object- со звездочкой.
апреля

Упс ... это на самом деле то, что у меня было, но 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;}

Где c требуется вид контроллеров класса.

ИСПОЛЬЗОВАНИЕ:

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

-5

Выхода нет.

Что я делаю, это передаю указатель UIViewController на UIView (или соответствующее наследование). Извините, я не могу помочь с подходом IB к проблеме, потому что я не верю в IB.

Чтобы ответить первому комментатору: иногда вам нужно знать, кто вам звонил, потому что он определяет, что вы можете сделать. Например, с базой данных вы можете иметь только права на чтение или чтение / запись ...


10
Что это значит - «Я не верю в IB»? Я запустил его, он, безусловно, существует.
2010 г.

5
Вам нужно лучше понимать веселье и абстракцию, особенно в отношении английского языка. Это значит, что мне это не нравится.
Джон Смит

4
Я не люблю авокадо. Но держу пари, что могу помочь кому-нибудь приготовить гуакамоле. Это может быть сделано в IB, поэтому, очевидно, ваш ответ «нет пути» неверен. Нравится ли вам IB или нет, не имеет значения.
Feloneous Cat
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.