-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Conversation
…select-currency-locale-notification
@dusi this is now ready for review at your convenience :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Requested couple questions
/// controller it is, and setting its `contentOffset`. | ||
var scrollToTop: Signal<UIViewController, NoError> { get } | ||
var scrollViewControllerAtIndexToTop: Signal<Int, NoError> { get } |
There was a problem hiding this comment.
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 }
...
There was a problem hiding this comment.
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 }) } |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
😄
There was a problem hiding this comment.
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 } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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] } |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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 } |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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? { |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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] } |
There was a problem hiding this comment.
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 } |
There was a problem hiding this comment.
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
)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚢 🚢 🚢
🚢 🚢 🚢
🚢 🚢 🚢
📲 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 inRootViewModel
.🤔 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 inRootViewModel
by emitting a new array of rootUIViewController
s.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
UIViewController
arrays to instead emit anenum
which we've calledRootViewControllerData
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