Passing data between ViewModels in MVVM-C - ios

I am using MVVM with Coordinator to design an application. One thing that i am having doubts on is on how to pass data between different ViewModels. Normally the previous viewModel would just create the next viewModel and would just do a method dependency injection in prepareforsegue. However now that i am responsible for all the navigation how do i achieve this ?
Class AppCoordinator : NSObject, Coordinator, UINavigationControllerDelegate {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
var dependencyContainer : MainDependencyContainer
func start() {
let vc = ViewController.instantiate()
vc.coordinator = self
vc.viewModel = dependencyContainer.makeMainViewModel()
navigationController.delegate = self
navigationController.pushViewController(vc, animated: true)
}
func createAccount() {
let vc = CreateAccountViewController.instantiate()
vc.coordinator = self
navigationController.pushViewController(vc, animated: true)
}
}
I could ofcourse create the ViewModel for CreateAccountViewController in MainViewModel and pass the ViewModel as a paramter in createAccount method but is it the right way to do it here ? What will be the unit testing implications here ?

Ideally, you don't want both ViewModels to interact with each other and keep both elements separated.
One way to deal with it is to pass through the minimum data required for the navigation.
class AppCoordinator : NSObject, Coordinator, UINavigationControllerDelegate {
var childCoordinators = [Coordinator]()
var navigationController: UINavigationController
var dependencyContainer : MainDependencyContainer
func start() {
let vc = ViewController.instantiate()
vc.coordinator = self
let viewModel = dependencyContainer.makeMainViewModel()
// for specific events from viewModel, define next navigation
viewModel.performAction = { [weak self] essentialData in
guard let strongSelf = self else { return }
strongSelf.showAccount(essentialData)
}
vc.viewModel = viewModel
navigationController.delegate = self
navigationController.pushViewController(vc, animated: true)
}
// we can go further in our flow if we need to
func showAccount(_ data: AnyObject) {
let vc = CreateAccountViewController.instantiate()
vc.viewModel = CreateAccountViewController(with: data)
vc.coordinator = self
navigationController.pushViewController(vc, animated: true)
}
}
Going further, you can create a specific Coordinator for CreateAccountViewController that will get initialized with those data. The start() method will do create whatever is needed for its ViewController.
// we can go further in our flow if we need to
func showAccount(_ data: AnyObject) {
let coordinator = CreateAccountCoordinator(data: data, navigationController: navigationController)
coordinator.start()
childCoordinators.append(coordinator)
}
In this last example, the coordinator is only responsible to build its view and pass through essential information to next coordinator whenever needed. The viewModel is only exposed to its view, and eventually the view is unaware of both. It could be a good alternative in your case.
Finally, you can test using a protocol abstraction to make sure performAction triggers showAccount, that showAccount create a child coordinator, and so on.

Related

Passing data in between controllers using coordinator pattern

I am trying to understand the working of Coordinator Pattern.
Here is my code
import UIKit
import Foundation
class CheckoutCoordinator: Coordinator, ScheduleDelegate {
var childCoordinator: [Coordinator] = [Coordinator]()
var navigationController: UINavigationController
init(nav: UINavigationController) {
self.navigationController = nav
}
func start() {
let ctrl = CheckoutController.initFromStoryboard()
ctrl.coordinator = self
self.navigationController.pushViewController(ctrl, animated: true)
}
func openSchedule() {
let ctrl = ScheduleController.initFromStoryboard()
ctrl.delegate = self
self.navigationController.pushViewController(ScheduleController.initFromStoryboard(), animated: true)
}
func didSelectTimings(date: NSDate, timings: NSString, distance: Double) {
}
}
From CheckoutController, i go to ScheduleController, do some work which calls its delegate method. The delegate should update some value in CheckoutController and pop scheduleController. I am unable to find any concrete explanation of above senario and how to implement it "properly".
Note that schedule controller has no navigation forward hence no coordinator class for it.
Any guidance will be appreciated
I would not handle the delegate logic in the coordinator. Instead I would move it right into your CheckoutController. So when calling the ScheduleController it would look in your coordinator like this:
func openSchedule(delegate: ScheduleDelegate?) {
let ctrl = ScheduleController.initFromStoryboard()
ctrl.delegate = delegate
navigationController.pushViewController(ScheduleController.initFromStoryboard(), animated: true)
}
And in your CheckoutController, conform to the ScheduleDelegate delegate:
class CheckoutController: ScheduleDelegate {
func didSelectTimings(date: NSDate, timings: NSString, distance: Double) {
// Do your staff
}
}
Then in your ScheduleController after calling the delegate method, I would call the coordinator to pop the self(in that case the ScheduleController).
delegate?.didSelectTimings(date: yourDate, timings: someTiming, distance: distance)
if let checkoutCoordinator = coordinator as? CheckoutCoordinator {
checkoutCoordinator.popViewController()
}
The popping logic can be solely in your viewController, but I like to keep the navigation in the Coordinator only. And in your CheckoutCoordinator, or better in your Coordinator(as this function is pretty general), implement the pop function.
extension Coordinator {
function popViewController(animated: Bool = true) {
navigationController?.popViewController(animated: animated)
}
}

