Performing navigation in mvvm with RxSwift - ios

Iam using MVVM with RxSwift i have tried Coordinator and RxFlow to navigate between viewcontroller.
Is there any simply approach to segue between viewcontroller with RxSwift
viewModel.users.subscribe {
model in
self.walkthrough = WalkthroughModel(country: (model.element?.country)!, countryCode: (model.element?.countryCode)!,PhoneNumber:"")
DispatchQueue.main.async {
self.performSegue(withIdentifier: "Walkthrough_phone", sender: self)
}
}.dispose()
these the normal approach iam doing right now but is there any way to bind segu to the button

RxFlow may become the answer.
RxFlow is a navigation framework for iOS applications based on a Reactive Flow Coordinator pattern
https://github.com/RxSwiftCommunity/RxFlow

I think segue should be perform in view controller because it is UI action, and it has nothing to do with view model. You should hold reference to viewModel inside viewController class, so you can easy access it properties while do tap on button in view controller class.
Performing tap to button with RxSwift look like that:
btn.rx.tap
.subscribe(onNext: { [weak self] _ in
// Do segue
})
.addDisposableTo(bag)

Related

Segues Programmatically Pattern

Does anybody know if there is a certain pattern for handling segues programmatically in a MVC way?
I would think the best way would be to work with an event system within a controller.
I want that all the view controllers connect to this navigationController instead of handling all the logic within the viewController logic itself. I want to out source this logic
In most of your view controllers, you will have access to a prepareForSegue function, with one parameter called sender.
If you kick off a segue programatically with performSegue(withIdentifier: "mySegueID", sender: yourVC) then this function will be called, and you'll be able to pass information from the sender to the new view controller.
In this function, to get a handle on the next VC, use segue.destinationViewController.
I don't know about a particular pattern but a simple way to programmatically handle transitions between 2 UIViewController could be to have a separated manager whose job is just to push/present/whatever new controllers over current, and to pop/dismiss/whatever current controllers to old ones.
The way I usually do this is by having a class we can name WorkflowManager, which will handle all transitions. Associated with this manager, you declare a WorkflowManagerComponent protocol and implement it :
protocol WorkflowManagerComponent {
var completionHandler: (hasCompleted:Bool,data:Any)->() {get set}
}
Make each UIViewController implement this, for example by calling completionHandler(true,someData) when the user taps a "next" button, or completionHandler(false,nil) when the user taps a "back" button.
Then in your workflow manager, you perform transitions to the next or previous UIViewController according to parameters sent in the completionHandler:
//init viewController1 ...
viewController1.completionHandler = onViewController1Completes
// ...
func onViewController1Completes(_ completed: Bool, data: Any) {
if hasCompleted {
//init viewController2 ...
viewController2.data = data
viewController2.completionHandler = onViewController2Completes
//Push the new vc
viewController1.navigationController.push(viewController2, animated: true)
} else {
//The vc1 was presented as a modal, dismiss it
viewController1.dismiss()
}
}
This way each UIViewController is separated from others, free off any transition logic.

MVVM coordinators and popping a UIViewController

