perform segue after view controller is dimissed - swift

I am using UINavigationController. I want to show an intermediate screen eg. White and then from there I want to dimiss and segue to green.
The reason I don't create a segue from white to green is because in the case the user goes back they should go back to blue because blue is my main screen.
Here's the code:
class BlueViewController: UIViewController {
#IBAction func tapBlue(_ sender: Any) {
self.performSegue(withIdentifier: "whiteSegue", sender: self)
}
}
class WhiteViewController: UIViewController {
#IBAction func tapGreen(_ sender: Any) {
navigationController?.popViewController(animated: true)
weak var pvc = self.presentingViewController
dismiss(animated: true){
pvc?.performSegue(withIdentifier: "greenSegue", sender: self)
}
}
}
Here's the codebase
https://github.com/omenking/DismissAndSegue
No error occurs but when white is dismissed it doesn't go to green.
I know this has been asked before on StackOverflow but the other examples did not work or were out of date with latest iOS.

The main issue is that since you are using a navigation view controller and pushing view controllers on and off the stack, the self.presentingViewController variable will be nil. That is used for modal presentations, not navigation view controllers.
Try this:
class WhiteViewController: UIViewController {
#IBAction func tapGreen(_ sender: Any) {
// Get navigation stack, remove last item (White VC)
var viewControllers = navigationController?.viewControllers
viewControllers.removeLast()
// Instantiate new Green VC from storyboard
let storyboard = UIStoryboard(name: "Main", bundle: nil) //Change bundle name
let greenViewController = storyboard.instantiateViewController(withIdentifier: "GreenViewController") //Change storyboard ID
viewControllers.append(greenViewController)
// Perform the transition to Green VC with animation
navigationController?.setViewControllers(viewControllers, animated: true)
}
}

A slightly different solution is to link your view controllers blue->white->green, and then in the green view controller, just remove the white view controller from the navigation stack.
Your green view controller then becomes as simple as this.
class GreenViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let count = self.navigationController?.viewControllers.count {
self.navigationController?.viewControllers.remove(at: count - 2)
}
}
}
No special handling needed in the other view controllers.

I do this, in white controller
performSegue(withIdentifier: "showSchedule", sender: date)
if let count = self.navigationController?.viewControllers.count {
self.navigationController?.viewControllers.remove(at: count - 2)
}

Related

Presenting ViewController just shows black screen

I am trying to present a ViewController from a Button action. But this seems to not work as i want it to. I do not get any error but after clicking the button the screen turns black and nothing happens. The only things i could manage to find are for objective-c and i am just trying to learn Swift. I am using storyboard to design this and added the Viewcontroller in the appropriate place in the designer
Code:
#IBAction func doShow(_ sender: Any)
{
var newWindowViewController = NewWindowViewController()
newWindowViewController.baseItem = self.baseItem
present(newWindowViewController, animated: false)
}
You need to instantiate the ViewController first from your storyboard. Else it won´t contain any View and turn up black.
#IBAction func doShow(_ sender: Any)
{
// Instantiate your ViewController here
var newWindowViewController = storyboard?.instantiateViewController(withIdentifier: "putyourIdentifierhere")) as! NewWindowViewController
newWindowViewController.baseItem = self.baseItem
present(newWindowViewController, animated: false)
}

What is the correct way to close a popover?

