RxSwift - Button Tap using Publish Subject - rx-swift

So I have a button inside my ViewController which is connected to ViewModel and than whenever the button is tapped, in my coordinator I navigate to another screen. The code is like this:
VC
btnShowShopsMap.rx.tap
.bind(to: viewModel.selectShowMap)
VM
let selectShowMap: AnyObserver<Void>
let showShopMap: Observable<Void>
//Inside init
let _selectShowMap = PublishSubject<Void>()
selectShowMap = _selectShowMap.asObserver()
showShopMap = _selectShowMap.asObservable()
Coordinator
viewModel.showShopMap
.subscribe(onNext: { _ in self.showShopMap()})
.disposed(by: userShopVC.disposeBag)
Is it possible to refactor above code? rather than using PublishSubject is there any other way to do what i am doing using PublishSubject
My VC, VM & Coordinator Flow
Coordinator
func showLoginScreen(logout: Bool = false) {
guard let viewController = LoginViewController.instantiate(storyboard: .main) else { return }
viewController.viewModelFactory = { inputs in
let viewModel = LoginViewModel(inputs: inputs)
viewModel.showHome
.subscribe(onNext: { isLogged in
if isLogged {
self.showHomeScreen()
}
})
.disposed(by: viewController.disposeBag)
inputs.showOnboarding
.subscribe(onNext: { _ in
self.showOnboardingScreen()
})
.disposed(by: viewController.disposeBag)
return viewModel
}
navController.pushViewController(viewController, animated: true)
VC
var viewModelFactory: (LoginViewModel.UIInputs) -> LoginViewModel = { _ in fatalError("factory not set")}
let inputs = LoginViewModel.UIInputs(userNumber: txtUserNumber.rx.text.orEmpty.asDriver(),
password: txtPassword.rx.text.orEmpty.asDriver(),
loginTapped: btnLogin.rx.tap.asSignal(),
userNumberLostFocus: txtUserNumber.rx.controlEvent(.editingDidEnd).asSignal(),
passwordLostFocus: txtPassword.rx.controlEvent(.editingDidEnd).asSignal(),
indicator: indicator,
showOnboarding: btnShowOnboarding.rx.tap.asObservable())
VM
struct UIInputs {
let userNumber: Driver<String>
let password: Driver<String>
let loginTapped: Signal<Void>
let userNumberLostFocus: Signal<Void>
let passwordLostFocus: Signal<Void>
let indicator: ActivityIndicator
let showOnboarding: Observable<Void>
}
init(inputs: UIInputs) {}

Assuming the view controller owns and instantiates the view model, you could pass the tap control event as an observable to the view model initializer, which then exposes it as an observable for the coordinator to subscribe to:
// VC:
let viewModel = ViewModel(..., showShopMap: btnShowShopMap.rx.tap.asObservable())
// VM:
let showShopMap: Observable<Void>
init(..., showShopMap: Observable<Void>) {
self.showShopMap = showShopMap
}
I try not to use subjects whenever possible and instead just expose transformed observables that were passed in.

I found very easy and simple way to solve my issue and avoid using Subject, As there was no logic related to my button in VM, I don't need pass my Button tap to my VM either by using Observable or using Subject. Instead I directly accessed my button in my Coordinator like this:
viewController.btnShowOnboarding.rx.tap
.subscribe(onNext: { _ in
self.showOnboardingScreen()
})
.disposed(by: viewController.disposeBag)

Related

Not understanding how to implement .drive(onNext: rxcocoa

// View controller call
viewModel.bindNotificationReadEvents(readNotificationID: readNotificationIDPublisher.asDriver(onErrorDriveWith: .empty()))
viewModel.reloadDataSourceForNotificationReadEvent.drive(reloadDataSourceForNotificationReadEventBinder).disposed(by: rx.disposeBag)
// View model
var reloadDataSourceForNotificationReadEvent: Driver<[NotificationItem]> = .empty()
fileprivate let dataSourceRelay = BehaviorRelay<[NotificationItem]>(value: [])
public func bindNotificationReadEvents(readNotificationID: Driver<String>) {
readNotificationID.drive(onNext: { [weak self] notificationID in
// read notification IDs on User Defaults
UserDefaults.main?.unreadNotificationIDs.append(notificationID)
// Update data source relay
self?.reloadDataSourceForNotificationReadEvent = readNotificationID.withLatestFrom(self?.dataSourceRelay.asDriver() ?? .empty())
}).disposed(by: rx.disposeBag)
}
when this method is called from the viewcontroller in the viewmodel it just skips both the lines and nothing executes when i checked it while debugging neither is the userdefaults updated nor is data source relay can someone please help me out.

How do I test a view controller after subscription to an Observable is done?

In the below example, how do I wait until the success callback is called to start asserting in my view controller tests?
class VC : NSViewController {
let observable: Observable<Int>
let compositeDisposable = CompositeDisposable()
override viewDidLoad() {
self.compositeDisposable.insert(
self.observable.toArray()
.observeOn(MainScheduler.instance)
.subscribe(
// draw onto the VC
)
)
}
}

Refresh/Reload Eureka PushRow's pushed ViewController options with button?

I am using Eureka forms in my project.
I have a PushRow that presents the default SelectorViewController with a list of options. In the pushed view, I have added a rightBarButtonItem that points to locationSelectorAddButton ... on click, this brings up a UIAlertController that should allow users to add options to this pushed controller.
Is it possible, without creating my own custom selector controller, to refresh the current controller with the newly saved options from UserDefaults?
let defaults = UserDefaults.standard
func setupForm() {
form
+++ PushRow<String>(K.SESSIONFIELD.location) {
$0.title = K.SESSIONFIELD.location
$0.options = defaults.array(forKey: K.SESSIONFIELD.location) as? [String]
$0.value = sessionResult?.sessionLocation ?? $0.options?.first
}
.onPresent { from, to in
to.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(named:"add_20pt"), style: UIBarButtonItemStyle.plain, target: from, action: #selector(self.locationSelectorAddButton(_:)))
}
}
the locationSelectorAddButton is implemented as follows:
#objc func locationSelectorAddButton(_ sender: UIBarButtonItem) {
var textField = UITextField()
let alert = UIAlertController(title: "Add New Location", message: "", preferredStyle: .alert)
let action = UIAlertAction(title: "Add", style: .default) { (action) in
var locArray = self.defaults.array(forKey: K.SESSIONFIELD.location) as? [String]
locArray?.append(textField.text!)
self.defaults.set(locArray, forKey: K.SESSIONFIELD.location)
///TODO: somehow refresh the pushed view controller here!!
// self.form.rowBy(tag: K.SESSIONFIELD.location)?.reload()
// print("this is the list of locations currently ...\(locArray)")
// self.tableView.reloadData()
///
}
alert.addTextField { (alertTextField) in
alertTextField.placeholder = "Location name ..."
textField = alertTextField
}
alert.addAction(action)
present(alert, animated:true, completion:nil)
}
Push row is not reloaded by using tag property. You need to implement the method
cellUpdate { cell, row in
row.options = (assign value here for updated options)
}

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

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