I have a simple extension for UISplitViewController:
extension UISplitViewController {
var masterViewController: UIViewController? {
return (viewControllers.first as? UINavigationController)?.topViewController
}
}
Within some UIViewController I did connect modal segue to my UISplitViewController. When segue is being performed then I prepare my destinationViewController like this:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let searchResultsViewController = (segue.destinationViewController as? UISplitViewController)?.masterViewController as? SearchResultsViewController {
searchResultsViewController.mode = mode
}
}
And it DOESNT WORK because of error:
*** Terminating app due to uncaught exception 'NSGenericException', reason: 'The content view controller argument must be the root of its associated view controller hierarchy.'
However when I remove extension for UISplitViewController and prepare destinationViewController like this:
if let searchResultsViewController = ((segue.destinationViewController as? UISplitViewController)?.viewControllers.first as? UINavigationController)?.topViewController as? SearchResultsViewController {
//prepare controller
}
IT WORKS
What is the reason that it is not working in first case?
Try changing the name of your computed variable from masterViewController to something else. It seems you are overriding an internal property of the UISplitViewController.
This works and is not called during the setup of the UISplitViewController.
var masterVC: UIViewController? {
return (viewControllers.first as? UINavigationController)?.topViewController
}
This is called by something in the SDK and causes a crash.
var masterViewController: UIViewController? {
return (viewControllers.first as? UINavigationController)?.topViewController
}
Related
I have a View controller that contains 2 Container Views. One of them has a scroll-view one of them is just a small view.
Now, I want to communicate between all 3, the way I'm doing it is by using the main ViewController as a delegate for the other 2. I originally didnt know how to set them as delegates, as there is no transition or presentation of the other ones (they're just there)
After asking here a few months back I got the following answer:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (segue.identifier == "embedSegue") {
resultsVC = (segue.destination as! GoalsVC)
resultsVC.calcDelegate = self
}
}
However, I dont know how to do this for BOTH contained views. they are both using the same segue so I cant have them both have the same ViewController.
I was thinking using storyboard IDs but how do I reference them in the prepareforsegue?
would it be something like
if (segue.identifier == "embedSegue" && storyboardidentifier = "myVC1") {
resultsVC = (segue.destination as! GoalsVC)
resultsVC.calcDelegate = self
} else if (segue.identifier == "embedSegue" && storyboardidentifier = "myVC2") {
otherVC = (segue.destination as! NewVC)
resultsVC.calcDelegate = self
}
Except I dont know the exact code to reference the storyboard
If you use ? instead of ! you can use the success of the conversion to indicate whether you have the correct viewcontroller. The code would be as follows
if segue.identifier == "embedSegue" {
if let resultsVC = segue.destination as? GoalsVC {
resultsVC.calcDelegate = self
}
if let otherVC = segue.destination as? NewVC {
resultsVC.calcDelegate = self
}
}
You could simplify this code even further by delegating the setting of the delegate. Setup a new delegate, ensure your two embedded viewcontrollers conform to it. In the code below I have assumed the delegate you have already written is called CalcDelegate.
protocol CalcDelegateDelegate {
var calcDelegate : CalcDelegate? {get set}
}
class GoalsVC : UIViewController, CalcDelegateDelegate {
var calcDelegate: CalcDelegate? = nil
}
class NewVC : UIViewController, CalcDelegateDelegate {
var calcDelegate: CalcDelegate? = nil
}
Then your prepare code is simplified to
if segue.identifier == "embedSegue" {
if var delegate = segue.destination as? CalcDelegateDelegate {
delegate.calcDelegate = self
}
}
Any new view controller that is embedded in your main controller just needs to conform to the delegate and it automatically gets the calcDelegate and can communicate with your other view controllers.
For some reason this prepare code for a segue crashes because newGridViewController is nil. Any ideas?
override func prepare(for segue: UIStoryboardSegue,sender: Any?) {
if segue.identifier == "Grid" {
if let newGridViewController = segue.destination as? GridViewController
if savePhotoWithSlicer.isOn {
newGridViewController?.savePhotoWithSlicer = true
if (newGridViewController?.savePhotoWithSlicer)! { print("TRUE") }
}
}
}
I guess the problem is let newGridController = segue.destination as? GridViewController
My thought is your GridViewController is nil. Since you force unwrap newGridViewController in this line if (newGridViewController?.savePhotoWithSlicer)! { print("TRUE") } the code crashes.
Show us some more code to give you detailed explanations.
You don't say what the exception is, but I imagine that it is "unexpectedly found nil..." caused by the force unwrap on the second last line,
I suspect that the root cause is the conditional downcast to GridViewController failed, so newGridViewController is actually nil. Since you use a conditional unwrap everywhere except the second last line, you don't get a crash until that point.
A better structure is to use an if let... conditional downcast:
if let newGridViewController = segue.destination as? GridViewController {
newGridViewController.savePhotoWithSlicer = savePhotoWithSlicer.isOn
if newGridViewController.savePhotoWithSlicer {
print("TRUE")
}
}
This will prevent the crash, but it probably still won't print "TRUE" as I strongly suspect that segue.destination is not a GridViewController - You need to check your storyboard and make sure that you have the right custom class for your scene.
UPDATE
Since you have now clarified that the segue leads to a navigation controller that embeds the GridViewController you can use this to get the desired view controller:
override func prepare(for segue: UIStoryboardSegue,sender: Any?) {
if segue.identifier == "Grid" {
if let navController = segue.destination as? UINavigationController {
if let newGridViewController = navController.viewControllers.first as? GridViewController {
newGridViewController.savePhotoWithSlicer = savePhotoWithSlicer.isOn
if newGridViewController.savePhotoWithSlicer {
print("TRUE")
}
}
}
}
}
Objective c and Swift will happily let you "assign" a value to nil, and that's probably what you are doing. Just b/c you assign "true" to nil, doesn't mean you actually did anything ("true" just gets ignored). So when you force unwrap the nil, you still crash.
I'm new to Swift and I was trying to make a side menu base on the tutorial that I was following but I ran across with this error. "Could not cast value of type 'UINavigationController' (0x1026054a8) to 'jacrs_ios.TopViewController' (0x1009ac510)." I noticed that there were same problems already existing here but I still dont understand. I'm gonna included the current storyboard in case I did something wrong. Thanks.
ViewController Error
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let topViewController = segue.destination as! TopViewController
topViewController.stringPassed = userName.text!
}
Storyboard
Try this
if let navigationController = segue.destination as? UINavigationController
{
let topViewController = navigationController?.topViewController as! TopViewController
topViewController.stringPassed = userName.text!
}
I currently have a button set to go to a TableViewController but decided I wanted to embed that TableViewController in a TabBarController. I am running into an error while trying to pass it to the UITabBarController.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "showListSegue") {
let tabBarController = segue.destinationViewController as! UITabBarController
tabBarController.selectedIndex = 0 // choose which tab is selected
let des = tabBarController.selectedViewController!
des.jsonfile = self.jsonfile
}
}
In the last line of code, des.jsonfile = self.jsonfile, I am getting the error...
Value of type 'UIViewController' has no member 'jsonfile'
I am trying to pass the jsonfile to the TableViewController which is now embedded in the UITabBarController. How can this be done? I have this variable in the TableViewController is was getting passed to but now that I threw this TabBarController in the mix I am getting all confused.
I also tried to create a Cocoa file for the TabBarcontroller and set the variable var jsonfile : JSON! but that did not work either. (That is the variable in my TableViewController that I want to pass it to) Please help. Thank you.
You need to let the compiler know that selectedViewController is a type with the member jsonFile. Also, you should check that it actually is existing and of the correct class at runtime. Here's the kind of pattern you should be using:
class JSONDisplayController: UIViewController {
var jsonfile: String
}
func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "showListSegue") {
guard let tabBarController = segue.destinationViewController as? UITabBarController else {
preconditionFailure("Unexpected destination.")
}
tabBarController.selectedIndex = 0 // choose which tab is selected
guard let des = tabBarController.selectedViewController as? JSONDisplayController else {
preconditionFailure("Unexpected selection.")
}
des.jsonfile = jsonfile
}
}
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.
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.
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)
}
}