-
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
MBL-1456: Stub PPO view and container #2080
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import Foundation | ||
import Library | ||
import SwiftUI | ||
|
||
public class PPOContainerViewController: PagedContainerViewController { | ||
public override func viewDidLoad() { | ||
super.viewDidLoad() | ||
|
||
// TODO: Translate these strings (MBL-1558) | ||
self.title = "Activity" | ||
let ppoViewController = UIHostingController(rootView: PPOView()) | ||
ppoViewController.title = "Project Alerts" | ||
|
||
let activitiesViewController = ActivitiesViewController.instantiate() | ||
activitiesViewController.title = "Activity Feed" | ||
|
||
self.setPagedViewControllers([ppoViewController, activitiesViewController]) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import SwiftUI | ||
|
||
struct PPOView: View { | ||
@StateObject private var viewModel = PPOViewModel() | ||
var body: some View { | ||
Text(self.viewModel.greeting) | ||
} | ||
} | ||
|
||
#Preview { | ||
PPOView() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import Combine | ||
import Foundation | ||
|
||
public class PPOViewModel: ObservableObject { | ||
let greeting = "Hello, PPO" | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import Foundation | ||
@testable import Kickstarter_Framework | ||
import XCTest | ||
|
||
class PPOViewModelTests: XCTestCase { | ||
func test_stub() { | ||
let vm = PPOViewModel() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import Combine | ||
import Foundation | ||
import UIKit | ||
|
||
open class PagedContainerViewController: UIViewController { | ||
private weak var activeController: UIViewController? = nil | ||
private var subscriptions = Set<AnyCancellable>() | ||
private let viewModel = PagedContainerViewModel() | ||
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. Philosophically, I put everything that seemed like |
||
|
||
// TODO: Use the correct page control, per the designs. | ||
// This may exist already in SortPagerViewController, or we can write one in SwiftUI. | ||
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. Worth calling out - we have another ticket for implementing the correct page control. I leave it up to someone else to determine if we should write a new one in SwiftUI or steal/refactor out the existing one. |
||
private lazy var toggle = UISegmentedControl( | ||
frame: .zero, | ||
actions: [] | ||
) | ||
|
||
open override func viewDidLoad() { | ||
super.viewDidLoad() | ||
|
||
self.view.backgroundColor = .white | ||
self.view.addSubview(self.toggle) | ||
self.toggle.translatesAutoresizingMaskIntoConstraints = false | ||
self.toggle.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true | ||
self.toggle.widthAnchor.constraint(equalTo: self.view.widthAnchor).isActive = true | ||
|
||
self.viewModel.pageTitles | ||
.sink { [weak self] titles in | ||
self?.configureSegmentedControl(withTitles: titles) | ||
}.store(in: &self.subscriptions) | ||
|
||
self.viewModel.displayChildViewControllerAtIndex.receive(on: RunLoop.main) | ||
.sink { [weak self] controller, index in | ||
self?.showChildController(controller, atIndex: index) | ||
}.store(in: &self.subscriptions) | ||
} | ||
|
||
/* | ||
The custom appearanceTransition code in this UIViewController was implemented | ||
so that the child view controllers will receive viewWillAppear with animated = true | ||
when a tab is selected. | ||
|
||
I initially implemented this because ActivitiesViewController filters unanimated | ||
calls to viewWillAppear; this behavior is relied on by our screenshot tests. | ||
|
||
While we should refactor the ActivitiesViewController, it also makes sense that transitions | ||
between pages in this container view controller will (eventually) be animated, even if | ||
I didn't animate them in this stub. | ||
*/ | ||
open override var shouldAutomaticallyForwardAppearanceMethods: Bool { | ||
return false | ||
} | ||
|
||
public override func viewWillAppear(_ animated: Bool) { | ||
super.viewWillAppear(animated) | ||
|
||
self.viewModel.viewWillAppear() | ||
|
||
if let activeController = self.activeController { | ||
activeController.beginAppearanceTransition(true, animated: animated) | ||
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. OK, so all this elaborate I was going to clean up Basically, it was rabbithole, and it seemed clearer to fix it this way. 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. Oh, I think that animated thing is what broke our surveys not showing up in activity, too. So it definitely might be worth looking into at some point but I agree that it's complicated and maybe not worth doing right now (and I definitely think it's not worth doing if there's a chance that we'll rewrite the view controller to stop basically being a webview). 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. I do also think it might be worth leaving a comment in the code about this; it doesn't need to be as detailed, though! 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. Good idea, I'll just pull in this comment and edit it a bit. |
||
} | ||
} | ||
|
||
public override func viewDidAppear(_ animated: Bool) { | ||
super.viewDidAppear(animated) | ||
|
||
if let activeController = self.activeController { | ||
activeController.endAppearanceTransition() | ||
} | ||
} | ||
|
||
override open func viewWillDisappear(_ animated: Bool) { | ||
super.viewWillDisappear(animated) | ||
|
||
if let activeController = self.activeController { | ||
activeController.beginAppearanceTransition(false, animated: animated) | ||
} | ||
} | ||
|
||
open override func viewDidDisappear(_ animated: Bool) { | ||
super.viewDidDisappear(animated) | ||
|
||
if let activeController = self.activeController { | ||
activeController.endAppearanceTransition() | ||
} | ||
} | ||
|
||
public func setPagedViewControllers(_ controllers: [UIViewController]) { | ||
self.viewModel.configure(withChildren: controllers) | ||
} | ||
|
||
private func configureSegmentedControl(withTitles titles: [String]) { | ||
self.toggle.removeAllSegments() | ||
|
||
for (idx, title) in titles.enumerated() { | ||
let action = UIAction( | ||
title: title, | ||
handler: { [weak self] _ in self?.viewModel.didSelectPage(atIndex: idx) } | ||
) | ||
self.toggle.insertSegment(action: action, at: idx, animated: false) | ||
} | ||
} | ||
|
||
private func showChildController(_ controller: UIViewController, atIndex index: Int) { | ||
if self.toggle.selectedSegmentIndex == UISegmentedControl.noSegment { | ||
self.toggle.selectedSegmentIndex = index | ||
} | ||
|
||
if let activeController = self.activeController { | ||
self.stopDisplayingChildViewController(activeController) | ||
} | ||
|
||
self.displayChildViewController(controller) | ||
self.activeController = controller | ||
} | ||
|
||
func displayChildViewController(_ controller: UIViewController) { | ||
guard let childView = controller.view else { | ||
return | ||
} | ||
|
||
controller.beginAppearanceTransition(true, animated: true) | ||
|
||
addChild(controller) | ||
|
||
self.view.addSubview(childView) | ||
|
||
childView.translatesAutoresizingMaskIntoConstraints = false | ||
childView.topAnchor.constraint(equalTo: self.toggle.bottomAnchor).isActive = true | ||
childView.leftAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leftAnchor).isActive = true | ||
childView.rightAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.rightAnchor).isActive = true | ||
childView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor).isActive = true | ||
|
||
controller.didMove(toParent: self) | ||
|
||
controller.endAppearanceTransition() | ||
} | ||
|
||
func stopDisplayingChildViewController(_ controller: UIViewController) { | ||
controller.beginAppearanceTransition(false, animated: true) | ||
|
||
controller.willMove(toParent: nil) | ||
for constraint in controller.view.constraints { | ||
constraint.isActive = false | ||
} | ||
controller.view.removeFromSuperview() | ||
controller.removeFromParent() | ||
|
||
controller.endAppearanceTransition() | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import Combine | ||
import Foundation | ||
import UIKit | ||
|
||
public class PagedContainerViewModel { | ||
// Internal | ||
private var subscriptions = Set<AnyCancellable>() | ||
|
||
init() { | ||
self.pageTitles = self.configureWithChildrenSubject.map { controllers in | ||
controllers.map { $0.title ?? "Page " } | ||
}.eraseToAnyPublisher() | ||
|
||
self.displayChildViewControllerAtIndex = Publishers.CombineLatest( | ||
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. I feel like this should really be a 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. I don't think there is an equivalent? I don't think it matters for this one since I doubt the other signal can change. But if we really end up needing a 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. Fair point that the other signal (probably) won't ever change! The only situation I could see would be if we ever added a new page of content to this tab, and that page of content was affected by logged in/logged out state. |
||
self.configureWithChildrenSubject, | ||
self.selectedIndex.compactMap { $0 } | ||
) | ||
.map { (controllers: [UIViewController], index: Int) -> (UIViewController, Int)? in | ||
if index < controllers.count { | ||
return (controllers[index], index) | ||
} else { | ||
return nil | ||
} | ||
}.compactMap { $0 } | ||
.eraseToAnyPublisher() | ||
} | ||
|
||
// Inputs | ||
func viewWillAppear() { | ||
if self.selectedIndex.value == nil { | ||
self.didSelectPage(atIndex: 0) | ||
amy-at-kickstarter marked this conversation as resolved.
Show resolved
Hide resolved
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. This tiny bit of state (in 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. I like it! |
||
} | ||
} | ||
|
||
private let configureWithChildrenSubject = CurrentValueSubject<[UIViewController], Never>([]) | ||
func configure(withChildren children: [UIViewController]) { | ||
self.configureWithChildrenSubject.send(children) | ||
} | ||
|
||
private let selectedIndex = CurrentValueSubject<Int?, Never>(nil) | ||
func didSelectPage(atIndex index: Int) { | ||
self.selectedIndex.send(index) | ||
} | ||
|
||
// Outputs | ||
public let displayChildViewControllerAtIndex: AnyPublisher<(UIViewController, Int), Never> | ||
public let pageTitles: AnyPublisher<[String], Never> | ||
} |
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.
🎉