防止函数instantiateViewController(withIdentifier :)的运行时崩溃

时间:2016-12-07 05:51:37

标签: ios swift

Swift 3.0

我在这里发现的是UIStoryboard总是返回函数instantiateViewController(withIdentifier:)中的非可选实例。

open class UIStoryboard : NSObject {
   ...
   open func instantiateViewController(withIdentifier identifier: String) -> UIViewController
}

如果我们添加错误的identifier值而没有注意到,则会发生崩溃。 这种情况可能发生在许多故事板中具有大量控制器的复杂项目中,并且错过了控制器StoryboardId

我找到的唯一解决方案是在启动应用程序委托时创建一个临时创建所有控制器的函数(此函数应仅在调试模式下调用)。

func validateControllers() {
    guard let _ = xyzStoryboard.instantiateViewController(withIdentifier: "ABC") as? ABCViewController else {
        fatalError("fail in init controller ABC from storyboard XYZ")
    }
    ...
}

但我想知道我们是否可以采用另一种方法来处理这种情况。或者我应该向Swift团队提出这个问题?谢谢!

3 个答案:

答案 0 :(得分:3)

据我所知,没有解决方案来防止此类崩溃。但这是一件好事,它意味着像这样崩溃,以显示你的代码有问题,你需要修复它们!

答案 1 :(得分:1)

写这个答案只是为了让你知道我们如何处理视图控制器标识符的意外错误,当你尝试使用视图控制器从故事板创建一个ViewController时,这可能会导致错误。标识符。

我们有一个复杂的项目,它有近15-20个ViewControllers,我们没有将它们放在一个故事板中,而是我们在多个故事板上共享这些VC,然后我们创建了一个名为StoryBoardManager的对象,它将创建一个来自各种故事板的VC并将其交给我们。

我们还创建了几个枚举来代表里面的各种storyboard(s)和viewController。

看起来有点像这样,

enum Storyboards : String {
    case main = "Main"
    case signup = "SignUp"
    case discover = "Discover"
    case utility = "Utility"
    case event = "Event"
}

enum ViewControllers : String {
    case login = "login"
    case onBoard = "on_board"
    case signup = "signup"
    case signupNavigation = "signupNavigaitonVC"
    case discoverNavigation = "discoverNavigation"
    case individualProfileSetUp = "individualProfileSetUp"
    case organizationProfileSetUp = "organizationProfileSetUp"
    case discover = "discover"
    case imagePickerMenuVC = "imagePickerMenuVC"
    case eventDiscoverMapNavigation = "eventDiscoverMapNavigationVC"
    case eventDiscoverMapVC = "eventDiscoverMapVC"
    case event = "event"
}

class StoryboardManager {

    static func storyboard(name:Storyboards) -> UIStoryboard {
        let aStoryboard = UIStoryboard(name: name.rawValue, bundle: nil)
        return aStoryboard
    }

    static func viewController(storyboardId:Storyboards, viewControllerId:ViewControllers) -> UIViewController {
        let storyboard = StoryboardManager.storyboard(storyboardId)
        let viewcontroller = storyboard.instantiateViewControllerWithIdentifier(viewControllerId.rawValue)
        return viewcontroller
    }
}

我们主要是为了避免ViewController标识符的拼写错误,这会导致运行时错误。您将把所有viewController标识符添加到ViewControllers enum和storyboard(s)名称到Storyboard枚举作为枚举大小写。每当我们在任何故事板中引入新的故事板或新的ViewController时,我们都更新了这两个枚举,这有助于所有团队成员不要为ViewController标识符输入错误。

然后我们使用下面的代码

创建了ViewController
let loginVC = StoryboardManager.viewController(.main, viewControllerId: .login)

HTH:)

答案 2 :(得分:0)

短暂的简短

目前,我有一个复杂的项目,有5个故事板,有20个控制器。就我而言,使用storyboard segue似乎不是一个好主意。因此,从故事板创建控制器实例并通过代码进行转换要好得多。

但问题是instantiateViewController(withIdentifier:)总是返回non-optional个实例,如果我们设置了无效的identifier(在故事板中没有定义),它就会崩溃。

解决方案(Swift 3.0)

有防止此问题的步骤。

  1. 在* .storyboard中,将控制器类名称用作StoryboardID
  2. 创建UIStoryboard+Ext.swift以定义所有故事板&控制器属于
  3. 创建BaseViewController.swift以从故事板
  4. 定义初始化
  5. 创建函数以初始化所有故事板&控制器,以防止在运行时崩溃。
  6. 如果您想尝试自己,请下载示例代码:https://github.com/nahung89/StoryboardPattern

    详细信息(TL; DR)

    第1步:

    enter image description here

    第2步:

    extension UIStoryboard {
    
        enum Identifier: String {
            case main = "Main"
            case explore = "Explore"
            case search = "Search"
            case profile = "Profile"
        }
    
        static func name(for controller: UIViewController.Type) -> String? {
            var identifier: Identifier?
            switch controller.className {
            case HomeViewController.className:
                identifier = .main
            case ExploreViewController.className:
                identifier = .explore
            case SearchViewController.className:
                identifier = .search
            case ProfileViewController.className:
                identifier = .profile
            default:
                break
            }
            return identifier?.rawValue
        }
    }
    
    extension UIStoryboard {
    
        static func instanceFromIdentifier(_ identifier: Identifier) -> UIStoryboard {
            return UIStoryboard(name: identifier.rawValue, bundle: Bundle.main)
        }
    
        static func instanceFromName(_ name: String) -> UIStoryboard? {
            guard let identifier = Identifier(rawValue: name) else { return nil }
            return instanceFromIdentifier(identifier)
        }
    
    }
    

    第3步:

    extension UIViewController {
    
        var className: String {
            return String(describing: type(of: self))
        }
    
        class var className: String {
            return String(describing: self)
        }
    
    }
    
    class BaseViewController: UIViewController {
    
        static var controllerId: String {
            return String(describing: self) // return slass name, i.e "ExploreViewController"
        }
    
        static func instanceFromStoryboard() -> Self? {
            guard let storyboardName = UIStoryboard.name(for: self) else { return nil }
            return instantiateFrom(storyboardName: storyboardName)
        }
    
        private static func instantiateFrom<VC: UIViewController>(storyboardName: String) -> VC? {
            let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
            let controller = storyboard.instantiateViewController(withIdentifier: controllerId) as? VC
            return controller
        }
    }
    

    第4步

    extension AppDelegate {
        func validateStoryboards() {
            guard
                let _ = UIStoryboard.instanceFromName(UIStoryboard.Identifier.main.rawValue),
                let _ = UIStoryboard.instanceFromName(UIStoryboard.Identifier.explore.rawValue),
                let _ = UIStoryboard.instanceFromName(UIStoryboard.Identifier.profile.rawValue),
                let _ = UIStoryboard.instanceFromName(UIStoryboard.Identifier.search.rawValue)
                else {
                    fatalError("fail to init storyboard by name")
            }
    
            guard let _ = HomeViewController.instanceFromStoryboard(),
                let _ = ExploreViewController.instanceFromStoryboard(),
                let _ = ProfileViewController.instanceFromStoryboard(),
                let _ = SearchViewController.instanceFromStoryboard()
                else {
                    fatalError("fail to init controller from storyboard")
            }
        }
    }