I've recently started using coordinators (Example: MVVM with Coordinators and RxSwift) to improve my current MVVM architecture. It's a nice solution to remove navigation related code from the UIViewController.
But I'm having trouble with 1 specific scenario. The issue arrises when a UIViewController is popped by the default back button or edge-swipe gesture.
Quick example using a list-detail interface:
A list UIViewController is shown by a ListCoordinator inside a UINavigationController. When an item is tapped, the ListCoordinator creates a DetailCoordinator, registers it as a child coordinator and starts it. The DetailCoordinator pushes the detail UIViewController onto the UINavigationController, just like every MVVM-C blog post illustrates.
What every MVVM-C blog post fails to illustrate is what happens when the detail UIViewController is popped by the default back button or edge-swipe gesture.
The DetailCoordinator should be responsible for popping the detail UIViewController, but a) it doesn't know the back button was tapped and b) the pop happens automatically. Also, the ListCoordinator wasn't able to remove the DetailCoordinator from its child coordinators.
One solution would be to use custom back buttons, which signal the tap and pass it on to the DetailCoordinator. Another one is probably using UINavigationControllerDelegate.
How have others solved this issue? I'm sure I'm not the first one.
I use Action for communication between coordinators and also between coordinator and a view controller.
AuthCoordinator
final class AuthCoordinator: Coordinator {
func startLogin(viewModel: LoginViewModel) {
let loginCoordinator = LoginCoordinator(navigationController: navigationController)
loginCoordinator.start(viewModel: viewModel)
viewModel.coordinator = loginCoordinator
// This is where a child coordinator removed
loginCoordinator.stopAction = CocoaAction { [unowned self] _ in
if let index = self.childCoordinators.index(where: { type(of: $0) == LoginCoordinator.self }) {
self.childCoordinators.remove(at: index)
}
return .empty()
}
}
}
LoginCoordinator
final class LoginCoordinator: Coordinator {
var stopAction: CocoaAction?
func start(viewModel: LoginViewModel) {
let loginViewController = UIStoryboard.auth.instantiate(LoginViewController.self)
loginViewController.setViewModel(viewModel: viewModel)
navigationController?.pushViewController(loginViewController, animated: true)
loginViewController.popAction = CocoaAction { [unowned self] _ in
self.stopAction?.execute(Void())
return .empty()
}
}
}
LoginViewController
class LoginViewController: UIViewController {
var popAction: CocoaAction?
override func didMove(toParentViewController parent: UIViewController?) {
super.didMove(toParentViewController: parent)
if parent == nil { // parent is `nil` when the vc is popped
popAction?.execute(Void())
}
}
}
So the LoginViewController executes the action when it's popped. It's coordinator LoginCoordinator is aware that the view is popped. It triggers another action from its parent coordinator AuthCoordinator. The parent coordinator AuthCoordinator removes its child LoginCoordinator from the childControllers array/set.
BTW, why do you need to keep the child coordinators in the array and then thinking about how to remove them. I tried another approach, the child coordinator retained by a view model, once the view model deallocated, the coordinator deallocates too. Worked for me.
But I'm personally don't like so many connections and thinking about a simpler approach using a single coordinator object for everything.
I wonder if you have already solved your architecture problem and if you'd like to share your solution. I asked something related to your problem here and Daniel T. suggested to subscribe to navigationController.rx.willShow: you get back events whenever a ViewController is popped OR pushed onto the view controller stack, so you need to check yourself what kind of event it is (a pop or a push). I think the viewModel / viewController shouldn't know anything of the next story to present, so I think the viewModel could emit an event ("show detail of table cell #n") and a coordinator should push or pop the right scene ("detail of cell #n"). This kind of architecture is too advanced for me to write, so I end up with a lot of circular references / memory leaks.
Unless I am missing something, you can solve this by using this piece of code in your coordinate method. I am specifically using didShow instead of willShow (which was suggested in another answer) for the possibility of edge swipe gestures.
if let topViewController = navigationController?.topViewController {
navigationController?.rx
.didShow
.filter { $0.viewController == topViewController }
.first()
.subscribe(onSuccess: { [weak self] _ in
// remove child coordinator
})
.disposed(by: disposeBag)
}

Retaining Data in a View Controller When Switching from One VC to Another

