-
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
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
cce5422
Update user locale notification on change currency
ifbarrera a8f6952
Refactor RootViewModel so that no view controllers are retained by si…
justinswart 2bceffd
Merge branch 'master' of https://github.com/kickstarter/ios-oss into …
ifbarrera 6ac2fc8
Merge branch 'master' into select-currency-locale-notification
ifbarrera 0129450
Merge branch 'master' into select-currency-locale-notification
justinswart 8768479
Add RootViewControllerIndex typealias
justinswart 57d44ac
Put tests back
justinswart 549c67b
Improve guard and remove unused code
justinswart 9af9a53
swiftlint
justinswart c585002
Use generics to make code more DRY
justinswart a1e35da
Use RootViewControllerIndex
justinswart 2079f91
swiftlint
justinswart 040fd22
Add index clamping
justinswart 695e5bb
Merge branch 'master' into select-currency-locale-notification
justinswart File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,29 +5,97 @@ import ReactiveSwift | |
import Result | ||
import UIKit | ||
|
||
typealias RootViewControllerIndex = Int | ||
|
||
internal enum RootViewControllerData: Equatable { | ||
case discovery | ||
case activities | ||
case search | ||
case dashboard(isMember: Bool) | ||
case profile(isLoggedIn: Bool) | ||
|
||
var viewController: UIViewController? { | ||
switch self { | ||
case .discovery: | ||
return DiscoveryViewController.instantiate() | ||
case .activities: | ||
return ActivitiesViewController.instantiate() | ||
case.search: | ||
return SearchViewController.instantiate() | ||
case .dashboard(let isMember): | ||
return isMember ? DashboardViewController.instantiate() : nil | ||
case .profile(let isLoggedIn): | ||
return isLoggedIn | ||
? BackerDashboardViewController.instantiate() | ||
: LoginToutViewController.configuredWith(loginIntent: .generic) | ||
} | ||
} | ||
|
||
static func == (lhs: RootViewControllerData, rhs: RootViewControllerData) -> Bool { | ||
switch (lhs, rhs) { | ||
case (.discovery, .discovery): return true | ||
case (.activities, .activities): return true | ||
case (.search, .search): return true | ||
case (.dashboard(let lhsIsMember), .dashboard(let rhsIsMember)): | ||
return lhsIsMember == rhsIsMember | ||
case (.profile(let lhsIsLoggedIn), .profile(let rhsIsLoggedIn)): | ||
return lhsIsLoggedIn == rhsIsLoggedIn | ||
default: | ||
return false | ||
} | ||
} | ||
|
||
var isNil: Bool { | ||
switch self { | ||
case .dashboard(let isMember): | ||
return !isMember | ||
default: | ||
return false | ||
} | ||
} | ||
|
||
var isDashboard: Bool { | ||
switch self { | ||
case .dashboard: | ||
return true | ||
default: | ||
return false | ||
} | ||
} | ||
|
||
var isProfile: Bool { | ||
switch self { | ||
case .profile: | ||
return true | ||
default: | ||
return false | ||
} | ||
} | ||
} | ||
|
||
internal struct TabBarItemsData { | ||
internal let items: [TabBarItem] | ||
internal let isLoggedIn: Bool | ||
internal let isMember: Bool | ||
} | ||
|
||
internal enum TabBarItem { | ||
case activity(index: Int) | ||
case dashboard(index: Int) | ||
case home(index: Int) | ||
case profile(avatarUrl: URL?, index: Int) | ||
case search(index: Int) | ||
case activity(index: RootViewControllerIndex) | ||
case dashboard(index: RootViewControllerIndex) | ||
case home(index: RootViewControllerIndex) | ||
case profile(avatarUrl: URL?, index: RootViewControllerIndex) | ||
case search(index: RootViewControllerIndex) | ||
} | ||
|
||
internal protocol RootViewModelInputs { | ||
/// Call when the controller has received a user updated notification. | ||
func currentUserUpdated() | ||
|
||
/// Call before selected tab bar index changes. | ||
func shouldSelect(index: Int?) | ||
func shouldSelect(index: RootViewControllerIndex?) | ||
|
||
/// Call when selected tab bar index changes. | ||
func didSelect(index: Int) | ||
func didSelect(index: RootViewControllerIndex) | ||
|
||
/// Call when we should switch to the activities tab. | ||
func switchToActivities() | ||
|
@@ -62,20 +130,20 @@ internal protocol RootViewModelInputs { | |
|
||
internal protocol RootViewModelOutputs { | ||
/// Emits when the discovery VC should filter with specific params. | ||
var filterDiscovery: Signal<(DiscoveryViewController, DiscoveryParams), NoError> { get } | ||
var filterDiscovery: Signal<(RootViewControllerIndex, DiscoveryParams), NoError> { get } | ||
|
||
/// Emits a controller that should be scrolled to the top. This requires figuring out what kind of | ||
/// Emits a controller index that should be scrolled to the top. This requires figuring out what kind of | ||
/// controller it is, and setting its `contentOffset`. | ||
var scrollToTop: Signal<UIViewController, NoError> { get } | ||
var scrollToTop: Signal<RootViewControllerIndex, NoError> { get } | ||
|
||
/// Emits an index that the tab bar should be switched to. | ||
var selectedIndex: Signal<Int, NoError> { get } | ||
var selectedIndex: Signal<RootViewControllerIndex, 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 } | ||
|
||
/// Emits when the dashboard should switch projects. | ||
var switchDashboardProject: Signal<(DashboardViewController, Param), NoError> { get } | ||
var switchDashboardProject: Signal<(RootViewControllerIndex, Param), NoError> { get } | ||
|
||
/// Emits data for setting tab bar item styles. | ||
var tabBarItemsData: Signal<TabBarItemsData, NoError> { get } | ||
|
@@ -103,25 +171,22 @@ internal final class RootViewModel: RootViewModelType, RootViewModelInputs, Root | |
|
||
let standardViewControllers = self.viewDidLoadProperty.signal.map { generateStandardViewControllers() } | ||
let personalizedViewControllers = userState.map { generatePersonalizedViewControllers(userState: $0) } | ||
.map { $0.compact() } | ||
|
||
let viewControllers = Signal.combineLatest(standardViewControllers, personalizedViewControllers).map(+) | ||
|
||
let refreshedViewControllers = userState.takeWhen(self.userLocalePreferencesChangedProperty.signal) | ||
.map { userState -> [UIViewController?] in | ||
.map { userState -> [RootViewControllerData] in | ||
let standard = generateStandardViewControllers() | ||
let personalized = generatePersonalizedViewControllers(userState: userState) | ||
|
||
return [standard, personalized].flatMap { $0 } | ||
return standard + personalized | ||
} | ||
.map { $0.compact() } | ||
|
||
self.setViewControllers = Signal.merge( | ||
viewControllers, | ||
refreshedViewControllers | ||
).map { | ||
$0.map(UINavigationController.init(rootViewController:)) | ||
} | ||
) | ||
.map { $0.filter { !$0.isNil } } | ||
|
||
let loginState = userState.map { $0.isLoggedIn } | ||
let vcCount = self.setViewControllers.map { $0.count } | ||
|
@@ -136,60 +201,50 @@ internal final class RootViewModel: RootViewModelType, RootViewModelInputs, Root | |
.filter { isTrue($1) } | ||
.map(first) | ||
|
||
let discovery = viewControllers | ||
.map(first(DiscoveryViewController.self)) | ||
let discoveryControllerIndex = self.setViewControllers | ||
.map { $0.index(of: .discovery) } | ||
.skipNil() | ||
|
||
self.filterDiscovery = discovery | ||
self.filterDiscovery = discoveryControllerIndex | ||
.takePairWhen(self.switchToDiscoveryProperty.signal.skipNil()) | ||
|
||
let dashboard = viewControllers | ||
.map(first(DashboardViewController.self)) | ||
let dashboardControllerIndex = self.setViewControllers | ||
.map { $0.index(where: { $0.isDashboard }) } | ||
.skipNil() | ||
|
||
self.switchDashboardProject = | ||
Signal.combineLatest(dashboard, self.switchToDashboardProperty.signal.skipNil(), | ||
loginState) | ||
self.switchDashboardProject = Signal | ||
.combineLatest(dashboardControllerIndex, self.switchToDashboardProperty.signal.skipNil(), loginState) | ||
.filter { _, _, loginState in | ||
isTrue(loginState) | ||
} | ||
.map { dashboard, param, _ in | ||
(dashboard, param) | ||
} | ||
|
||
self.selectedIndex = | ||
Signal.combineLatest( | ||
.merge( | ||
self.viewDidLoadProperty.signal.mapConst(0), | ||
self.didSelectIndexProperty.signal, | ||
self.switchToActivitiesProperty.signal.mapConst(1), | ||
self.switchToDiscoveryProperty.signal.mapConst(0), | ||
self.switchToSearchProperty.signal.mapConst(2), | ||
switchToLogin, | ||
switchToProfile, | ||
self.switchToDashboardProperty.signal.mapConst(3) | ||
), | ||
self.setViewControllers, | ||
self.viewDidLoadProperty.signal) | ||
.map { idx, vcs, _ in clamp(0, vcs.count - 1)(idx) } | ||
self.selectedIndex = Signal.combineLatest( | ||
.merge( | ||
self.viewDidLoadProperty.signal.mapConst(0), | ||
self.didSelectIndexProperty.signal, | ||
self.switchToActivitiesProperty.signal.mapConst(1), | ||
self.switchToDiscoveryProperty.signal.mapConst(0), | ||
self.switchToSearchProperty.signal.mapConst(2), | ||
switchToLogin, | ||
switchToProfile, | ||
self.switchToDashboardProperty.signal.mapConst(3) | ||
), | ||
self.setViewControllers, | ||
self.viewDidLoadProperty.signal | ||
) | ||
.map { idx, vcs, _ in clamp(0, vcs.count - 1)(idx) } | ||
|
||
let shouldSelectIndex = self.shouldSelectIndexProperty.signal | ||
.skipNil() | ||
|
||
let selectedTabAgain = self.selectedIndex | ||
self.scrollToTop = self.selectedIndex | ||
.takePairWhen(shouldSelectIndex) | ||
.filter { prev, next in prev == next } | ||
.map { $1 } | ||
|
||
self.scrollToTop = Signal.combineLatest( | ||
self.setViewControllers, | ||
selectedTabAgain | ||
) | ||
.filter { vcs, idx in idx < vcs.count } | ||
.map { vcs, idx in vcs[idx] } | ||
.map(extractViewController) | ||
.skipNil() | ||
|
||
self.tabBarItemsData = Signal.combineLatest(currentUser, .merge( | ||
self.viewDidLoadProperty.signal, | ||
self.userLocalePreferencesChangedProperty.signal.ignoreValues()) | ||
|
@@ -212,26 +267,32 @@ internal final class RootViewModel: RootViewModelType, RootViewModelInputs, Root | |
internal func didSelect(index: Int) { | ||
self.didSelectIndexProperty.value = index | ||
} | ||
|
||
fileprivate let switchToActivitiesProperty = MutableProperty(()) | ||
internal func switchToActivities() { | ||
self.switchToActivitiesProperty.value = () | ||
} | ||
|
||
fileprivate let switchToDashboardProperty = MutableProperty<Param?>(nil) | ||
internal func switchToDashboard(project param: Param?) { | ||
self.switchToDashboardProperty.value = param | ||
} | ||
|
||
fileprivate let switchToDiscoveryProperty = MutableProperty<DiscoveryParams?>(nil) | ||
internal func switchToDiscovery(params: DiscoveryParams?) { | ||
self.switchToDiscoveryProperty.value = params | ||
} | ||
|
||
fileprivate let switchToLoginProperty = MutableProperty(()) | ||
internal func switchToLogin() { | ||
self.switchToLoginProperty.value = () | ||
} | ||
|
||
fileprivate let switchToProfileProperty = MutableProperty(()) | ||
internal func switchToProfile() { | ||
self.switchToProfileProperty.value = () | ||
} | ||
|
||
fileprivate let switchToSearchProperty = MutableProperty(()) | ||
internal func switchToSearch() { | ||
self.switchToSearchProperty.value = () | ||
|
@@ -256,33 +317,24 @@ internal final class RootViewModel: RootViewModelType, RootViewModelInputs, Root | |
self.viewDidLoadProperty.value = () | ||
} | ||
|
||
internal let filterDiscovery: Signal<(DiscoveryViewController, DiscoveryParams), NoError> | ||
internal let scrollToTop: Signal<UIViewController, NoError> | ||
internal let selectedIndex: Signal<Int, NoError> | ||
internal let setViewControllers: Signal<[UIViewController], NoError> | ||
internal let switchDashboardProject: Signal<(DashboardViewController, Param), NoError> | ||
internal let filterDiscovery: Signal<(RootViewControllerIndex, DiscoveryParams), NoError> | ||
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 commentThe reason will be displayed to describe this comment to others. Learn more. Is this |
||
internal let tabBarItemsData: Signal<TabBarItemsData, NoError> | ||
|
||
internal var inputs: RootViewModelInputs { return self } | ||
internal var outputs: RootViewModelOutputs { return self } | ||
} | ||
|
||
private func generateStandardViewControllers() -> [UIViewController] { | ||
return [ | ||
DiscoveryViewController.instantiate(), | ||
ActivitiesViewController.instantiate(), | ||
SearchViewController.instantiate() | ||
] | ||
private func generateStandardViewControllers() -> [RootViewControllerData] { | ||
return [.discovery, .activities, .search] | ||
} | ||
|
||
private func generatePersonalizedViewControllers(userState: (isMember: Bool, isLoggedIn: Bool)) | ||
-> [UIViewController?] { | ||
let dashboardViewController: UIViewController? = userState.isMember | ||
? DashboardViewController.instantiate() : nil | ||
let loginProfileViewController: UIViewController = userState.isLoggedIn | ||
? profileController() : LoginToutViewController.configuredWith(loginIntent: .generic) | ||
|
||
return [dashboardViewController, loginProfileViewController] | ||
-> [RootViewControllerData] { | ||
return [.dashboard(isMember: userState.isMember), .profile(isLoggedIn: userState.isLoggedIn)] | ||
} | ||
|
||
private func tabData(forUser user: User?) -> TabBarItemsData { | ||
|
@@ -324,25 +376,3 @@ extension TabBarItem: Equatable { | |
} | ||
} | ||
} | ||
|
||
private func first<VC: UIViewController>(_ viewController: VC.Type) -> ([UIViewController]) -> VC? { | ||
|
||
return { viewControllers in | ||
viewControllers | ||
.index { $0 is VC } | ||
.flatMap { viewControllers[$0] as? VC } | ||
} | ||
} | ||
|
||
private func profileController() -> UIViewController { | ||
|
||
return BackerDashboardViewController.instantiate() | ||
} | ||
|
||
private func extractViewController(_ viewController: UIViewController) -> UIViewController? { | ||
if let navigationController = viewController as? UINavigationController { | ||
return navigationController.viewControllers.count == 1 ? navigationController.viewControllers.first : nil | ||
} else { | ||
return viewController | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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
andRootTabBarViewController
so it shouldn't really be used anywhere else.