In my NSDocument subclass I instantiate an NSPopover, with .semitransient behaviour, and show it:
popover.show(relativeTo: rect, of: sender, preferredEdge: .maxX)
popover is declared locally. A button method in the popover controller calls:
view.window?.close()
The popover closes, but I have become aware that it remains in memory, deinit() is never called and the NSApp.windows count increases, whereas if I dismiss it by pressing escape or clicking outside it, deinit is called and the windows count doesn't increase.
If I set the window's .isReleasedWhenClosed to true, the windows count doesn't increase, but deinit is still not called.
(Swift 3, Xcode 8)
You have to call performClose (or close) on the popover, not the window.
Thanks -DrummerB for your interest. It has taken me some time to get around to making a simple test application I might send you, and of course it wasn't a document-based one as mine was, and that seemed to be clouding the issue. My way of opening the popover was based on an example I'd recently read, but can't now find or I'd warn people. It went like this:
let popover = NSPopover
let controller = MyPopover(...)! // my convenience init for NSViewController descendant
popover.controller = controller
popover.behaviour = .semitransient // and setting other properties
popover.show(relativeTo: rect, of: sender, preferredEdge: .maxX)
Here's the improved way I've come across:
let controller = MyPopover(...)! // descendant of NSViewController
controller.presentViewController(controller,
asPopoverRelativeTo: rect, of: sender, preferredEdge: .maxX,
behavior: .semitransient) // sender was a NSTable
In the view controller, the 'Done' button's action simply does:
dismissViewController(self)
which never worked before. And now I find the app's windows list doesn't grow, and the controller's deinit happens reliably.
I would suggest doing the following:
Define a protocol like this
protocol PopoverManager {
func dismissPopover(_ sender: Any)
}
In your popoverViewController (in this example we are displaying a filter view controller as a popover) add a variable for the popoverManager like this
/// Filter shown as a NSPopover()
class FilterViewController: NSViewController {
// Delegate
var popoverManager: PopoverManager?
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
}
// Bind this to the close button or action on your popover view controller
#IBAction func closeAction(_ sender: Any) {
self.popoverManager?.dismissPopover(sender)
}
...
}
Now in your viewController that you show the popover from add an extension like this
extension MainViewController: NSPopoverDelegate, PopoverManager {
#IBAction func setFilter(_ sender: AnyObject) {
self.showFilterPopover(sender)
}
func showFilterPopover(_ sender: AnyObject) {
let storyboard = NSStoryboard(name: "Filter", bundle: nil)
guard let controller = storyboard.instantiateController(withIdentifier: "FilterViewController") as? FilterViewController else {
return
}
// Set the delegate to self so we can dismiss the popover from the popover view controller itself.
controller.popoverManager = self
self.popover = NSPopover()
self.popover.delegate = self
self.popover.contentViewController = controller
self.popover.contentSize = controller.view.frame.size
self.popover.behavior = .applicationDefined
self.popover.animates = true
self.popover.show(relativeTo: sender.bounds, of: sender as! NSView, preferredEdge: NSRectEdge.maxY)
}
func dismissPopover(_ sender: Any) {
self.popover?.performClose(sender)
// If you don't want to reuse it
self.popover = nil
}
}

Swift: Switch between NSViewController inside Container View / NSView

I want to achieve a really simple task—changing the ViewController of a Container View by pressing a button:
In my example the ViewController1 is embedded into the Container View using Interface Builder. By pressing the Button ViewController2 I want to change the view to the second ViewController.
I’m confused because the Container View itself seems to be a NSView if I create an Outlet and as far as I know a NSView can’t contain a VC. Really appreciate your help!
Just note that in order for this to work you have to add storyboard identifiers to your view controllers, which can by going to your storyboard then selecting the Identity Inspector in the right hand pane and then entering the Storyboard ID in the Identity subcategory.
Then this implementation of ViewController would achieve what you are looking for.
import Cocoa
class ViewController: NSViewController {
// link to the NSView Container
#IBOutlet weak var container : NSView!
var vc1 : ViewController1!
var vc2 : ViewController2!
var vc1Active : Bool = false
override func viewDidLoad() {
super.viewDidLoad()
// Make sure to set your storyboard identiefiers on ViewController1 and ViewController2
vc1 = NSStoryboard(name: "name", bundle: nil).instantiateController(withIdentifier: "ViewController1") as! ViewController1
vc2 = NSStoryboard(name: "name", bundle: nil).instantiateController(withIdentifier: "ViewController2") as! ViewController2
self.addChild(vc1)
self.addChild(vc2)
vc1.view.frame = self.container.bounds
self.container.addSubview(vc1.view)
vc1Active = true
}
// You can link this action to both buttons
#IBAction func switchViews(sender: NSButton) {
for sView in self.container.subviews {
sView.removeFromSuperview()
}
if vc1Active == true {
vc1Active = false
vc2.view.frame = self.container.bounds
self.container.addSubview(vc2.view)
} else {
vc1Active = true
vc1.view.frame = self.container.bounds
self.container.addSubview(vc1.view)
}
}
}
maybe this is a late answer but I will post my solution anyways. Hope it helps someone.
I embedded NSTabViewController in ContainerView. Then, in order not to see tabs on the top I did this:
go to NSTabViewController in storyboard
in Attributes inspector change style to be Unspecified
then click on TabView in Tab Bar View Controller, and set style to be "tabless":
After this you need to:
store tabViewController reference to mainViewController in order to select tabs from code
add a button to mainViewController (where your container is) with which you will change tabs in tabViewController.
You do this by storing the reference to tabViewController when overriding prepare for segue function. Here is my code:
first add property to the mainViewController
private weak var tabViewController: NSTabViewController?
then override this function and keep the reference to tabViewController:
override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
guard let tabViewController = segue.destinationController
as? NSTabViewController else { return }
**self.tabViewController = tabViewController as? NSTabViewController**
}
After this you will have reference to tabViewController all set up.
Next (last) thing you have to do is make an action for button in order to move to first (or second) view controller, like this:
#IBAction func changeToSecondTab(_ sender: Any) {
self.tabViewController?.selectedTabViewItemIndex = 0 // or 1 for second VC
}
All the best!