I have three views, each with its own view controller: VC1, VC2, VC3.
The user will frequently switch back and forth between each of the three views, both forward and backward.
Each view contains data: both shared from the previous view and data unique to that view.
When the user goes back to a View that he has already visited, the data displayed on that view needs to be retained (the same data as he saw the last time he visited that view), and not set to the default values the first time he visited the view.
In the first view controller, VC1, I am using a prepare for segue to push data from VC1 to VC2 or VC3:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "segueToVC2” {
let destinationViewController: VC2 = segue.destination as! VC2;
destinationViewController.passedData1 = firstAmount
destinationViewController.passedData2 = secondAmount
destinationViewController.passedData3 = thirdAmount
} else {
let destinationViewController: VC3 = segue.destination as! VC3;
destinationViewController.passedData1 = firstAmount
destinationViewController.passedData2 = secondAmount
destinationViewController.passedData3 = thirdAmount
destinationViewController.passedData4 = fourthAmount
}
By tapping the GO BACK button on each view, I return to the previous view:
#IBAction func goBackButtonPressed(_ sender: Any) {
print("Back Button Pressed!")
self.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
}
I am having trouble passing data backwards. And when I return to VC2 from VC1, data has been reset to 0. I have no segues going back from VC2 to VC1 or from VC3 to VC2. Would that be the cleanest way to pass the data back: to create another segue in Main.storyboard from VC2 to VC1 and then add another ‘if’ to my prepare for segue that checks for VC1?
I am passing ALL these variables back and forth between view controllers but only using some of them in each view controller. It seems like a waste and I don't think I am on the right track here.
Any help or suggestions?
View controllers should never store data. They are responsible for coordinating between model objects and view objects. That's their whole point. The pattern you're looking for is called MVC (Model-View-Controller) and it's a core part of iOS development.
Move your data out of the view controllers and put it into model classes. Each view controller should fetch data out of the model, and send updates into the model. The only thing the view controllers should pass between themselves is what model objects to work on, and most of the time that only needs to pass in one direction (down the stack).
Delegation can be a useful tool here, and you can also investigate "unwind segues" which are built to help you send data upstream. But again, the data you should be sending is mostly references to the model, and the model itself needs to live outside the view controllers.
It's in Objective-C, but still one of the best simple examples from Apple on MVC design is TheElements, and is worth exploring as a basis. Even without reading the Objective-C, you can see how the various pieces fit together.
I haven't studied it as much as TheElements, but Lister claims to be a good demonstration of MVC patterns in Swift using modern iOS techniques.
Why don't you call a delegate which passes the data to the view controller when you press back button.
Or if the data shared by all view controllers reflect the same value. Make a singleton class and use those values across the app.
example singleton class:
class SomeModel {
static let shared = SomeModel()
private init() {}
}

Pass data between three viewController, all in navigationController, popToRootView

The issue I'm having is this.
I have a navigation controller with 3 viewController. In the 1st controller, I have the user select an image. This image is passed to 2nd and 3rd controller via prepareForSegue.
At the 3rd controller, I have a button that takes the user back to the 1st view controller. I explored 2 ways in doing this:
1) use performSegue, but I don't like this because it just push the 1st controller to my navigation stack. So I have this weird "Back" button at the 1st Viewcontroller now, which is not what I want. I want the app to take user directly to 1st viewcontroller without the back button.
2) I tried Poptorootviewcontroller. This solves the issue of the "back" button. But, when I pop back to the 1st viewcontroller, the user's selected image is still on screen. I want to clear this image when the user goes from the 3rd viewcontroller back to the 1st viewcontroller.
So with approach 2), how do I make sure all memory is refreshed and the image becomes nil in the 1st viewcontroller? Since I'm not using performSegue, 3rd viewcontroller does not have access to the 1st Viewcontroller.
For refresh, you'd have to clear it in viewWillAppear but I find this rather dangerous. Best you can do there is to create a new copy of the view controller everytime and Swift will take care of the rest. I don't know if you are using the storyboard but I would recommend using the class UIStoryboard and the function instiantiateViewControllerWithIdentifier("something") as! YourCustomVC
As long as you stay in the navigation stack, you'll not lose any of the current configurations of previous View Controllers.
As for passing data back to the first controller. You can either just throw it in the global scope which is the easiest way but might be difficult to know when it was updated or if the data is fresh. But you can always just:
var something: String = ""
class someView: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
something = "foo"
}
}
Something will be availabe everywhere then.
You could make a protocol and pass the delegate along the 3 view controllers. So when you are starting it you could do:
func someAction() {
let v = SomeViewController()
v.delegate = self
self.navigationController?.pushViewController(v, animated: true)
}
And then with each following view:
func someOtherAction() {
let v = SomeOtherViewController()
v.delegate = self.delegate
self.navigationController?.pushViewController(v, animated: true)
}
Although personally I find it hard to keep track of this.
Lastly you could use the NSNotificationCenter to pass an object along with all the data and catch it in a function on your first controller.
To do this you first register your VC for the action in viewDidLoad() or something:
NSNotificationCenter.defaultCenter().addObserver(self, selector: "someAction:", name: "someNotification", object: nil)
Then when you are done in the 3rd view make some object or a collection of string and send it back as follows:
NSNotificationCenter.defaultCenter().postNotificationName("someNotification", object: CustomObject())
And then lastly you'll catch it in the function "someAction"
func someAction(note: NSNotification) {
if let object = note.object as? CustomObject {
//Do something with it
}
}
Hope this helps!
Use an unwind segue which provides the functionality to unwind from the 3rd to the 1st (root) view controller.
The unwind segue is tied to an action in the root view controller. Within this action, you simply nil the image:
#IBAction func unwindToRootViewController(sender: UIStoryboardSegue)
{
let sourceViewController = sender.sourceViewController
// Pull any data from the view controller which initiated the unwind segue.
// Nil the selected image
myImageView.image = nil
}
As you can see in the action, segues also let you pass data back from the source view controller. This is a much simpler approach than needing to resort to using delegates, notifications, or global variables.
It also helps keep things encapsulated, as the third view controller should never need to know specifics about a parent view controller, or try to nil any image that belongs to another view controller.
In general, you pass details to a controller, which then acts on it itself, instead of trying to manipulate another controller's internals.

