SWIFT: how to use performSegueWithIdentifier from different class - ios

I'm trying to make my App to automatically go back to the main menu after 2 minutes of inactivity. So far I have both parts working, but not together..
The App starts a counter if there's no touch input:
see user4806509's anwer on Detecting when an app is active or inactive through touches in Swift
And from my main viewcontroller I can control the segue I need with code:
func goToMenu()
{
performSegueWithIdentifier("backToMenu", sender: self)
}
I've implemented the code from Rob's answer on How to call performSegueWithIdentifier from xib?
So I've created the following class and protocol:
protocol CustomViewDelegate: class {
func goToMenu()
}
class CustomView: UIView
{
weak var delegate: CustomViewDelegate?
func go() {
delegate?.goToMenu()
}
}
The Function go() gets (successfully) called when the timer runs out. But the delegate?.goToMenu() doesn't work. If I change it to: delegate!.goToMenu(), the App crashes with:
fatal error: unexpectedly found nil while unwrapping an Optional value
My main viewcontroller is a CustomViewDelegate and the viewDidLoad contains:
let myCustomView = NSBundle.mainBundle().loadNibNamed("customView", owner: self, options: nil)[0] as! CustomView
myCustomView.delegate = self
I have the correct xib file, and that part is working.
I can't seem to find the solution to this seemingly easy problem, does anyone have a fix? Or better yet, a more elegant solution to my problem?
Thank you!
edit: SOLUTION:
I've removed all my old code and implemented the NSNotification method:
In the UIApplication:
let CallForUnwindSegue = "nl.timfi.unwind"
func delayedAction()
{
NSNotificationCenter.defaultCenter().postNotificationName(CallForUnwindSegue, object: nil)
}
In the main ViewController:
let CallForUnwindSegue = "nl.timfi.unwind"
func goToMenu(notification: NSNotification)
{
performSegueWithIdentifier("backToMenu", sender: self)
}
deinit
{
NSNotificationCenter.defaultCenter().removeObserver(self)
}
In the viewDidLoad: NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(PeriodViewController.goToMenu), name:CallForUnwindSegue , object: nil)

I believe that your issue is that because your delegate is declared as a weak var, your delegate is getting disposed while you wait for the timer to complete, thus the reference to the optional is lost (and returns nil when you force unwrap it).
try removing the weak keyword from your CustomView class.
Another solution would be to use the notification center to notify your view to call for the segue.
Something along the lines of what I proposed here
I hope this helps.

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 3 call function from parent ViewController

I have a ViewController, this view container has a class which creates 2 container views, and adds a table to the first container and a HashtagPicker for the second.
The hashTagPicker has a function which is called whenever a change to the selected hashTags happens.
question: i want to call a update table function whenever a tag is changed. How can i call a function from the hashtagclass which is defined in the class that contains the containers?
I personally like the delegate approach over notifications - the latter solution almost always leads to confusing architecture. Sadly, the example for the delegate approach, which is also the accepted answer, is even worse - it basically opens an opportunity for memory leaks. I'll explain. In the accepted solution, ParentView is holding a strong reference to HashtagPicker and, in turn, HastagPicker is holding a strong reference to ParentView, this creates a retain cycle and means neither of the controllers will be picked up by ARC and be deinitialized. So, if you are, for example, presenting ParentView from some other view and you keep going to ParentView and back, you will keep spawning new instances of ParentView (and HashtagPicker) with old ones still occupying memory.
Now, how this should have been done. I'll use exactly the same names as in the accepted answer.
The protocol should be defined like so:
// note the ": class" part
protocol HashTagPickerDelegate: class {
func picked(hashtag: String)
}
If we specify class, it means the protocol can only be used on classes. This will allow use to create weak reference, which otherwise would have been impossible.
class HashtagPicker: UIViewController {
// if HashTagPickerDelegate wouldn't be limited to class,
// we couldn't have made a weak reference here!
weak var delegate: HashTagPickerDelegate?
// at some point, you call the delegate, it can be anywhere, this is just an example
#IBAction func tappedHashtag(_ sender: Any) {
delegate?.picked(hashtag: "bla")
}
}
Now we have a weak reference to delegate, so there is not retain cycle and ARC can clean up everything nicely!
I'll throw in the rest of the code to have this as a complete answer:
class ParentView: UIViewController {
func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// we are presenting the nested controller
if segue.identifier == "SegueHastagPickerContainer",
let destinationController = segue.destination as? HashtagPicker {
destinationController.delegate = self
}
}
}
extension ParentView: HashTagPickerDelegate {
func picked(hashtag: String) {
// we just got info from the child controller, do something with it!
}
}
You can use delegates as mentioned in above answer. Or you can use notifications. So here is a solution using notifications.
First of all register a notification in your parent viewController's viewDidLoad like this
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ParentViewController.someActionToBePerformed), name: "myNotification", object: nil)
Create a function in your parent viewController named same as above so it will be like
func someActionToBePerformed () {
// this will be called when hashTag is changed
// do something when hashTag is changed
}
Now you can simply post notification from your Hashtag viewController. When you want like this.
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "myNotification"), object: nil)
You can use this (no notification, no delegate)
func exitButtonTapped() {
if let pdfVC : YourParnetViewController = self.parent as? YourParnetViewController {
pdfVC.removeBlurEffect()
self.removeFromParentViewController()
self.view.removeFromSuperview()
}
}

