从UIView到UIViewController?


Answers:


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,2010年

6
@MattDiPasquale:是的,这是糟糕的设计。
菲利普·莱伯特

23
@Phillipe Leybaert我很好奇您对按钮单击事件的最佳设计的想法,该事件应该在控制器上调用某些操作,而视图没有引用控制器。谢谢
Jonathon Horsman

3
@PhilippeLeybaert苹果的示例项目倾向于演示特定API功能的用法。我很多人都提到牺牲好的或可扩展的设计,以提供对该主题的简洁演示。我花了很长时间才意识到这一点,尽管这很有意义,但我感到很不幸。我认为许多开发人员都将这些务实的项目作为Apple最佳设计实践的指南,但我可以肯定它们不是。
Benjohn 2014年

11
关于“您不应该这样做”的所有这些都是这样。如果一个视图想知道其视图控制器,则由程序员决定。期。
Daniel Kanaan

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子类时。
Phil M

真棒,我不得不访问我的ViewController显示这是由一个子视图中创建一个自定义弹出
aryaxt

2
UIView推送模式视图不是不好的做法吗?我现在这样做的权利,但我觉得这是不该做..正确的事
凡杜陈德良

6
Phil,您的自定义视图应调用一个委托方法,该方法将被视图控制器侦听,然后从那里推送。
malhal 2013年

9
我只是喜欢有多少个SO问题得到了一个“智者”,学术上可接受的答案,只有几点,而第二个答案却是实用,肮脏和违反规则的,得分是十倍:-)
hariseldon78

114

UIView是的子类UIResponder。用返回的实现UIResponder布置方法。重写此方法,如下所述(出于某种原因而不是in中所述):如果视图具有视图控制器,则由返回。如果没有视图控制器,则该方法将返回超级视图。-nextRespondernilUIViewUIResponderUIView-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。Phil M具有递归的答案是必须走的路。
奥利维尔

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方法之一。
mahboudz

2
这是观察者模式背后的想法。被观察者(在这种情况下为View)不应直接知道其观察者。观察员应该只收到回调,他们感兴趣的问题。
Ushox

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文档,既然[[self navigationController] view]是窗口的“主”(子)视图,则必须将窗口的rootViewController属性设置为navigationController立即控制“主”视图的窗口。
2011年

6

尽管这些答案在技术上都是正确的,包括Ushox,但我认为批准的方法是实施新协议或重用现有协议。协议使观察者与被观察者隔离,就像在他们之间放置一个邮件槽一样。实际上,这就是Gabriel通过pushViewController方法调用所做的;该视图“知道”礼貌地要求您的navigationController推送视图是正确的协议,因为viewController符合navigationController协议。虽然您可以创建自己的协议,但是仅使用Gabriel的示例并重新使用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

我认为在某些情况下找出谁是视图控制器并不是一个“坏”主意。一个坏主意是保存对该控制器的引用,因为它可能会随着超级视图的更改而更改。就我而言,我有一个遍历响应者链的吸气剂。

//。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的最简单的while循环。

-(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。为此,我需要访问viewUIViewController


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

我并不是说这是正确的方法,这似乎比运行第一个响应程序链还干净。我使用此代码在UITableView上实现了UIMenuController,并将事件传递回UIViewController,以便可以对数据进行处理。


3

这肯定是一个坏主意和错误的设计,但是我敢肯定我们都可以享受@Phil_M提出的最佳答案的Swift解决方案:

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

我喜欢这个答案。我不确定它是否更快捷。Swift是另一种无法决定要与语言结合的多种范式。在这种情况下,您将很好地演示一种更实用的方法(sequence即位)。因此,如果“ 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现在不需要关键字🤓

解决方案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
问题不在于点语法,而在于类型。在目标C中声明您编写的对象ClassName *object-带星号。
2011年

糟糕...这实际上是我所拥有的,但是StackOverflow HTML小部件看起来好像认为星号表示斜体...我将其更改为代码块,现在可以正确显示。谢谢!
gulchrider 2011年

0

我认为在某些情况下被观察者需要告知观察者。

我看到一个类似的问题,其中UIViewController中的UIView响应情况,它需要首先告诉其父视图控制器隐藏后退按钮,然后在完成时告诉父视图控制器它需要将自己弹出堆栈。

我一直在与代表们进行尝试,但没有成功。

我不明白为什么这应该是个坏主意?


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是在AppDelegate类中设置的UINavigationViewController,则

    + (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”?我已经启动了它,它确实存在。
marcc 2010年

5
您需要更好地掌握乐趣和抽象性,尤其是在英语方面。这意味着我不喜欢它。
约翰·史密斯,2010年

4
我不喜欢鳄梨。但我敢打赌我可以帮助某人制作鳄梨调味酱。可以在IB中完成,因此显然您对“没有办法”的回答是错误的。您是否喜欢IB都无关紧要。
Feloneous Cat
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.