Swift: Perform a function on ViewController after dismissing modal

A user is in a view controller which calls a modal. When self.dismissViewController is called on the modal, a function needs to be run on the initial view controller. This function also requires a variable passed from the modal.
This modal can be displayed from a number of view controllers, so the function cannot be directly called in a viewDidDisappear on the modal view.
How can this be accomplished in swift?
How about delegate?
Or you can make a ViewController like this:
typealias Action = (x: AnyObject) -> () // replace AnyObject to what you need
class ViewController: UIViewController {
func modalAction() -> Action {
return { [unowned self] x in
// the x is what you want to passed by the modal viewcontroller
// now you got it
}
}
}
And in modal:
class ModalViewController: UIViewController {
var callbackAction: Action?
override func viewDidDisappear(_ animated: Bool) {
let x = … // the x is what you pass to ViewController
callbackAction?(x)
}
}
Of course, when you show ModalViewController need to set callbackAction like this modal.callbackAction = modalAction() in ViewController
The answer supplied and chosen by the question asker (Michael Voccola) didn't work for me, so I wanted to supply another answer option. His answer didn't work for me because viewDidAppear does not appear to run when I dismiss the modal view.
I have a table and a modal VC that appears and takes some table input. I had no trouble sending the initial VC the modal's new variable info. However, I was having trouble getting the table to automatically run a tableView.reloadData function upon dismissing the modal view.
The answer that worked for me was in the comments above:
You likely want to do this using an unwind segue on the modal, that
way you can set up a function on the parent that gets called when it
unwinds. stackoverflow.com/questions/12561735/… – porglezomp Dec 15
'14 at 3:41
And if you're only unwinding one step (VC2 to VC1), you only need a snippet of the given answer:
Step 1: Insert method in VC1 code
When you perform an unwind segue, you need to specify an action, which
is an action method of the view controller you want to unwind to:
#IBAction func unwindToThisViewController(segue: UIStoryboardSegue) {
//Insert function to be run upon dismiss of VC2
}
Step 2: In storyboard, in the presented VC2, drag from the button to the exit icon and select "unwindToThisViewController"
After the action method has been added, you can define the unwind
segue in the storyboard by control-dragging to the Exit icon.
And that's it. Those two steps worked for me. Now when my modal view is dismissed, my table updates. Just figured I'd add this, in case anyone else's issue wasn't solved by the chosen answer.
I was able to achieve the desired result by setting a Global Variable as a boolean value from the modal view controller. The variable is initiated and made available from a struct in a separate class.
When the modal is dismissed, the viewDidAppear method on the initial view controller responds accordingly to the value of the global variable and, if needed, flips the value on the global variable.
I am not sure if this is the most efficient way from a performance perspective, but it works perfectly in my scenario.

Resources