Weird retain cycle when using the Coordinator pattern

I am building a new app using MVVM + Coordinators. Specifically, I am using the Coordinator pattern found on https://github.com/daveneff/Coordinator.
On the top level I have an AppCoordinator that can start the child coordinator RegisterCoordinator. When the sign up flow is complete, the AppCoordinator then switches the root viewcontroller of its navigator, and the coordinators and viewcontrollers used in the sign up flow should be released from memory.
final class AppCoordinator: CoordinatorNavigable {
var dependencies: AppDependencies
var childCoordinators: [Coordinator] = []
var rootViewController = UINavigationController()
var navigator: NavigatorType
init(window: UIWindow, dependencies: AppDependencies) {
self.dependencies = dependencies
navigator = Navigator(navigationController: rootViewController)
dependencies.userManager.delegate = self
window.rootViewController = rootViewController
window.makeKeyAndVisible()
}
func start() {
if dependencies.properties[.user] == nil {
// Logged out state
let vc = AuthFlowViewController.instantiate(storyboardName: Constants.Storyboards.authFlow)
vc.delegate = self
navigator.setRootViewController(vc, animated: false)
} else {
// Logged in
let vc = HomeViewController.instantiate(storyboardName: Constants.Storyboards.home)
vc.viewModel = HomeViewModel(dependencies: dependencies)
navigator.setRootViewController(vc, animated: false)
}
childCoordinators = []
}
}
extension AppCoordinator: UserManagerDelegate {
func authStateChanged() {
// User logged in or logged out; show the correct root view controller
start()
}
func userChanged() {}
}
extension AppCoordinator: AuthFlowViewControllerDelegate {
func login() {
dependencies.userManager.changeUser(newUser: User(id: 1, name: "Kevin"))
}
func startRegisterFlow() {
let registerCoordinator = RegisterCoordinator(dependencies: dependencies, navigator: navigator)
pushCoordinator(registerCoordinator, animated: true)
}
}
The RegisterCoordinator meanwhile simply pushes multiple viewcontrollers onto the navigator's stack:
class RegisterCoordinator: CoordinatorNavigable {
var dependencies: AppDependencies
var childCoordinators: [Coordinator] = []
var navigator: NavigatorType
let rootViewController = PhoneInputViewController.instantiate(storyboardName: Constants.Storyboards.authFlow)
init(dependencies: AppDependencies, navigator: NavigatorType) {
self.dependencies = dependencies
self.navigator = navigator
rootViewController.delegate = self
}
func start() {}
}
extension RegisterCoordinator: PhoneInputViewControllerDelegate {
func phoneInputDone() {
let vc = PhoneValidationViewController.instantiate(storyboardName: Constants.Storyboards.authFlow)
vc.delegate = self
navigator.push(vc, animated: true)
}
}
extension RegisterCoordinator: PhoneValidationViewControllerDelegate {
func phoneValidationDone() {
let vc = GenderSelectionViewController.instantiate(storyboardName: Constants.Storyboards.authFlow)
vc.viewModel = GenderSelectionViewModel(dependencies: dependencies)
navigator.push(vc, animated: true)
}
}
When the entire sign up flow is complete, the last page can save the user, which triggers the authStateChanged method in the AppCoordinator, which then changes the navigator's rootViewController. This should then clean up its child coordinators as well.
Sadly though, the RegisterCoordinator and its rootViewController (PhoneInputViewController) are kept alive - although the other viewcontrollers in the flow are properly released.
I tried to manually do childCoordinators = [] in the start method to make sure the AppCoordinator doesn't have a strong reference to the RegisterCoordinator, but even that doesn't help.
I have no clue what is keeping the strong reference, causing the retain cycle / memory leak. I have a super minimal version of my app with basically everything removed except the bare essentials to show the problem, up on GitHub: https://github.com/kevinrenskers/coordinator-problem.
First of all, you're capturing your coordinator inside a block in Coordinator.self line 132:
I found this using Debug Memory Graph:
there's also PhoneInputViewController still alive, you can examine why using the same method
I can't fully understand how your coordinator pattern implementation work, but it's a good idea not to keep strong refs to your controllers.
I've been using some implementation, where controllers being kept only by UINavigationController's stack, and window keeps UINavigationController.
It guarantees that your controllers will always die once popped/replaced.
In your case, I would start by trying making childCoordinators of Coordinator to keep weak refs to your controllers.
The answer from rkyr pushed me in the right direction, and I found the source of the problem and sent a PR with the fix to the original Coordinator library that I am using. So check it out there for the one-line fix: https://github.com/daveneff/Coordinator/pull/1.