How do I pass data from a View controller into my pop up view controller (swift/ios)

I'm quite new with Swift and I'm making this mini game type app that counts the score and updates the label in the view controller. I want to pass that score from a view controller into another external pop up view controller I created.
#IBAction func Button7Tapped(_ sender: AnyObject)
{
if Index == 13 {
game.score += 1
} else {
let scorepopVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "finalScorePop") as! finalScoreViewController
self.addChildViewController(scorepopVC)
scorepopVC.view.frame = self.view.frame
self.view.addSubview(scorepopVC.view)
scorepopVC.didMove(toParentViewController: self)
}
updateGame()
}
Above is my code for the external pop up view controller I created, which also has a separated .swift file. How would I go about taking my game.score and passing that into my Popup view controller?
In your finalScoreViewController swift file add a new property.
final class FinalScoreViewController: UIViewController {
var score: Int?
}
And then just assign it when you're instantiating it.
#IBAction func Button7Tapped(_ sender: AnyObject) {
if Index == 13 {
game.score += 1
} else {
let scorepopVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "finalScorePop") as! finalScoreViewController
scorepopVC.score = game.score //THIS LINE
self.addChildViewController(scorepopVC)
scorepopVC.view.frame = self.view.frame
self.view.addSubview(scorepopVC.view)
scorepopVC.didMove(toParentViewController: self)
}
updateGame()
}
It is better to use storyboard to open the ViewController. In storyboard, right click and drag from you button to the second view controller (the one that you wish to open).
Choose the segue type that you wish to use. In your case, I think Present Modally will work fine.
You will see a line between the two UIViewControllers in storyboard. That is the segue. Tap on it. In the Attributes inspector give the segue an identifier. For instance "myFirstSegue".
Then in the code of the UIViewController that contains your button override prepare(for:sender:). This method is called when preparing for the segue to happen. I.o.w when you tap on the button. You have access to the destination UIViewController and can therefor access and set the properties on it.
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "myFirstSegue" {
if let vc = segue.destination as? MyViewController {
//here you set your data on the destination view controller
vc.myString = "Hello World"
}
}
}
Note that we check the identifier, because all segues that go from this ViewController to other ViewControllers will call prepare(for:sender:)
It's quite simple, Just add a property in your finalScoreViewController (if you are not already done this) and -for example- call it score:
class finalScoreViewController: UIViewController {
var score: String?
// ...
Add this line to the Button7Tapped action (where you set a value for finalScoreViewController's score):
let scorepopVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "finalScorePop") as! finalScoreViewController
// add this line:
scorepopVC.score = "My score"
self.addChildViewController(scorepopVC)
scorepopVC.view.frame = self.view.frame
self.view.addSubview(scorepopVC.view)
scorepopVC.didMove(toParentViewController: self)
Finally, in finalScoreViewController:
override func viewDidLoad() {
super.viewDidLoad()
if let scr = score {
print(scr)
}
}
Hope that helped.
You do not actually have to pass the variable to the next view controller. All you have to do is create a variable outside of the View Controller class, and voila, you can access your variable from anywhere, in any swift file. For example:
var score = 0
class ViewController: UIViewController {
override func viewDidLoad(){
super.viewDidLoad()
}
#IBAction func Button7Tapped(_ sender: AnyObject){
score += 1
}
}
And then in the other View Controller, you would have something like this:
#IBOutlet weak var scoreLabel: UILabel!
class ViewController: UIViewController {
override func viewDidLoad(){
super.viewDidLoad()
var timer1 = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(updateScore), userInfo: nil, repeats: true)
}
#objc func updateScore() {
scoreLabel.text = "You have \(score) points!"
}

