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

MBL-1456: Stub PPO view and container #2080

Merged
merged 2 commits into from
Jun 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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])
}
}
12 changes: 12 additions & 0 deletions Kickstarter-iOS/Features/PledgedProjectsOverview/PPOView.swift
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
Expand Up @@ -275,6 +275,8 @@ public final class RootTabBarViewController: UITabBarController, MessageBannerVi
return DiscoveryViewController.instantiate()
case .activities:
return ActivitiesViewController.instantiate()
case .pledgedProjectsAndActivities:
return PPOContainerViewController.instantiate()
case .search:
return SearchViewController.instantiate()
case let .profile(isLoggedIn):
Expand Down
40 changes: 40 additions & 0 deletions Kickstarter.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1520,6 +1520,8 @@
E135005C2C07ABA600A30161 /* PledgePaymentMethodsAndSelectionData.swift in Sources */ = {isa = PBXBuildFile; fileRef = E135005B2C07ABA600A30161 /* PledgePaymentMethodsAndSelectionData.swift */; };
E16794282B7EAA5200064063 /* OAuthTokenExchange.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16794272B7EAA5200064063 /* OAuthTokenExchange.swift */; };
E167942A2B85136900064063 /* OAuthTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16794292B85136900064063 /* OAuthTests.swift */; };
E16ECA702C245A34002A1D25 /* PagedContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16ECA6F2C245A34002A1D25 /* PagedContainerViewController.swift */; };
E16ECA722C245A51002A1D25 /* PagedContainerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16ECA712C245A51002A1D25 /* PagedContainerViewModel.swift */; };
E170B9112B20E83B001BEDD7 /* MockGraphQLClient+CombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E170B9102B20E83B001BEDD7 /* MockGraphQLClient+CombineTests.swift */; };
E17611E02B7287CF00DF2F50 /* PaginationExampleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E11CFE4E2B7162A400497375 /* PaginationExampleView.swift */; };
E17611E22B73D9A400DF2F50 /* Data+PKCE.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17611E12B73D9A400DF2F50 /* Data+PKCE.swift */; };
Expand All @@ -1539,6 +1541,10 @@
E1B813C52BC8340700DF33CF /* FetchMySavedProjectsQuery.graphql in Resources */ = {isa = PBXBuildFile; fileRef = E1B813C42BC8340700DF33CF /* FetchMySavedProjectsQuery.graphql */; };
E1B813C72BC851CB00DF33CF /* FetchMyBackedProjectsQueryRequestForTests.graphql_test in Resources */ = {isa = PBXBuildFile; fileRef = E1B813C62BC851CB00DF33CF /* FetchMyBackedProjectsQueryRequestForTests.graphql_test */; };
E1B813C92BC851E100DF33CF /* FetchMyBackedProjectsQuery.json in Resources */ = {isa = PBXBuildFile; fileRef = E1B813C82BC851E100DF33CF /* FetchMyBackedProjectsQuery.json */; };
E1BAA03A2C1A1CCD004F8B06 /* PPOViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BAA0362C1A1B13004F8B06 /* PPOViewModel.swift */; };
E1BAA03B2C1A1CCD004F8B06 /* PPOView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BAA0342C1A1907004F8B06 /* PPOView.swift */; };
E1BAA03C2C1A1CCD004F8B06 /* PPOContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BAA0382C1A1C56004F8B06 /* PPOContainerViewController.swift */; };
E1BAA03E2C1A1CE4004F8B06 /* PPOViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BAA03D2C1A1CE4004F8B06 /* PPOViewModelTests.swift */; };
E1BB25642B1E81AA000BD2D6 /* Publisher+Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BB25632B1E81AA000BD2D6 /* Publisher+Service.swift */; };
E1C880AF2BBC6CDA008B9612 /* GraphQLSelectionSet+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C880AE2BBC6CDA008B9612 /* GraphQLSelectionSet+String.swift */; };
E1EEED292B684AA7009976D9 /* PKCE.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1EEED282B684AA7009976D9 /* PKCE.swift */; };
Expand Down Expand Up @@ -3151,6 +3157,8 @@
E135005B2C07ABA600A30161 /* PledgePaymentMethodsAndSelectionData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PledgePaymentMethodsAndSelectionData.swift; sourceTree = "<group>"; };
E16794272B7EAA5200064063 /* OAuthTokenExchange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthTokenExchange.swift; sourceTree = "<group>"; };
E16794292B85136900064063 /* OAuthTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuthTests.swift; sourceTree = "<group>"; };
E16ECA6F2C245A34002A1D25 /* PagedContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagedContainerViewController.swift; sourceTree = "<group>"; };
E16ECA712C245A51002A1D25 /* PagedContainerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PagedContainerViewModel.swift; sourceTree = "<group>"; };
E170B9102B20E83B001BEDD7 /* MockGraphQLClient+CombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MockGraphQLClient+CombineTests.swift"; sourceTree = "<group>"; };
E17611E12B73D9A400DF2F50 /* Data+PKCE.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+PKCE.swift"; sourceTree = "<group>"; };
E17611E32B751E8100DF2F50 /* Paginator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Paginator.swift; sourceTree = "<group>"; };
Expand All @@ -3167,6 +3175,10 @@
E1B813C42BC8340700DF33CF /* FetchMySavedProjectsQuery.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = FetchMySavedProjectsQuery.graphql; sourceTree = "<group>"; };
E1B813C62BC851CB00DF33CF /* FetchMyBackedProjectsQueryRequestForTests.graphql_test */ = {isa = PBXFileReference; lastKnownFileType = text; path = FetchMyBackedProjectsQueryRequestForTests.graphql_test; sourceTree = "<group>"; };
E1B813C82BC851E100DF33CF /* FetchMyBackedProjectsQuery.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = FetchMyBackedProjectsQuery.json; sourceTree = "<group>"; };
E1BAA0342C1A1907004F8B06 /* PPOView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PPOView.swift; sourceTree = "<group>"; };
E1BAA0362C1A1B13004F8B06 /* PPOViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PPOViewModel.swift; sourceTree = "<group>"; };
E1BAA0382C1A1C56004F8B06 /* PPOContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PPOContainerViewController.swift; sourceTree = "<group>"; };
E1BAA03D2C1A1CE4004F8B06 /* PPOViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PPOViewModelTests.swift; sourceTree = "<group>"; };
E1BB25632B1E81AA000BD2D6 /* Publisher+Service.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+Service.swift"; sourceTree = "<group>"; };
E1C880AE2BBC6CDA008B9612 /* GraphQLSelectionSet+String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GraphQLSelectionSet+String.swift"; sourceTree = "<group>"; };
E1EA34EE2AE1B28400942A04 /* Signal+Combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Signal+Combine.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4768,6 +4780,7 @@
1937A6F028C92F0000DD732D /* PledgeAmountSummary */,
19A97D3928C802230031B857 /* PledgePaymentMethods */,
1937A70428C9392600DD732D /* PledgeShippingLocation */,
E1BAA0332C1A18DA004F8B06 /* PledgedProjectsOverview */,
1937A70528C9399D00DD732D /* PledgeView */,
1965436E28C810CC00457EC6 /* ProjectNotifications */,
1965437528C812F100457EC6 /* ProjectPamphletContentDataSource_DEPRECATED_09_06_2022 */,
Expand Down Expand Up @@ -6143,6 +6156,7 @@
E11CFE492B6C41B400497375 /* OAuth.swift */,
E16794292B85136900064063 /* OAuthTests.swift */,
94C92E7B2659EDBF00A96818 /* PaddingLabel.swift */,
E16ECA6E2C245A12002A1D25 /* PagedContainer */,
A77D7B061CBAAF5D0077586B /* Paginate.swift */,
A7ED1F1C1E830FDC00BFFA01 /* PaginateTests.swift */,
E17611E32B751E8100DF2F50 /* Paginator.swift */,
Expand Down Expand Up @@ -7049,6 +7063,15 @@
path = PaginationExample;
sourceTree = "<group>";
};
E16ECA6E2C245A12002A1D25 /* PagedContainer */ = {
isa = PBXGroup;
children = (
E16ECA6F2C245A34002A1D25 /* PagedContainerViewController.swift */,
E16ECA712C245A51002A1D25 /* PagedContainerViewModel.swift */,
);
path = PagedContainer;
sourceTree = "<group>";
};
E1A149252ACE060E00F49709 /* templates */ = {
isa = PBXGroup;
children = (
Expand All @@ -7071,6 +7094,17 @@
path = combine;
sourceTree = "<group>";
};
E1BAA0332C1A18DA004F8B06 /* PledgedProjectsOverview */ = {
isa = PBXGroup;
children = (
E1BAA0382C1A1C56004F8B06 /* PPOContainerViewController.swift */,
E1BAA0342C1A1907004F8B06 /* PPOView.swift */,
E1BAA0362C1A1B13004F8B06 /* PPOViewModel.swift */,
E1BAA03D2C1A1CE4004F8B06 /* PPOViewModelTests.swift */,
);
path = PledgedProjectsOverview;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXHeadersBuildPhase section */
Expand Down Expand Up @@ -7861,6 +7895,7 @@
A7169BF61DDD064200480C0D /* UIScrollView+Extensions.swift in Sources */,
597582E31D5D12AE008765DE /* SettingsStyles.swift in Sources */,
A7F441AF1D005A9400FE6FC5 /* ActivityFriendBackingViewModel.swift in Sources */,
E16ECA702C245A34002A1D25 /* PagedContainerViewController.swift in Sources */,
8AAA9BD822F49EC200F12976 /* UIColor+Mixing.swift in Sources */,
8AC3E13A269F781D00168BF8 /* ErrorEnvelope+LocalizedDescription.swift in Sources */,
D04AAC34218BB70D00CF713E /* SettingsAccountPickerCellViewModel.swift in Sources */,
Expand Down Expand Up @@ -7922,6 +7957,7 @@
9DD1E3881D50035E00D4829E /* ProjectActivityData.swift in Sources */,
7061848D29BE577B008F9941 /* MessageBannerViewViewModel.swift in Sources */,
778215E820F6922100F3D09F /* HelpType.swift in Sources */,
E16ECA722C245A51002A1D25 /* PagedContainerViewModel.swift in Sources */,
01940B261D42DC1A0074FCE3 /* HelpViewModel.swift in Sources */,
D6B6766520FE85010082717D /* SettingsNewslettersCellViewModel.swift in Sources */,
39B5E10E2B86C56600FFB720 /* RefInfo.swift in Sources */,
Expand Down Expand Up @@ -8392,6 +8428,7 @@
0634C2F727CFEE40003A6D6E /* ExternalSourceViewElementCell.swift in Sources */,
47F95ED72672C594001365B2 /* ViewRepliesView.swift in Sources */,
37CA16AD23300376006044F9 /* ManageViewPledgeRewardReceivedViewController.swift in Sources */,
E1BAA03B2C1A1CCD004F8B06 /* PPOView.swift in Sources */,
6049D0242AA7940E0015BB0D /* DemoCTAContainerView.swift in Sources */,
778CCC5222822B5D00FB8D35 /* SheetOverlayTransitionAnimator.swift in Sources */,
D04F48D41E0313FB00EDC98A /* ActivityProjectStatusCell.swift in Sources */,
Expand All @@ -8402,6 +8439,7 @@
94BA168E2667C37F0034CC3F /* CommentTableViewFooterView.swift in Sources */,
37E9E2A0225EABB000D29DD7 /* AmountInputView.swift in Sources */,
A77352ED1D5E70FC0017E239 /* MostPopularCell.swift in Sources */,
E1BAA03C2C1A1CCD004F8B06 /* PPOContainerViewController.swift in Sources */,
D79F0F3721028C2600D3B32C /* SettingsPrivacyRecommendationCell.swift in Sources */,
D6B6766920FF8D850082717D /* SettingsNewslettersDataSource.swift in Sources */,
4751A675272B317500F81DD5 /* ProjectTabCategoryDescriptionCell.swift in Sources */,
Expand All @@ -8413,6 +8451,7 @@
D764370A224040D100DAFC9E /* SharedFunctions.swift in Sources */,
77C5E252214182CA002E1670 /* SettingsPrivacySwitchCell.swift in Sources */,
A7C795B41C873AC90081977F /* DiscoveryViewController.swift in Sources */,
E1BAA03A2C1A1CCD004F8B06 /* PPOViewModel.swift in Sources */,
3767EDAB22CFFED40088E8E4 /* ShippingRuleCell.swift in Sources */,
47C500712696481300BB4BF2 /* CommentViewMoreRepliesFailedCell.swift in Sources */,
8ACB32A824ABC2DB00A03968 /* RewardAddOnSelectionViewController.swift in Sources */,
Expand Down Expand Up @@ -8586,6 +8625,7 @@
A7ED20171E83229E00BFFA01 /* DiscoveryProjectsDataSourceTest.swift in Sources */,
776989AA242E747200AAC48D /* CuratedProjectsViewControllerTests.swift in Sources */,
19A97D1A28C7F0EC0031B857 /* DiscoveryNavigationHeaderViewControllerTests.swift in Sources */,
E1BAA03E2C1A1CE4004F8B06 /* PPOViewModelTests.swift in Sources */,
8A23EF0822F11470001262E1 /* RewardCardContainerViewTests.swift in Sources */,
1965436D28C807FB00457EC6 /* PledgePaymentMethodsViewControllerTests.swift in Sources */,
D764377C224174B700DAFC9E /* SharedFunctionsTests.swift in Sources */,
Expand Down
150 changes: 150 additions & 0 deletions Library/PagedContainer/PagedContainerViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import Combine
Copy link
Contributor Author

Choose a reason for hiding this comment

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

🎉

import Foundation
import UIKit

open class PagedContainerViewController: UIViewController {
private weak var activeController: UIViewController? = nil
private var subscriptions = Set<AnyCancellable>()
private let viewModel = PagedContainerViewModel()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Philosophically, I put everything that seemed like UIKit logic in the ViewController, and everything that seemed like "how it's displayed" logic in the view model. However, that ended up meaning the view model was very small since it's a pretty simple concept for a page 🤷‍♀️


// TODO: Use the correct page control, per the designs.
// This may exist already in SortPagerViewController, or we can write one in SwiftUI.
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)
Copy link
Contributor Author

@amy-at-kickstarter amy-at-kickstarter Jun 20, 2024

Choose a reason for hiding this comment

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

OK, so all this elaborate appearanceTransition stuff. This exists so that the child view controllers will receive viewWillAppear with animated = true. The reason we need that is because ActivitiesViewController filters out calls that are unanimated; the result is that it was never showing its content when used as a child view controller.

I was going to clean up ActivitiesViewController, instead, but it turns out that we implicitly use that animated as a filter for whether or not the ActivitiesViewController is being instantiated by a screenshot test. Changing how we handle viewWillAppear in ActivitiesViewController then breaks those tests. It's unclear if we're also relying on that filter in other ways, but I didn't want to muck with something I don't understand yet.

Basically, it was rabbithole, and it seemed clearer to fix it this way.

Copy link
Contributor

Choose a reason for hiding this comment

The 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).

Copy link
Contributor

Choose a reason for hiding this comment

The 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!

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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()
}
}
48 changes: 48 additions & 0 deletions Library/PagedContainer/PagedContainerViewModel.swift
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(
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 feel like this should really be a takeWhen but I can't find the Combine equivalent. We want the index + children when the index changes.

Copy link
Contributor

Choose a reason for hiding this comment

The 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 takeWhen we may need to build our own. (Or, alternatively, store more state so that we don't need to grab everything from publishers?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This tiny bit of state (in self.selectedIndex, a CurrentValueSubject instead of a straight PassthroughSubject), saves a lot of lines of confusing stateless code. I'm OK with that.

Copy link
Contributor

Choose a reason for hiding this comment

The 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>
}
Loading