Way to pass data to another vc with segues without open var

Is there a way to avoid open variables when using segues (or not segues)?
Everybody saw code like this:
if segue.identifier == ListViewController.className()
{
guard let indexPath = tableView.indexPathForSelectedRow else { return }
let destinationVC = segue.destination as? ListViewController
var data: CategoryModel
data = filteredData[indexPath.row]
destinationVC?.passedData = data
}
}
But in ListViewController now we have a var that open for access.
class ListViewController: UIViewController
{
//MARK: - DataSource
var passedData: CategoryModel?
Maybe exist way to avoid this?
I was thinking about dependency injection with init(data: data), but how to initiate this vc right?
Edited.
Using segue it's not a main goal. Main is to make var private. If there exist nice way to not to use segues and push data private I will glad to know.
I was trying to use init() and navigationController?.pushViewController(ListViewController(data: data), animated: true)
but
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value on line:
self.tableView.register(ListTableViewCell.nib(), forCellReuseIdentifier: ListTableViewCell.identifier())
You can't actually make Interface builder use a custom init for your view controller, it will always use init?(coder:).
So the easiest way to pass data to your view controller is to use a non-private property.
But if you really don't want to use an internal or public var you can always try something with a Singleton or Notifications but I don't think it would be wise
You could do it like so
class ListViewController {
private var passedData: CategoryModel?
private init () {
}
public convenience init (passedData: CategoryModel?) {
self.init()
self.passedData = passedData
}
}
And in tableView(_:didSelectRowAt:) of your initial table view controller:
let data: CategoryModel = filteredData[indexPath.row]
let destinationVC = ListViewController(passedData: data)
self.present(destinationVC, animated: true, completion: nil)

Swift 3 : Back to last ViewController with sending data [duplicate]

This question already has answers here:
Passing data between view controllers
(45 answers)
Closed 5 years ago.
I'm trying to go back to my las viewController with sending data, but it doesn't work.
When I just use popViewController, I can go back to the page, but I can't move my datas from B to A.
Here is my code :
func goToLastViewController() {
let vc = self.navigationController?.viewControllers[4] as! OnaylarimTableViewController
vc.onayCode.userId = taskInfo.userId
vc.onayCode.systemCode = taskInfo.systemCode
self.navigationController?.popToViewController(vc, animated: true)
}
To pass data from Child to parent Controller, you have to pass data using Delegate pattern.
Steps to implement delegation pattern, Suppose A is Parent viewController and B is Child viewController.
Create protocol, and create delegate variable in B
Extend protocol in A
pass reference to B of A when Push or Present viewcontroller
Define delegate Method in A, receive action.
After that, According to your condition you can call delegate method from B.
You should do it using delegate protocol
class MyClass: NSUserNotificationCenterDelegate
The implementation will be like following:
func userDidSomeAction() {
//implementation
}
And ofcourse you have to implement delegete in your parent class like
childView.delegate = self
Check this for more information
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html
You have to send back to last ViewController with 2 options.
1. Unwind segue. (With use of storyboard)
You can refer this link.
2. Use of delegate/protocol.
You can refer this link.
Also this link will be useful for you.
You can use Coordinator Pattern
For example, I have 2 screens. The first displays information about the user, and from there, he goes to the screen for selecting his city. Information about the changed city should be displayed on the first screen.
final class CitiesViewController: UITableViewController {
// MARK: - Output -
var onCitySelected: ((City) -> Void)?
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
onCitySelected?(cities[indexPath.row])
}
...
}
UserEditViewController:
final class UserEditViewController: UIViewController, UpdateableWithUser {
// MARK: - Input -
var user: User? { didSet { updateView() } }
#IBOutlet private weak var userLabel: UILabel?
private func updateView() {
userLabel?.text = "User: \(user?.name ?? ""), \n"
+ "City: \(user?.city?.name ?? "")"
}
}
And Coordinator:
protocol UpdateableWithUser: class {
var user: User? { get set }
}
final class UserEditCoordinator {
// MARK: - Properties
private var user: User { didSet { updateInterfaces() } }
private weak var navigationController: UINavigationController?
// MARK: - Init
init(user: User, navigationController: UINavigationController) {
self.user = user
self.navigationController = navigationController
}
func start() {
showUserEditScreen()
}
// MARK: - Private implementation
private func showUserEditScreen() {
let controller = UIStoryboard.makeUserEditController()
controller.user = user
controller.onSelectCity = { [weak self] in
self?.showCitiesScreen()
}
navigationController?.pushViewController(controller, animated: false)
}
private func showCitiesScreen() {
let controller = UIStoryboard.makeCitiesController()
controller.onCitySelected = { [weak self] city in
self?.user.city = city
_ = self?.navigationController?.popViewController(animated: true)
}
navigationController?.pushViewController(controller, animated: true)
}
private func updateInterfaces() {
navigationController?.viewControllers.forEach {
($0 as? UpdateableWithUser)?.user = user
}
}
}
Then we just need to start coordinator:
coordinator = UserEditCoordinator(user: user, navigationController: navigationController)
coordinator.start()