Unable to segue from Spritekit SKScene to another UIViewController, XCODE8/Swift

I'm completely stuck at trying to perform a segue out of the 5th and final scene of my SpriteKit game to another View Controller in the project(not the GameViewController, nor the root view controller).
I tried running self.view!.window!.rootViewController!.performSegueWithIdentifier("finalSegue", sender: self), from my finalScene, but it literally does nothing (the line gets triggered, it reads the right ViewController - i check by "print(self.view!.window!.rootViewController!)" console prints "segue read" as I instructed it, right after the segue command, as a check, the segue identifier is correct, but nothing happens).
Have tried calling a method that performs the segue from the GameViewController ( the view controller from which I am launching the view of the 5 SKScenes), I get "unexpectedly found nil while unwrapping an optional value". Tried performing the segue from the final scene ("finalScene.swift"), same error.
Have tried everything and all relevant solutions in other questions in the forum, as well as all combinations of nil/self/viewController in the "sender:" field of the performSegue method, to no avail. Here is the code that I am trying to make work which gives "unexpectedly found nil while unwrapping an optional value", pointing at the viewController var, but giving uncomprehensible debugging when loaded both on the device and on the simulator. It seems "nil" passes into the viewController var I am declaring, instead of the original GameViewController?
All my segue identifiers are correct in Storyboard, everything checked multiple times...What am I doing wrong? Should I do something different, given its the 5th SKScene and not the 1st (as in other solutions)? The segue into the SKScenes by segueing into the GameViewController from another UIViewController works fine - its the exit out of them that does not work. Many thanks for any help, completely stuck here!
Here is my relevant code:
In my GameViewController (UIViewController that launches my 5 consecutive SKScenes):
class GameViewController: UIViewController {
let myScene = finalScene()
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
//this is the 1st out of 5 SKScenes in the project
let scene = GameScene(size: CGSize(width: 2048, height: 2742))
//this is the 5th out of 5 scenes, that I am trying to trigger the segue out of
myScene.viewController = self
view.presentScene(scene)
view.ignoresSiblingOrder = true
}
}
}
Im my 5th scene, finalScene:
class finalScene: SKScene {
var viewController: GameViewController!
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch: AnyObject in touches{
let positionOfTouch = touch.location(in: self)
let tappedNode = atPoint(positionOfTouch)
let nameOfTappedNode = tappedNode.name
if nameOfTappedNode == "continue" {
self.viewController.performSegue(withIdentifier: "finalSegue", sender: self)
}
}
}
Just for anyone who might come across this, I solved it by using notification center. Ie added a notification observer on the parent view controller of the game scenes (which calls a function that performs the segue), and at the end point in the game where I wanted to do the segue, I posted to that notification, and it works great.
adding observer in ViewDidLoad of UIViewController:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.doaSegue), name: NSNotification.Name(rawValue: "doaSegue"), object: nil)
}
func doaSegue(){
performSegue(withIdentifier: "toNext", sender: self)
self.view.removeFromSuperview()
self.view = nil
}
And then calling it from within the game SKScene:
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "doaSegue"), object: nil)
You are calling the segue on the root viewController. I think that is the problem. You need to call the segue on the scene's viewController instead (where I am assuming you have created the segue, hence it is not being found on the root viewController).
Now the problem is that an SKScene does not have direct access to it's viewController, but just the view in which it is contained. You need to create a pointer to it manually. This can be done by creating a property for the SKScene:
class GameScene: SKScene {
var viewController: UIViewController?
...
}
Then, in the viewController class, just before skView.presentScene(scene)
scene.viewController = self
Now, you can access the viewController directly. Simply call the segue on this viewController:
func returnToMainMenu(){
self.viewController.performSegueWithIdentifier("menu", sender: vc)
}
Found this method to work just as well. But I do prefer the method mentioned above since it's fewer lines. I do wonder which is better for performance...