prepareForSegue when embedding Tab Bar Controller into Navigation Controller

I currently have a navigation controller setup like this:
and my prepareForSegue, that passes data between the initial view (Login View Controller) and the Navigation controller looks like such:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
super.prepareForSegue(segue, sender: sender)
let navVc = segue.destinationViewController as! UINavigationController // 1
let chatVc = navVc.viewControllers.first as! ChatViewController // 2
chatVc.senderId = userID // 3
chatVc.senderDisplayName = "" // 4
}
However, when I try to embed in a Tab Bar controller (to add more pages/functionality to my app) like this...
...and run my application, my program crashes at the line let navVc = segue.destinationViewController as! UINavigationController
I know that the problem is that after my initial view, it goes to the tab bar which is type UITabBarController rather than UINavigationController however if I change it, my data does not go to the view that I want it to go to...it is kind of confusing.
Please let me know if you have any ideas how to implement this, or if you have any questions feel free to ask me for clarification.
Thanks!
P.s. The error that I am receiving in the console is:
Could not cast value of type 'UITabBarController' (0x10b8e48b0) to 'UINavigationController' (0x10b8e4860).
Try this:
Start by casting destinationViewController to UITabBarController and then using the viewControllers property to access the first viewController in the tabBarController:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
let tabVc = segue.destinationViewController as! UITabBarController
let navVc = tabVc.viewControllers!.first as! UINavigationController
let chatVc = navVc.viewControllers.first as! ChatViewController
chatVc.senderId = userID
chatVc.senderDisplayName = ""
}
In the CS193p class, Paul Hegarty shows the uses of extensions to deal with Navigation Controller segues (Lecture 8: 23'). The UIViewController extension introduces a new computed property: contentViewController available to all UIViewControllers (and subclasses).
The code I posted below was adapted to work with TabBarViewControllers as well.
When you are attempting to cast your navigation controller as your ChatViewController, the segue.destinationController.contentViewController will recursively return the ChatViewController.
class LoginViewController: UIViewController {
/* ... */
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if let chatVc = segue.destinationViewController.contentViewController as? ChatViewController {
chatVc.senderId = userID
chatVc.senderDisplayName = ""
}
}
}
extension UIViewController {
var contentViewController: UIViewController {
if let navcon = self as? UINavigationController {
return navcon.visibleViewController ?? self
} else if let tabcon = self as? UITabBarController {
return tabcon.selectedViewController ?? self
} else {
return self
}
}
}
A clean way of doing this would be that right after you log in, you use something like:
let controller = self.storyboard?.instantiateViewControllerWithIdentifier("setthisisyourstoryboard") as! UITabBarController // This would instantiate the TabBarController.
let navInstance = controller[0] as! UINavigationController // This would instantiate the navigationController, that is placed at 0th index in the array of all view controllers that are child to TabBarController, since you only have one child, you can use 0 as index
if navInstance.viewControllers[0] is YourClassName { // YouClassName is the name of the class right next to the navigation view controller, and it is also the only child of navigation Controller(0 index)
// You can also send some data here (for example the sender id)
(navInstance.viewControllers[0] as! YourClassName).someProperty = Value
}
// This line would present the tabBar controller, that would ultimately reach the end of the stack. I have used this approach in many apps, it works great!
self.presentViewController(controller, animated: true, completion: nil)
You can delete the segue after this and set storyboard id for the tabbarcontroller.
I like to do it this way because it is more natural. What I mean is the UITabBarController is mostly a parent to View Controllers, I don't like the idea of setting a UITabBarController as a child to UIView Controller.
Maybe there is nothing wrong with it, but I don't prefer it.
Here. it's working pretty fine for me:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let tabVC = segue.destination as? UITabBarController {
if let navVC = tabVC.viewControllers!.first as? UINavigationController {
if let nextVC = navVC.viewControllers.first as? NextVC {
nextVC.varName = "works like a charm"
}
}
}
}
NextVC is your target VC which you want to send your variable into.