I can't subscribe publishSubject from embed view controller in swift - rx-swift

I have MainViewController that contains ContainerView.
and ContainerView have embeded segue that indicate ThirdViewController
In ThirdViewController, I create PublishSubject, If I select something in PickerView then emit the value into PublishSubject.
enter image description here
then MainViewController subscribe PublishSubject from ThirdViewController.
so if this success , in MainViewController I will get value from PickerView.
But my problem is I can't get anything, even maybe can't subscribe.
what is problem??? what can I do ???
MainViewController
if let thirdVC = self.storyboard?.instantiateViewController(withIdentifier:"ThirdViewController") as? ThirdViewController {
print("third segue success")
thirdVC.selectFeelingSubject.subscribe(onNext : {
print($0)
}).disposed(by: self.disposeBag)
ThirdViewController
var selectFeelingSubject = PublishSubject<String>()
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
selectFeelingSubject.onNext(feelingArray[row])
}

The code:
if let thirdVC = self.storyboard?.instantiateViewController(withIdentifier:"ThirdViewController") as? ThirdViewController {
print("third segue success")
thirdVC.selectFeelingSubject.subscribe(onNext : {
print($0)
})
.disposed(by: self.disposeBag)
Is not giving you the view controller that is in the main view controller. It is instantiating an entirely new view controller.
You need the actual one that is in the main view controller. Try this instead:
let thirdVC = self.children[0] as! ThirdViewController
thirdVC.selectFeelingSubject.subscribe(onNext : {
print($0)
})
.disposed(by: self.disposeBag)
If you have more than one child view controller, you will need to change the 0 to the appropriate number so you are getting the correct child.

Related

Enable or disable menu items on different view controllers in cocoa app?

I have 3 view controllers say main1, main2 and child. I have added a menu item, on click of that it should open child view controller as modal.
Whenever user is in main1 VC, menu item should be enabled. If user in main2 VC, menu should be disabled. Right now I’ve added modal segue between menu item and child VC.
I followed following approaches to disable, but they are not working.
Method 1:
In main2 VC, I’ve added
func validateUserInterfaceItem(_ anItem: NSValidatedUserInterfaceItem) -> Bool {
return false
}
override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
return false
}
Method 2:
override func viewDidLoad() {
super.viewDidLoad()
let mainMenu = NSApplication.shared().mainMenu!
let appMenu = mainMenu.item(at: 0)!.submenu
appMenu?.item(withTitle: someMenuTitle)?.isEnabled = false
}
If you use a modal segue it will be always activated.
To enable/disable dependent on the presented view controller I would add an action to the view controller to open the view controller manualy as modal. The menu item has to be connected to the action (openModalViewController) with the first responder.
#IBAction func openModalViewController(_ sender: AnyObject) {
let storyboard = NSStoryboard(name: "Main", bundle: nil)
let viewController = storyboard.instantiateController(withIdentifier: "MyViewController") as! NSViewController
presentAsModalWindow(viewController)
}
Consider there must be at least one view able to get the first responder in main1/main2 that the menu item will activate. If this is not the case you would have to implement acceptsFirstResponder for the corresponding view.
override var acceptsFirstResponder: Bool{
return true
}
To implement validateUserInterfaceItem would be not required in this case, only if you want to control activation/deactivation dependent on an additional state as in the example below.
extension ViewController: NSMenuItemValidation {
func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
if menuItem.action == #selector(delete(_:)) {
return tableView.selectedRow < 0 ? false : true
}
return true
}
}

Obtaining a View Controller Reference

