Swift 3中如何实现通用方法检测视图控制器是否来自指定类
适配导航控制器的通用来源检测方案
针对你遇到的问题——新增控制器时重复硬编码判断、编译失败且要适配导航控制器场景,我给你两个递进的解决方案,都是基于Swift 3和Xcode 8.3.3的:
方案一:通用的呈现链查找扩展(解决硬编码+导航适配)
首先,我们给UIViewController写一个扩展,让它能自动遍历呈现链,处理导航控制器的嵌套情况,找到你要的来源控制器:
extension UIViewController { /// 在呈现链中查找指定类型的控制器,自动适配导航控制器 func findPresentingController<T: UIViewController>(ofType type: T.Type) -> T? { var currentPresenter = self.presentingViewController while let presenter = currentPresenter { // 先检查当前控制器是否是目标类型 if let target = presenter as? T { return target } // 如果是导航控制器,先检查它的顶层控制器是否匹配 if let navController = presenter as? UINavigationController { if let topTarget = navController.topViewController as? T { return topTarget } // 不匹配的话,继续往上找导航控制器的呈现者 currentPresenter = navController.presentingViewController } else { // 非导航控制器,直接往上遍历 currentPresenter = presenter.presentingViewController } } return nil } }
然后在你的TestViewController里,就可以用这个方法替代硬编码的isFromA/isFromB:
class TestViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() configureContent() } private func configureContent() { if findPresentingController(ofType: AViewController.self) != nil { loadContentForA() } else if findPresentingController(ofType: BViewController.self) != nil { loadContentForB() } else if findPresentingController(ofType: CViewController.self) != nil { loadContentForC() } else { loadDefaultContent() } } // 以下是各个场景的内容加载方法 private func loadContentForA() { // 展示来自A控制器的数据 } private func loadContentForB() { // 展示来自B控制器的数据 } private func loadContentForC() { // 展示来自C控制器的数据 } private func loadDefaultContent() { // 默认展示内容 } }
这个方案的好处是:
- 新增控制器时,只需要在
configureContent里加一行else if判断,不需要修改核心的查找逻辑 - 自动适配导航控制器场景——不管来源控制器是直接呈现Test,还是被包裹在导航控制器里呈现,都能正确找到
方案二:基于协议的解耦(更优的扩展性)
如果后续还要新增更多控制器,方案一的else if链还是会越来越长。我们可以用协议来解耦,让来源控制器自己负责提供数据,Test控制器只需要拿数据展示:
第一步:定义数据提供协议
// 假设你有一个TestData模型用来承载展示内容 struct TestData { let title: String let content: String // 其他字段根据你的需求定义 } protocol TestContentProvider { func provideTestContent() -> TestData }
第二步:让来源控制器遵守协议
class AViewController: UIViewController, TestContentProvider { func provideTestContent() -> TestData { return TestData(title: "来自A的内容", content: "这是A控制器对应的展示数据") } } class BViewController: UIViewController, TestContentProvider { func provideTestContent() -> TestData { return TestData(title: "来自B的内容", content: "这是B控制器对应的展示数据") } } // 新增C控制器时,只需要让它遵守协议并实现方法即可 class CViewController: UIViewController, TestContentProvider { func provideTestContent() -> TestData { return TestData(title: "来自C的内容", content: "这是C控制器对应的展示数据") } }
第三步:修改Test控制器的内容配置逻辑
我们可以扩展之前的查找方法,让它支持协议类型的查找:
extension UIViewController { func findContentProvider() -> TestContentProvider? { var currentPresenter = self.presentingViewController while let presenter = currentPresenter { if let provider = presenter as? TestContentProvider { return provider } if let navController = presenter as? UINavigationController { if let topProvider = navController.topViewController as? TestContentProvider { return topProvider } currentPresenter = navController.presentingViewController } else { currentPresenter = presenter.presentingViewController } } return nil } }
然后在Test控制器里:
class TestViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() configureContent() } private func configureContent() { guard let contentProvider = findContentProvider() else { loadDefaultContent() return } let content = contentProvider.provideTestContent() // 用content配置你的UI组件,比如label.text = content.title } private func loadDefaultContent() { // 默认展示内容 } }
这个方案的优势是:
- 新增控制器时,Test控制器不需要做任何修改,只需要新控制器遵守协议即可
- 数据提供逻辑完全封装在来源控制器里,符合单一职责原则
- 彻底告别硬编码的类型判断,代码更简洁易维护
关于你之前编译失败的可能原因
你之前写通用方法编译失败,大概率是以下情况之一:
- 没有正确处理导航控制器的可选值(比如直接强制解包
navigationController!.topViewController) - 通用方法的类型约束写错(比如没有指定
T: UIViewController) - 递归遍历呈现链时没有正确终止条件,导致编译器报错
上面的两个方案都已经规避了这些问题,你可以直接复用。
内容的提问来源于stack exchange,提问作者Tim Fuqua
相关产品推荐
相关产品推荐

