Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Custom segue to a different storyboard

Question:

How might one write a custom segue that would allow you to embed view controllers from a different storyboard?

Context:

I am trying to write a custom segue with which I can link from one storyboard to another. A good article on atomicobject.com illustrates how to create a segue that originates from a button / event etc. Translated into swift, and allowing for non UINavigationControllers, the code looks like:

public class SegueToStoryboard : UIStoryboardSegue {

    private class func viewControllerInStoryBoard(identifier:String, bundle:NSBundle? = nil)
        -> UIViewController?
    {
        let boardScene = split(identifier, { $0 == "." }, maxSplit: Int.max, allowEmptySlices: false)
        switch boardScene.count {
        case 2:
            let sb = UIStoryboard(name: boardScene[0], bundle: bundle)
            return sb.instantiateViewControllerWithIdentifier(boardScene[1]) as? UIViewController
        case 1:
            let sb = UIStoryboard(name: boardScene[0], bundle: bundle)
            return sb.instantiateInitialViewController() as? UIViewController
        default:
            return nil
        }
    }

    override init(identifier: String!,
        source: UIViewController,
        destination ignore: UIViewController) {
        let target = SegueToStoryboard.viewControllerInStoryBoard(identifier, bundle: nil)
        super.init(identifier: identifier, source: source,
            destination:target != nil ? target! : ignore)
    }

    public override func perform() {
        let source = self.sourceViewController as UIViewController
        let dest = self.destinationViewController as UIViewController
        source.addChildViewController(dest)
        dest.didMoveToParentViewController(source)
        source.view.addSubview(dest.view)

//      source.navigationController?.pushViewController(dest, animated: true)
    }
}

Problem:

The problem that I am having with both their Obj-C and the above Swift code is that when you try to use the via a container view (with semantics of an embed segue - starting with an embed segue, deleting the segue, and then use the above custom segue), it crashes before ever calling the segue code with the following method-not-found error:

*** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<UIStoryboardSegueTemplate 0x7ffc8432a4f0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key containerView.'

I have tried to inspect the address listed but get no meaningful results. I do the see the bold statement that it expecting the containerView but don't know how one might isolate, satisfy, and/or work around this problem.

Summary:

My end goal is to embed view controllers defined in separate storyboards to facilitate collaboration and testing without having to write additional code (a non invasive solution). Does anyone have any insight into how to accomplish this greater task? I could fall back to hybrid approach of calling performSegue, but would like to find a single, contained, and complete solution. The above code gets there for event driven (buttons etc) segues, but not with the embed segue.

Any input is appreciated, thanks in advance.

like image 318
Chris Conover Avatar asked Oct 04 '14 05:10

Chris Conover


2 Answers

Your approach works fine for custom segues to push / display modally other view controllers but not for embed segues. The reason for this is that the "Embed" segue is not a subclass of UIStoryboardSegue but inherits from UIStoryboardSegueTemplate, which is a private API.

Unfortunately I couldn't find a better way to achieve what you want than with the hybrid approach.

like image 96
zmit Avatar answered Oct 23 '22 01:10

zmit


My way is to link the containerView and delete the viewDidLoad segue from it. and manually call the segue on viewdidLoad

public protocol EmbeddingContainerView {
    var containerView: UIView! { get set }
}

public class CoreSegue: UIStoryboardSegue {
    public static func instantiateViewControllerWithIdentifier(identifier: String) -> UIViewController {
        let storyboard = UIStoryboard(name: "Core", bundle: NSBundle(forClass: self))
        let controller = storyboard.instantiateViewControllerWithIdentifier(identifier) as! UIViewController
        return controller
    }

    var isPresent = false
    var isEmbed = false
    override init!(identifier: String?, source: UIViewController, destination: UIViewController) {
        if var identifier = identifier {
            if identifier.hasPrefix("present ") {
                isPresent = true
                identifier = identifier.substringFromIndex(advance(identifier.startIndex, count("present ")))
            }
            if identifier.hasPrefix("embed ") {
                isEmbed = true
                identifier = identifier.substringFromIndex(advance(identifier.startIndex, count("embed ")))
            }
            let controller = CoreSegue.instantiateViewControllerWithIdentifier(identifier)
            super.init(identifier: identifier, source: source, destination: controller)
        } else {
            super.init(identifier: identifier, source: source, destination: destination)
        }
    }

    public override func perform() {
        if let source = sourceViewController as? UIViewController, dest = destinationViewController as? UIViewController {
            if isPresent {
                let nav = UINavigationController(rootViewController: dest)
                nav.navigationBarHidden = true // you might not need this line
                source.presentViewController(nav, animated: true, completion: nil)
            } else if isEmbed {
                if let contentView = (source as? EmbeddingContainerView)?.containerView {
                    source.addChildViewController(dest)
                    contentView.addSubview(dest.view)
                    dest.view.fullDimension() // which comes from one of my lib
                }
            } else {
                source.navigationController?.pushViewController(destinationViewController as! UIViewController, animated: true)
            }
        }
    }
}

and later in your code:

class MeViewController: UIViewController, EmbeddingContainerView {
    @IBOutlet weak var containerView: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()
        performSegueWithIdentifier("embed Bookings", sender: nil)
    }
}
like image 29
rexsheng Avatar answered Oct 23 '22 01:10

rexsheng