Why all IBOutlet(s) found nil when viewdidload is called in App Delegate?

My Xcode debugger reports, every other values include alive (bool value) is set, except all the IBOutlets found nil (myLine, etc.). By the way, everything works when I delete all the code in App Delegate, but I need this view to update frequently, so implement it in applicationWillEnterForeground is necessary. And another thing worth pointing out, in the configure view 1 and 2, I set each outlet's value. And I make app delegate call viewdidload method before that, so all the outlets should hooked up with code already, so those outlets shouldn't be nil.
An error message in the debugger -- fatal error: unexpectedly found nil
while unwrapping an Optional value
import UIKit
class ViewController: UIViewController {
var alive : Bool = true
var aliveDefault = NSUserDefaults.standardUserDefaults()
//IBOulet Connections
#IBOutlet weak var myLine: UILabel!
#IBAction func buttontapped(sender: AnyObject) {
alive = false
NSUserDefaults.standardUserDefaults().setObject(alive, forKey: "alive")
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
loadView()
}
func loadView(){
alive = aliveDefault.boolForKey("alive")
if alive = true {
configureView()
}else{
configureView2()
}
}
func configureView(){
myLine.text = "Random text"
}
func configureView2(){
myLine.text = "Random text 2"
}
}
App Delegate
func applicationWillEnterForeground(application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
ViewController().viewDidLoad()
ViewController().configureView()
}
Since you are creating two new instances of ViewController in applicationWillEnterForeground using the default initialiser rather than from the storyboard, none of the IBOutlets will be set. You need to update the current instance of the view controller that is on screen.
Rather than doing this from the appDelegate it is probably easier to have your view controller subscribe to the UIApplicationWillEnterForegroundNotification NSNotification and handle the refresh locally. This way you don't need to closely couple your app delegate and your view controller and you don't need to worry if the view controller isn't currently on screen:
class ViewController: UIViewController {
...
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.loadView), name: UIApplicationWillEnterForegroundNotification, object: nil)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
NSNotificationCenter.defaultCenter().removeObserver(self)
}
Do ViewController.loadViewIfNeeded() before assigning values to IBOutlets

Swift delegates isn't working

I'm trying to use delegates between two controllers but it doesn't work as it should be
protocol saveDelegate: class {
func saveSite()
}
class AuditSiteViewController: UIViewController {
weak var delegate: saveDelegate?
#IBAction func saveButton(sender: UIBarButtonItem) {
print("Saved")
delegate?.saveSite()
}
}
class AuditDetailsViewController: UIViewController, saveDelegate {
var mainView: AuditSiteViewController?
override func viewDidLoad() {
super.viewDidLoad()
mainView?.delegate = self
}
func saveSite() {
print("delegated")
}
}
it should print delegated but it only prints "saved"?
You can use delegate, but have you debug and check that mainView is the correct instance?
My suggestion in this case would be to use NSNotification instead. You can add a observer in your viewDidLoad and post a notification on the saveButton()
class AuditDetailsViewController: UIViewController {
override func viewDidLoad() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(AuditDetailsViewController.saveSite), name: "SaveSite", object: nil)
}
}
class AuditSiteViewController: UIViewController {
#IBAction func saveButton(sender: UIBarButtonItem) {
NSNotificationCenter.defaultCenter().postNotificationName("SaveSite", object: nil)
}
}
In my opinion there are only two reasons possible:
First:
In the moment of calling mainView?.delegate = self mainView is nil. Then the delegate isn't assigned. Set a breakpoint there and you will see it.
Second:
In the moment of calling delegate?.saveSite() the delegate is nil. That may be because your instance of AuditDetailsViewController was deinit by you or system. System removes the instance if noone holds a strong reference to it anymore. Implement the deinit method and set a breakpoint in it to see when it happens.
Looks like the mainView is nil when you set the delegate. Try to set the reference when you instantiate the detail view controller.
Anyway, maybe what you want is to delegate the saving action from the detailViewController to the AuditSiteViewController and handle in this last VC the savings.

Resources