Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix RootViewModel memory leaks #623

Merged
merged 14 commits into from
Apr 5, 2019

Conversation

justinswart
Copy link
Contributor

@justinswart justinswart commented Mar 1, 2019

📲 What

@ifbarrera and I paired on bringing back the .ksr_userLocalePreferencesChanged and fixing the memory leaks that we suspected were being created by retaining view controllers in Signals in RootViewModel.

🤔 Why

In order for currency selection changes to have immediate effect in the app, we need to recreate the underlying view controllers on the tab bar when a currency preference change is made. For this reason we post the .ksr_userLocalePreferencesChanged notification and respond to that in RootViewModel by emitting a new array of root UIViewControllers.

Previously this was causing these view controllers to leak due to the way the Signals were set up, they were retaining their last emission in some cases.

🛠 How

Updated the Signals that previously emitted actual UIViewControllerarrays to instead emit an enum which we've called RootViewControllerData so that the instantiation of those view controllers could be handled outside of the view model and thereby not be retained and leaked.

NB: This is a change to our app's navigation so it should be tested fairly thoroughly for regressions.

✅ Acceptance criteria

  • Tests pass
  • Navigation works as before
  • Scroll to top works as before

@ifbarrera
Copy link
Contributor

@ifbarrera
Copy link
Contributor

@ifbarrera ifbarrera marked this pull request as ready for review March 13, 2019 15:39
@ifbarrera ifbarrera changed the title Post notification on currency change and fix memory leaks Fix RootViewModel memory leaks Mar 13, 2019
@ifbarrera
Copy link
Contributor

@dusi this is now ready for review at your convenience :)

@dusi dusi added blocked a PR that is blocked for external reasons and removed needs review labels Mar 21, 2019
Copy link
Contributor

@dusi dusi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requested couple questions :trollface:

/// controller it is, and setting its `contentOffset`.
var scrollToTop: Signal<UIViewController, NoError> { get }
var scrollViewControllerAtIndexToTop: Signal<Int, NoError> { get }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we have renamed this signal to be more verbose and explicitly state that we're dealing with view controller at index... but still haven't renamed the other signals.

I wonder if using a more specific type would help us be more descriptive?

i.e.

typealias ViewControllerAtIndex = Int
// or
typealias RootViewControllerIndex = Int

var filterDiscovery: Signal<(ViewControllerAtIndex, DiscoveryParams), NoError> { get }
var scrollToTop: Signal<ViewControllerAtIndex, NoError> { get }
...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, can make that change.

let dashboard = viewControllers
.map(first(DashboardViewController.self))
let dashboardControllerIndex = self.setViewControllers
.map { $0.index(where: { $0.isDashBoard }) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there actually a need for isDashBoard property? Can't we simply do $0.index(of: .dashboard)? Also looks like we don't use DashBoard casing anywhere else (mostly dashboard or Dashboard) in case we're keeping this property

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.map { $0.index(of: .dashboard) } doesn't work because dashboard has an associated value. Also, we can't make it isdashboard 😄

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isDashboard? 😄


/// Emits an index that the tab bar should be switched to.
var selectedIndex: Signal<Int, NoError> { get }

/// Emits the array of view controllers that should be set on the tab bar.
var setViewControllers: Signal<[UIViewController], NoError> { get }
var setViewControllers: Signal<[RootViewControllerData], NoError> { get }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to rename this to viewControllersData or similar?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this type is specific to the RootViewModel and RootTabBarViewController so it shouldn't really be used anywhere else.

.map { $0.map { $0.viewController }.compact() }

Signal.combineLatest(viewControllers, self.vm.outputs.scrollViewControllerAtIndexToTop)
.map { $0[$1] }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder if .map { (vcs, idx) in vcs[idx] } would make this more readable 🤔

@@ -162,10 +166,6 @@ final class RootViewModelTests: TestCase {
self.vm.inputs.didSelect(index: 0)

self.selectedIndex.assertValues([0, 1, 0], "Selects index immediately.")

self.vm.inputs.didSelect(index: 10)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we still keep this test? I'm thinking about the scenario where a creator is logged in (5 tabs) and logs out (4 tabs).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, can't remember why I removed it.

self.viewModel.outputs.scrollViewControllerAtIndexToTop
.observeForControllerAction()
.map { [weak self] index -> UIViewController? in
guard let vc = self?.viewControllers?[index] else { return nil }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make sure index is smaller than viewControllers.count - 1?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, can't remember because it's been pretty long since I made this change but I think I determined that because of this change this should be impossible, but doesn't hurt to keep it in 👍

.observeValues { $0.filter(with: $1) }

self.viewModel.outputs.switchDashboardProject
.observeForControllerAction()
.observeValues { $0.`switch`(toProject: $1) }
.map { [weak self] index, param -> (DashboardViewController, Param)? in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like duplicate logic happening in these two signals...should we extract this to a private helper function to keep it more DRY?

@@ -291,3 +313,22 @@ private func strokedRoundImage(fromImage image: UIImage?,

return UIGraphicsGetImageFromCurrentImageContext()?.withRenderingMode(.alwaysOriginal)
}

private func first<VC: UIViewController>(_ viewController: VC.Type) -> ([UIViewController]) -> VC? {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the code compiles even without this func 🤔

internal let scrollToTop: Signal<RootViewControllerIndex, NoError>
internal let selectedIndex: Signal<RootViewControllerIndex, NoError>
internal let setViewControllers: Signal<[RootViewControllerData], NoError>
internal let switchDashboardProject: Signal<(Int, Param), NoError>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this Int or RootViewControllerIndex?

.map { $0.map { $0.viewController }.compact() }

Signal.combineLatest(viewControllers, self.vm.outputs.scrollToTop)
.map { (vcs, idx) in vcs[idx] }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about also checkin whether index < vcs.count - 1

@@ -126,6 +143,11 @@ public final class RootTabBarViewController: UITabBarController {
self.viewModel.inputs.switchToSearch()
}

private func viewControllerAndParam<T, P>(with index: RootViewControllerIndex, param: P) -> (T, P)? {
guard let vc = self.viewControllers?[index] as? T else { return nil }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same index check here (index < self.viewControllers.count - 1)?

@dusi dusi removed the blocked a PR that is blocked for external reasons label Apr 5, 2019
Copy link
Contributor

@dusi dusi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚢 🚢 🚢
🚢 🚢 🚢
🚢 🚢 🚢

@justinswart justinswart merged commit f0bf3cf into master Apr 5, 2019
@justinswart justinswart deleted the select-currency-locale-notification branch April 5, 2019 17:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants