如何在Swift中获取UIStoryboard数组?

时间:2017-06-02 04:28:10

标签: ios swift xcode uistoryboard

我发现我的故事板变得非常复杂,并决定将其拆分为不同的故事板。但是我希望能够自由地实例化UIViewController,无论我把视图控制器放在哪里。这样,我可以将View Controller从Storyboard移动到Storyboard,而不需要记住我把View Controller放在哪里,我也是不必更新代码,因为它们都使用相同的代码来实例化具有该名称的相同View Controller,无论它位于何处。

因此,我想创建一个UIViewController的扩展,如下所示:

extension UIViewController {

    func instantiate (named: String?, fromStoryboard: String? = nil) -> UIViewController? {
    guard let named = named else { return nil; }
    if let sbName = fromStoryboard {
        let sb = UIStoryboard(name: sbName, bundle: nil);
        return sb.instantiateViewController(withIdentifier: named);
    }
    else {
        for sb in UIStoryboard.storyboards {
            if let vc = sb.instantiateViewController(withIdentifier: named) {
                return vc;
            }
        }
    }
    return nil;
}

问题是,我找不到属性/方法来返回任何地方的.storyboards等故事板实例列表。这有什么解决方法吗?我知道我可能有一个静态的故事板名称列表,但是这样,扩展名不会是动态的,也不依赖于项目。

有人可以帮忙吗?感谢。

修改

结合accepted answerhere的答案来安全地实例化viewcontroller(如果没有找到则返回nil),这是我的代码:

UIStoryboard + Storyboards.swift:

extension UIStoryboard {

    static var storyboards : [UIStoryboard] {
        let directory = Bundle.main.resourcePath! + "/Base.lproj"
        let allResources = try! FileManager.default.contentsOfDirectory(atPath: directory)
        let storyboardFileNames = allResources.filter({ $0.hasSuffix(".storyboardc" )})
        let storyboardNames = storyboardFileNames.map({ ($0 as NSString).deletingPathExtension as String })
        let storyboardArray = storyboardNames.map({ UIStoryboard(name: $0, bundle: Bundle.main )})
        return storyboardArray;
    }

    func instantiateViewControllerSafe(withIdentifier identifier: String) -> UIViewController? {
        if let availableIdentifiers = self.value(forKey: "identifierToNibNameMap") as? [String: Any] {
            if availableIdentifiers[identifier] != nil {
                return self.instantiateViewController(withIdentifier: identifier)
            }
        }
        return nil
    }

}

的UIViewController + Instantiate.swift:

extension UIViewController {

    static func instantiate (named: String?, fromStoryboard: String? = nil) -> UIViewController? {
        guard let named = named else { return nil; }
        if let sbName = fromStoryboard {
            let sb = UIStoryboard(name: sbName, bundle: nil);
            return sb.instantiateViewControllerSafe(withIdentifier: named);
        }
        else {
            for sb in UIStoryboard.storyboards {
                if let vc = sb.instantiateViewControllerSafe(withIdentifier: named) {
                    return vc;
                }
            }
        }
        return nil;
    }
}

限制所有故事板文件必须位于Base.lproj文件夹。

我知道,就运行时间而言,它不是最有效的代码。但就目前而言,它很容易被理解,我可以忍受这个。 :)感谢所有帮助过的人!

4 个答案:

答案 0 :(得分:3)

故事板通常设置为本地化,因此您应该在捆绑包的资源目录的Base.lproj子目录中查找它们。故事板被编译成扩展名为.storyboardc的文件,因此您应该查找具有该后缀的文件并将其删除。

let directory = Bundle.main.resourcePath! + "/Base.lproj"
let allResources = try! FileManager.default.contentsOfDirectory(atPath: directory)
let storyboardFileNames = allResources.filter({ $0.hasSuffix(".storyboardc" )})
let storyboardNames = storyboardFileNames.map({ ($0 as NSString).deletingPathExtension as String })
Swift.print(storyboardNames)

如果您已创建特定于设备的故事板(通过向故事板文件名添加~ipad~iphone),那么您还需要删除这些后缀并消除重复项。

请注意,特定编译的故事板后缀不是SDK的文档部分,因此可能会在未来版本的iOS / Xcode中更改。

答案 1 :(得分:1)

在你的情况下,如果你把故事板放在一起就可能会更清洁。并以编程方式创建所有视图控制器。

答案 2 :(得分:0)

不要那样做。

相反,请根据需要使用Storyboard References来转换到不同的故事板。

答案 3 :(得分:0)

如其他地方所述,在编译和运行应用程序时,情节提要板将编译为不透明的(即二进制文件,未记录).storyboardc文件。 UIStoryboard API仅允许实例化初始View Controller或一个(已知的)命名实例,因此没有幼稚的方法来为“未知”情节提要元素询问您的应用。实例化未显示的UI元素也可能会有副作用。但是...

如果您将.storyboard文件放入“复制文件”构建阶段,则可以查询XML并进行恢复,例如UIViewController / UIView标识符,自定义属性等。IBDecodable似乎可以做到这一点。在安装方面(cocoapods)仅适用于MacOS,但如果将其嵌入到iOS应用中(安装SWXMLHash先决条件并克隆IBDecodable / git-submodule,则可以成功运行)。我可能会忽略效率和本地化问题,但是对于我的用例(在未明确说明的情况下,从自定义属性构建入职弹出窗口)就可以了。

要回答更具体的问题,查询故事板中的ID将允许该应用(通过字典或类似方法)查找并实例化一个View Controller,无论它在视觉上存储在哪里。

例如:

Bundle.main.urls(forResourcesWithExtension: "storyboard", subdirectory: nil)?
    .forEach({ (url) in
        do {
            let file = try StoryboardFile(url: url)
            if !file.document.launchScreen {
                file.document.scenes?.forEach({ (scene) in
                    scene.viewController?.viewController.rootView?.subviews?
                        .forEach({ (view) in
                            // Do stuff...
                        })
                    })
                }
            } catch { /* ... */ }
        })