Iboutlets have nil value on present call - ios

#objc func handleGoToSearch() {
// present(UINavigationController(rootViewController: searchDisplayController()), animated: true, completion: nil)
performSegue(withIdentifier: "searchSegue", sender: nil)
}
I was using the present call to SearchViewController but the ib outlets on where nil, I have a feeling it has something to do with passing a new instance, and the outlets are not loading in time. (What I read somewhere else). when I use perform segue not errors.
I would like to know exactly what's happening. I also thought that maybe present() doesn't work with storyboard, or I'm passing in the wrong value type.

Your outlets are nil because you are not creating the viewcontroller from the Swift class only, not from the storyboard. To create the viewcontroller from the storyboard, do this:
let viewController = UIStoryboard(name: "Name of the storyboard").instantiateViewController(withIdentifier: "Identifier of the viewcontroller")
You should fill in the name of the storyboard (usually Main) and give the viewcontroller an identifier in the storyboard, and use that identifier in the code.

Related

How to programmatically change a view controller while not in a ViewController Class

I know this question has been asked countless times already, and I've seen many variations including
func performSegue(withIdentifier identifier: String,
sender: Any?)
and all these other variations mentioned here: How to call a View Controller programmatically
but how would you change a view controller outside of a ViewController class? For example, a user is currently on ViewController_A, when a bluetooth device has been disconnected (out of range, weak signal, etc) the didDisconnectPeripheral method of CBCentral gets triggered. In that same method, I want to change current view to ViewController_B, however this method doesn't occur in a ViewController class, so methods like performSegue won't work.
One suggestion I've implemented in my AppDelegate that seems to work (used to grab the appropriate storyboard file for the iphone screen size / I hate AutoLayout with so much passion)
var storyboard: UIStoryboard = self.grabStoryboard()
display storyboard
self.window!.rootViewController = storyboard.instantiateInitialViewController()
self.window!.makeKeyAndVisible()
And then I tried to do the same in my non-ViewController class
var window: UIWindow?
var storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil) //assume this is the same storyboard pulled in `AppDelegate`
self.window!.rootViewController = storyboard.instantiateViewController(withIdentifier: "ViewController_B")
self.window!.makeKeyAndVisible()
However I get an exception thrown saying fatal error: unexpectedly found nil while unwrapping an Optional value presumably from the window!
Any suggestions on what I can do, and what the correct design pattern is?
Try this:
protocol BTDeviceDelegate {
func deviceDidDisconnect()
func deviceDidConnect()
}
class YourClassWhichIsNotAViewController {
weak var deviceDelegate: BTDeviceDelegate?
func yourMethod() {
deviceDelegate?.deviceDidDisconnect()
}
}
class ViewController_A {
var deviceManager: YourClassWhichIsNotAViewController?
override func viewDidLoad() {
deviceManager = YourClassWhichIsNotAViewController()
deviceManager.delegate = self
}
}
extension ViewController_A: BTDeviceDelegate {
func deviceDidDisconnect() {
DispatchQueue.main.async {
// change the VC however you want here :)
// updated answer with 2 examples.
// The DispatchQueue.main.async is used here because you always want to do UI related stuff on the main queue
// and I am fairly certain that yourMethod is going to get called from a background queue because it is handling
// the status of your BT device which is usually done in the background...
// There are numerous ways to change your current VC so the decision is up to your liking / use-case.
// 1. If you are using a storyboard - create a segue from VC_A to VC_B with an identifier and use it in your code like this
performSegue(withIdentifier: "YourSegueIdentifierWhichYouveSpecifiedInYourSeguesAttibutesInspector", sender: nil)
// 2. Instantiate your VC_B from a XIB file which you've created in your project. You could think of a XIB file as a
// mini-storyboard made for one controller only. The nibName argument is the file's name.
let viewControllerB = ViewControllerB(nibName: "VC_B", bundle: nil)
// This presents the VC_B modally
present(viewControllerB, animated: true, completion: nil)
}
}
func deviceDidConnect() {}
}
YourClassWhichIsNotAViewController is the class which handles the bluetooth device status. Initiate it inside the VC_A and respond to the delegate methods appropriately. This should be the design pattern you are looking for.
I prefer dvdblk's solution, but I wasn't sure how to implement DispatchQueue.main.async (I'm still pretty new at Swift). So this is my roundabout, inefficient solution:
In my didDisconnectPeripheral I have a singleton with a boolean attribute that would signify whenever there would be a disconnect.
In my viewdidload of my ViewController I would run a scheduledTimer function that would periodically check the state of the boolean attribute. Subsequently, in my viewWillDisappear I invalidated the timer.

Swift/iOS: IBOutlet nil after loading view controller

I'm building an app (in XCode 8.2.1) where some objects are displayed on a 2D board, and when the user taps one of these objects some info should be displayed about it as a styled modal info box. My design is to have the info written in a separate view controller, which I would display when needed.
I've designed a basic stub for the second view controller and added a single label to it in the interface builder. Then I've ctrl-linked this label to my custom VC class:
class InfoViewController: UIViewController {
#IBOutlet weak var info: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
}
func displayInfo() {
info.attributedText = NSAttributedString(string: "abc")
}
}
However, when I test my app and tap the object, the info field is nil even in the viewDidLoad() method of my custom VC class. The way I'm displaying my VC is as follows:
let infoViewController = InfoViewController()
infoViewController.modalPresentationStyle = .overCurrentContext
self.present(infoViewController, animated: true, completion: nil)
infoViewController.displayInfo()
(Note: In the end I will have only one single instance of InfoViewController but this is just for testing. I don't expect having a global instance would make any difference?)
As I said, be it inside the viewDidLoad() method or in the displayInfo() method, info is always nil, such that setting its attributedString attribute crashes the app. Thinking the present method might be called asynchronously, I've tried calling displayInfo() from inside viewDidLoad(), but that didn't make any difference.
Can anyone tell my what I've forgotten that would allow my IBOutlet from being properly initialized properly?
Thanks!
David
The problem is the reference to InfoViewController(), which instantiates the view controller independent of any storyboard scene. You want to use instantiateViewController:
let infoViewController = storyboard?.instantiateViewController(withIdentifier: "Info") as! InfoViewController
infoViewController.modalPresentationStyle = .overCurrentContext
present(infoViewController, animated: true) {
infoViewController.displayInfo()
}
A couple of notes:
This assumes that (a) you've given the scene in the storyboard a "storyboard id"; (b) you've set the base class for that scene to InfoViewController.
Note, I called displayInfo in the completion handler of present because you probably don't want that called until the scene has been presented and the outlets have been hooked up.
Alternatively, you can update non-outlet properties of the InfoViewController immediately after instantiating it and then have its viewDidLoad take those properties and update the outlets, e.g.:
class InfoViewController: UIViewController {
var info: String!
#IBOutlet weak var infoLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
infoLabel.attributedText = NSAttributedString(string: info)
}
}
Note, I changed the #IBOutlet name to be infoLabel and added the String property called info. That tends to be the convention, that outlets bear some suffix indicating the type of control, and model objects, like the String property, are without the suffix. (You'll just want to make sure you remove that old outlet in the connections inspector in IB so that you don't have problems with these property name changes.)
Anyway, you can then do:
let infoViewController = storyboard?.instantiateViewController(withIdentifier: "Info") as! InfoViewController
infoViewController.info = "abc"
infoViewController.modalPresentationStyle = .overCurrentContext
present(infoViewController, animated: true, completion: nil)
The key point is don't try to update outlets of the scene immediately after instantiating it, but make sure that this is deferred until after viewDidLoad was called.
I Replaced
let vc = CCDetailViewController()
With
let vc = self.storyboard?.instantiateViewController(withIdentifier: "CCDetailViewController")
Finally
self.present(vc!, animated: true, completion: nil)
Now It Works...
In my case, I had created new view controller for the same class. Which had ended up with two view controllers in storyboard, but referring to the same class. After deleting old view controller, everything worked fine.
Hope it helps to someone.

