给定一个视图,如何获取它对应的视图控制器?

168

我有一个指向UIView的指针。如何访问它的UIViewController[self superview]是另一个UIView,但不是UIViewController,对吗?


我认为这个线程有答案:如何在iPhone上从UIView获取到UIViewController? - Ushox
基本上,我正在尝试在我的视图被解除显示时调用我的viewController的viewWillAppear。视图是由视图本身解除显示的,它检测到一个轻拍并调用[self removeFromSuperview]。viewController并没有自己调用viewWillAppear / viewWillDisappear / viewDidAppear / viewDidDisappear。 - mahboudz
我的意思是当视图被解除时,我正在尝试调用viewWillDisappear。 - mahboudz
1
可能是重复的问题:如何从 UIView 获取 UIViewController? - Efren
12个回答

325

根据 UIResponder 的文档,nextResponder 方法不会自动存储或设置下一个响应者,而是默认返回 nil。子类必须重写此方法来设置下一个响应者。 UIView 实现此方法,通过返回管理它的 UIViewController 对象(如果有)或其父视图(如果没有)来设置下一个响应者;UIViewController 通过返回其视图的父视图来实现该方法;UIWindow 返回应用程序对象,UIApplication 返回 nil。

因此,如果您递归查找视图的 nextResponder 直到它是 UIViewController 类型,则可以获得任何视图的父视图控制器。

请注意,它仍然可能 没有 父视图控制器。但这只有在视图不属于视图控制器的视图层次结构时才会发生。

Swift 3 和 Swift 4.1 扩展:

extension UIView {
    var parentViewController: UIViewController? {
        // Starts from next (As we know self is not a UIViewController).
        var parentResponder: UIResponder? = self.next
        while parentResponder != nil {
            if let viewController = parentResponder as? UIViewController {
                return viewController
            }
            parentResponder = parentResponder?.next
        }
        return nil
    }
}

Swift 2扩展:

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

Objective-C 类别(category):

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

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

这个宏避免了类别污染:

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

只有一件事:如果你担心类别污染,那么就将其定义为静态函数而不是宏。此外,粗暴的类型转换是危险的,而且宏可能不正确,但我不确定。 - mojuba
宏是有效的,我个人只使用宏版本。我开始很多项目,并在每个地方放置一个头文件,其中包含许多这些宏实用程序函数。这样可以节省时间。如果您不喜欢宏,可以将其改编为函数,但静态函数似乎很繁琐,因为您必须在要使用它的每个文件中都放置一个。似乎您希望在头文件中声明非静态函数,并在某个地方定义它? - mxcl
很高兴能够避免一些委托或通知。谢谢! - Ferran Maylinch
你应该将它作为 UIResponder 的扩展 ;)。非常有启发性的文章。 - ScottyBlades

57

@andrey 的一行回答(在Swift 4.1中测试):

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

用法:

 let vc: UIViewController = view.parentViewController

如果扩展(extension)与您的UIView在同一个文件中,那么parentViewController不能被定义为public,您可以将其设置为fileprivate,虽然它可以编译但是无法正常工作! - user5281587
它正常工作。我在通用头文件.xib中使用了它来执行返回按钮操作。 - McDonal_11
在 Xamarin 中: public static class Extensions { public static UIViewController ParentViewController(this UIResponder view) => (view.NextResponder as UIViewController) ?? view.NextResponder?.ParentViewController(); } - Softlion

44

是的,superview 是包含你的视图的视图。你的视图不应该知道它的视图控制器是哪一个,因为那会违反 MVC 原则。

另一方面,控制器知道它所负责的视图 (self.view = myView),通常这个视图会委托方法 / 事件处理给控制器。

通常情况下,你应该有一个指向你的控制器的指针,控制器再执行一些控制逻辑或者将某些东西传递给它的视图。


27
我不确定这是否违反了MVC原则。在任何时候,一个视图只有一个视图控制器。能够获取它以便将消息传回给它应该是一项自动功能,而不是需要付出努力才能实现的功能(例如添加属性来跟踪)。对于视图也可以说同样的事情:为什么你需要知道你是哪个子视图?或者是否有其他兄弟视图。然而,有方法可以获取这些对象。 - mahboudz
你对视图的看法有一定道理,了解其父级视图并不是非常清晰的设计决策,但已经建立了一些操作,可以直接使用superview成员变量(检查父类型、从父级中删除等)。最近使用PureMVC后,我对设计抽象化变得更加挑剔 :) 我会将iPhone的UIView和UIViewController类与PureMVC的View和Mediator类进行比较 - 大多数情况下,View类不需要知道其MVC处理程序/接口(UIViewController/Mediator)。 - Dimitar Dimitrov

25
仅为调试目的,您可以在视图上调用_viewDelegate来获取它们的视图控制器。这是私有API,因此不适合App Store,但对于调试非常有用。
其他有用的方法:
  • _viewControllerForAncestor - 获取在父视图链中管理视图的第一个控制器。(感谢n00neimp0rtant)
  • _rootAncestorViewController - 获取祖先控制器,其视图层次结构设置在当前窗口中。

这正是我来到这个问题的原因。显然,“nextResponder”也可以做同样的事情,但我很感激这个答案提供的见解。我理解并喜欢MVC,但调试是一种不同的动物! - mbm29414
4
看起来这只在视图控制器的主视图上起作用,而不是其任何子视图上起作用。"_viewControllerForAncestor" 将遍历 superviews 直到找到属于视图控制器的第一个视图。 - n00neimp0rtant
谢谢@n00neimp0rtant!我正在点赞这个答案,以便人们能看到你的评论。 - eyuelt
更新答案,加入更多的方法。 - Léo Natan
1
这在调试时非常有用。 - evanchin

10
为了获取具有UIView的UIViewController的引用,您可以创建UIResponder的扩展(它是UIView和UIViewController的超类),它允许通过响应者链向上移动,从而到达UIViewController(否则返回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

7

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

最优雅的答案! - mojuba

3
如果您对代码不熟悉,但想要找到与给定视图对应的ViewController,则可以尝试以下步骤:
  1. 在debug模式下运行应用
  2. 导航到屏幕
  3. 启动View inspector
  4. 获取您想要查找的View(最好是子View)
  5. 从右侧面板获取地址(例如0x7fe523bd3000)
  6. 在debug控制台中开始编写命令:
    po (UIView *)0x7fe523bd3000
    po [(UIView *)0x7fe523bd3000 nextResponder]
    po [[(UIView *)0x7fe523bd3000 nextResponder] nextResponder]
    po [[[(UIView *)0x7fe523bd3000 nextResponder] nextResponder] nextResponder]
    ...
通常情况下,您将获得UIView,但有时会出现基于UIViewController的类。

1

我认为你可以将tap事件传递给视图控制器,并让它处理。这是更可接受的方法。至于从视图中访问视图控制器,你应该保持对视图控制器的引用,因为没有其他方式。参见此线程,它可能有所帮助:从视图访问视图控制器


如果您有一些视图,其中一个关闭了所有视图,并且您需要调用viewWillDisappear,那么让该视图检测到点击是否比将点击传递给视图控制器并让视图控制器检查所有视图以查看哪个被点击更容易呢? - mahboudz

1
更多类型安全的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
唉,除非你对视图进行子类化并提供一个实例属性或类似的东西来存储视图控制器引用,否则这是不可能的,一旦将视图添加到场景中...
在大多数情况下,解决此帖子的原始问题非常容易,因为大多数视图控制器都是程序员添加任何子视图到ViewController的View的责任人所熟知的实体;-)这就是我猜苹果从未费心添加该属性的原因。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接