How to hand over delegate to controller in different places of project - delegates

I have a UINavigationController which includes 4 screens. Each has a back button respectively. Task: by pressing the back button on Controller4 get to Controller2, which will immediately switch to Controller1. I want to do this through a delegate (connection of 2 and 4 controllers), but I don’t understand how I can access the ViewController4.delegate = self property in Controller2. There is already an action for clicking and switching to 3Controller, to do something like
let storyboard = UIStoryboard(name: "VC4", bundle: nil)
guard let vc4 = story.instantiateViewController(withIdentifier: "VC4") as? VC4 else { return }
vc4.delegate = self
}
Please, help. How to link two controllers that are in completely different places in the project through a delegate (or something else)
P.S. This is my first time asking a question, sorry in advance if it's crooked
Example
protocol Controller2Delegate: AnyObject {
func showVC1()
}
class Controller2: UIViewController, Controller2Delegate {
// need to connect between 2 and 4 controller
let story = UIStoryboard(name: "VC4", bundle: nil)
guard let vc4 = story.instantiateViewController(withIdentifier: "VC4") as? VC4 else { return }
vc4.delegate = self //this is not work, and i already have same method for push to VC3
func showVC1() {
//any logic to call VC1 from VC2
//break point never called
}
class Controller4: UIViewController {
weak var delegate: Controller2Delegate?
func GoToVC1() {
//method who's call logic for pop to VC2
delegate?.showVC1
}// breakpoint is called
}

Related

Replace ViewController

I would like to change between two View Controllers. Both of the ViewControllers inherit from CenterViewController. The code below is to add one ViewController (CertificatenViewController) to the stack:
centerViewController = CertificatenViewController(nibName: "CertificatenViewController", bundle: nil)
centerViewController.delegate = self
centerNavigationController = UINavigationController(rootViewController: centerViewController)
view.addSubview(centerNavigationController.view)
addChildViewController(centerNavigationController)
centerNavigationController.didMoveToParentViewController(self)
This happens in the viewDidLoad and in the same class I have a switch statement to check which View has to be loaded, something like this:
switch menuItem.getNibname() {
case "CertificatenViewController":
print(menuItem.getNibname())
centerViewController = CertificatenViewController(nibName: menuItem.getNibname(), bundle: nil)
centerViewController.delegate = self
case "SettingsViewController":
print(menuItem.getNibname())
centerViewController = SettingsViewController(nibName: menuItem.getNibname(), bundle: nil)
centerViewController.delegate = self
default: break
}
I don't know exactly what the problem is, but it seems to be that the view is not reloaded. I have tried popViewControllerAnimated but this is not working. Maybe you can help me solving this problem.
Solution:
func changeView(menuItem: MenuItem){
self.centerNavigationController.viewControllers.removeAll()
switch menuItem.getNibname() {
case "CertificatenViewController":
print(menuItem.getNibname())
self.centerNavigationController.pushViewController(self.centerViewController, animated: false)
case "SettingsViewController":
print(menuItem.getNibname())
self.centerNavigationController.pushViewController(self.settingsViewController, animated: false)
case "MessagesViewController":
print(menuItem.getNibname())
self.centerNavigationController.pushViewController(self.messagesViewController, animated: false)
default: break
}
}
Generally, this is NOT the way to present view controllers:
view.addSubview(centerNavigationController.view)
addChildViewController(centerNavigationController)
centerNavigationController.didMoveToParentViewController(self)
Just remove that code.
I am not sure what exactly you are trying to do, but something like
self.presentViewController(centerNavigationController, animated: true, complettion: nil)
is typical. This makes the current view controller show the navigation controller (usually just on top of itself). Then the nav-controller will show the root you set up inside of itself (so the centerViewController).
When you want to get rid of it, you call dismissViewControllerAnimated:completion: on the view controller instance that did the presenting. It dismisses the navigation controller (do not call dismiss on the navigation controller)
Later, when you want to replace, where you do this:
centerViewController = CertificatenViewController(nibName: menuItem.getNibname(), bundle: nil)
centerViewController.delegate = self
Add:
centerNavigationController.setViewControllers([centerViewController], animated: true)

Conditionally showing NSViewController at app launch

I'm developing an OSX app where I show first a login/register window if the user hasn't logged in yet.
After login success I show my main view controller.
If the user is already logged in (a token is stored), then the app has to launch directly with the main view controller.
I'm new to OSX development, I googled for this kind of scenario but couldn't find anything.
So I went up with what I think should work. It works sometimes, sometimes I get a blank window.
In the storyboard I let the Main Menu and the Window Controller. I removed the "contains" segue to my main view controller.
In AppDelegate, I put this:
func applicationDidFinishLaunching(aNotification: NSNotification) {
if loggedIn {
self.showViewController(NSStoryboard.mainViewController())
} else {
let loginController = NSStoryboard.loginViewController()
loginController.delegate = self
self.showViewController(loginController)
}
}
private func showViewController(viewController: NSViewController) {
if let mainWindow = NSApplication.sharedApplication().mainWindow {
mainWindow.contentViewController = viewController
} else {
print("Error: No main window!")
}
}
About half of the times the window is empty and I see in the console "Error: No main window!". I thought maybe I can use applicationDidBecomeActive but this is called basically when it comes to the foreground and this is not what I need.
Further, the times when it works, and I log in, then I want to show the main view controller:
func onLoginSuccess() {
self.showViewController(NSStoryboard.mainViewController())
}
And here I also get "Error: No main window!" (always) and nothing happens.
The docs say following about mainWindow being nil:
The value in this property is nil when the app’s storyboard or nib file has not yet finished loading. It might also be nil when the app is inactive or hidden.
But why is the storyboard not finished loading or the app inactive when I'm launching it? And on login success the app is definitely active and in the foreground and the main window is always nil.
What am I doing wrong? How can I implement this workflow? Alternatively I could create a "parent" view controller, have that one connected to the window in the storyboard, and add the login or main view controller as nested view controllers to that. But don't really like having to add a do nothing view controller.
I'm using XCode 7(beta 4), Swift 2, OSX 10.10.4
Edit:
The NSStoryboard methods come from an extension, it looks like this:
extension NSStoryboard {
private class func mainStoryboard() -> NSStoryboard { return NSStoryboard(name: "Main", bundle: NSBundle.mainBundle()) }
private class func signupStoryboard() -> NSStoryboard { return NSStoryboard(name: "LoginRegister", bundle: NSBundle.mainBundle()) }
class func mainViewController() -> ViewController {
return self.mainStoryboard().instantiateControllerWithIdentifier("MainViewController") as! ViewController
}
class func loginViewController() -> LoginViewController {
return self.signupStoryboard().instantiateControllerWithIdentifier("LoginViewController") as! LoginViewController
}
class func registerViewController() -> RegisterViewController {
return self.signupStoryboard().instantiateControllerWithIdentifier("RegisterViewController") as! RegisterViewController
}
}
To put the solution we found in the comments as an answer:
Apparently NSApplication.sharedApplication().mainWindow is a different window than my main window in the storyboard.
So, I created an NSWindowController subclass and assigned it to the window in the storyboard, using the identity inspector.
Then I moved the logic I had in app delegate to this NSWindowController. It looks like this:
class MainWindowController: NSWindowController, LoginDelegate {
override func windowDidLoad() {
if loggedIn {
self.onLoggedIn()
} else {
let loginController = NSStoryboard.loginViewController()
loginController.delegate = self
self.contentViewController = loginController
}
}
func onLoggedIn() {
self.contentViewController = NSStoryboard.mainViewController()
}
func onLoginSuccess() {
self.onLoggedIn()
}
}
* Thanks Lucas Derraugh for pointing me in the right direction!
enum Storyboards: String {
case main = "Main"
case settings = "Settings"
func instantiateVC<T>(_ identifier: T.Type) -> T? {
let storyboard = NSStoryboard(name: rawValue, bundle: nil)
guard let viewcontroller = storyboard.instantiateController(withIdentifier: String(describing: identifier)) as? T else { return nil}
return viewcontroller
}
}
//Need to use like this
//Make sure Storyboard Id and class-name are the same
if let windowController = Storyboards.main.instantiateVC(IDMainController.self) {
windowController.showWindow(nil)
//----- OR -----
self.contentViewController = windowController
} else {
print("Cannot find IDMainController")
}

Add completion handler to presentViewControllerAsSheet(NSViewController)?

I am attempting to present a sheet configuration view (AddSoundEffect) for my main window/view controller (I'm using storyboards), and when the configuration view controller is dismissed, take the values entered in the AddSoundEffect view and pass that back to the main view. My current code in the main view controller:
presentViewControllerAsSheet(self.storyboard!.instantiateControllerWithIdentifier("AddSoundEffect") as! AddSoundViewController
And in the AddSoundViewController.swift file, the code to dismiss it is:
self.dismissViewController(self)
To pass the data, I have a class-independent tuple that I save data to. How do I add a completion handler to presentViewControllerAsSheet, and (optionally) is there a better way to pass the data between view controllers?
Setup: Xcode version 6.4, OS X 10.10.4
Delegation pattern is the easiest way for you.
// Replace this with your tuple or whatever data represents your sound effect
struct SoundEffect {}
protocol AddSoundViewControllerDelegate: class {
func soundViewController(controller: AddSoundViewController, didAddSoundEffect: SoundEffect)
}
//
// Let's say this controller is a modal view controller for adding new sound effects
//
class AddSoundViewController: UIViewController {
weak var delegate: AddSoundViewControllerDelegate?
func done(sender: AnyObject) {
// Dummy sound effect info, replace it with your own data
let soundEffect = SoundEffect()
//
// Call it whenever you would like to inform presenting view controller
// about added sound effect (in case of Done, Add, ... button tapped, do not call it
// when user taps on Cancel to just dismiss AddSoundViewController)
//
self.delegate?.soundViewController(self, didAddSoundEffect: soundEffect)
// Dismiss self
self.dismissViewControllerAnimated(true, completion: {})
}
}
//
// Let's say this controller is main view controller, which contains list of all sound effects,
// with button to add new sound effect via AddSoundViewController
//
class SoundEffectsViewController: UIViewController, AddSoundViewControllerDelegate {
func presentAddSoundEffectController(sender: AnyObject) {
if let addSoundController = self.storyboard?.instantiateViewControllerWithIdentifier("AddSoundEffect") as? AddSoundViewController {
addSoundController.delegate = self
self.presentViewController(addSoundController, animated: true, completion: {})
}
}
func soundViewController(controller: AddSoundViewController, didAddSoundEffect: SoundEffect) {
// This method is called only when new sound effect is added
}
}
Another way is to use closures:
// Replace this with your tuple or whatever data represents your sound effect
struct SoundEffect {}
//
// Let's say this controller is a modal view controller for adding new sound effects
//
class AddSoundViewController: UIViewController {
var completionHandler: ((SoundEffect) -> ())?
func done(sender: AnyObject) {
// Dummy sound effect info, replace it with your own data
let soundEffect = SoundEffect()
//
// Call it whenever you would like to inform presenting view controller
// about added sound effect (in case of Done, Add, ... button tapped, do not call it
// when user taps on Cancel to just dismiss AddSoundViewController)
//
self.completionHandler?(soundEffect)
// Dismiss self
self.dismissViewControllerAnimated(true, completion: {})
}
}
//
// Let's say this controller is main view controller, which contains list of all sound effects,
// with button to add new sound effect via AddSoundViewController
//
class SoundEffectsViewController: UIViewController {
func presentAddSoundEffectController(sender: AnyObject) {
if let addSoundController = self.storyboard?.instantiateViewControllerWithIdentifier("AddSoundEffect") as? AddSoundViewController {
addSoundController.completionHandler = { [weak self] (soundEffect) -> () in
// Called when new sound effect is added
}
self.presentViewController(addSoundController, animated: true, completion: {})
}
}
}
Or many other ways like sending notification, ... Whatever suits your needs. But delegation pattern or closures is the best way to go in this specific case.
I missed that your question is about NSViewController. This example is for iOS, but same pattern can be used on OS X without any issues.
The easiest way to detect sheet opening or closing is to use the Sheet Notifications:
class ViewController: NSViewController, NSWindowDelegate {
override func viewDidLoad(){
NSApplication.sharedApplication().windows.first?.delegate = self
}
func windowDidEndSheet(notification: NSNotification) {
}
func windowWillBeginSheet(notification: NSNotification) {
}
}

Xcode Swift: How to stop variables from changing back after each viewDidLoad, how to save and update data from different ViewControllers?

I have some vars in my Main VC and when user clicks a button in another VC the prepareForSegue passes along a new value to the Main VC and updates a label.
But when the user clicks again it's back to initial value, so it doesn't increment since the value is set back in the viewDidLoad?
MainVC:
var statsHealth:Int = 0
override func viewDidLoad() {
super.viewDidLoad()
healthLabel.text = String("Health: \(statsHealth)/10")
}
Another VC:
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "startSegue") {
let startVC = segue.destinationViewController as ViewController
startVC.statsHealth += 1
}
It's displayed as 0, then 1 but then 0 again and then 1 instead of 2,3,4 etc.
Any ideas?
BR
Nils
Perhaps not the most 'Swift' way to do it, but certainly works well....
Create a file called Variables.swift which will hold all your 'universal' variables (if these are going to be on every page, I see no reason this isn't the 'best' way to do it - certainly it is the most simple to understand!)
in Variables.swift, hold all your universal variables
struct Variables {
static var statsHealth = 0
.....
}
Then, in each other page, access them at any time
healthLabel.text = String("Health: \(Variables.statsHealth)/10")
or set them
Variables.statsHealth += 1
So based on your description, I assume the view controller structure is like this:
AnotherVC -> MainVC
MainVC is presented on top of AnotherVC. When you go back to AnotherVC, did you dismiss MainVC completely? If so, then every time you go from AnotherVC to MainVC, it initiate a new ViewController, and the variables you saved before doesn't exist anymore.
If you want to keep this structure and change variables in MainVC, keep a reference of mainVC in AnotherVC. Then instead of connecting in storyboard, you may want to present it programmatically.
class AnotherVC {
var mainVC: MainVC?
func presentMainVC() {
var targetVC = UIViewController()
if self.mainVC != nil {
targetVC = self.mainVC
} else {
let storyboard = UIStoryboard(name: "Your-storyboard-name", bundle: nil)
targetVC: MainVC = storyboard.instantiateViewControllerWithIdentifier("The-main-VC-identifier") as MainVC
self.mainVC = targetVC
}
//you can change your variable here
mainVC.statsHealth += 1
self.presentViewController(self.mainVC, animated: true, completion: nil)
}
If you mainVC is on top of AnotherVC in any case, you can just revert the reference direction.

Passing Data in Swift

I have been looking for an answer for this, but have only found answers for segues.
I have viewController1 with a button that segues to viewController2. There is no code for this, I set it up through Interface builder. On viewController2 I have a button that dismisses itself with
self.dismissViewControllerAnimated(true, completion, nil)
I want to pass a string from viewController2 back to viewController1 when the view is dismissed. How do I go about doing this? Also, I am using swift.
Thanks in advance!
There are two common patterns, both of which eliminate the need for viewController2 to know explicitly about viewController1 (which is great for maintainability):
Create a delegate protocol for your for viewController2 and set viewController1 as the delegate. Whenever you want to send data back to viewController1, have viewController2 send the "delegate" the data
Setup a closure as a property that allows passing the data. viewController1 would implement that closure on viewController2 when displaying viewController2. Whenever viewController2 has data to pass back, it would call the closure. I feel that this method is more "swift" like.
Here is some example code for #2:
class ViewController2 : UIViewController {
var onDataAvailable : ((data: String) -> ())?
func sendData(data: String) {
// Whenever you want to send data back to viewController1, check
// if the closure is implemented and then call it if it is
self.onDataAvailable?(data: data)
}
}
class ViewController1 : UIViewController {
func doSomethingWithData(data: String) {
// Do something with data
}
override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
// When preparing for the segue, have viewController1 provide a closure for
// onDataAvailable
if let viewController = segue.destinationViewController as? ViewController2 {
viewController.onDataAvailable = {[weak self]
(data) in
if let weakSelf = self {
weakSelf.doSomethingWithData(data)
}
}
}
}
}
I used the code from the first answer in a transition between controllers WITHOUT prepareForSegue and worked for me as well.
Here's the sample code.
The First View Controller:
#IBAction func dpAgendaClick(sender:UIBarButtonItem) {
///instantiating view controller with identifier
if let datePickerViewController = storyboard?.instantiateViewControllerWithIdentifier("DatePickerViewController")
as? DatePickerViewController {
///bring instantiated view controller to front
self.presentViewController(datePickerViewController, animated: true, completion: nil)
///wrapping the data returned
datePickerViewController.onDataFiltroAvailable = {[weak self]
(dataFiltro) in
if let weakSelf = self {
///use dataFiltro here
}
}
The second View Controller:
var onDataFiltroAvailable: ((dataFiltro: String) -> ())?
///private var
var dataFiltro: String = ""
///the returning data is obtained on the datePickerChanged event
#IBAction func datePickerChanged(sender: UIDatePicker) {
let dateFormatter = NSDateFormatter()
dateFormatter.dateStyle = NSDateFormatterStyle.ShortStyle
dateFormatter.dateFormat = "yyyy-MM-dd"
dataFiltro = dateFormatter.stringFromDate(datePicker.date)
}
///dismiss the controller on button click
#IBAction func dpOkClick(sender: UIButton) {
///"returning" the data
self.onDataFiltroAvailable?(dataFiltro: dataFiltro)
dismissViewControllerAnimated(true, completion: nil)
}
(Swift 2.1, Xcode 7, iOS9)
If you don't want it to be tightly coupled only between 2 ViewControllers,
You can also use the Notification Design Pattern (Post & Observe), which is mainly used to pass on the same object/information from one VC to multiple View Controllers.
For your scenario :
In VC2.swift :
#IBAction func BackBtn(sender: UIButton) {
NSNotificationCenter.defaultCenter().postNotificationName("ThisIsTheMessage", object: nil, userInfo:["ObjectBeingSent":yourObject])
}
And in VC1.swift :
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("yourFunction:"), name: "ThisIsTheMessage", object: nil)
}
func yourFunction(theNotification : NSNotification) {
if let extractInfo = theNotification.userInfo {
//code to use the object sent from VC2, by extracting the object details
}
}
Common Practise is:
Pass data forward -> Use PrepareForSegue
Pass data backward to the previous View Controller-> Protocol and Delegation
Pass data across multiple View Controllers -> Notifications : Post and Observe(observe in all the View controllers where you are using the object details)

Resources