Cant use Functions in one file in another

I created a file called "Util.swift" and it has a class "class VC: UIViewController" and has a 2 functions:
Next(segue: String) {
performSegueWithIdentifier(segue, completion: nil)
}
Back() {
dismissViewControllerAnimated(true, completion: nil)
}
So basically, the functions just tell the current viewController to go to the next View or go back to the previous view.
However, in another file, I have an IBAction which is attached to a button and in that action I call the function "Back" in the Util file by doing this:
#IBACTION func ~~~ {
nextVC = Util()
nextVC.back()
}
However, when I click the button, it doesn't do anything.
When I try to connect another button to an action that calls the "next" function in the Util file by doing the same thing and putting in the segue indetifier parameter, it says SIGABRT error: no segue identifier called "xxx".
Can anyone help me? Should I simply use XIB and not use segues?
You have to initialize the UIViewController either with storyBoard or with XIB. If you are doing with factory init() method then it doesn't do anything for you; that means, that is not a viewController which is associated either with storyBoard or XIB. So all you need to do is instantiate the viewController with either options. Before that you need to set an identifier for the viewController to instantiate it.
let storyBoard = UIStoryBoard(name: "MainStoryBoard", bundle: nil)
let vc = storyBoard.instanstiateViewControllerWithIdentifier("YOUR_IDENTIFIER") as! Util
//call your Next method like this
vc.Next(segue:"identifier")
Please refer the document
Apple Doc
In order to use segues, the view controller instance needs to be associated with a Storyboard. In order to be associated with a storyboard, a view controller instance either needs to be the result of a segue or instantiated from a storyboard via instantiateViewControllerWithIdentifier.
When you create an instance of Util via Util() you have a view controller instance that isn't associated with a storyboard and isn't actually presented on screen.
As a result, when you try and perform a segue, you get an error since the segue can't be found.
Also, when you try and dismiss the view controller in back() you are trying to dismiss a view controller that isn't presented.
I am not sure why you want to wrap two fairly simple functions inside next and back, but you can do this using a superclass for all of your view controllers and have this superclass implement your next and back functions:
class MyViewContollerSuperclass: UIViewController {
func next(segue: String) {
self.performSegueWithIdentifier(segue, sender: self)
}
func back() {
self.dismissViewControllerAnimated(true, completion: nil)
}
}
Then you actual view controller would be declared as:
class MyActualViewController: MyViewContollerSuperclass {
#IBACTION func ~~~ {
self.back()
}
}

