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)
}
Related
I'm trying to keep a timer running even if I switch view controllers. I played around with the Singleton architecture, but I don't quite get it. Pushing a new view controller seems a little easier, but when I call the below method, the view controller that is pushed is blank (doesn't look like the view controller that I created in Storyboards). The timer view controller that I'm trying to push is also the second view controller, if that changes anything.
#objc func timerPressed() {
let timerVC = TimerViewController()
navigationController?.pushViewController(timerVC, animated: true)
}
You need to load it from storyboard
let vc = self.storyboard!.instantiateViewController(withIdentifier: "VCName") as! TimerViewController
self.navigationController?.pushViewController(timerVC, animated: true)
Not sure if your problem is that your controller is blank or that the timer resets. Anyway, in case that you want to keep the time in the memory and not deallocate upon navigating somewhere else I recommend you this.
Create some kind of Constants class which will have a shared param inside.
It could look like this:
class AppConstants {
static let shared = AppConstants()
var timer: Timer?
}
And do whatever you were doing with the timer here accessing it via the shared param.
AppConstants.shared.timer ...
There are different parts to your question. Sh_Khan told you what was wrong with the way you were loading your view controller (simply invoking a view controller’s init method does not load it’s view hierarchy. Typically you will define your view controller’s views in a storyboard, so you need to instantiate it from that storyboard.)
That doesn’t answer the question of how to manage a timer however. A singleton is a good way to go if you want your timer to be global instead of being tied to a particular view controller.
Post the code that you used to create your singleton and we can help you with that.
Edit: Updated to give the TimeManager a delegate:
The idea is pretty simple. Something like this:
protocol TimeManagerDelegate {
func timerDidFire()
}
class TimerManager {
static let sharedTimerManager = TimerManager()
weak var delegate: TimeManagerDelegate?
//methods/vars to manage a shared timer.
func handleTimer(timer: Timer) {
//Put your housekeeping code to manage the timer here
//Now tell our delegate (if any) that the timer has updated.
//Note the "optional chaining" syntax with the `?`. That means that
//If `delegate` == nil, it doesn't do anything.
delegate?.timerDidFire() //Send a message to the delegate, if there is one.
}
}
And then in your view controller:
//Declare that the view controller conforms to the TimeManagerDelegate protocol
class SomeViewController: UIViewController, TimeManagerDelegate {
//This is the function that gets called on the current delegate
func timerDidFire() {
//Update my clock label (or whatever I need to do in response to a timer update.)
}
override func viewWillAppear() {
super.viewWillAppear()
//Since this view controller is appearing, make it the TimeManager's delegate.
sharedTimerManager.delegate = self
}
I have two Views:
UITableViewController (View A)
UIViewController (View B)
I was wondering, if it's possible to load and setup the table from View B and then segue to View A, when the loading is done. I need this, since the Table View loads Data from Core Data and that takes some time; I would then show a Loading Animation or something. I have a function called loadData() in View A, which fetches all Elements from Core Data and then calls tableView.reloadData().
Does anyone know, how I could implement this? Or should I somehow show the loading View directly from View A with a SubView or something?
Remember to not think about the specifics but instead, think generally:
You want to move from one VC to another and you have some data that needs to be fetched asynchronically. Let's assume you can't know how long it will take.
My suggestion is to contain all data fetching related to a VC inside that VC itself (or services/facades related to it). So basically you should present the UITableViewController and then have it fetch the data while showing skeleton-cells/spinner/etc.
You want to have separation of concerns which means you don't want your ViewController to handle data related to another view controller.
Think about the following use-case: if you have code to fetch data in the previous VC, before presenting the TVC, what happens when you need to re-fetch the data or refresh something? You will have to duplicate the code in both the VC and the TVC.
That's why it's suggested to keep data fetching inside the view controller that needs it.
If, for some reason, you still want to have your answer for this specific question:
You can have the initial VC create the TVC, but not present it yet, call its methods to fetch the data, and have it send a callback (closure/delegate/etc) when it's done fetching. When the fetching is done, present the TVC.
Here is a quick example:
class MyTableVC: UITableViewController {
private var myData: [Int] = []
public func fetchData(completion: () -> Void) {
//Fetch data asyncly
myData = [1, 2 ,3]
completion()
}
}
class MyVC: ViewController {
private func loadTableVC() {
let tableVC = MyTableVC()
tableVC.fetchData { [weak self] in
self?.present(tableVC, animated: true, completion: nil)
}
}
}
Again, I wouldn't use this due to having tight coupling between the 2 view controllers, but it's always up to you to decide how to design your code.
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)
Newbie question here.
Imagine a very basic storyboard with 2 vc (A and B).
A is embedded in a navController. A has a collectionView showing a grid of images. B is displaying the clicked grid item in big. So simple list->detail.
Doing all with IB, I ctrl-dragged from collectionView cell item to B and selected 'show (e.g Push)' segue.
Now when I run the app and click multiple times on image in grid and then on '< Back' button, I explore the memory graph.
I can see 10 'B' view controllers if I did the navigation 10 times.
That causes a lot of memory to be used and it grows every time.
I found a few posts speaking about unwind, and pop to root vc, but all are dealing with programmatic navigation. Here's just the case of simple storyboard done all with IB.
Expected: A->B->A. Memory: A
Reality: A->B->A. Memory: A, B
How can I avoid retaining the memory for those vc that are dismissed?
in A I have:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "dreamDetail" {
let newViewController = segue.destination as! DreamDetailViewController
newViewController.dream = allDreams?[(collectionView.indexPathsForSelectedItems?.first?.item)!]
newViewController.dreams = allDreams
}
}
in B I have:
weak var dream: DreamRealm?{
didSet {
}
}
var dreams: [DreamRealm]?{
didSet {
}
}
DreamRealm is just a Realm model.
class DreamRealm: Object {
#objc dynamic var filename: String? = nil
#objc dynamic var path = ""
To avoid retaining the memory for your VC that are dismissed, you need to check if there is any retain cycle.
One step to help you to check if your VCs are correctly deinitialised, you can implement a method called deinit. It's a method called when your VC is deinit and no longer in the memory. You can print a message to see if it's the case or not.
If it's not the case, you probably have a strong reference somewhere in your code. You need to avoid it by weakening your reference with weak keyword or unowned or just delete it if you don't need it.
All IB connections with UI elements(in view controller) must be weak!
Try to release strong objects in dealloc method;
Sometimes there are some delay before garbage collector deallocate objects, do "show" and "back", wait ~10 seconds and see if more memory are released.
As an addition to #Arrabidas92's answer:
watch out for double nested blocks
I built a retain cycle with the following code:
navigationItem.reactive.rightBarButtonItems <~ user.producer.map{ $0
.map{ [weak self] user in
guard let self = self else { return [] }
I thought, that the [weak self] of the inner map would suffice. It does not. The outer map already captures self (to pass it to the inner?). My retain cycle went away after writing user.producer.map{ [weak self] $0.
I have 3 ViewController.
The first ViewController is checking if the user is logged in.
If yes performSegue to the mainVC and if no performSegue to loginVC.
When I am in loginVC, I log in and performSegue to mainVC.
What I want now is, I want to have all ViewControllers which are unused being "deleted", to save memory.
How is that going to work?
I found here in StackOverflow this piece of code:
class ManualSegue: UIStoryboardSegue {
override func perform() {
sourceViewController.presentViewController(destinationViewController, animated: true) {
self.sourceViewController.navigationController?.popToRootViewControllerAnimated(false)
UIApplication.sharedApplication().delegate?.window??.rootViewController = self.destinationViewController
}
}
}
Is that going to do what I want? It seems like yes because this method is popping the ViewController.
I am using "Show Detail" - segues only, except when using this method I created a custom Segue Segue.
Deletion should be handled by Apple, you (theoretically) shouldn't have to worry about it, so long as you don't create any retain cycles. As a rule, just don't have any strong references to self in blocks. Funny enough, the code you have above, that should dismiss the ViewController (and therefore delete it) also has a retain cycle. Adding [weak self] and strongSelf casts as needed should help:
override func perform() {
sourceViewController.presentViewController(destinationViewController, animated: true) { [weak self] in
guard let strongSelf = self else { return }
strongSelf.sourceViewController.navigationController?.popToRootViewControllerAnimated(false)
UIApplication.sharedApplication().delegate?.window??.rootViewController = strongSelf.destinationViewController
}
}
Memory question
Yes, that is how it works. You do not need to take care of freeing view controllers.
The system will keep track of references to view controller objects. When you do not have references to these anymore then the memory is deallocated. You can read about this more in swift language documentation:
https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html
What the code is doing
presentViewController method is showing a view controller modally. The completion closure is performed after presenting the new view controller finishes. Inside closer 2 things happen
popToRootViewControllerRemoves all view controllers inside the sourceViewController object.
rootViewController of the window is set to new value.
This practically changes the root view controller to another one. This seems like a valid action after successful login.
I do not know if step 1 is necessary. That navigation view controller is going to go away anyway so why to pop view controllers inside it?
More about view controllers
You might be also interested in view controller life cycle. UIKit developer documentation contains in-depth details about view controllers:
https://developer.apple.com/documentation/uikit/uiviewcontroller