Using prepareForSegue to pass data to ViewController later in app

I was wondering, when passing data using prepareForSegue, can you pass data to a View Controller later in the app? For example on the first ViewController I have the user enter their name. It's not until the very end, so a few views later, do I need to display their name. Is there a way to pass their name without having to go to the end view right away?
Use a Coordinator.
It's really easy to decouple your ViewControllers:
instead of using segues give every ViewController a delegate
create a coordinator object (this object knows your screen flow, not your screens)
the coordinator creates the ViewControllers (it can use UIStoryboard instantiateViewController(withIdentifier:) so ViewController A does not have to know that ViewController B exists
instead of calling performSegue you just call your delegate and pass in the data
Benefits
Simple to use
Easy to reorder screens in a flow
Highly decoupled (easier testing)
Very nice for A/B testing
Scales a lot (you can have multiple coordinators, one for each flow)
Sample
Let's say you have 3 VCs, the first one asks for your name, the second for your age and the third displays the data. It would make no sense that AgeViewController knew that NameViewController existed, later on you may want to change their order or even merge them.
Name View Controller
protocol NameViewControllerDelegate: class {
func didInput(name: String)
}
class NameViewController: UIViewController {
weak var delegate: NameViewControllerDelegate?
#IBOutlet var nameTextField: UITextField!
//Unimportant stuff ommited
#IBAction func submitName(sender: Any) {
guard let name = nameTextField.text else {
// Do something, it's up to you what
return
}
delegate?.didInput(name: name)
}
}
Age View Controller
protocol AgeViewControllerDelegate: class {
func didInput(age: Int)
}
class AgeViewController: UIViewController {
weak var delegate: AgeViewControllerDelegate?
#IBOutlet var ageTextField: UITextField!
//Unimportant stuff ommited
#IBAction func submitAge(sender: Any) {
guard let ageString = ageTextField.text,
let age = Int(ageString) else {
// Do something, it's up to you what
return
}
delegate?.didInput(age: age)
}
}
Displayer View Controller
class DisplayerViewController: UIViewController {
var age: Int?
var name: String?
}
Coordinator
class Coordinator {
var age: Int?
var name: String?
var navigationController: UINavigationController
init(navigationController: UINavigationController) {
self.navigationController = navigationController
}
fileprivate lazy var storyboard: UIStoryboard = {
return UIStoryboard(name: "MyStoryboard", bundle: nil)
}()
//This works if you name your screns after their classes
fileprivate func viewController<T: UIViewController>(withType type: T.Type) -> T {
return storyboard.instantiateViewController(withIdentifier: String(describing: type(of: type))) as! T
}
func start() -> UIViewController {
let viewController = self.viewController(withType: NameViewController.self)
viewController.delegate = self
navigationController.viewControllers = [viewController]
return viewController
}
}
Coordinator + Name View Controller Delegate
extension Coordinator: NameViewControllerDelegate {
func didInput(name: String){
self.name = name
let viewController = self.viewController(withType: AgeViewController.self)
viewController.delegate = self
navigationController.pushViewController(viewController, animated: true)
}
}
Coordinator + Age View Controller Delegate
extension Coordinator: AgeViewControllerDelegate {
func didInput(age: Int) {
self.age = age
let viewController = self.viewController(withType: DisplayerViewController.self)
viewController.age = age
viewController.name = name
navigationController.pushViewController(viewController, animated: true)
}
}
Not really. You can pass view by view the item but it's not a proper way of doing things.
I suggest you to have a Static Manager or this kind of stuff to store the information globally in your app to retrieve it later
All the solution are pretty good. Possible you can try the below model also
1. DataModel class
1.1 Should be singleton class
1.2 Declare value
Step 1 : ViewCOntroller-one
1 Create the Sharedinstance of singleton class
1.1 Assign the value
Step 3 :ViewController-two
1 Create the Sharedinstance of singleton class
1.1 Get the value

Resources