Performing Segue from - TableViewCell.xib to UIView - and - TableView.xib to UIViewController -

How can I perform segue from one .xib (TableCell.xib) to another .xib(UIViewController) on click on TableCell?
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
print(results?[indexPath.row]) // prints successfully
...
// apply below chunks here...
}
First, I tried setting up a manual segue in Storyboard. Note that, TableView & TableViewCell are external xib but parent of TableView is in Storyboard. So, I tried control-dragging from parent-ViewController to the detailViewController (and identifier: showDetails)..However, doesn't work.
...
NSOperationQueue.mainQueue().addOperationWithBlock({ () -> Void in
self.performSegueWithIdentifier("showDetails", sender: AnyObject?())
})
...
Obviously, I received this error (because TableView is in .xib whereas parent and detailsVC are in Storyboard and I ctrl+dragged from parentVC to detailsVC but TableView1 apparently doesn't recognise the segue in Main Storyboard):
TableView1: 0x7f97626a3280>) has no segue with identifier 'showDetails''
Then I tried creating a ViewController and tried adding var parentNavigationController : UINavigationController! at the top and referencing below chunk inside didSelectRowAtIndex
...
let newVC : UIViewController = CellDetailsVC()
newVC.title = "DetailsView"
print(newVC)
parentNavigationController!.pushViewController(newVC, animated: true)
However, I am receiving an error:
DetailsVC: 0x7ff7794ae550>
fatal error: unexpectedly found nil while unwrapping an Optional value
Finally, I also tried below chunk inside didSelectRowAtIndex, and got something close (as I can use dismissView to < Back); but not exactly what I want. Its problem is that the segue animation looks like the page is coming from bottom.
...
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyboard.instantiateViewControllerWithIdentifier("VideoDetailsVC") as UIViewController
self.presentViewController(vc, animated: true, completion: nil)
}
I am also receiving a warning (but not crash) for some reason I couldn't figure out:
Presenting view controllers on detached view controllers is discouraged
Which approach is the right one? Or is there a better way to adapt to achieve what I am trying to?
Firstly, what is the right way to perform segue from didSelectRowAtIndex (in TableView.xib class) to a new ViewController (either Storyboard or xib)?
Secondly, assume that there is an image thumbnail in the TableViewCell.xib. What is right way of performing segue to a new UIView on click on that view? (like full-screen image / dismiss-able)
Your second approach looks to be right one but of course you are doing something wrong there. You need to drag and drop Segue from Parent ViewController to Detail ViewController and give it a Segue identifier in Storyboard (check attached Image below). IN didSelectRowAtIndexPath you need to do below code:
self.performSegueWithIdentifier("showDetailViewController", sender: nil)
As I can see you already tried that but it looks like you missed some step either setting Segue Identifier or giving wrong identifier in performSegueWithIdentifier method. Also your Parent ViewController should be embedded in UINavigationController in case you are missing that thing :)
Also keep in mind you should perform navigation (here its Segue) from one ViewController to another ViewController at same level not from one View to another ViewController.