I read quite a few questions and answers no this problem. Some are for Ojective C. Some are for iOS. The ones that were close to what I need didn't work.
I've set up a protocol for delegation. It doesn't work. The problem is that delegate variable isn't set. I need the reference to an active controller.
Delegator
protocol SwitchTabDelegate: class {
func selectTab(tab: Int)
}
class ViewController: NSViewController {
weak var delegate: SwitchTabDelegate?
override func viewDidLoad() {
super.viewDidLoad()
}
#IBAction func selectCompositions(_ sender: NSButton) {
if let delegate = self.delegate {
delegate.selectTab(tab: 2)
}
else {
print("self.delegate is nil")
}
print("delegate called")
}
}
Delegatee
class TabViewController: NSTabViewController, SwitchTabDelegate {
var viewController : ViewController?;
override func viewDidLoad() {
super.viewDidLoad()
//viewController = storyboard?.instantiateController(withIdentifier: "viewController") as? ViewController
// viewController?.delegate = self
// print(viewController)
}
func selectTab(tab: Int) {
print("In the delegate")
switchToDataTab()
}
func switchToDataTab() {
Timer.scheduledTimer(timeInterval: 0.2, target: self,
selector: #selector(switchToDataTabCont),
userInfo: nil, repeats: false)
}
func switchToDataTabCont(){
self.selectedTabViewItemIndex = 2
}
}
The delegatee is the main NSViewContoller. On the storyboard, it contains two buttons and a Container view controller. Embedded in the container view controller is the TabViewController, the delegatee. You can see in the delegatee where I tried to get a reference. It does get a reference, presumably to the newly instantiated instance. I need a reference to the original view controller that was spun up when the application started.
Answer
I added the following code to the delegator:
override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
let controller = segue.destinationController as! TabViewController
self.delegate = controller as SwitchTabDelegate
}
That's not how it should work following the design pattern. The delegator should have no knowledge of the delegatee. I've spent way too much time on this issue so a hack is going to do.
When using storyboards, you want to "push" references to children when they are created vs. pulling them from an upstream controller. This is what -prepareForSegue:sender: is used for.

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")
}

AlertView button to specific ViewController

I am looking at how to do an AlertView that when the button "OK" is pressed it takes you to a specific View Controller called DiveNumberViewController.
I have the AlertView code done (see below) but can't figure out how to have the OK button to the DiveNumberViewController. Any Help is appreciated.
I am using Xcode 6.3 and Swift
var Alert:UIAlertView = UIAlertView(title: "Alert", message: "Hello", delegate: self, cancelButtonTitle: "OK")
Alert.show()
Try this instead:
//// MARK - UIAlertViewDelegate
func alertView(alertView: UIAlertView, clickedButtonAtIndex buttonIndex: Int) {
//index of cancel button
if buttonIndex == 0
{
//add code if needed
}
//index of OK button
if buttonIndex == 1
{
//add code to go to your controller
var divenumberViewController = self.storyboard?.instantiateViewControllerWithIdentifier("DiveNumberViewController") as! DiveNumberViewController
self.navigationController?.pushViewController(divenumberViewController, animated: true)
}
}
Also, check your storyboard to be sure to have your controller class and your storyboard id setup inside.
Custom class | class : DiveNumberViewController
Identity | Storyboard ID : DiveNumberViewController
example here:
You can do like this :
var alert:UIAlertView = UIAlertView(title: "Alert", message: "Hello", delegate: self, cancelButtonTitle: "Cancel", otherButtonTitles:"OK")
alert.show()
Or
var alert:UIAlertView = UIAlertView(title: "Alert", message: "Hello", delegate: self, cancelButtonTitle: "Cancel")
alert.addButtonWithTitle("OK")
alert.show()
Don't forget to add the delegate
MyController : UIViewcontroller, UIAlertViewDelegate
When the user will click on the buttons, the delegate will fire this function below. So add your code here to go to DiveNumberViewController
//// MARK - UIAlertViewDelegate
func alertView(alertView: UIAlertView, clickedButtonAtIndex buttonIndex: Int) {
//index of cancel button
if buttonIndex == 0
{
//add code if needed
}
//index of OK button
if buttonIndex == 1
{
//add code to go to your controller
}
}
}
If you use a lot of UIAlertView in a same controller, you can add a tag to each UIAlertView like that. It will allow to add a specific action depending of the UIAlertView which is clicked
alertView.tag = 999
For more information, take a look on Apple's doc :
https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIAlertView_Class/index.html

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