Weird retain cycle when using the Coordinator pattern - ios

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.

Related

Passing data between ViewModels in MVVM-C

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.

iOS New-Contact-Style Segue in Swift

I am trying to emulate the iOS contacts segueing between two view controllers.
I have a simple Person class given by:
class Person {
var name = ""
}
and a UIViewController that contains an array of Person, which is embedded in a UINavigationController:
class PeopleViewController: UIViewController {
var people = [Person]()
var selectedPerson: Person?
switch segueIdentifier(for: segue) {
case .showPerson:
guard let vc = segue.destination as? PersonViewController else { fatalError("!") }
vc.person = selectedPerson
}
}
This controller uses a Show segue to PersonViewController to display the selectedPerson:
class PersonViewController: UIViewController {
var person: Person!
}
PeopleViewController can also add a new Person to the array of Person. The NewPersonViewController is presented modally, however:
class NewPersonViewController: UIViewController {
var person: Person?
}
If a new Person is added, I want NewPersonViewController to dismiss but show the new Person in the PersonViewController that is part of the navigation stack. My best guess for doing this is:
extension NewPersonViewController {
func addNewPerson() {
weak var pvc = self.presentingViewController as! UINavigationController
if let cvc = pvc?.childViewControllers.first as? PeopleViewController {
self.dismiss(animated: false, completion: {
cvc.selectedPerson = self.person
cvc.performSegue(withIdentifier: .showPerson, sender: nil)
}
}
}
}
However, (1) I'm not too happy about forcing the downcast to UINavigationController as I would have expected self.presentingViewController to be of type PeopleViewController? And (2), is there a memory leak in the closure as I've used weak var pvc = self.presentingViewController for pvc but not for cvc? Or, finally (3) is there a better way of doing this?
Many thanks for any help, suggestions etc.
(1) I'm not too happy about forcing the downcast to UINavigationController as I would have expected self.presentingViewController to be of type PeopleViewController?
There is nothing wrong in downcasting. I would definitely remove force unwrapping.
(2), is there a memory leak in the closure as I've used weak var pvc = self.presentingViewController for pvc but not for cvc?
I think, there is none.
(3) is there a better way of doing this?
You can present newly added contact from NewContactVC. When you about to dismiss, call dismiss on presentingVC.
// NewPersonViewController.swift
func addNewPerson() {
// New person is added
// Show PeopleViewController modally
}
Note: Using presentingViewController this way will dismiss top two/one Modal(s). You will see only top view controller getting dismissed.
If you can't determine how many modals going to be, you should look-into different solution or possibly redesigning navigation flow.
// PeopleViewController.swift
func dismiss() {
if let presentingVC = self.presentingViewController?.presentingViewController {
presentingVC.dismiss(animated: true, completion: nil)
} else {
self.dismiss(animated: true, completion: nil)
}
}

About strong reference cycles for closures in Swift

I have defined a class called Person. This is my code:
class Person {
var closure: (() -> ())?
var name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
then I use Person in class called ViewController:
class ViewController: UIViewController {
var person = Person(name: "john")
let aStr = "john is a cute boy"
override func viewDidLoad() {
super.viewDidLoad()
person.closure = {
print("\(self.aStr)")
}
person.closure!()
}
}
In my opinion, the picture of memory about my code like this :
So, from above picture, in my opinion, it will cause strong reference cycle between the three instances, but I can not get any leak from Instruments, so I have some confusion.
Does this code cause strong reference cycle?
If not, when will ARC deallocate the instance of Person? the method named deinit in Person class is never called.
Yes, this's a typical retain cycle.
To solve this problem use [weak self] in your closure
person.closure = { [weak self] in
guard let strongSelf = self else { return }
print("\(strongSelf.aStr)")
}
To really create a leak.
I create a demo App. Root is a navController.
The navController has a root controller. Let's call it buttonController.
When you click button in the buttonController, it create your ViewController and push to navController.
When you click back button in navigation bar, the navController pop your ViewController instance.
Profile it, then you will see the leak and the retain cycle in Instruments.
Xcode default template of iOS App use a single page, which always retain your ViewController instance. If the ViewController instance is still used by the system, it's actually not a leak yet.
So push & pop show that leak for you.

How to test deinit of viewController

I want to test if I remove all the key value observers in the deinit of my view controller.
In the test class I have defined following method to start view controller lifecycle
func startLifecycle() {
_ = viewController.view
}
In my test method I'm trying to invoke deinit by simply assigning nil to my view controller instance
testViewController = nil
XCTAssert for stuff...
But deinit is not called when I execute my test. I see no obvious retain cycles in my VC's code, what's more when I run the code in my app, not the test environment, deinit is called so it doesn't seem like something is keeping view controller in memory.
What is the correct way to release a view controller when testing?
I had the same problem.
When you examine the memory graph, you will see that an object of type UIStoryboardScene maintains a reference to your UIViewController via an #autorelease property 'sceneViewController'.
If you're unfamiliar with #autorelease, this is a helpful article: https://swiftrocks.com/autoreleasepool-in-swift. It explains that objects created with autorelease are released at the end of the current thread's run loop, but not before.
In Swift, we can use autoreleasepool to release UIStoryboardScene's reference to the UIViewController.
It might look something like this:
var testViewController: UIViewController?
autoreleasepool {
testViewController = UIStoryboard(name: "main", bundle: nil).instantiateInitialViewController()
}
Then, when you execute testViewController = nil, the UIViewController will actually deinit.
Try smth like this
class ViewController: UIViewController {
var deinitCalled: (() -> Void)?
deinit { deinitCalled?() }
}
class ViewControllerTest: XCTestCase {
func test_deinit() {
var instance: ViewController? = ViewController()
let exp = expectation(description: "Viewcontroller has deinited")
instance?.deinitCalled = {
exp.fulfill()
}
DispatchQueue.global(qos: .background).async {
instance = nil
}
waitForExpectations(timeout: 2)
}
}
In case someone else needs this i managed to do it this way.
// Given
var optionalSubject: UIViewController? = UIViewController()
// When
optionalSubject = nil // Call deinit
// Then
// TEST DEINIT

change ViewController with SegmentedControl without destroy it

I have a few controllers. for some reason, I'm using UISegmentedControl instead of tab bar.
each few controllers download data from the servers. my problem is, if I move to next view controller and go back to the previous view controllers, I need to redownload again.
how to change view controller with UISegmentedControl without destroy the previous controller, so I don't need to redownload again. each time I move to different viewcontroller
here's my code
class ContentViewController: UIViewController {
private let homeViewController: HomeViewController!
private let aboutViewController: AboutViewController!
private let liveTVViewController: LiveTVViewController!
private let programsViewController: ProgramsViewController!
private var currentViewController: UIViewController!
var userDeviceType: Int!
override func viewDidLoad() {
super.viewDidLoad()
let viewController = viewControllerForSegmentIndex(0)
self.addChildViewController(viewController)
viewController.view.frame = self.view.bounds
self.view.addSubview(viewController.view)
currentViewController = viewController
NSNotificationCenter.defaultCenter().addObserver(self, selector: "segmentChanged:", name: "SegmentChangedNotification", object: nil)
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
func segmentChanged(notification: NSNotification) {
let userInfo = notification.userInfo as [String: AnyObject]
let selectedIndex = userInfo["selectedIndex"] as Int
let viewController = viewControllerForSegmentIndex(selectedIndex)
self.addChildViewController(viewController)
self.transitionFromViewController(currentViewController,
toViewController: viewController,
duration: 0.0,
options: UIViewAnimationOptions.CurveEaseIn,
animations: { () -> Void in
self.currentViewController.view.removeFromSuperview()
viewController.view.frame = self.view.bounds
self.view.addSubview(viewController.view)
}) { (finished: Bool) -> Void in
viewController.didMoveToParentViewController(self)
self.currentViewController.removeFromParentViewController()
self.currentViewController = viewController
}
}
func viewControllerForSegmentIndex(index: Int) -> UIViewController {
var viewController: UIViewController
if index == 0 {
viewController = self.storyboard!.instantiateViewControllerWithIdentifier("HomePage") as HomeViewController
(viewController as HomeViewController).userDeviceType = userDeviceType
} else if index == 1 {
viewController = self.storyboard!.instantiateViewControllerWithIdentifier("ProgramsPage") as ProgramsViewController
} else if index == 2 {
viewController = self.storyboard!.instantiateViewControllerWithIdentifier("LiveTVPage") as LiveTVViewController
} else if index == 3 {
viewController = self.storyboard!.instantiateViewControllerWithIdentifier("AboutPage") as AboutViewController
} else {
viewController = self.storyboard!.instantiateViewControllerWithIdentifier("HomePage") as HomeViewController
}
return viewController
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
thank you very much and sorry for my bad English
You should adopt the MVC design pattern. This will allow you to store the data you need downloaded in the Model. Then, when a certain view controller is loaded, you simply ask the model if that data exists. If it does, you'll get it back. Otherwise, you can download it as normal.
To further explain:
The Model-View-Controller (MVC) design pattern assigns objects in an application one of three roles: model, view, or controller. The pattern defines not only the roles objects play in the application, it defines the way objects communicate with each other.
Model objects encapsulate the data specific to an application and define the logic and computation that manipulate and process that data. For example, a model object might represent a character in a game or a contact in an address book. A model object can have to-one and to-many relationships with other model objects, and so sometimes the model layer of an application effectively is one or more object graphs. Much of the data that is part of the persistent state of the application (whether that persistent state is stored in files or databases) should reside in the model objects after the data is loaded into the application. Because model objects represent knowledge and expertise related to a specific problem domain, they can be reused in similar problem domains. Ideally, a model object should have no explicit connection to the view objects that present its data and allow users to edit that data—it should not be concerned with user-interface and presentation issues.
User actions in the view layer that create or modify data are communicated through a controller object and result in the creation or updating of a model object. When a model object changes (for example, new data is received over a network connection), it notifies a controller object, which updates the appropriate view objects
The above quotes are from the link I mentioned in the first paragraph.
You are instantiate new view controllers in viewControllerForSegmentIndex(index: Int) by mistake.
What you should do to avoid instantiate each view controller every time you switch back to it is to modify the properties as:
private let homeViewController: HomeViewController = self.storyboard!.instantiateViewControllerWithIdentifier("HomePage") as HomeViewController
private let aboutViewController: AboutViewController = self.storyboard!.instantiateViewControllerWithIdentifier("AboutPage") as AboutViewController
private let liveTVViewController: LiveTVViewController = self.storyboard!.instantiateViewControllerWithIdentifier("LiveTVPage") as LiveTVViewController
private let programsViewController: ProgramsViewController = self.storyboard!.instantiateViewControllerWithIdentifier("ProgramsPage") as ProgramsViewController
And change viewControllerForSegmentIndex(index: Int) to:
func viewControllerForSegmentIndex(index: Int) -> UIViewController {
var viewController: UIViewController
switch index {
case 0:
return homeViewController
case 1:
return programsViewController
case 2:
return liveTVViewController
case 3:
return aboutViewController
default:
return homeViewController
}
}

Resources