What is the difference between using instantiateViewControllerWithIdentifier and performseguewithidentifier?

Both methods allow me to present a new view controller (one by calling presentviewcontroller), so I don't understand the difference between the two and when I should use them.
They both reference storyboard related identifiers. The main difference is one (performSegueWithIdentifer) instantiates an object based on a segue's end (where the segue points to), while the other (instantiateViewControllerWithIdentifier) instantiates a unique VC based on the VC's identifier (not the segue).
You can have multiple segue's with the same identifier in different places in the storyboard, while VC's in a storyboard cannot have the same identifier.
performSegueWithIdentifer and instantiateViewControllerWithIdentifier both are used to move from one viewController to another viewController.
But there is so much differences....
The identifier of the 1st case defines a segue like push, modal, custom etc which are used to perform a specific type of transition from one VC to another VC.
eg.
self.performSegueWithIdentifier("push", sender: self);`
where "push" is an identifier of a push segue.
The identifier of the 2nd case defines a VC like myViewController, myTableViewController, myNavigationController etc. 2nd function is used to go to the specific VC ( with identifier.) from a VC in the storyBoard.
eg.
var vc = mainStoryboard.instantiateViewControllerWithIdentifier("GameView") as GameViewController;
self.presentViewController(VC, animated: true, completion: nil) ;
where "GameView" is the identifier of GameViewController.
Here a instance of GameViewController is created and then the function presentViewController is called to go to the instantiated vc.
For the 1st case with the help of segue identifier u can pass one are more values of variables to the next VC.
eg.
override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!)
{
if (segue.identifier == "push")
{
let game = segue.destinationViewController as GameViewController
game.value = self.myvalue // *value* is an Int variable of GameViewController class and *myvalue* is an Int variable of recent VC class.
}
}
This funcion is also called when self.performSegueWithIdentifier("push", sender: self); is called to pass the value to GameViewController.
But in 2nd case it possible directly like,
var vc = mainStoryboard.instantiateViewControllerWithIdentifier("GameView") as GameViewController;
vc.value = self.myvalue;
self.presentViewController(VC, animated: true, completion: nil) ;

Resources