Repository: nsoojin/MiniSuperApp-fastcampus Branch: main Commit: 0caee426f2ca Files: 212 Total size: 359.2 KB Directory structure: gitextract_mzfutfxp/ ├── .gitignore ├── MiniSuperApp/ │ ├── AppDelegate/ │ │ ├── AppComponent.swift │ │ └── AppDelegate.swift │ ├── AppHome/ │ │ ├── AppHomeBuilder.swift │ │ ├── AppHomeInteractor.swift │ │ ├── AppHomeRouter.swift │ │ ├── AppHomeViewController.swift │ │ ├── HomeWidgetModel.swift │ │ └── Views/ │ │ └── HomeWidgetView.swift │ ├── AppRoot/ │ │ ├── AppRootBuilder.swift │ │ ├── AppRootInteractor.swift │ │ ├── AppRootRouter.swift │ │ └── RootTabBarController.swift │ ├── Assets.xcassets/ │ │ ├── AccentColor.colorset/ │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── map_seoul.imageset/ │ │ └── Contents.json │ ├── Base.lproj/ │ │ └── LaunchScreen.storyboard │ ├── FinanceHome/ │ │ ├── FinanceHomeBuilder.swift │ │ ├── FinanceHomeInteractor.swift │ │ ├── FinanceHomeRouter.swift │ │ └── FinanceHomeViewController.swift │ ├── Info.plist │ ├── ProfileHome/ │ │ ├── ProfileHomeBuilder.swift │ │ ├── ProfileHomeInteractor.swift │ │ ├── ProfileHomeRouter.swift │ │ └── ProfileHomeViewController.swift │ ├── TransportHome/ │ │ ├── TransportHomeBuilder.swift │ │ ├── TransportHomeInteractor.swift │ │ ├── TransportHomeRouter.swift │ │ ├── TransportHomeViewController.swift │ │ └── Views/ │ │ ├── RideTypeView.swift │ │ └── SuperPayView.swift │ └── Utils/ │ ├── Array+Utils.swift │ ├── PushModalPresentationController.swift │ ├── RIBs+Utils.swift │ ├── UIColor+Super.swift │ ├── UIColor+Utils.swift │ ├── UIImage+Utils.swift │ ├── UITableView+Utils.swift │ └── UIView+Utils.swift ├── MiniSuperApp.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm/ │ │ └── Package.resolved │ └── xcshareddata/ │ └── xcschemes/ │ └── MiniSuperApp.xcscheme ├── README.md ├── Samples/ │ ├── DefaultsStore/ │ │ └── DefaultsStore.swift │ ├── Network/ │ │ ├── HTTPMethod.swift │ │ ├── Network.swift │ │ └── NetworkError.swift │ ├── NetworkImp/ │ │ └── NetworkImp.swift │ ├── RIBsTestSupport/ │ │ ├── RoutingMock.swift │ │ ├── ViewControllableMock.swift │ │ └── ViewableRoutingMock.swift │ ├── TestUtil.swift │ ├── Topup/ │ │ ├── CardOnFileCell.swift │ │ ├── CardOnFileViewController.swift │ │ ├── EnterAmountViewController.swift │ │ └── Views/ │ │ ├── EnterAmountWidget.swift │ │ └── SelectedPaymentMethodView.swift │ └── TopupDependencyMock.swift └── completed/ └── MiniSuperApp/ ├── .gitignore ├── AddPaymentMethodIntegrationTests/ │ └── AddPaymentMethodIntegrationTests.swift ├── CX/ │ ├── .gitignore │ ├── Package.swift │ ├── README.md │ └── Sources/ │ └── AppHome/ │ ├── AppHomeBuilder.swift │ ├── AppHomeInteractor.swift │ ├── AppHomeRouter.swift │ ├── AppHomeViewController.swift │ ├── HomeWidgetModel.swift │ └── Views/ │ └── HomeWidgetView.swift ├── Finance/ │ ├── .gitignore │ ├── .swiftpm/ │ │ └── xcode/ │ │ └── xcshareddata/ │ │ └── xcschemes/ │ │ ├── TopupImp.xcscheme │ │ └── TopupImpTests.xcscheme │ ├── Package.swift │ ├── README.md │ ├── Sources/ │ │ ├── AddPaymentMethod/ │ │ │ └── AddPaymentMethodInterface.swift │ │ ├── AddPaymentMethodImp/ │ │ │ ├── AddPaymentMethodBuilder.swift │ │ │ ├── AddPaymentMethodInteractor.swift │ │ │ ├── AddPaymentMethodRouter.swift │ │ │ └── AddPaymentMethodViewController.swift │ │ ├── AddPaymentMethodTestSupport/ │ │ │ └── AddPaymentMethodTestSupport.swift │ │ ├── FinanceEntity/ │ │ │ ├── AddPaymentMethodInfo.swift │ │ │ └── PaymentMethod.swift │ │ ├── FinanceHome/ │ │ │ ├── CardOnFileDashboard/ │ │ │ │ ├── CardOnFileDashboardBuilder.swift │ │ │ │ ├── CardOnFileDashboardInteractor.swift │ │ │ │ ├── CardOnFileDashboardRouter.swift │ │ │ │ ├── CardOnFileDashboardViewController.swift │ │ │ │ ├── PaymentMethodViewModel.swift │ │ │ │ └── Views/ │ │ │ │ ├── AddPaymentMethodButton.swift │ │ │ │ └── PaymentMethodView.swift │ │ │ ├── FinanceHomeBuilder.swift │ │ │ ├── FinanceHomeInteractor.swift │ │ │ ├── FinanceHomeRouter.swift │ │ │ ├── FinanceHomeViewController.swift │ │ │ └── SuperPayDashboard/ │ │ │ ├── Formatter.swift │ │ │ ├── SuperPayDashboardBuilder.swift │ │ │ ├── SuperPayDashboardInteractor.swift │ │ │ ├── SuperPayDashboardRouter.swift │ │ │ └── SuperPayDashboardViewController.swift │ │ ├── FinanceRepository/ │ │ │ ├── AddCardRequest.swift │ │ │ ├── CardOnFileRepository.swift │ │ │ ├── CardOnFileRequest.swift │ │ │ ├── SuperPayRepository.swift │ │ │ └── TopupRequest.swift │ │ ├── FinanceRepositoryTestSupport/ │ │ │ ├── CardOnFileRepositoryMock.swift │ │ │ └── SuperPayRepositoryMock.swift │ │ ├── Topup/ │ │ │ └── TopupInterface.swift │ │ ├── TopupImp/ │ │ │ ├── Array+Utils.swift │ │ │ ├── CardOnFile/ │ │ │ │ ├── CardOnFileBuilder.swift │ │ │ │ ├── CardOnFileCell.swift │ │ │ │ ├── CardOnFileInteractor.swift │ │ │ │ ├── CardOnFileRouter.swift │ │ │ │ └── CardOnFileViewController.swift │ │ │ ├── EnterAmount/ │ │ │ │ ├── EnterAmountBuilder.swift │ │ │ │ ├── EnterAmountInteractor.swift │ │ │ │ ├── EnterAmountRouter.swift │ │ │ │ ├── EnterAmountViewController.swift │ │ │ │ ├── EnterAmountWidget.swift │ │ │ │ └── SelectedPaymentMethodView.swift │ │ │ ├── Models/ │ │ │ │ └── PaymentMethodViewModel.swift │ │ │ ├── TopupBuilder.swift │ │ │ ├── TopupInteractor.swift │ │ │ └── TopupRouter.swift │ │ └── TopupTestSupport/ │ │ └── TopupMock.swift │ └── Tests/ │ └── TopupImpTests/ │ ├── CardOnFile/ │ │ ├── CardOnFileMock.swift │ │ └── CardOnFileViewTests.swift │ ├── EnterAmount/ │ │ ├── EnterAmountInteractorTests.swift │ │ ├── EnterAmountMock.swift │ │ ├── EnterAmountRouterTests.swift │ │ └── EnterAmountViewTests.swift │ └── Topup/ │ ├── TopupInteractorTests.swift │ ├── TopupMock.swift │ └── TopupRouterTests.swift ├── MiniSuperApp/ │ ├── AppDelegate/ │ │ ├── AppComponent.swift │ │ └── AppDelegate.swift │ ├── AppRoot/ │ │ ├── AppRootBuilder.swift │ │ ├── AppRootComponent.swift │ │ ├── AppRootInteractor.swift │ │ ├── AppRootRouter.swift │ │ ├── BaseURL.swift │ │ ├── RootTabBarController.swift │ │ ├── SetupURLProtocol.swift │ │ └── SuperAppURLProtocol.swift │ ├── Assets.xcassets/ │ │ ├── AccentColor.colorset/ │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj/ │ │ └── LaunchScreen.storyboard │ ├── Info.plist │ └── Utils/ │ └── Array+Utils.swift ├── MiniSuperApp.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm/ │ │ └── Package.resolved │ └── xcshareddata/ │ └── xcschemes/ │ ├── AddPaymentMethodIntegrationTests.xcscheme │ ├── MiniSuperApp.xcscheme │ ├── MiniSuperAppUITests.xcscheme │ └── TestHost.xcscheme ├── MiniSuperAppUITests/ │ ├── Response/ │ │ ├── cardOnFile.json │ │ └── topupSuccessResponse.json │ ├── TestUtil.swift │ └── TopupImpUITests.swift ├── Platform/ │ ├── .gitignore │ ├── Package.swift │ ├── README.md │ └── Sources/ │ ├── CombineUtil/ │ │ └── Combine+Utils.swift │ ├── DefaultsStore/ │ │ └── DefaultsStore.swift │ ├── Network/ │ │ ├── HTTPMethod.swift │ │ ├── Network.swift │ │ └── NetworkError.swift │ ├── NetworkImp/ │ │ └── NetworkImp.swift │ ├── PlatformTestSupport/ │ │ └── PlatformTestSupport.swift │ ├── RIBsTestSupport/ │ │ ├── RoutingMock.swift │ │ ├── ViewControllableMock.swift │ │ └── ViewableRoutingMock.swift │ ├── RIBsUtil/ │ │ └── RIBs+Util.swift │ └── SuperUI/ │ ├── AdaptivePresentationControllerDelegate.swift │ ├── PushModalPresentationController.swift │ ├── UIColor+Super.swift │ ├── UIColor+Utils.swift │ ├── UIImage+Utils.swift │ ├── UITableView+Utils.swift │ ├── UIView+Utils.swift │ └── UIViewController+Utils.swift ├── Profile/ │ ├── .gitignore │ ├── Package.swift │ ├── README.md │ └── Sources/ │ └── ProfileHome/ │ ├── ProfileHomeBuilder.swift │ ├── ProfileHomeInteractor.swift │ ├── ProfileHomeRouter.swift │ └── ProfileHomeViewController.swift ├── Samples/ │ ├── TestUtil.swift │ └── TopupDependencyMock.swift ├── TestHost/ │ ├── AppDelegate.swift │ ├── Assets.xcassets/ │ │ ├── AccentColor.colorset/ │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj/ │ │ └── LaunchScreen.storyboard │ └── Info.plist └── Transport/ ├── .gitignore ├── Package.swift ├── README.md └── Sources/ ├── TransportHome/ │ └── TransportHomeInterface.swift └── TransportHomeImp/ ├── Formatter.swift ├── TransportHomeBuilder.swift ├── TransportHomeInteractor.swift ├── TransportHomeRouter.swift ├── TransportHomeViewController.swift └── Views/ ├── RideTypeView.swift └── SuperPayView.swift ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ xcuserdata/ ================================================ FILE: MiniSuperApp/AppDelegate/AppComponent.swift ================================================ import Foundation import ModernRIBs final class AppComponent: Component, AppRootDependency { init() { super.init(dependency: EmptyComponent()) } } ================================================ FILE: MiniSuperApp/AppDelegate/AppDelegate.swift ================================================ import UIKit import ModernRIBs @main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? private var launchRouter: LaunchRouting? private var urlHandler: URLHandler? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let window = UIWindow(frame: UIScreen.main.bounds) self.window = window let result = AppRootBuilder(dependency: AppComponent()).build() self.launchRouter = result.launchRouter self.urlHandler = result.urlHandler launchRouter?.launch(from: window) return true } } protocol URLHandler: AnyObject { func handle(_ url: URL) } ================================================ FILE: MiniSuperApp/AppHome/AppHomeBuilder.swift ================================================ import ModernRIBs public protocol AppHomeDependency: Dependency { } final class AppHomeComponent: Component, TransportHomeDependency { } // MARK: - Builder public protocol AppHomeBuildable: Buildable { func build(withListener listener: AppHomeListener) -> ViewableRouting } public final class AppHomeBuilder: Builder, AppHomeBuildable { public override init(dependency: AppHomeDependency) { super.init(dependency: dependency) } public func build(withListener listener: AppHomeListener) -> ViewableRouting { let component = AppHomeComponent(dependency: dependency) let viewController = AppHomeViewController() let interactor = AppHomeInteractor(presenter: viewController) interactor.listener = listener let transportHomeBuilder = TransportHomeBuilder(dependency: component) return AppHomeRouter( interactor: interactor, viewController: viewController, transportHomeBuildable: transportHomeBuilder ) } } ================================================ FILE: MiniSuperApp/AppHome/AppHomeInteractor.swift ================================================ import ModernRIBs protocol AppHomeRouting: ViewableRouting { func attachTransportHome() func detachTransportHome() } protocol AppHomePresentable: Presentable { var listener: AppHomePresentableListener? { get set } func updateWidget(_ viewModels: [HomeWidgetViewModel]) } public protocol AppHomeListener: AnyObject { // TODO: Declare methods the interactor can invoke to communicate with other RIBs. } final class AppHomeInteractor: PresentableInteractor, AppHomeInteractable, AppHomePresentableListener { weak var router: AppHomeRouting? weak var listener: AppHomeListener? override init(presenter: AppHomePresentable) { super.init(presenter: presenter) presenter.listener = self } override func didBecomeActive() { super.didBecomeActive() let viewModels = [ HomeWidgetModel( imageName: "car", title: "슈퍼택시", tapHandler: { [weak self] in self?.router?.attachTransportHome() } ), HomeWidgetModel( imageName: "cart", title: "슈퍼마트", tapHandler: { } ) ] presenter.updateWidget(viewModels.map(HomeWidgetViewModel.init)) } func transportHomeDidTapClose() { router?.detachTransportHome() } } ================================================ FILE: MiniSuperApp/AppHome/AppHomeRouter.swift ================================================ import ModernRIBs protocol AppHomeInteractable: Interactable, TransportHomeListener { var router: AppHomeRouting? { get set } var listener: AppHomeListener? { get set } } protocol AppHomeViewControllable: ViewControllable { } final class AppHomeRouter: ViewableRouter, AppHomeRouting { private let transportHomeBuildable: TransportHomeBuildable private var transportHomeRouting: Routing? private let transitioningDelegate: PushModalPresentationController init( interactor: AppHomeInteractable, viewController: AppHomeViewControllable, transportHomeBuildable: TransportHomeBuildable ) { self.transitioningDelegate = PushModalPresentationController() self.transportHomeBuildable = transportHomeBuildable super.init(interactor: interactor, viewController: viewController) interactor.router = self } func attachTransportHome() { if transportHomeRouting != nil { return } let router = transportHomeBuildable.build(withListener: interactor) presentWithPushTransition(router.viewControllable, animated: true) attachChild(router) self.transportHomeRouting = router } func detachTransportHome() { guard let router = transportHomeRouting else { return } viewController.dismiss(completion: nil) self.transportHomeRouting = nil detachChild(router) } private func presentWithPushTransition(_ viewControllable: ViewControllable, animated: Bool) { viewControllable.uiviewController.modalPresentationStyle = .custom viewControllable.uiviewController.transitioningDelegate = transitioningDelegate viewController.present(viewControllable, animated: true, completion: nil) } } ================================================ FILE: MiniSuperApp/AppHome/AppHomeViewController.swift ================================================ import ModernRIBs import UIKit protocol AppHomePresentableListener: AnyObject { } final class AppHomeViewController: UIViewController, AppHomePresentable, AppHomeViewControllable { weak var listener: AppHomePresentableListener? private let widgetStackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .horizontal stackView.distribution = .fillEqually stackView.alignment = .top stackView.spacing = 20 return stackView }() init() { super.init(nibName: nil, bundle: nil) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } func updateWidget(_ viewModels: [HomeWidgetViewModel]) { let views = viewModels.map { HomeWidgetView(viewModel: $0) } views.forEach { $0.addShadowWithRoundedCorners(12) widgetStackView.addArrangedSubview($0) } } private func setupViews() { title = "홈" tabBarItem = UITabBarItem(title: "홈", image: UIImage(systemName: "house"), selectedImage: UIImage(systemName: "house.fill")) view.backgroundColor = .backgroundColor view.addSubview(widgetStackView) NSLayoutConstraint.activate([ widgetStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20), widgetStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), widgetStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20) ]) } } ================================================ FILE: MiniSuperApp/AppHome/HomeWidgetModel.swift ================================================ import Foundation struct HomeWidgetModel { let imageName: String let title: String let tapHandler: () -> Void } ================================================ FILE: MiniSuperApp/AppHome/Views/HomeWidgetView.swift ================================================ import UIKit struct HomeWidgetViewModel { let image: UIImage? let title: String let tapHandler: () -> Void init(_ model: HomeWidgetModel) { image = UIImage(systemName: model.imageName) title = model.title tapHandler = model.tapHandler } } final class HomeWidgetView: UIView { init(viewModel: HomeWidgetViewModel) { super.init(frame: .zero) setupViews() update(with: viewModel) } required init?(coder: NSCoder) { fatalError() } private var tapHandler: (() -> Void)? private func update(with viewModel: HomeWidgetViewModel) { imageView.image = viewModel.image titleLabel.text = viewModel.title tapHandler = viewModel.tapHandler } private let imageView: UIImageView = { let imageView = UIImageView() imageView.tintColor = .black imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() private let titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textAlignment = .center label.font = UIFont.systemFont(ofSize: 16, weight: .semibold) return label }() private func setupViews() { addSubview(imageView) addSubview(titleLabel) backgroundColor = .white let tap = UITapGestureRecognizer(target: self, action: #selector(didTap)) addGestureRecognizer(tap) NSLayoutConstraint.activate([ imageView.topAnchor.constraint(equalTo: self.topAnchor, constant: 15), imageView.centerXAnchor.constraint(equalTo: self.centerXAnchor), imageView.widthAnchor.constraint(equalToConstant: 50), imageView.heightAnchor.constraint(equalToConstant: 50), titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 5), titleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor), titleLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor), titleLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -14) ]) } @objc private func didTap() { tapHandler?() } } ================================================ FILE: MiniSuperApp/AppRoot/AppRootBuilder.swift ================================================ import ModernRIBs import UIKit protocol AppRootDependency: Dependency { // TODO: Declare the set of dependencies required by this RIB, but cannot be // created by this RIB. } final class AppRootComponent: Component, AppHomeDependency, FinanceHomeDependency, ProfileHomeDependency { // TODO: Declare 'fileprivate' dependencies that are only used by this RIB. } // MARK: - Builder protocol AppRootBuildable: Buildable { func build() -> (launchRouter: LaunchRouting, urlHandler: URLHandler) } final class AppRootBuilder: Builder, AppRootBuildable { override init(dependency: AppRootDependency) { super.init(dependency: dependency) } func build() -> (launchRouter: LaunchRouting, urlHandler: URLHandler) { let component = AppRootComponent(dependency: dependency) let tabBar = RootTabBarController() let interactor = AppRootInteractor(presenter: tabBar) let appHome = AppHomeBuilder(dependency: component) let financeHome = FinanceHomeBuilder(dependency: component) let profileHome = ProfileHomeBuilder(dependency: component) let router = AppRootRouter( interactor: interactor, viewController: tabBar, appHome: appHome, financeHome: financeHome, profileHome: profileHome ) return (router, interactor) } } ================================================ FILE: MiniSuperApp/AppRoot/AppRootInteractor.swift ================================================ import Foundation import ModernRIBs protocol AppRootRouting: ViewableRouting { func attachTabs() } protocol AppRootPresentable: Presentable { var listener: AppRootPresentableListener? { get set } // TODO: Declare methods the interactor can invoke the presenter to present data. } protocol AppRootListener: AnyObject { // TODO: Declare methods the interactor can invoke to communicate with other RIBs. } final class AppRootInteractor: PresentableInteractor, AppRootInteractable, AppRootPresentableListener, URLHandler { weak var router: AppRootRouting? weak var listener: AppRootListener? // TODO: Add additional dependencies to constructor. Do not perform any logic // in constructor. override init(presenter: AppRootPresentable) { super.init(presenter: presenter) presenter.listener = self } override func didBecomeActive() { super.didBecomeActive() router?.attachTabs() } override func willResignActive() { super.willResignActive() // TODO: Pause any business logic. } func handle(_ url: URL) { } } ================================================ FILE: MiniSuperApp/AppRoot/AppRootRouter.swift ================================================ import ModernRIBs protocol AppRootInteractable: Interactable, AppHomeListener, FinanceHomeListener, ProfileHomeListener { var router: AppRootRouting? { get set } var listener: AppRootListener? { get set } } protocol AppRootViewControllable: ViewControllable { func setViewControllers(_ viewControllers: [ViewControllable]) } final class AppRootRouter: LaunchRouter, AppRootRouting { private let appHome: AppHomeBuildable private let financeHome: FinanceHomeBuildable private let profileHome: ProfileHomeBuildable private var appHomeRouting: ViewableRouting? private var financeHomeRouting: ViewableRouting? private var profileHomeRouting: ViewableRouting? init( interactor: AppRootInteractable, viewController: AppRootViewControllable, appHome: AppHomeBuildable, financeHome: FinanceHomeBuildable, profileHome: ProfileHomeBuildable ) { self.appHome = appHome self.financeHome = financeHome self.profileHome = profileHome super.init(interactor: interactor, viewController: viewController) interactor.router = self } func attachTabs() { let appHomeRouting = appHome.build(withListener: interactor) let financeHomeRouting = financeHome.build(withListener: interactor) let profileHomeRouting = profileHome.build(withListener: interactor) attachChild(appHomeRouting) attachChild(financeHomeRouting) attachChild(profileHomeRouting) let viewControllers = [ NavigationControllerable(root: appHomeRouting.viewControllable), NavigationControllerable(root: financeHomeRouting.viewControllable), profileHomeRouting.viewControllable ] viewController.setViewControllers(viewControllers) } } ================================================ FILE: MiniSuperApp/AppRoot/RootTabBarController.swift ================================================ import UIKit import ModernRIBs protocol AppRootPresentableListener: AnyObject { } final class RootTabBarController: UITabBarController, AppRootViewControllable, AppRootPresentable { weak var listener: AppRootPresentableListener? override func viewDidLoad() { super.viewDidLoad() tabBar.isTranslucent = false tabBar.tintColor = .black tabBar.backgroundColor = .white } func setViewControllers(_ viewControllers: [ViewControllable]) { super.setViewControllers(viewControllers.map(\.uiviewController), animated: false) } } ================================================ FILE: MiniSuperApp/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: MiniSuperApp/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { "idiom" : "iphone", "scale" : "3x", "size" : "29x29" }, { "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { "idiom" : "iphone", "scale" : "3x", "size" : "60x60" }, { "idiom" : "ipad", "scale" : "1x", "size" : "20x20" }, { "idiom" : "ipad", "scale" : "2x", "size" : "20x20" }, { "idiom" : "ipad", "scale" : "1x", "size" : "29x29" }, { "idiom" : "ipad", "scale" : "2x", "size" : "29x29" }, { "idiom" : "ipad", "scale" : "1x", "size" : "40x40" }, { "idiom" : "ipad", "scale" : "2x", "size" : "40x40" }, { "idiom" : "ipad", "scale" : "1x", "size" : "76x76" }, { "idiom" : "ipad", "scale" : "2x", "size" : "76x76" }, { "idiom" : "ipad", "scale" : "2x", "size" : "83.5x83.5" }, { "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: MiniSuperApp/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: MiniSuperApp/Assets.xcassets/map_seoul.imageset/Contents.json ================================================ { "images" : [ { "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "filename" : "map_seoul.png", "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: MiniSuperApp/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: MiniSuperApp/FinanceHome/FinanceHomeBuilder.swift ================================================ import ModernRIBs protocol FinanceHomeDependency: Dependency { // TODO: Declare the set of dependencies required by this RIB, but cannot be // created by this RIB. } final class FinanceHomeComponent: Component { // TODO: Declare 'fileprivate' dependencies that are only used by this RIB. } // MARK: - Builder protocol FinanceHomeBuildable: Buildable { func build(withListener listener: FinanceHomeListener) -> FinanceHomeRouting } final class FinanceHomeBuilder: Builder, FinanceHomeBuildable { override init(dependency: FinanceHomeDependency) { super.init(dependency: dependency) } func build(withListener listener: FinanceHomeListener) -> FinanceHomeRouting { let _ = FinanceHomeComponent(dependency: dependency) let viewController = FinanceHomeViewController() let interactor = FinanceHomeInteractor(presenter: viewController) interactor.listener = listener return FinanceHomeRouter(interactor: interactor, viewController: viewController) } } ================================================ FILE: MiniSuperApp/FinanceHome/FinanceHomeInteractor.swift ================================================ import ModernRIBs protocol FinanceHomeRouting: ViewableRouting { // TODO: Declare methods the interactor can invoke to manage sub-tree via the router. } protocol FinanceHomePresentable: Presentable { var listener: FinanceHomePresentableListener? { get set } // TODO: Declare methods the interactor can invoke the presenter to present data. } protocol FinanceHomeListener: AnyObject { // TODO: Declare methods the interactor can invoke to communicate with other RIBs. } final class FinanceHomeInteractor: PresentableInteractor, FinanceHomeInteractable, FinanceHomePresentableListener { weak var router: FinanceHomeRouting? weak var listener: FinanceHomeListener? // TODO: Add additional dependencies to constructor. Do not perform any logic // in constructor. override init(presenter: FinanceHomePresentable) { super.init(presenter: presenter) presenter.listener = self } override func didBecomeActive() { super.didBecomeActive() // TODO: Implement business logic here. } override func willResignActive() { super.willResignActive() // TODO: Pause any business logic. } } ================================================ FILE: MiniSuperApp/FinanceHome/FinanceHomeRouter.swift ================================================ import ModernRIBs protocol FinanceHomeInteractable: Interactable { var router: FinanceHomeRouting? { get set } var listener: FinanceHomeListener? { get set } } protocol FinanceHomeViewControllable: ViewControllable { // TODO: Declare methods the router invokes to manipulate the view hierarchy. } final class FinanceHomeRouter: ViewableRouter, FinanceHomeRouting { // TODO: Constructor inject child builder protocols to allow building children. override init(interactor: FinanceHomeInteractable, viewController: FinanceHomeViewControllable) { super.init(interactor: interactor, viewController: viewController) interactor.router = self } } ================================================ FILE: MiniSuperApp/FinanceHome/FinanceHomeViewController.swift ================================================ import ModernRIBs import UIKit protocol FinanceHomePresentableListener: AnyObject { // TODO: Declare properties and methods that the view controller can invoke to perform // business logic, such as signIn(). This protocol is implemented by the corresponding // interactor class. } final class FinanceHomeViewController: UIViewController, FinanceHomePresentable, FinanceHomeViewControllable { weak var listener: FinanceHomePresentableListener? init() { super.init(nibName: nil, bundle: nil) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private let label: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false return label }() func setupViews() { title = "슈퍼페이" tabBarItem = UITabBarItem(title: "슈퍼페이", image: UIImage(systemName: "creditcard"), selectedImage: UIImage(systemName: "creditcard.fill")) label.text = "Finance Home" view.backgroundColor = .systemBlue view.addSubview(label) NSLayoutConstraint.activate([ label.centerXAnchor.constraint(equalTo: view.centerXAnchor), label.centerYAnchor.constraint(equalTo: view.centerYAnchor) ]) } } ================================================ FILE: MiniSuperApp/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString 1.0 CFBundleVersion 1 LSRequiresIPhoneOS UIApplicationSupportsIndirectInputEvents UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: MiniSuperApp/ProfileHome/ProfileHomeBuilder.swift ================================================ import ModernRIBs protocol ProfileHomeDependency: Dependency { // TODO: Declare the set of dependencies required by this RIB, but cannot be // created by this RIB. } final class ProfileHomeComponent: Component { // TODO: Declare 'fileprivate' dependencies that are only used by this RIB. } // MARK: - Builder protocol ProfileHomeBuildable: Buildable { func build(withListener listener: ProfileHomeListener) -> ProfileHomeRouting } final class ProfileHomeBuilder: Builder, ProfileHomeBuildable { override init(dependency: ProfileHomeDependency) { super.init(dependency: dependency) } func build(withListener listener: ProfileHomeListener) -> ProfileHomeRouting { let _ = ProfileHomeComponent(dependency: dependency) let viewController = ProfileHomeViewController() let interactor = ProfileHomeInteractor(presenter: viewController) interactor.listener = listener return ProfileHomeRouter(interactor: interactor, viewController: viewController) } } ================================================ FILE: MiniSuperApp/ProfileHome/ProfileHomeInteractor.swift ================================================ import ModernRIBs protocol ProfileHomeRouting: ViewableRouting { // TODO: Declare methods the interactor can invoke to manage sub-tree via the router. } protocol ProfileHomePresentable: Presentable { var listener: ProfileHomePresentableListener? { get set } // TODO: Declare methods the interactor can invoke the presenter to present data. } protocol ProfileHomeListener: AnyObject { // TODO: Declare methods the interactor can invoke to communicate with other RIBs. } final class ProfileHomeInteractor: PresentableInteractor, ProfileHomeInteractable, ProfileHomePresentableListener { weak var router: ProfileHomeRouting? weak var listener: ProfileHomeListener? // TODO: Add additional dependencies to constructor. Do not perform any logic // in constructor. override init(presenter: ProfileHomePresentable) { super.init(presenter: presenter) presenter.listener = self } override func didBecomeActive() { super.didBecomeActive() // TODO: Implement business logic here. } override func willResignActive() { super.willResignActive() // TODO: Pause any business logic. } } ================================================ FILE: MiniSuperApp/ProfileHome/ProfileHomeRouter.swift ================================================ import ModernRIBs protocol ProfileHomeInteractable: Interactable { var router: ProfileHomeRouting? { get set } var listener: ProfileHomeListener? { get set } } protocol ProfileHomeViewControllable: ViewControllable { // TODO: Declare methods the router invokes to manipulate the view hierarchy. } final class ProfileHomeRouter: ViewableRouter, ProfileHomeRouting { // TODO: Constructor inject child builder protocols to allow building children. override init(interactor: ProfileHomeInteractable, viewController: ProfileHomeViewControllable) { super.init(interactor: interactor, viewController: viewController) interactor.router = self } } ================================================ FILE: MiniSuperApp/ProfileHome/ProfileHomeViewController.swift ================================================ import ModernRIBs import UIKit protocol ProfileHomePresentableListener: AnyObject { // TODO: Declare properties and methods that the view controller can invoke to perform // business logic, such as signIn(). This protocol is implemented by the corresponding // interactor class. } final class ProfileHomeViewController: UIViewController, ProfileHomePresentable, ProfileHomeViewControllable { weak var listener: ProfileHomePresentableListener? init() { super.init(nibName: nil, bundle: nil) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private let label: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false return label }() func setupViews() { tabBarItem = UITabBarItem(title: "프로필", image: UIImage(systemName: "person"), selectedImage: UIImage(systemName: "person.fill")) label.text = "Profile Home" view.backgroundColor = .systemTeal view.addSubview(label) NSLayoutConstraint.activate([ label.centerXAnchor.constraint(equalTo: view.centerXAnchor), label.centerYAnchor.constraint(equalTo: view.centerYAnchor) ]) } } ================================================ FILE: MiniSuperApp/TransportHome/TransportHomeBuilder.swift ================================================ import ModernRIBs protocol TransportHomeDependency: Dependency { } final class TransportHomeComponent: Component { } // MARK: - Builder protocol TransportHomeBuildable: Buildable { func build(withListener listener: TransportHomeListener) -> TransportHomeRouting } final class TransportHomeBuilder: Builder, TransportHomeBuildable { override init(dependency: TransportHomeDependency) { super.init(dependency: dependency) } func build(withListener listener: TransportHomeListener) -> TransportHomeRouting { _ = TransportHomeComponent(dependency: dependency) let viewController = TransportHomeViewController() let interactor = TransportHomeInteractor(presenter: viewController) interactor.listener = listener return TransportHomeRouter( interactor: interactor, viewController: viewController ) } } ================================================ FILE: MiniSuperApp/TransportHome/TransportHomeInteractor.swift ================================================ import ModernRIBs import Combine import Foundation protocol TransportHomeRouting: ViewableRouting { } protocol TransportHomePresentable: Presentable { var listener: TransportHomePresentableListener? { get set } } protocol TransportHomeListener: AnyObject { func transportHomeDidTapClose() } protocol TransportHomeInteractorDependency { } final class TransportHomeInteractor: PresentableInteractor, TransportHomeInteractable, TransportHomePresentableListener { weak var router: TransportHomeRouting? weak var listener: TransportHomeListener? override init(presenter: TransportHomePresentable) { super.init(presenter: presenter) presenter.listener = self } override func didBecomeActive() { super.didBecomeActive() } override func willResignActive() { super.willResignActive() // TODO: Pause any business logic. } func didTapBack() { listener?.transportHomeDidTapClose() } } ================================================ FILE: MiniSuperApp/TransportHome/TransportHomeRouter.swift ================================================ import ModernRIBs protocol TransportHomeInteractable: Interactable { var router: TransportHomeRouting? { get set } var listener: TransportHomeListener? { get set } } protocol TransportHomeViewControllable: ViewControllable { // TODO: Declare methods the router invokes to manipulate the view hierarchy. } final class TransportHomeRouter: ViewableRouter, TransportHomeRouting { override init( interactor: TransportHomeInteractable, viewController: TransportHomeViewControllable ) { super.init(interactor: interactor, viewController: viewController) interactor.router = self } } ================================================ FILE: MiniSuperApp/TransportHome/TransportHomeViewController.swift ================================================ import ModernRIBs import UIKit protocol TransportHomePresentableListener: AnyObject { func didTapBack() } final class TransportHomeViewController: UIViewController, TransportHomePresentable, TransportHomeViewControllable { weak var listener: TransportHomePresentableListener? private let mapView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.contentMode = .scaleAspectFill imageView.image = UIImage(named: "map_seoul") return imageView }() private let searchView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.addShadowWithRoundedCorners(8) view.backgroundColor = .white return view }() private let departureLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.systemFont(ofSize: 18, weight: .medium) label.text = "우리집" return label }() private let destinationLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.systemFont(ofSize: 18, weight: .medium) label.text = "회사" return label }() private let arrowImageView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.tintColor = .black imageView.image = UIImage( systemName: "arrow.right", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .semibold) ) return imageView }() private let rideTypeView: RideTypeView = { let view = RideTypeView() view.translatesAutoresizingMaskIntoConstraints = false return view }() private let superPayView: SuperPayView = { let view = SuperPayView() view.translatesAutoresizingMaskIntoConstraints = false return view }() private lazy var backButton: UIButton = { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false button.backgroundColor = .white button.roundCorners(25) button.tintColor = .black button.setImage( UIImage( systemName: "chevron.backward", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .semibold) ), for: .normal ) button.addTarget(self, action: #selector(backButtonDidTap), for: .touchUpInside) return button }() private let rideTypeStackView: UIStackView = { let stack = UIStackView() return stack }() private let paymentStackView: UIStackView = { let stack = UIStackView() return stack }() private let rideInfoPane: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = .white view.addShadowWithRoundedCorners() return view }() private lazy var rideConfirmButton: UIButton = { let button = UIButton(type: .system) button.translatesAutoresizingMaskIntoConstraints = false button.setTitle("슈퍼택시 호출하기", for: .normal) button.backgroundColor = .primaryRed button.tintColor = .white button.addTarget(self, action: #selector(didTapRideConfirmButton), for: .touchUpInside) button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .semibold) return button }() private let separatorView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = .systemGray6 return view }() func setSuperPayBalance(_ balanceText: String) { superPayView.setBalanceText("잔고: \(balanceText)원") } init() { super.init(nibName: nil, bundle: nil) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private func setupViews() { view.addSubview(mapView) view.addSubview(searchView) searchView.addSubview(arrowImageView) searchView.addSubview(departureLabel) searchView.addSubview(destinationLabel) view.addSubview(backButton) view.addSubview(rideInfoPane) rideInfoPane.addSubview(rideTypeView) rideInfoPane.addSubview(superPayView) rideInfoPane.addSubview(separatorView) rideInfoPane.addSubview(rideConfirmButton) NSLayoutConstraint.activate([ mapView.topAnchor.constraint(equalTo: view.topAnchor), mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor), mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor), mapView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.7), searchView.leadingAnchor.constraint(equalTo: backButton.trailingAnchor, constant: 10), searchView.centerYAnchor.constraint(equalTo: backButton.centerYAnchor), searchView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), searchView.heightAnchor.constraint(equalToConstant: 50), departureLabel.leadingAnchor.constraint(equalTo: searchView.leadingAnchor, constant: 60), departureLabel.centerYAnchor.constraint(equalTo: searchView.centerYAnchor), destinationLabel.trailingAnchor.constraint(equalTo: searchView.trailingAnchor, constant: -60), destinationLabel.centerYAnchor.constraint(equalTo: searchView.centerYAnchor), rideInfoPane.bottomAnchor.constraint(equalTo: view.bottomAnchor), rideInfoPane.leadingAnchor.constraint(equalTo: view.leadingAnchor), rideInfoPane.trailingAnchor.constraint(equalTo: view.trailingAnchor), rideInfoPane.topAnchor.constraint(equalTo: mapView.bottomAnchor), backButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), backButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20), backButton.widthAnchor.constraint(equalToConstant: 50), backButton.heightAnchor.constraint(equalToConstant: 50), arrowImageView.centerXAnchor.constraint(equalTo: searchView.centerXAnchor), arrowImageView.centerYAnchor.constraint(equalTo: searchView.centerYAnchor), rideTypeView.leadingAnchor.constraint(equalTo: rideInfoPane.leadingAnchor, constant: 30), rideTypeView.trailingAnchor.constraint(equalTo: rideInfoPane.trailingAnchor, constant: -30), rideTypeView.topAnchor.constraint(equalTo: rideInfoPane.topAnchor, constant: 10), rideTypeView.heightAnchor.constraint(equalToConstant: 70), separatorView.topAnchor.constraint(equalTo: rideTypeView.bottomAnchor), separatorView.leadingAnchor.constraint(equalTo: rideInfoPane.leadingAnchor), separatorView.trailingAnchor.constraint(equalTo: rideInfoPane.trailingAnchor), separatorView.heightAnchor.constraint(equalToConstant: 1), superPayView.leadingAnchor.constraint(equalTo: rideInfoPane.leadingAnchor, constant: 30), superPayView.trailingAnchor.constraint(equalTo: rideInfoPane.trailingAnchor, constant: -30), superPayView.topAnchor.constraint(equalTo: separatorView.bottomAnchor, constant: 0), superPayView.bottomAnchor.constraint(equalTo: rideConfirmButton.topAnchor), rideConfirmButton.leadingAnchor.constraint(equalTo: rideInfoPane.leadingAnchor, constant: 30), rideConfirmButton.trailingAnchor.constraint(equalTo: rideInfoPane.trailingAnchor, constant: -30), rideConfirmButton.bottomAnchor.constraint(equalTo: rideInfoPane.safeAreaLayoutGuide.bottomAnchor, constant: -20), rideConfirmButton.heightAnchor.constraint(equalToConstant: 60) ]) } @objc private func backButtonDidTap() { listener?.didTapBack() } @objc private func didTapRideConfirmButton() { } } ================================================ FILE: MiniSuperApp/TransportHome/Views/RideTypeView.swift ================================================ import UIKit final class RideTypeView: UIView { private let thumbnailView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.contentMode = .scaleAspectFit imageView.tintColor = .black imageView.image = UIImage( systemName: "bolt.car", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .semibold) ) return imageView }() private let rideTypeNameLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.systemFont(ofSize: 18, weight: .medium) label.text = "슈퍼전기차 택시" return label }() private let priceLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 18, weight: .medium) label.translatesAutoresizingMaskIntoConstraints = false label.text = "18,000원" return label }() init() { super.init(frame: .zero) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private func setupViews() { addSubview(thumbnailView) addSubview(priceLabel) addSubview(rideTypeNameLabel) NSLayoutConstraint.activate([ thumbnailView.leadingAnchor.constraint(equalTo: self.leadingAnchor), thumbnailView.centerYAnchor.constraint(equalTo: self.centerYAnchor), thumbnailView.widthAnchor.constraint(equalToConstant: 40), thumbnailView.heightAnchor.constraint(equalToConstant: 40), rideTypeNameLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), rideTypeNameLabel.leadingAnchor.constraint(equalTo: thumbnailView.trailingAnchor, constant: 10), priceLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor), priceLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor) ]) } } ================================================ FILE: MiniSuperApp/TransportHome/Views/SuperPayView.swift ================================================ import UIKit final class SuperPayView: UIView { private let thumbnailView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.backgroundColor = .systemBlue imageView.roundCorners(4) return imageView }() private let nameLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.systemFont(ofSize: 18, weight: .medium) label.text = "슈퍼페이" return label }() private let balanceLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.systemFont(ofSize: 18, weight: .medium) label.text = "---원" return label }() func setBalanceText(_ text: String) { balanceLabel.text = text } init() { super.init(frame: .zero) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private func setupViews() { addSubview(thumbnailView) addSubview(nameLabel) addSubview(balanceLabel) NSLayoutConstraint.activate([ thumbnailView.widthAnchor.constraint(equalToConstant: 46), thumbnailView.heightAnchor.constraint(equalToConstant: 34), thumbnailView.centerYAnchor.constraint(equalTo: self.centerYAnchor), nameLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), nameLabel.leadingAnchor.constraint(equalTo: thumbnailView.trailingAnchor, constant: 10), balanceLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor), balanceLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), ]) } } ================================================ FILE: MiniSuperApp/Utils/Array+Utils.swift ================================================ import Foundation extension Array { subscript(safe index: Int) -> Element? { return indices ~= index ? self[index] : nil } } ================================================ FILE: MiniSuperApp/Utils/PushModalPresentationController.swift ================================================ import UIKit public final class PushModalPresentationController: NSObject, UIViewControllerTransitioningDelegate { public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { return PushModalPresentTransitioning() } public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { return PushModalDismissTransitioning() } } private final class PushModalPresentTransitioning: NSObject, UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return 0.25 } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let toViewController = transitionContext.viewController(forKey: .to) else { return } let containerView = transitionContext.containerView let toView = transitionContext.view(forKey: .to) var toViewInitialFrame = transitionContext.initialFrame(for: toViewController) let toViewFinalFrame = transitionContext.finalFrame(for: toViewController) toView.map(containerView.addSubview) toViewInitialFrame.origin = CGPoint(x: containerView.bounds.maxX, y: containerView.bounds.minY) toViewInitialFrame.size = toViewFinalFrame.size toView?.frame = toViewInitialFrame UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.allowUserInteraction, .curveEaseOut], animations: { toView?.frame = toViewFinalFrame }) { _ in let isCompleted = !transitionContext.transitionWasCancelled transitionContext.completeTransition(isCompleted) } } } private final class PushModalDismissTransitioning: NSObject, UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return 0.25 } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let fromViewController = transitionContext.viewController(forKey: .from) else { return } let containerView = transitionContext.containerView let toView = transitionContext.view(forKey: .to) let fromView = transitionContext.view(forKey: .from) var fromViewFinalFrame = transitionContext.finalFrame(for: fromViewController) toView.map(containerView.addSubview) if let fromView = fromView { fromViewFinalFrame = fromView.frame.offsetBy(dx: fromView.frame.width, dy: 0) } UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.allowUserInteraction, .curveEaseOut], animations: { fromView?.frame = fromViewFinalFrame }) { _ in let isCompleted = !transitionContext.transitionWasCancelled transitionContext.completeTransition(isCompleted) } } } ================================================ FILE: MiniSuperApp/Utils/RIBs+Utils.swift ================================================ import UIKit import ModernRIBs final class NavigationControllerable: ViewControllable { var uiviewController: UIViewController { self.navigationController } let navigationController: UINavigationController public init(root: ViewControllable) { let navigation = UINavigationController(rootViewController: root.uiviewController) navigation.navigationBar.isTranslucent = false navigation.navigationBar.backgroundColor = .white navigation.navigationBar.scrollEdgeAppearance = navigation.navigationBar.standardAppearance self.navigationController = navigation } } extension ViewControllable { func present(_ viewControllable: ViewControllable, animated: Bool, completion: (() -> Void)?) { self.uiviewController.present(viewControllable.uiviewController, animated: true, completion: completion) } func dismiss(completion: (() -> Void)?) { self.uiviewController.dismiss(animated: true, completion: completion) } func pushViewController(_ viewControllable: ViewControllable, animated: Bool) { if let nav = self.uiviewController as? UINavigationController { nav.pushViewController(viewControllable.uiviewController, animated: animated) } else { self.uiviewController.navigationController?.pushViewController(viewControllable.uiviewController, animated: animated) } } func popViewController(animated: Bool) { if let nav = self.uiviewController as? UINavigationController { nav.popViewController(animated: animated) } else { self.uiviewController.navigationController?.popViewController(animated: animated) } } func popToRoot(animated: Bool) { if let nav = self.uiviewController as? UINavigationController { nav.popToRootViewController(animated: animated) } else { self.uiviewController.navigationController?.popToRootViewController(animated: animated) } } func setViewControllers(_ viewControllerables: [ViewControllable]) { if let nav = self.uiviewController as? UINavigationController { nav.setViewControllers(viewControllerables.map(\.uiviewController), animated: true) } else { self.uiviewController.navigationController?.setViewControllers(viewControllerables.map(\.uiviewController), animated: true) } } } ================================================ FILE: MiniSuperApp/Utils/UIColor+Super.swift ================================================ import UIKit extension UIColor { static let backgroundColor = UIColor(hex: "#F1F5F9FF")! static let primaryRed = UIColor(hex: "#eb445aff")! } ================================================ FILE: MiniSuperApp/Utils/UIColor+Utils.swift ================================================ import UIKit extension UIColor { convenience init?(hex: String) { let r, g, b, a: CGFloat if hex.hasPrefix("#") { let start = hex.index(hex.startIndex, offsetBy: 1) let hexColor = String(hex[start...]) if hexColor.count == 8 { let scanner = Scanner(string: hexColor) var hexNumber: UInt64 = 0 if scanner.scanHexInt64(&hexNumber) { r = CGFloat((hexNumber & 0xff000000) >> 24) / 255 g = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255 b = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255 a = CGFloat(hexNumber & 0x000000ff) / 255 self.init(red: r, green: g, blue: b, alpha: a) return } } } return nil } } ================================================ FILE: MiniSuperApp/Utils/UIImage+Utils.swift ================================================ import UIKit public extension UIImage { convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) { let rect = CGRect(origin: .zero, size: size) UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0) color.setFill() UIRectFill(rect) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() guard let cgImage = image?.cgImage else { return nil } self.init(cgImage: cgImage) } } ================================================ FILE: MiniSuperApp/Utils/UITableView+Utils.swift ================================================ import UIKit public protocol Reusable: AnyObject { static var reuseIdentifier: String { get } } public extension Reusable { static var reuseIdentifier: String { return String(describing: self) } } extension UITableViewCell: Reusable {} public extension UITableView { func register(cellType: T.Type) { self.register(cellType, forCellReuseIdentifier: T.reuseIdentifier) } func dequeueReusableCell(for indexPath: IndexPath, cellType: T.Type = T.self) -> T { guard let cell = self.dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else { fatalError("Failed to dequeue reusable cell") } return cell } } ================================================ FILE: MiniSuperApp/Utils/UIView+Utils.swift ================================================ import UIKit extension UIView { func addShadowWithRoundedCorners( _ radius: CGFloat = 16, shadowColor: CGColor = UIColor.black.cgColor, opacity: Float = 0.1 ) { self.layer.cornerCurve = .continuous self.layer.masksToBounds = false self.layer.shadowColor = shadowColor self.layer.shadowOffset = CGSize(width: 0, height: 0) self.layer.shadowOpacity = opacity self.layer.shadowRadius = 2.5 self.layer.cornerRadius = radius } func roundCorners( _ radius: CGFloat = 16 ) { self.layer.cornerCurve = .continuous self.layer.cornerRadius = radius self.clipsToBounds = true } } ================================================ FILE: MiniSuperApp.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 52; objects = { /* Begin PBXBuildFile section */ F57EBE542738468700FE9319 /* UIImage+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57EBE512738468700FE9319 /* UIImage+Utils.swift */; }; F57EBE552738468700FE9319 /* Array+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57EBE522738468700FE9319 /* Array+Utils.swift */; }; F57EBE562738468700FE9319 /* UITableView+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57EBE532738468700FE9319 /* UITableView+Utils.swift */; }; F57F6AE826DB4A2700C0117D /* FinanceHomeRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57F6AE426DB4A2700C0117D /* FinanceHomeRouter.swift */; }; F57F6AE926DB4A2700C0117D /* FinanceHomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57F6AE526DB4A2700C0117D /* FinanceHomeViewController.swift */; }; F57F6AEA26DB4A2700C0117D /* FinanceHomeBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57F6AE626DB4A2700C0117D /* FinanceHomeBuilder.swift */; }; F57F6AEB26DB4A2700C0117D /* FinanceHomeInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57F6AE726DB4A2700C0117D /* FinanceHomeInteractor.swift */; }; F57F6AF026DB4A2D00C0117D /* ProfileHomeRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57F6AEC26DB4A2D00C0117D /* ProfileHomeRouter.swift */; }; F57F6AF126DB4A2D00C0117D /* ProfileHomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57F6AED26DB4A2D00C0117D /* ProfileHomeViewController.swift */; }; F57F6AF226DB4A2D00C0117D /* ProfileHomeBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57F6AEE26DB4A2D00C0117D /* ProfileHomeBuilder.swift */; }; F57F6AF326DB4A2D00C0117D /* ProfileHomeInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57F6AEF26DB4A2D00C0117D /* ProfileHomeInteractor.swift */; }; F57F6AFE26DB4CD700C0117D /* RootTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57F6AFD26DB4CD700C0117D /* RootTabBarController.swift */; }; F5829F5926DB2BC400BFA8CD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5829F5826DB2BC400BFA8CD /* AppDelegate.swift */; }; F5829F6226DB2BC500BFA8CD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F5829F6126DB2BC500BFA8CD /* Assets.xcassets */; }; F5829F6526DB2BC500BFA8CD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F5829F6326DB2BC500BFA8CD /* LaunchScreen.storyboard */; }; F5829F6E26DB2DE900BFA8CD /* ModernRIBs in Frameworks */ = {isa = PBXBuildFile; productRef = F5829F6D26DB2DE900BFA8CD /* ModernRIBs */; }; F5829F7426DB34AA00BFA8CD /* AppRootRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5829F7026DB34AA00BFA8CD /* AppRootRouter.swift */; }; F5829F7626DB34AA00BFA8CD /* AppRootBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5829F7226DB34AA00BFA8CD /* AppRootBuilder.swift */; }; F5829F7726DB34AA00BFA8CD /* AppRootInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5829F7326DB34AA00BFA8CD /* AppRootInteractor.swift */; }; F5829F7926DB397300BFA8CD /* AppComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5829F7826DB397300BFA8CD /* AppComponent.swift */; }; F5EAA219270B395900EF2B70 /* HomeWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EAA213270B395800EF2B70 /* HomeWidgetView.swift */; }; F5EAA21A270B395900EF2B70 /* AppHomeBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EAA214270B395800EF2B70 /* AppHomeBuilder.swift */; }; F5EAA21B270B395900EF2B70 /* AppHomeInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EAA215270B395900EF2B70 /* AppHomeInteractor.swift */; }; F5EAA21C270B395900EF2B70 /* AppHomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EAA216270B395900EF2B70 /* AppHomeViewController.swift */; }; F5EAA21D270B395900EF2B70 /* HomeWidgetModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EAA217270B395900EF2B70 /* HomeWidgetModel.swift */; }; F5EAA21E270B395900EF2B70 /* AppHomeRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EAA218270B395900EF2B70 /* AppHomeRouter.swift */; }; F5EAA220270B39DC00EF2B70 /* PushModalPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EAA21F270B39DC00EF2B70 /* PushModalPresentationController.swift */; }; F5EAA229270B3A4500EF2B70 /* TransportHomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EAA222270B3A4500EF2B70 /* TransportHomeViewController.swift */; }; F5EAA22A270B3A4500EF2B70 /* TransportHomeRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EAA223270B3A4500EF2B70 /* TransportHomeRouter.swift */; }; F5EAA22B270B3A4500EF2B70 /* TransportHomeBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EAA224270B3A4500EF2B70 /* TransportHomeBuilder.swift */; }; F5EAA22C270B3A4500EF2B70 /* TransportHomeInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EAA225270B3A4500EF2B70 /* TransportHomeInteractor.swift */; }; F5EAA22D270B3A4500EF2B70 /* RideTypeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EAA227270B3A4500EF2B70 /* RideTypeView.swift */; }; F5EAA22E270B3A4500EF2B70 /* SuperPayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EAA228270B3A4500EF2B70 /* SuperPayView.swift */; }; F5EAA230270B3A7100EF2B70 /* RIBs+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EAA22F270B3A7100EF2B70 /* RIBs+Utils.swift */; }; F5EAA232270B3A8F00EF2B70 /* UIView+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EAA231270B3A8F00EF2B70 /* UIView+Utils.swift */; }; F5EAA234270B3A9A00EF2B70 /* UIColor+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EAA233270B3A9A00EF2B70 /* UIColor+Utils.swift */; }; F5EAA236270B3AA600EF2B70 /* UIColor+Super.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5EAA235270B3AA600EF2B70 /* UIColor+Super.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ F57EBE512738468700FE9319 /* UIImage+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Utils.swift"; sourceTree = ""; }; F57EBE522738468700FE9319 /* Array+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Utils.swift"; sourceTree = ""; }; F57EBE532738468700FE9319 /* UITableView+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+Utils.swift"; sourceTree = ""; }; F57F6AE426DB4A2700C0117D /* FinanceHomeRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinanceHomeRouter.swift; sourceTree = ""; }; F57F6AE526DB4A2700C0117D /* FinanceHomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinanceHomeViewController.swift; sourceTree = ""; }; F57F6AE626DB4A2700C0117D /* FinanceHomeBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinanceHomeBuilder.swift; sourceTree = ""; }; F57F6AE726DB4A2700C0117D /* FinanceHomeInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinanceHomeInteractor.swift; sourceTree = ""; }; F57F6AEC26DB4A2D00C0117D /* ProfileHomeRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHomeRouter.swift; sourceTree = ""; }; F57F6AED26DB4A2D00C0117D /* ProfileHomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHomeViewController.swift; sourceTree = ""; }; F57F6AEE26DB4A2D00C0117D /* ProfileHomeBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHomeBuilder.swift; sourceTree = ""; }; F57F6AEF26DB4A2D00C0117D /* ProfileHomeInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHomeInteractor.swift; sourceTree = ""; }; F57F6AFD26DB4CD700C0117D /* RootTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootTabBarController.swift; sourceTree = ""; }; F5829F5526DB2BC400BFA8CD /* MiniSuperApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MiniSuperApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; F5829F5826DB2BC400BFA8CD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F5829F6126DB2BC500BFA8CD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F5829F6426DB2BC500BFA8CD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; F5829F6626DB2BC500BFA8CD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F5829F7026DB34AA00BFA8CD /* AppRootRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRootRouter.swift; sourceTree = ""; }; F5829F7226DB34AA00BFA8CD /* AppRootBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRootBuilder.swift; sourceTree = ""; }; F5829F7326DB34AA00BFA8CD /* AppRootInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRootInteractor.swift; sourceTree = ""; }; F5829F7826DB397300BFA8CD /* AppComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppComponent.swift; sourceTree = ""; }; F5EAA213270B395800EF2B70 /* HomeWidgetView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeWidgetView.swift; sourceTree = ""; }; F5EAA214270B395800EF2B70 /* AppHomeBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppHomeBuilder.swift; sourceTree = ""; }; F5EAA215270B395900EF2B70 /* AppHomeInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppHomeInteractor.swift; sourceTree = ""; }; F5EAA216270B395900EF2B70 /* AppHomeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppHomeViewController.swift; sourceTree = ""; }; F5EAA217270B395900EF2B70 /* HomeWidgetModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeWidgetModel.swift; sourceTree = ""; }; F5EAA218270B395900EF2B70 /* AppHomeRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppHomeRouter.swift; sourceTree = ""; }; F5EAA21F270B39DC00EF2B70 /* PushModalPresentationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PushModalPresentationController.swift; sourceTree = ""; }; F5EAA222270B3A4500EF2B70 /* TransportHomeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransportHomeViewController.swift; sourceTree = ""; }; F5EAA223270B3A4500EF2B70 /* TransportHomeRouter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransportHomeRouter.swift; sourceTree = ""; }; F5EAA224270B3A4500EF2B70 /* TransportHomeBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransportHomeBuilder.swift; sourceTree = ""; }; F5EAA225270B3A4500EF2B70 /* TransportHomeInteractor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransportHomeInteractor.swift; sourceTree = ""; }; F5EAA227270B3A4500EF2B70 /* RideTypeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RideTypeView.swift; sourceTree = ""; }; F5EAA228270B3A4500EF2B70 /* SuperPayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SuperPayView.swift; sourceTree = ""; }; F5EAA22F270B3A7100EF2B70 /* RIBs+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "RIBs+Utils.swift"; sourceTree = ""; }; F5EAA231270B3A8F00EF2B70 /* UIView+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIView+Utils.swift"; sourceTree = ""; }; F5EAA233270B3A9A00EF2B70 /* UIColor+Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Utils.swift"; sourceTree = ""; }; F5EAA235270B3AA600EF2B70 /* UIColor+Super.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIColor+Super.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ F5829F5226DB2BC400BFA8CD /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( F5829F6E26DB2DE900BFA8CD /* ModernRIBs in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ F57F6AD926DB49E500C0117D /* AppHome */ = { isa = PBXGroup; children = ( F5EAA214270B395800EF2B70 /* AppHomeBuilder.swift */, F5EAA215270B395900EF2B70 /* AppHomeInteractor.swift */, F5EAA218270B395900EF2B70 /* AppHomeRouter.swift */, F5EAA216270B395900EF2B70 /* AppHomeViewController.swift */, F5EAA217270B395900EF2B70 /* HomeWidgetModel.swift */, F5EAA212270B395800EF2B70 /* Views */, ); path = AppHome; sourceTree = ""; }; F57F6ADA26DB49EA00C0117D /* FinanceHome */ = { isa = PBXGroup; children = ( F57F6AE426DB4A2700C0117D /* FinanceHomeRouter.swift */, F57F6AE526DB4A2700C0117D /* FinanceHomeViewController.swift */, F57F6AE626DB4A2700C0117D /* FinanceHomeBuilder.swift */, F57F6AE726DB4A2700C0117D /* FinanceHomeInteractor.swift */, ); path = FinanceHome; sourceTree = ""; }; F57F6ADB26DB49FD00C0117D /* ProfileHome */ = { isa = PBXGroup; children = ( F57F6AEC26DB4A2D00C0117D /* ProfileHomeRouter.swift */, F57F6AED26DB4A2D00C0117D /* ProfileHomeViewController.swift */, F57F6AEE26DB4A2D00C0117D /* ProfileHomeBuilder.swift */, F57F6AEF26DB4A2D00C0117D /* ProfileHomeInteractor.swift */, ); path = ProfileHome; sourceTree = ""; }; F57F6AFF26DB585100C0117D /* Utils */ = { isa = PBXGroup; children = ( F57EBE522738468700FE9319 /* Array+Utils.swift */, F57EBE512738468700FE9319 /* UIImage+Utils.swift */, F57EBE532738468700FE9319 /* UITableView+Utils.swift */, F5EAA235270B3AA600EF2B70 /* UIColor+Super.swift */, F5EAA233270B3A9A00EF2B70 /* UIColor+Utils.swift */, F5EAA231270B3A8F00EF2B70 /* UIView+Utils.swift */, F5EAA21F270B39DC00EF2B70 /* PushModalPresentationController.swift */, F5EAA22F270B3A7100EF2B70 /* RIBs+Utils.swift */, ); path = Utils; sourceTree = ""; }; F5829F4C26DB2BC400BFA8CD = { isa = PBXGroup; children = ( F5829F5726DB2BC400BFA8CD /* MiniSuperApp */, F5829F5626DB2BC400BFA8CD /* Products */, ); sourceTree = ""; }; F5829F5626DB2BC400BFA8CD /* Products */ = { isa = PBXGroup; children = ( F5829F5526DB2BC400BFA8CD /* MiniSuperApp.app */, ); name = Products; sourceTree = ""; }; F5829F5726DB2BC400BFA8CD /* MiniSuperApp */ = { isa = PBXGroup; children = ( F5829F7A26DB3AF900BFA8CD /* AppDelegate */, F5829F6F26DB33CF00BFA8CD /* AppRoot */, F57F6AD926DB49E500C0117D /* AppHome */, F5EAA221270B3A4500EF2B70 /* TransportHome */, F57F6ADA26DB49EA00C0117D /* FinanceHome */, F57F6ADB26DB49FD00C0117D /* ProfileHome */, F57F6AFF26DB585100C0117D /* Utils */, F5829F6126DB2BC500BFA8CD /* Assets.xcassets */, F5829F6326DB2BC500BFA8CD /* LaunchScreen.storyboard */, F5829F6626DB2BC500BFA8CD /* Info.plist */, ); path = MiniSuperApp; sourceTree = ""; }; F5829F6F26DB33CF00BFA8CD /* AppRoot */ = { isa = PBXGroup; children = ( F5829F7026DB34AA00BFA8CD /* AppRootRouter.swift */, F5829F7226DB34AA00BFA8CD /* AppRootBuilder.swift */, F5829F7326DB34AA00BFA8CD /* AppRootInteractor.swift */, F57F6AFD26DB4CD700C0117D /* RootTabBarController.swift */, ); path = AppRoot; sourceTree = ""; }; F5829F7A26DB3AF900BFA8CD /* AppDelegate */ = { isa = PBXGroup; children = ( F5829F5826DB2BC400BFA8CD /* AppDelegate.swift */, F5829F7826DB397300BFA8CD /* AppComponent.swift */, ); path = AppDelegate; sourceTree = ""; }; F5EAA212270B395800EF2B70 /* Views */ = { isa = PBXGroup; children = ( F5EAA213270B395800EF2B70 /* HomeWidgetView.swift */, ); path = Views; sourceTree = ""; }; F5EAA221270B3A4500EF2B70 /* TransportHome */ = { isa = PBXGroup; children = ( F5EAA222270B3A4500EF2B70 /* TransportHomeViewController.swift */, F5EAA223270B3A4500EF2B70 /* TransportHomeRouter.swift */, F5EAA224270B3A4500EF2B70 /* TransportHomeBuilder.swift */, F5EAA225270B3A4500EF2B70 /* TransportHomeInteractor.swift */, F5EAA226270B3A4500EF2B70 /* Views */, ); path = TransportHome; sourceTree = ""; }; F5EAA226270B3A4500EF2B70 /* Views */ = { isa = PBXGroup; children = ( F5EAA227270B3A4500EF2B70 /* RideTypeView.swift */, F5EAA228270B3A4500EF2B70 /* SuperPayView.swift */, ); path = Views; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ F5829F5426DB2BC400BFA8CD /* MiniSuperApp */ = { isa = PBXNativeTarget; buildConfigurationList = F5829F6926DB2BC500BFA8CD /* Build configuration list for PBXNativeTarget "MiniSuperApp" */; buildPhases = ( F5829F5126DB2BC400BFA8CD /* Sources */, F5829F5226DB2BC400BFA8CD /* Frameworks */, F5829F5326DB2BC400BFA8CD /* Resources */, ); buildRules = ( ); dependencies = ( ); name = MiniSuperApp; packageProductDependencies = ( F5829F6D26DB2DE900BFA8CD /* ModernRIBs */, ); productName = SuperRedApp; productReference = F5829F5526DB2BC400BFA8CD /* MiniSuperApp.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ F5829F4D26DB2BC400BFA8CD /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1250; LastUpgradeCheck = 1250; TargetAttributes = { F5829F5426DB2BC400BFA8CD = { CreatedOnToolsVersion = 12.5.1; }; }; }; buildConfigurationList = F5829F5026DB2BC400BFA8CD /* Build configuration list for PBXProject "MiniSuperApp" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = F5829F4C26DB2BC400BFA8CD; packageReferences = ( F5829F6C26DB2DE900BFA8CD /* XCRemoteSwiftPackageReference "ModernRIBs" */, ); productRefGroup = F5829F5626DB2BC400BFA8CD /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( F5829F5426DB2BC400BFA8CD /* MiniSuperApp */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ F5829F5326DB2BC400BFA8CD /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( F5829F6526DB2BC500BFA8CD /* LaunchScreen.storyboard in Resources */, F5829F6226DB2BC500BFA8CD /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ F5829F5126DB2BC400BFA8CD /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F5EAA229270B3A4500EF2B70 /* TransportHomeViewController.swift in Sources */, F57F6AF226DB4A2D00C0117D /* ProfileHomeBuilder.swift in Sources */, F5EAA22A270B3A4500EF2B70 /* TransportHomeRouter.swift in Sources */, F5EAA232270B3A8F00EF2B70 /* UIView+Utils.swift in Sources */, F57F6AEA26DB4A2700C0117D /* FinanceHomeBuilder.swift in Sources */, F5EAA22E270B3A4500EF2B70 /* SuperPayView.swift in Sources */, F5EAA220270B39DC00EF2B70 /* PushModalPresentationController.swift in Sources */, F57F6AF126DB4A2D00C0117D /* ProfileHomeViewController.swift in Sources */, F57F6AF326DB4A2D00C0117D /* ProfileHomeInteractor.swift in Sources */, F5829F7626DB34AA00BFA8CD /* AppRootBuilder.swift in Sources */, F5EAA230270B3A7100EF2B70 /* RIBs+Utils.swift in Sources */, F57EBE542738468700FE9319 /* UIImage+Utils.swift in Sources */, F5EAA22B270B3A4500EF2B70 /* TransportHomeBuilder.swift in Sources */, F5EAA21D270B395900EF2B70 /* HomeWidgetModel.swift in Sources */, F5EAA21E270B395900EF2B70 /* AppHomeRouter.swift in Sources */, F5EAA21A270B395900EF2B70 /* AppHomeBuilder.swift in Sources */, F5EAA234270B3A9A00EF2B70 /* UIColor+Utils.swift in Sources */, F5EAA22C270B3A4500EF2B70 /* TransportHomeInteractor.swift in Sources */, F5829F7726DB34AA00BFA8CD /* AppRootInteractor.swift in Sources */, F5EAA22D270B3A4500EF2B70 /* RideTypeView.swift in Sources */, F5EAA219270B395900EF2B70 /* HomeWidgetView.swift in Sources */, F57F6AFE26DB4CD700C0117D /* RootTabBarController.swift in Sources */, F57F6AEB26DB4A2700C0117D /* FinanceHomeInteractor.swift in Sources */, F5EAA21B270B395900EF2B70 /* AppHomeInteractor.swift in Sources */, F57F6AE826DB4A2700C0117D /* FinanceHomeRouter.swift in Sources */, F57F6AE926DB4A2700C0117D /* FinanceHomeViewController.swift in Sources */, F5829F5926DB2BC400BFA8CD /* AppDelegate.swift in Sources */, F5EAA21C270B395900EF2B70 /* AppHomeViewController.swift in Sources */, F5EAA236270B3AA600EF2B70 /* UIColor+Super.swift in Sources */, F5829F7926DB397300BFA8CD /* AppComponent.swift in Sources */, F57F6AF026DB4A2D00C0117D /* ProfileHomeRouter.swift in Sources */, F57EBE562738468700FE9319 /* UITableView+Utils.swift in Sources */, F57EBE552738468700FE9319 /* Array+Utils.swift in Sources */, F5829F7426DB34AA00BFA8CD /* AppRootRouter.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ F5829F6326DB2BC500BFA8CD /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( F5829F6426DB2BC500BFA8CD /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ F5829F6726DB2BC500BFA8CD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.5; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; F5829F6826DB2BC500BFA8CD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.5; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; VALIDATE_PRODUCT = YES; }; name = Release; }; F5829F6A26DB2BC500BFA8CD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = MiniSuperApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.red.MiniSuperApp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; F5829F6B26DB2BC500BFA8CD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = MiniSuperApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.red.MiniSuperApp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ F5829F5026DB2BC400BFA8CD /* Build configuration list for PBXProject "MiniSuperApp" */ = { isa = XCConfigurationList; buildConfigurations = ( F5829F6726DB2BC500BFA8CD /* Debug */, F5829F6826DB2BC500BFA8CD /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F5829F6926DB2BC500BFA8CD /* Build configuration list for PBXNativeTarget "MiniSuperApp" */ = { isa = XCConfigurationList; buildConfigurations = ( F5829F6A26DB2BC500BFA8CD /* Debug */, F5829F6B26DB2BC500BFA8CD /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ F5829F6C26DB2DE900BFA8CD /* XCRemoteSwiftPackageReference "ModernRIBs" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/DevYeom/ModernRIBs"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.0.1; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ F5829F6D26DB2DE900BFA8CD /* ModernRIBs */ = { isa = XCSwiftPackageProductDependency; package = F5829F6C26DB2DE900BFA8CD /* XCRemoteSwiftPackageReference "ModernRIBs" */; productName = ModernRIBs; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = F5829F4D26DB2BC400BFA8CD /* Project object */; } ================================================ FILE: MiniSuperApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: MiniSuperApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: MiniSuperApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved ================================================ { "object": { "pins": [ { "package": "ModernRIBs", "repositoryURL": "https://github.com/DevYeom/ModernRIBs", "state": { "branch": null, "revision": "5e0a67365a1fb18ca06b919dbf53608843ddc284", "version": "1.0.1" } } ] }, "version": 1 } ================================================ FILE: MiniSuperApp.xcodeproj/xcshareddata/xcschemes/MiniSuperApp.xcscheme ================================================ ================================================ FILE: README.md ================================================ ### "모바일 개발자에게 확장성(scalability)이란 모바일 팀과 앱의 규모가 계속 커져도 사용자 경험과 개발자 경험 모두를 안정적으로 유지하는 것이라고 생각합니다. 개발자의 기술력은 개발 과정에서 발생하는 병목 현상을 얼마나 잘 처리하는지에서 보여지죠. 서버의 경우에는 많은 사용자가 몰릴 때 병목 현상이 발생하지만, 모바일의 경우에는 하나의 프로그램에 다수의 개발자들의 코드가 몰릴 때 병목이 발생한다고 볼 수 있습니다." 관련글: [모바일 개발자에게 scalability란 뭘까](https://soojin.ro/blog/scalability)
# 강의 내용 ### 1부. 코드 레벨 아키텍처: 재사용 가능한 코드를 만드는 스킬 객체를 작게 만들고, 작은 객체를 조합해서 복잡한 기능으로 합치는 것이 아키텍처의 시작입니다. Massive View Controller, Massive View Model, Massive Interactor는 아키텍처만의 문제가 아니라 개발자의 [composition](https://en.wikipedia.org/wiki/Object_composition) 활용 능력에 따라 달라질 수 있습니다. Composition이 강력한 아키텍처 프레임워크 RIBs를 기반으로 미니 슈퍼앱을 만들어봅니다. 관련1: [스위프트로 다시보는 객체지향 프로그래밍: 피해야할 코딩 습관](https://soojin.ro/blog/solid-principles-in-swift)
관련2: [개발자와 라면 조리법](https://soojin.ro/blog/programmer-and-ramyun)
관련3: [google/promises를 활용한 스위프트 비동기 프로그래밍과 에러 핸들링](https://soojin.ro/blog/using-google-promises-swift) ### 2부. 모듈 레벨 아키텍처: 유지 보수와 개발 속도를 고려하는 모듈화 '느슨하게 결합된 모듈 구조'는 '확장성 있는 아키텍처'와 같은 말이나 다름없습니다. 200명의 iOS 앱 개발자가 기여하는 슈퍼앱 그랩, 약 75명이 기여하는 [에어비엔비](https://medium.com/airbnb-engineering/designing-for-productivity-in-a-large-scale-ios-application-9376a430a0bf) 같은 회사의 개발자들이 생산성을 지킬 수 있는 방법입니다. 왜 모듈화를 하면 빌드 시간이 줄어들어서 생산성이 오르는지 원리를 알아보고, 실습을 통해 미니 슈퍼앱에 적용해봅니다. 관련1: [모바일 앱의 느슨한 결합](https://soojin.ro/blog/loose-coupling)
관련2: [Sourcery 개발자로부터 배우는 모바일 아키텍처와 개발자 경험](https://soojin.ro/blog/pragmatic-programmer) ### 3부. 자동화 테스팅 현업 개발자들이 테스트를 처음 시작하기 어려운 이유는 레거시 코드가 테스트 불가능한 구조로 짜여져 있기 때문입니다. 하지만 실습에서 짜는 코드는 99% 테스트 가능한 코드입니다. 테스트 가능한 코드의 특징을 한번이라도 익히고 직접 테스트를 작성해보면 레거시 코드에 조금씩 도입하기도 쉽습니다. 유닛테스트, 스냅샷테스트, UI테스트, 통합테스트를 작성해봅니다. 관련1: [테스트와 좋은 설계의 관계, 그리고 나쁜 설계의 영향](https://soojin.ro/blog/tests-and-design)
관련2: [테스트 코드 작성하면 좋은 점](https://soojin.ro/blog/writing-test-code)
관련3: [uber/RIBs 유닛 테스트 짜기](https://soojin.ro/blog/unit-testing-ribs)
관련4: [XCTest 소요시간 단축하기](https://soojin.ro/blog/application-library-test) ### 4부. 확장성 있는 인프라: 코드만으로 해결할 수 없는 문제들 확장성 있는 아키텍처를 만들고 유지하려면 코드 뿐 아니라 개발 프로세스도 뒷받침해줘야 합니다. 피쳐플래그와 품질 모니터링을 도입해서 얻을 수 있는 것들과 제가 경험해본 좋은 개발 문화 사례를 공유합니다. 관련1: [앱 안정성을 향한 끊임없는 여정](https://soojin.ro/blog/journey-to-app-stability)
관련2: [팀워크](https://soojin.ro/blog/teamwork)
관련3: [개인과 팀이 성장하는 모바일 개발 환경](https://soojin.ro/blog/mobile-platform) ================================================ FILE: Samples/DefaultsStore/DefaultsStore.swift ================================================ import Foundation public protocol DefaultsStore { var isInitialLaunch: Bool { get set } var lastNoticeDate: Double { get set } } public struct DefaultsStoreImp: DefaultsStore { public var isInitialLaunch: Bool { get { userDefaults.bool(forKey: kIsInitialLaunch) } set { userDefaults.set(newValue, forKey: kIsInitialLaunch) } } public var lastNoticeDate: Double { get { userDefaults.double(forKey: kLastNoticeDate) } set { userDefaults.set(newValue, forKey: kLastNoticeDate) } } private let userDefaults: UserDefaults private let kIsInitialLaunch = "kIsInitialLaunch" private let kLastNoticeDate = "kLastNoticeDate" public init(defaults: UserDefaults) { self.userDefaults = defaults } } ================================================ FILE: Samples/Network/HTTPMethod.swift ================================================ import Foundation public enum HTTPMethod: String, Encodable { case get = "GET" case post = "POST" case put = "PUT" } ================================================ FILE: Samples/Network/Network.swift ================================================ import Combine import Foundation public typealias QueryItems = [String: AnyHashable] public typealias HTTPHeader = [String: String] public protocol Request: Hashable { associatedtype Output: Decodable var endpoint: URL { get } var method: HTTPMethod { get } var query: QueryItems { get } var header: HTTPHeader { get } } public protocol Network { func send(_ request: T) -> AnyPublisher, Error> } public struct Response { public let output: T public let statusCode: Int public init(output: T, statusCode: Int) { self.output = output self.statusCode = statusCode } } ================================================ FILE: Samples/Network/NetworkError.swift ================================================ import Foundation public enum NetworkError: Error { case invalidURL(url: String?) } ================================================ FILE: Samples/NetworkImp/NetworkImp.swift ================================================ import Foundation import Network import Combine public final class NetworkImp: Network { private let session: URLSession public init( session: URLSession ) { self.session = session } public func send(_ request: T) -> AnyPublisher, Error> where T: Request { do { let urlRequest = try RequestFactory(request: request).urlRequestRepresentation() return session.dataTaskPublisher(for: urlRequest) .tryMap { data, response in let output = try JSONDecoder().decode(T.Output.self, from: data) return Response(output: output, statusCode: (response as? HTTPURLResponse)?.statusCode ?? 0) } .eraseToAnyPublisher() } catch { return Fail(error: error).eraseToAnyPublisher() } } } private final class RequestFactory { let request: T private var urlComponents: URLComponents? init(request: T) { self.request = request self.urlComponents = URLComponents(url: request.endpoint, resolvingAgainstBaseURL: true) } func urlRequestRepresentation() throws -> URLRequest { switch request.method { case .get: return try makeGetRequest() case .post: return try makePostRequest() case .put: return try makePutRequest() } } private func makeGetRequest() throws -> URLRequest { if request.query.isEmpty == false { urlComponents?.queryItems = request.query.map { URLQueryItem(name: $0.key, value: "\($0.value)") } } return try makeURLRequest() } private func makePostRequest() throws -> URLRequest { let body = try JSONSerialization.data(withJSONObject: request.query, options: []) return try makeURLRequest(httpBody: body) } private func makePutRequest() throws -> URLRequest { if request.query.isEmpty == false { urlComponents?.queryItems = request.query.map { URLQueryItem(name: $0.key, value: "\($0.value)") } } return try makeURLRequest() } private func makeURLRequest(httpBody: Data? = nil) throws -> URLRequest { guard let url = urlComponents?.url else { throw NetworkError.invalidURL(url: request.endpoint.absoluteString) } var urlRequest = URLRequest(url: url) request.header.forEach { urlRequest.setValue($0.value, forHTTPHeaderField: $0.key) } urlRequest.httpMethod = request.method.rawValue urlRequest.httpBody = httpBody return urlRequest } } ================================================ FILE: Samples/RIBsTestSupport/RoutingMock.swift ================================================ import Foundation import ModernRIBs import Combine public final class RoutingMock: Routing { public var loadHandler: (() -> ())? public var loadCallCount: Int = 0 public var attachChildHandler: ((_ child: Routing) -> ())? public var attachChildCallCount: Int = 0 public var detachChildHandler: ((_ child: Routing) -> ())? public var detachChildCallCount: Int = 0 public var interactable: Interactable public var children: [Routing] = [Routing]() { didSet { childrenSetCallCount += 1 } } public var childrenSetCallCount = 0 public init( interactable: Interactable ) { self.interactable = interactable } public func load() { loadCallCount += 1 if let loadHandler = loadHandler { return loadHandler() } } public func attachChild(_ child: Routing) { attachChildCallCount += 1 if let attachChildHandler = attachChildHandler { return attachChildHandler(child) } } public func detachChild(_ child: Routing) { detachChildCallCount += 1 if let detachChildHandler = detachChildHandler { return detachChildHandler(child) } } public var lifecycleSubject = PassthroughSubject() { didSet { lifecycleSubjectSetCallCount += 1 } } public var lifecycleSubjectSetCallCount = 0 public var lifecycle: AnyPublisher { return lifecycleSubject.eraseToAnyPublisher() } } ================================================ FILE: Samples/RIBsTestSupport/ViewControllableMock.swift ================================================ import Foundation import ModernRIBs import UIKit public final class ViewControllableMock: UIViewController, ViewControllable { public init() { super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } public var presentCallCount = 0 public override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) { presentCallCount += 1 } public var dismissCallCount = 0 public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { dismissCallCount += 1 } } ================================================ FILE: Samples/RIBsTestSupport/ViewableRoutingMock.swift ================================================ import Foundation import ModernRIBs import Combine open class ViewableRoutingMock: ViewableRouting { // Variables public var viewControllable: ViewControllable public var interactable: Interactable { didSet { interactableSetCallCount += 1 } } public var interactableSetCallCount = 0 public var children: [Routing] = [Routing]() { didSet { childrenSetCallCount += 1 } } public var childrenSetCallCount = 0 public var lifecycleSubject = PassthroughSubject() { didSet { lifecycleSubjectSetCallCount += 1 } } public var lifecycleSubjectSetCallCount = 0 public var lifecycle: AnyPublisher { return lifecycleSubject.eraseToAnyPublisher() } // Function Handlers public var loadHandler: (() -> ())? public var loadCallCount: Int = 0 public var attachChildHandler: ((_ child: Routing) -> ())? public var attachChildCallCount: Int = 0 public var detachChildHandler: ((_ child: Routing) -> ())? public var detachChildCallCount: Int = 0 public init( interactable: Interactable, viewControllable: ViewControllable ) { self.interactable = interactable self.viewControllable = viewControllable } public func load() { loadCallCount += 1 if let loadHandler = loadHandler { return loadHandler() } } public func attachChild(_ child: Routing) { attachChildCallCount += 1 if let attachChildHandler = attachChildHandler { return attachChildHandler(child) } } public func detachChild(_ child: Routing) { detachChildCallCount += 1 if let detachChildHandler = detachChildHandler { return detachChildHandler(child) } } } ================================================ FILE: Samples/TestUtil.swift ================================================ import Foundation enum TestUtilError: Error { case fileNotFound } final class TestUtil { static func path(for fileName: String, in bundleClass: AnyClass) throws -> String { if let path = Bundle(for: bundleClass).path(forResource: fileName, ofType: nil) { return path } else { throw TestUtilError.fileNotFound } } } ================================================ FILE: Samples/Topup/CardOnFileCell.swift ================================================ import UIKit final class CardOnFileCell: UITableViewCell { func setImage(_ image: UIImage?) { thumbnailView.image = image } func setTitle(_ title: String) { titleLabel.text = title } private let thumbnailView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true imageView.roundCorners(4) return imageView }() private let titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.numberOfLines = 1 return label }() required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupViews() } private func setupViews() { contentView.addSubview(thumbnailView) contentView.addSubview(titleLabel) NSLayoutConstraint.activate([ thumbnailView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), thumbnailView.widthAnchor.constraint(equalToConstant: 46), thumbnailView.heightAnchor.constraint(equalToConstant: 34), thumbnailView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), titleLabel.centerYAnchor.constraint(equalTo: thumbnailView.centerYAnchor), titleLabel.leadingAnchor.constraint(equalTo: thumbnailView.trailingAnchor, constant: 14) ]) } } ================================================ FILE: Samples/Topup/CardOnFileViewController.swift ================================================ import ModernRIBs import UIKit protocol CardOnFilePresentableListener: AnyObject { func didTapClose() func didSelectItem(at: Int) } final class CardOnFileViewController: UIViewController, CardOnFilePresentable, CardOnFileViewControllable, UITableViewDataSource, UITableViewDelegate { weak var listener: CardOnFilePresentableListener? func update(with viewModels: [PaymentMethodViewModel]) { self.viewModels = viewModels tableView.reloadData() } init() { super.init(nibName: nil, bundle: nil) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private var viewModels: [PaymentMethodViewModel] = [] private lazy var tableView: UITableView = { let tableView = UITableView() tableView.translatesAutoresizingMaskIntoConstraints = false tableView.dataSource = self tableView.delegate = self tableView.register(cellType: CardOnFileCell.self) tableView.tableFooterView = UIView() tableView.rowHeight = 60 tableView.separatorInset = .zero return tableView }() private func setupViews() { title = "카드 선택" view.backgroundColor = .white view.addSubview(tableView) setupNavigationItem(with: .back, target: self, action: #selector(didTapClose)) NSLayoutConstraint.activate([ tableView.topAnchor.constraint(equalTo: view.topAnchor), tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) } // MARK: - UITableView func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return viewModels.count + 1 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: CardOnFileCell = tableView.dequeueReusableCell(for: indexPath) if let viewModel = viewModels[safe: indexPath.row] { cell.setImage(UIImage(color: viewModel.color)) cell.setTitle("\(viewModel.name) \(viewModel.digits)") } else { cell.setImage(UIImage(systemName: "plus.rectangle")) cell.setTitle("카드 추가") } return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) listener?.didSelectItem(at: indexPath.row) } @objc private func didTapClose() { listener?.didTapClose() } } ================================================ FILE: Samples/Topup/EnterAmountViewController.swift ================================================ import ModernRIBs import UIKit protocol EnterAmountPresentableListener: AnyObject { func didTapClose() func didTapPaymentMethod() func didTapTopup(with amount: Double) } final class EnterAmountViewController: UIViewController, EnterAmountPresentable, EnterAmountViewControllable { weak var listener: EnterAmountPresentableListener? func updateSelectedPaymentMethod(with viewModel: SelectedPaymentMethodViewModel) { selectedPaymentMethodView.update(with: viewModel) } func startLoading() { activityIndicator.startAnimating() ctaButton.isEnabled = false } func stopLoading() { activityIndicator.stopAnimating() ctaButton.isEnabled = true } private lazy var selectedPaymentMethodView: SelectedPaymentMethodView = { let view = SelectedPaymentMethodView() view.translatesAutoresizingMaskIntoConstraints = false view.addShadowWithRoundedCorners() let tap = UITapGestureRecognizer(target: self, action: #selector(didTapPaymentMethod)) view.addGestureRecognizer(tap) return view }() private let enterAmountWidget: EnterAmountWidget = { let widget = EnterAmountWidget() widget.translatesAutoresizingMaskIntoConstraints = false widget.addShadowWithRoundedCorners() return widget }() private lazy var ctaButton: UIButton = { let cta = UIButton(type: .system) cta.translatesAutoresizingMaskIntoConstraints = false cta.roundCorners() cta.setTitle("충전", for: .normal) cta.titleLabel?.font = UIFont.systemFont(ofSize: 20, weight: .semibold) cta.setBackgroundImage(UIImage(color: .primaryRed), for: .normal) cta.tintColor = .white cta.addTarget(self, action: #selector(didTapCTAButton), for: .touchUpInside) return cta }() private let activityIndicator: UIActivityIndicatorView = { let activity = UIActivityIndicatorView(style: .medium) activity.translatesAutoresizingMaskIntoConstraints = false activity.hidesWhenStopped = true activity.stopAnimating() return activity }() init() { super.init(nibName: nil, bundle: nil) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private func setupViews() { title = "충전하기" view.backgroundColor = .backgroundColor setupNavigationItem(with: .close, target: self, action: #selector(didTapClose)) view.addSubview(selectedPaymentMethodView) view.addSubview(enterAmountWidget) view.addSubview(ctaButton) view.addSubview(activityIndicator) NSLayoutConstraint.activate([ selectedPaymentMethodView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), selectedPaymentMethodView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20), selectedPaymentMethodView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), selectedPaymentMethodView.heightAnchor.constraint(equalToConstant: 70), enterAmountWidget.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), enterAmountWidget.topAnchor.constraint(equalTo: selectedPaymentMethodView.bottomAnchor, constant: 20), enterAmountWidget.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), ctaButton.heightAnchor.constraint(equalToConstant: 60), ctaButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), ctaButton.topAnchor.constraint(equalTo: enterAmountWidget.bottomAnchor, constant: 40), ctaButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), activityIndicator.centerXAnchor.constraint(equalTo: ctaButton.centerXAnchor), activityIndicator.centerYAnchor.constraint(equalTo: ctaButton.centerYAnchor), ]) } @objc private func didTapClose() { listener?.didTapClose() } @objc private func didTapCTAButton() { if let amount = enterAmountWidget.text.flatMap(Double.init) { listener?.didTapTopup(with: amount) } } @objc private func didTapPaymentMethod() { listener?.didTapPaymentMethod() } } ================================================ FILE: Samples/Topup/Views/EnterAmountWidget.swift ================================================ import UIKit final class EnterAmountWidget: UIView { var text: String? { amountTextField.text } init() { super.init(frame: .zero) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private let titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.systemFont(ofSize: 18, weight: .semibold) label.text = "금액" label.numberOfLines = 1 return label }() private lazy var amountStackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false let button = UIButton() stackView.axis = .horizontal stackView.alignment = .fill stackView.distribution = .fill stackView.spacing = 5 stackView.addArrangedSubview(self.amountTextField) stackView.addArrangedSubview(self.currencyLabel) return stackView }() private let amountTextField: UITextField = { let textField = UITextField() textField.translatesAutoresizingMaskIntoConstraints = false textField.borderStyle = .none textField.font = UIFont.systemFont(ofSize: 18, weight: .semibold) textField.textAlignment = .right textField.keyboardType = .numberPad return textField }() private let currencyLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.systemFont(ofSize: 18, weight: .semibold) label.text = "원" return label }() private func setupViews() { self.backgroundColor = .white self.addSubview(titleLabel) self.addSubview(amountStackView) NSLayoutConstraint.activate([ titleLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 16), titleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16), titleLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16), amountStackView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16), amountStackView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16), amountStackView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16), amountStackView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -16) ]) } } ================================================ FILE: Samples/Topup/Views/SelectedPaymentMethodView.swift ================================================ import UIKit struct SelectedPaymentMethodViewModel { let image: UIImage? let name: String init(_ paymentMethod: PaymentMethod) { image = UIColor(hex: paymentMethod.color).flatMap { UIImage(color: $0) } name = "\(paymentMethod.name) \(paymentMethod.digits)" } } final class SelectedPaymentMethodView: UIView { func update(with viewModel: SelectedPaymentMethodViewModel) { self.thumbnailView.image = viewModel.image self.nameLabel.text = viewModel.name } init() { super.init(frame: .zero) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private let thumbnailView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.contentMode = .scaleToFill imageView.roundCorners(4) imageView.backgroundColor = .systemGray3 return imageView }() private let nameLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.systemFont(ofSize: 16, weight: .semibold) label.numberOfLines = 1 return label }() private let rightChevronIcon: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.image = UIImage( systemName: "chevron.right", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .medium) ) imageView.tintColor = .systemGray3 return imageView }() private func setupViews() { self.backgroundColor = .white self.addSubview(thumbnailView) self.addSubview(nameLabel) self.addSubview(rightChevronIcon) NSLayoutConstraint.activate([ thumbnailView.centerYAnchor.constraint(equalTo: self.centerYAnchor), thumbnailView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 20), thumbnailView.widthAnchor.constraint(equalToConstant: 46), thumbnailView.heightAnchor.constraint(equalToConstant: 34), nameLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), nameLabel.leadingAnchor.constraint(equalTo: thumbnailView.trailingAnchor, constant: 22), rightChevronIcon.centerYAnchor.constraint(equalTo: self.centerYAnchor), rightChevronIcon.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -24) ]) } } ================================================ FILE: Samples/TopupDependencyMock.swift ================================================ @testable import TopupImp import Foundation import CombineUtil import FinanceEntity import FinanceRepositoryTestSupport import FinanceRepository import Combine import ModernRIBs import RIBsUtil import Topup import SuperUI final class TopupDependencyMock: TopupInteractorDependency { var cardOnFileRepository: CardOnFileRepository = CardOnFileRepositoryMock() var paymentMethodStream: CurrentValuePublisher = .init( PaymentMethod(id: "", name: "", digits: "", color: "", isPrimary: false) ) } final class TopupRoutingMock: TopupRouting { var attachAddPaymentMethodCallCount = 0 var attachAddPaymentMethodCloseButtonType: DismissButtonType? func attachAddPaymentMethod(closeButtonType: DismissButtonType) { attachAddPaymentMethodCallCount += 1 attachAddPaymentMethodCloseButtonType = closeButtonType } var detachAddPaymentMethodCallCount = 0 func detachAddPaymentMethod() { detachAddPaymentMethodCallCount += 1 } var attachEnterAmountCallCount = 0 func attachEnterAmount() { attachEnterAmountCallCount += 1 } var detachEnterAmountCallCount = 0 func detachEnterAmount() { detachEnterAmountCallCount += 1 } var attachCardOnFileCallCount = 0 var attachCardOnFileCallCountPaymentMethods: [PaymentMethod]? func attachCardOnFile(paymentMethods: [PaymentMethod]) { attachCardOnFileCallCount += 1 } var detachCardOnFileCallCount = 0 func detachCardOnFile() { detachCardOnFileCallCount += 1 } var popToRootCallCount = 0 func popToRoot() { popToRootCallCount += 1 } // Variables var interactable: Interactable { didSet { interactableSetCallCount += 1 } } var interactableSetCallCount = 0 var children: [Routing] = [Routing]() { didSet { childrenSetCallCount += 1 } } var childrenSetCallCount = 0 var lifecycleSubject = PassthroughSubject() { didSet { lifecycleSubjectSetCallCount += 1 } } var lifecycleSubjectSetCallCount = 0 var lifecycle: AnyPublisher { return lifecycleSubject.eraseToAnyPublisher() } // Function Handlers var loadHandler: (() -> ())? var loadCallCount: Int = 0 var attachChildHandler: ((_ child: Routing) -> ())? var attachChildCallCount: Int = 0 var detachChildHandler: ((_ child: Routing) -> ())? var detachChildCallCount: Int = 0 init( interactable: Interactable ) { self.interactable = interactable } var cleanupViewsCallCount = 0 func cleanupViews() { cleanupViewsCallCount += 1 } func load() { loadCallCount += 1 if let loadHandler = loadHandler { return loadHandler() } } func attachChild(_ child: Routing) { attachChildCallCount += 1 if let attachChildHandler = attachChildHandler { return attachChildHandler(child) } } func detachChild(_ child: Routing) { detachChildCallCount += 1 if let detachChildHandler = detachChildHandler { return detachChildHandler(child) } } } final class TopupInteractableMock: TopupInteractable { var router: TopupRouting? var listener: TopupListener? var presentationDelegateProxy = AdaptivePresentationControllerDelegateProxy() var addPaymentMethodDidTapCloseCallCount = 0 func addPaymentMethodDidTapClose() { addPaymentMethodDidTapCloseCallCount += 1 } var addPaymentMethodDidAddCardCallCount = 0 var addPaymentMethodDidAddCardPaymentMethod: PaymentMethod? func addPaymentMethodDidAddCard(paymentMethod: PaymentMethod) { addPaymentMethodDidAddCardCallCount += 1 addPaymentMethodDidAddCardPaymentMethod = paymentMethod } var enterAmountDidTapCloseCallCount = 0 func enterAmountDidTapClose() { enterAmountDidTapCloseCallCount += 1 } var enterAmountDidTapPaymentMethodCallCount = 0 func enterAmountDidTapPaymentMethod() { enterAmountDidTapPaymentMethodCallCount += 1 } var enterAmountDidFinishTopupCallCount = 0 func enterAmountDidFinishTopup() { enterAmountDidFinishTopupCallCount += 1 } var cardOnFileDidTapCloseCallCount = 0 func cardOnFileDidTapClose() { cardOnFileDidTapCloseCallCount += 1 } var cardOnFileDidTapAddCardCallCount = 0 func cardOnFileDidTapAddCard() { cardOnFileDidTapAddCardCallCount += 1 } var cardOnFileDidSelectCardCallCount = 0 var cardOnFileDidSelectCardIndex: Int? func cardOnFileDidSelectCard(at index: Int) { cardOnFileDidSelectCardCallCount += 1 cardOnFileDidSelectCardIndex = index } func activate() { } func deactivate() { } var isActive: Bool { isActiveSubject.value } var isActiveStream: AnyPublisher { isActiveSubject.eraseToAnyPublisher() } private let isActiveSubject = CurrentValueSubject(false) } ================================================ FILE: completed/MiniSuperApp/.gitignore ================================================ xcuserdata/ ================================================ FILE: completed/MiniSuperApp/AddPaymentMethodIntegrationTests/AddPaymentMethodIntegrationTests.swift ================================================ import XCTest import Hammer import FinanceRepository import FinanceRepositoryTestSupport import AddPaymentMethodTestSupport import ModernRIBs import RIBsUtil import FinanceEntity @testable import AddPaymentMethodImp class AddPaymentMethodIntegrationTests: XCTestCase { private var eventGenerator: EventGenerator! private var dependency: AddPaymentMethodDependencyMock! private var listener: AddPaymentMethodListenerMock! private var viewController: UIViewController! private var router: Routing! private var repository: CardOnFileRepositoryMock { dependency.cardOnFileRepository as! CardOnFileRepositoryMock } override func setUpWithError() throws { try super.setUpWithError() self.dependency = AddPaymentMethodDependencyMock() self.listener = AddPaymentMethodListenerMock() let builder = AddPaymentMethodBuilder(dependency: self.dependency) let router = builder.build(withListener: self.listener, closeButtonType: .close) let navigation = NavigationControllerable(root: router.viewControllable) self.viewController = navigation.uiviewController eventGenerator = try EventGenerator(viewController: navigation.navigationController) router.load() router.interactable.activate() self.router = router } func testAddPaymentMethod() throws { // given repository.addedPaymentMethod = PaymentMethod( id: "1234", name: "", digits: "", color: "", isPrimary: false ) let cardNumberTF = try eventGenerator.viewWithIdentifier("addpaymentmethod_cardnumber_textfield") try eventGenerator.fingerTap(at: cardNumberTF) try eventGenerator.keyType("1234123412341234") let cvc = try eventGenerator.viewWithIdentifier("addpaymentmethod_security_textfield") try eventGenerator.fingerTap(at: cvc) try eventGenerator.keyType("123") let expiry = try eventGenerator.viewWithIdentifier("addpaymentmethod_expiry_textfield") try eventGenerator.fingerTap(at: expiry) try eventGenerator.keyType("1212") // when let confirm = try eventGenerator.viewWithIdentifier("addpaymentmethod_addcard_button") try eventGenerator.fingerTap(at: confirm) // then XCTAssertEqual(repository.addCardCallCount, 1) try eventGenerator.wait(0.2) XCTAssertEqual(listener.addPaymentMethodDidAddCardCallCount, 1) XCTAssertEqual(listener.addPaymentMethodDidAddCardPaymentMethod?.id, "1234") } } final class AddPaymentMethodDependencyMock: AddPaymentMethodDependency { var cardOnFileRepository: CardOnFileRepository = CardOnFileRepositoryMock() } ================================================ FILE: completed/MiniSuperApp/CX/.gitignore ================================================ .DS_Store /.build /Packages /*.xcodeproj xcuserdata/ DerivedData/ .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata ================================================ FILE: completed/MiniSuperApp/CX/Package.swift ================================================ // swift-tools-version:5.5 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "CX", platforms: [.iOS(.v14)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( name: "AppHome", targets: ["AppHome"] ), ], dependencies: [ .package(name: "ModernRIBs", url: "https://github.com/DevYeom/ModernRIBs", .exact("1.0.1")), .package(path: "../Finance"), .package(path: "../Transport"), .package(path: "../Platform") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "AppHome", dependencies: [ "ModernRIBs", .product(name: "FinanceRepository", package: "Finance"), .product(name: "TransportHome", package: "Transport"), .product(name: "SuperUI", package: "Platform"), ] ), ] ) ================================================ FILE: completed/MiniSuperApp/CX/README.md ================================================ # CX A description of this package. ================================================ FILE: completed/MiniSuperApp/CX/Sources/AppHome/AppHomeBuilder.swift ================================================ import ModernRIBs import FinanceRepository import TransportHome public protocol AppHomeDependency: Dependency { var cardOnFileRepository: CardOnFileRepository { get } var superPayRepository: SuperPayRepository { get } var transportHomeBuildable: TransportHomeBuildable { get } } final class AppHomeComponent: Component { var cardOnFileRepository: CardOnFileRepository { dependency.cardOnFileRepository } var superPayRepository: SuperPayRepository { dependency.superPayRepository } var transportHomeBuildable: TransportHomeBuildable { dependency.transportHomeBuildable } } // MARK: - Builder public protocol AppHomeBuildable: Buildable { func build(withListener listener: AppHomeListener) -> ViewableRouting } public final class AppHomeBuilder: Builder, AppHomeBuildable { public override init(dependency: AppHomeDependency) { super.init(dependency: dependency) } public func build(withListener listener: AppHomeListener) -> ViewableRouting { let component = AppHomeComponent(dependency: dependency) let viewController = AppHomeViewController() let interactor = AppHomeInteractor(presenter: viewController) interactor.listener = listener return AppHomeRouter( interactor: interactor, viewController: viewController, transportHomeBuildable: component.transportHomeBuildable ) } } ================================================ FILE: completed/MiniSuperApp/CX/Sources/AppHome/AppHomeInteractor.swift ================================================ import ModernRIBs protocol AppHomeRouting: ViewableRouting { func attachTransportHome() func detachTransportHome() } protocol AppHomePresentable: Presentable { var listener: AppHomePresentableListener? { get set } func updateWidget(_ viewModels: [HomeWidgetViewModel]) } public protocol AppHomeListener: AnyObject { } final class AppHomeInteractor: PresentableInteractor, AppHomeInteractable, AppHomePresentableListener { weak var router: AppHomeRouting? weak var listener: AppHomeListener? override init(presenter: AppHomePresentable) { super.init(presenter: presenter) presenter.listener = self } override func didBecomeActive() { super.didBecomeActive() let viewModels = [ HomeWidgetModel( imageName: "car", title: "슈퍼택시", tapHandler: { [weak self] in self?.router?.attachTransportHome() } ), HomeWidgetModel( imageName: "cart", title: "슈퍼마트", tapHandler: { } ) ] presenter.updateWidget(viewModels.map(HomeWidgetViewModel.init)) } func transportHomeDidTapClose() { router?.detachTransportHome() } } ================================================ FILE: completed/MiniSuperApp/CX/Sources/AppHome/AppHomeRouter.swift ================================================ import ModernRIBs import SuperUI import TransportHome protocol AppHomeInteractable: Interactable, TransportHomeListener { var router: AppHomeRouting? { get set } var listener: AppHomeListener? { get set } } protocol AppHomeViewControllable: ViewControllable { } final class AppHomeRouter: ViewableRouter, AppHomeRouting { private let transportHomeBuildable: TransportHomeBuildable private var transportHomeRouting: Routing? private let transitioningDelegate: PushModalPresentationController init( interactor: AppHomeInteractable, viewController: AppHomeViewControllable, transportHomeBuildable: TransportHomeBuildable ) { self.transitioningDelegate = PushModalPresentationController() self.transportHomeBuildable = transportHomeBuildable super.init(interactor: interactor, viewController: viewController) interactor.router = self } func attachTransportHome() { if transportHomeRouting != nil { return } let router = transportHomeBuildable.build(withListener: interactor) presentWithPushTransition(router.viewControllable, animated: true) attachChild(router) self.transportHomeRouting = router } func detachTransportHome() { guard let router = transportHomeRouting else { return } viewController.dismiss(completion: nil) self.transportHomeRouting = nil detachChild(router) } private func presentWithPushTransition(_ viewControllable: ViewControllable, animated: Bool) { viewControllable.uiviewController.modalPresentationStyle = .custom viewControllable.uiviewController.transitioningDelegate = transitioningDelegate viewController.present(viewControllable, animated: true, completion: nil) } } ================================================ FILE: completed/MiniSuperApp/CX/Sources/AppHome/AppHomeViewController.swift ================================================ import ModernRIBs import UIKit protocol AppHomePresentableListener: AnyObject { } final class AppHomeViewController: UIViewController, AppHomePresentable, AppHomeViewControllable { weak var listener: AppHomePresentableListener? private let widgetStackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .horizontal stackView.distribution = .fillEqually stackView.alignment = .top stackView.spacing = 20 return stackView }() init() { super.init(nibName: nil, bundle: nil) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } func updateWidget(_ viewModels: [HomeWidgetViewModel]) { let views = viewModels.map { HomeWidgetView(viewModel: $0) } views.forEach { $0.addShadowWithRoundedCorners(12) widgetStackView.addArrangedSubview($0) } } private func setupViews() { title = "홈" tabBarItem = UITabBarItem(title: "홈", image: UIImage(systemName: "house"), selectedImage: UIImage(systemName: "house.fill")) view.backgroundColor = .backgroundColor view.addSubview(widgetStackView) NSLayoutConstraint.activate([ widgetStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20), widgetStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), widgetStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20) ]) } } ================================================ FILE: completed/MiniSuperApp/CX/Sources/AppHome/HomeWidgetModel.swift ================================================ import Foundation struct HomeWidgetModel { let imageName: String let title: String let tapHandler: () -> Void } ================================================ FILE: completed/MiniSuperApp/CX/Sources/AppHome/Views/HomeWidgetView.swift ================================================ import UIKit struct HomeWidgetViewModel { let image: UIImage? let title: String let tapHandler: () -> Void init(_ model: HomeWidgetModel) { image = UIImage(systemName: model.imageName) title = model.title tapHandler = model.tapHandler } } final class HomeWidgetView: UIView { init(viewModel: HomeWidgetViewModel) { super.init(frame: .zero) setupViews() update(with: viewModel) } required init?(coder: NSCoder) { fatalError() } private var tapHandler: (() -> Void)? private func update(with viewModel: HomeWidgetViewModel) { imageView.image = viewModel.image titleLabel.text = viewModel.title tapHandler = viewModel.tapHandler } private let imageView: UIImageView = { let imageView = UIImageView() imageView.tintColor = .black imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() private let titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textAlignment = .center label.font = UIFont.systemFont(ofSize: 16, weight: .semibold) return label }() private func setupViews() { addSubview(imageView) addSubview(titleLabel) backgroundColor = .white let tap = UITapGestureRecognizer(target: self, action: #selector(didTap)) addGestureRecognizer(tap) NSLayoutConstraint.activate([ imageView.topAnchor.constraint(equalTo: self.topAnchor, constant: 15), imageView.centerXAnchor.constraint(equalTo: self.centerXAnchor), imageView.widthAnchor.constraint(equalToConstant: 50), imageView.heightAnchor.constraint(equalToConstant: 50), titleLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 5), titleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor), titleLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor), titleLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -14) ]) } @objc private func didTap() { tapHandler?() } } ================================================ FILE: completed/MiniSuperApp/Finance/.gitignore ================================================ .DS_Store /.build /Packages /*.xcodeproj xcuserdata/ DerivedData/ .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata ================================================ FILE: completed/MiniSuperApp/Finance/.swiftpm/xcode/xcshareddata/xcschemes/TopupImp.xcscheme ================================================ ================================================ FILE: completed/MiniSuperApp/Finance/.swiftpm/xcode/xcshareddata/xcschemes/TopupImpTests.xcscheme ================================================ ================================================ FILE: completed/MiniSuperApp/Finance/Package.swift ================================================ // swift-tools-version:5.5 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Finance", platforms: [.iOS(.v14)], products: [ .library( name: "AddPaymentMethod", targets: ["AddPaymentMethod"] ), .library( name: "AddPaymentMethodImp", targets: ["AddPaymentMethodImp"] ), .library( name: "AddPaymentMethodTestSupport", targets: ["AddPaymentMethodTestSupport"] ), .library( name: "Topup", targets: ["Topup"] ), .library( name: "TopupImp", targets: ["TopupImp"] ), .library( name: "TopupTestSupport", targets: ["TopupTestSupport"] ), .library( name: "FinanceHome", targets: ["FinanceHome"] ), .library( name: "FinanceEntity", targets: ["FinanceEntity"] ), .library( name: "FinanceRepository", targets: ["FinanceRepository"] ), .library( name: "FinanceRepositoryTestSupport", targets: ["FinanceRepositoryTestSupport"] ), ], dependencies: [ .package(name: "ModernRIBs", url: "https://github.com/DevYeom/ModernRIBs", .exact("1.0.1")), .package(path: "../Platform") ], targets: [ .target( name: "AddPaymentMethod", dependencies: [ "ModernRIBs", "FinanceEntity", .product(name: "RIBsUtil", package: "Platform"), ] ), .target( name: "AddPaymentMethodImp", dependencies: [ "ModernRIBs", "AddPaymentMethod", "FinanceEntity", "FinanceRepository", .product(name: "RIBsUtil", package: "Platform"), .product(name: "SuperUI", package: "Platform") ] ), .target( name: "AddPaymentMethodTestSupport", dependencies: [ "ModernRIBs", "FinanceEntity", "AddPaymentMethod", .product(name: "RIBsUtil", package: "Platform"), .product(name: "RIBsTestSupport", package: "Platform"), ] ), .target( name: "Topup", dependencies: [ "ModernRIBs" ] ), .target( name: "TopupImp", dependencies: [ "ModernRIBs", "Topup", "FinanceEntity", "FinanceRepository", "AddPaymentMethod", .product(name: "RIBsUtil", package: "Platform"), .product(name: "SuperUI", package: "Platform") ] ), .target( name: "TopupTestSupport", dependencies: [ "Topup" ] ), .target( name: "FinanceHome", dependencies: [ "ModernRIBs", "FinanceEntity", "FinanceRepository", "AddPaymentMethod", "Topup", .product(name: "RIBsUtil", package: "Platform"), .product(name: "SuperUI", package: "Platform") ] ), .target( name: "FinanceEntity", dependencies: [ ] ), .target( name: "FinanceRepository", dependencies: [ "FinanceEntity", .product(name: "CombineUtil", package: "Platform"), .product(name: "Network", package: "Platform") ] ), .target( name: "FinanceRepositoryTestSupport", dependencies: [ "FinanceEntity", "FinanceRepository", .product(name: "CombineUtil", package: "Platform") ] ), .testTarget( name: "TopupImpTests", dependencies: [ "TopupImp", "FinanceRepositoryTestSupport", "TopupTestSupport", "AddPaymentMethodTestSupport", .product(name: "RIBsTestSupport", package: "Platform"), .product(name: "PlatformTestSupport", package: "Platform") ], exclude: [ "EnterAmount/__Snapshots__", "CardOnFile/__Snapshots__" ] ) ] ) ================================================ FILE: completed/MiniSuperApp/Finance/README.md ================================================ # Finance A description of this package. ================================================ FILE: completed/MiniSuperApp/Finance/Sources/AddPaymentMethod/AddPaymentMethodInterface.swift ================================================ import Foundation import ModernRIBs import FinanceEntity import RIBsUtil public protocol AddPaymentMethodBuildable: Buildable { func build(withListener listener: AddPaymentMethodListener, closeButtonType: DismissButtonType) -> ViewableRouting } public protocol AddPaymentMethodListener: AnyObject { func addPaymentMethodDidTapClose() func addPaymentMethodDidAddCard(paymentMethod: PaymentMethod) } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/AddPaymentMethodImp/AddPaymentMethodBuilder.swift ================================================ import ModernRIBs import FinanceRepository import RIBsUtil import AddPaymentMethod public protocol AddPaymentMethodDependency: Dependency { var cardOnFileRepository: CardOnFileRepository { get } } final class AddPaymentMethodComponent: Component, AddPaymentMethodInteractorDependency { var cardOnFileRepository: CardOnFileRepository { dependency.cardOnFileRepository } } // MARK: - Builder public final class AddPaymentMethodBuilder: Builder, AddPaymentMethodBuildable { public override init(dependency: AddPaymentMethodDependency) { super.init(dependency: dependency) } public func build(withListener listener: AddPaymentMethodListener, closeButtonType: DismissButtonType) -> ViewableRouting { let component = AddPaymentMethodComponent(dependency: dependency) let viewController = AddPaymentMethodViewController(closeButtonType: closeButtonType) let interactor = AddPaymentMethodInteractor( presenter: viewController, dependency: component ) interactor.listener = listener return AddPaymentMethodRouter(interactor: interactor, viewController: viewController) } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/AddPaymentMethodImp/AddPaymentMethodInteractor.swift ================================================ import ModernRIBs import Combine import FinanceEntity import FinanceRepository import AddPaymentMethod import Foundation protocol AddPaymentMethodRouting: ViewableRouting { } protocol AddPaymentMethodPresentable: Presentable { var listener: AddPaymentMethodPresentableListener? { get set } } protocol AddPaymentMethodInteractorDependency { var cardOnFileRepository: CardOnFileRepository { get } } final class AddPaymentMethodInteractor: PresentableInteractor, AddPaymentMethodInteractable, AddPaymentMethodPresentableListener { weak var router: AddPaymentMethodRouting? weak var listener: AddPaymentMethodListener? private let dependency: AddPaymentMethodInteractorDependency private var cancellables: Set init( presenter: AddPaymentMethodPresentable, dependency: AddPaymentMethodInteractorDependency ) { self.dependency = dependency self.cancellables = .init() super.init(presenter: presenter) presenter.listener = self } override func didBecomeActive() { super.didBecomeActive() } override func willResignActive() { super.willResignActive() } func didTapClose() { listener?.addPaymentMethodDidTapClose() } func didTapConfirm(with number: String, cvc: String, expiry: String) { let info = AddPaymentMethodInfo(number: number, cvc: cvc, expiration: expiry) dependency.cardOnFileRepository.addCard(info: info) .receive(on: DispatchQueue.main) .sink( receiveCompletion: { _ in }, receiveValue: { [weak self] method in self?.listener?.addPaymentMethodDidAddCard(paymentMethod: method) } ).store(in: &cancellables) } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/AddPaymentMethodImp/AddPaymentMethodRouter.swift ================================================ import ModernRIBs import AddPaymentMethod protocol AddPaymentMethodInteractable: Interactable { var router: AddPaymentMethodRouting? { get set } var listener: AddPaymentMethodListener? { get set } } protocol AddPaymentMethodViewControllable: ViewControllable { } final class AddPaymentMethodRouter: ViewableRouter, AddPaymentMethodRouting { override init(interactor: AddPaymentMethodInteractable, viewController: AddPaymentMethodViewControllable) { super.init(interactor: interactor, viewController: viewController) interactor.router = self } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/AddPaymentMethodImp/AddPaymentMethodViewController.swift ================================================ import ModernRIBs import UIKit import RIBsUtil import SuperUI protocol AddPaymentMethodPresentableListener: AnyObject { func didTapClose() func didTapConfirm(with number: String, cvc: String, expiry: String) } final class AddPaymentMethodViewController: UIViewController, AddPaymentMethodPresentable, AddPaymentMethodViewControllable { weak var listener: AddPaymentMethodPresentableListener? private let cardNumberTextField: UITextField = { let textField = makeTextField() textField.placeholder = "카드 번호" textField.accessibilityIdentifier = "addpaymentmethod_cardnumber_textfield" return textField }() private let stackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .horizontal stackView.alignment = .center stackView.distribution = .fillEqually stackView.spacing = 14 return stackView }() private let securityTextField: UITextField = { let textField = makeTextField() textField.placeholder = "CVC" textField.accessibilityIdentifier = "addpaymentmethod_security_textfield" return textField }() private let expirationTextField: UITextField = { let textField = makeTextField() textField.placeholder = "유효기한" textField.accessibilityIdentifier = "addpaymentmethod_expiry_textfield" return textField }() private lazy var addCardButton: UIButton = { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false button.roundCorners() button.backgroundColor = .primaryRed button.setTitle("추가하기", for: .normal) button.accessibilityIdentifier = "addpaymentmethod_addcard_button" button.addTarget(self, action: #selector(didTapAddCard), for: .touchUpInside) return button }() private static func makeTextField() -> UITextField { let textField = UITextField() textField.translatesAutoresizingMaskIntoConstraints = false textField.backgroundColor = .white textField.borderStyle = .roundedRect textField.keyboardType = .numberPad return textField } init(closeButtonType: DismissButtonType) { super.init(nibName: nil, bundle: nil) setupViews() setupNavigationItem(with: closeButtonType, target: self, action: #selector(didTapClose)) } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() setupNavigationItem(with: .close, target: self, action: #selector(didTapClose)) } private func setupViews() { title = "카드 추가" view.backgroundColor = .backgroundColor view.addSubview(cardNumberTextField) view.addSubview(stackView) view.addSubview(addCardButton) stackView.addArrangedSubview(securityTextField) stackView.addArrangedSubview(expirationTextField) NSLayoutConstraint.activate([ cardNumberTextField.topAnchor.constraint(equalTo: view.topAnchor, constant: 40), cardNumberTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40), cardNumberTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40), cardNumberTextField.bottomAnchor.constraint(equalTo: stackView.topAnchor, constant: -20), stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40), stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40), stackView.bottomAnchor.constraint(equalTo: addCardButton.topAnchor, constant: -20), addCardButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40), addCardButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40), cardNumberTextField.heightAnchor.constraint(equalToConstant: 60), securityTextField.heightAnchor.constraint(equalToConstant: 60), expirationTextField.heightAnchor.constraint(equalToConstant: 60), addCardButton.heightAnchor.constraint(equalToConstant: 60) ]) } @objc private func didTapAddCard() { if let number = cardNumberTextField.text, let cvc = securityTextField.text, let expiry = expirationTextField.text { listener?.didTapConfirm(with: number, cvc: cvc, expiry: expiry) } } @objc private func didTapClose() { listener?.didTapClose() } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/AddPaymentMethodTestSupport/AddPaymentMethodTestSupport.swift ================================================ import Foundation import AddPaymentMethod import ModernRIBs import RIBsUtil import RIBsTestSupport import FinanceEntity public final class AddPaymentMethodBuildableMock: AddPaymentMethodBuildable { public var buildCallCount = 0 public var closeButtonType: DismissButtonType? public func build(withListener listener: AddPaymentMethodListener, closeButtonType: DismissButtonType) -> ViewableRouting { buildCallCount += 1 self.closeButtonType = closeButtonType return ViewableRoutingMock( interactable: Interactor(), viewControllable: ViewControllableMock() ) } public init() { } } public final class AddPaymentMethodListenerMock: AddPaymentMethodListener { public var addPaymentMethodDidTapCloseCallCount = 0 public func addPaymentMethodDidTapClose() { addPaymentMethodDidTapCloseCallCount += 1 } public var addPaymentMethodDidAddCardCallCount = 0 public var addPaymentMethodDidAddCardPaymentMethod: PaymentMethod? public func addPaymentMethodDidAddCard(paymentMethod: PaymentMethod) { addPaymentMethodDidAddCardCallCount += 1 addPaymentMethodDidAddCardPaymentMethod = paymentMethod } public init() { } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceEntity/AddPaymentMethodInfo.swift ================================================ import Foundation public struct AddPaymentMethodInfo { public let number: String public let cvc: String public let expiration: String public init( number: String, cvc: String, expiration: String ) { self.number = number self.cvc = cvc self.expiration = expiration } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceEntity/PaymentMethod.swift ================================================ import Foundation public struct PaymentMethod: Decodable { public let id: String public let name: String public let digits: String public let color: String public let isPrimary: Bool public init( id: String, name: String, digits: String, color: String, isPrimary: Bool ) { self.id = id self.name = name self.digits = digits self.color = color self.isPrimary = isPrimary } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceHome/CardOnFileDashboard/CardOnFileDashboardBuilder.swift ================================================ import ModernRIBs import FinanceRepository protocol CardOnFileDashboardDependency: Dependency { var cardOnFileRepository: CardOnFileRepository { get } } final class CardOnFileDashboardComponent: Component, CardOnFileDashboardInteractorDependency { var cardOnFileRepository: CardOnFileRepository { dependency.cardOnFileRepository } } // MARK: - Builder protocol CardOnFileDashboardBuildable: Buildable { func build(withListener listener: CardOnFileDashboardListener) -> CardOnFileDashboardRouting } final class CardOnFileDashboardBuilder: Builder, CardOnFileDashboardBuildable { override init(dependency: CardOnFileDashboardDependency) { super.init(dependency: dependency) } func build(withListener listener: CardOnFileDashboardListener) -> CardOnFileDashboardRouting { let component = CardOnFileDashboardComponent(dependency: dependency) let viewController = CardOnFileDashboardViewController() let interactor = CardOnFileDashboardInteractor( presenter: viewController, dependency: component ) interactor.listener = listener return CardOnFileDashboardRouter(interactor: interactor, viewController: viewController) } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceHome/CardOnFileDashboard/CardOnFileDashboardInteractor.swift ================================================ import ModernRIBs import Combine import FinanceRepository import Foundation protocol CardOnFileDashboardRouting: ViewableRouting { } protocol CardOnFileDashboardPresentable: Presentable { var listener: CardOnFileDashboardPresentableListener? { get set } func update(with viewModels: [PaymentMethodViewModel]) } protocol CardOnFileDashboardListener: AnyObject { func cardOnFileDashboardDidTapAddPaymentMethod() } protocol CardOnFileDashboardInteractorDependency { var cardOnFileRepository: CardOnFileRepository { get } } final class CardOnFileDashboardInteractor: PresentableInteractor, CardOnFileDashboardInteractable, CardOnFileDashboardPresentableListener { weak var router: CardOnFileDashboardRouting? weak var listener: CardOnFileDashboardListener? private let dependency: CardOnFileDashboardInteractorDependency private var cancellables: Set init( presenter: CardOnFileDashboardPresentable, dependency: CardOnFileDashboardInteractorDependency ) { self.dependency = dependency self.cancellables = .init() super.init(presenter: presenter) presenter.listener = self } override func didBecomeActive() { super.didBecomeActive() dependency.cardOnFileRepository.cardOnFile .receive(on: DispatchQueue.main) .sink { methods in let viewModels = methods.prefix(5).map(PaymentMethodViewModel.init) self.presenter.update(with: viewModels) }.store(in: &cancellables) } override func willResignActive() { super.willResignActive() cancellables.forEach { $0.cancel() } cancellables.removeAll() } func didTapAddPaymentMethod() { listener?.cardOnFileDashboardDidTapAddPaymentMethod() } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceHome/CardOnFileDashboard/CardOnFileDashboardRouter.swift ================================================ import ModernRIBs protocol CardOnFileDashboardInteractable: Interactable { var router: CardOnFileDashboardRouting? { get set } var listener: CardOnFileDashboardListener? { get set } } protocol CardOnFileDashboardViewControllable: ViewControllable { } final class CardOnFileDashboardRouter: ViewableRouter, CardOnFileDashboardRouting { override init(interactor: CardOnFileDashboardInteractable, viewController: CardOnFileDashboardViewControllable) { super.init(interactor: interactor, viewController: viewController) interactor.router = self } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceHome/CardOnFileDashboard/CardOnFileDashboardViewController.swift ================================================ import ModernRIBs import UIKit protocol CardOnFileDashboardPresentableListener: AnyObject { func didTapAddPaymentMethod() } final class CardOnFileDashboardViewController: UIViewController, CardOnFileDashboardPresentable, CardOnFileDashboardViewControllable { weak var listener: CardOnFileDashboardPresentableListener? private let headerStackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false stackView.alignment = .fill stackView.distribution = .equalSpacing stackView.axis = .horizontal return stackView }() private let titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = .systemFont(ofSize: 22, weight: .semibold) label.text = "카드 및 계좌" return label }() private lazy var seeAllButton: UIButton = { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false button.setTitle("전체보기", for: .normal) button.setTitleColor(.systemBlue, for: .normal) button.addTarget(self, action: #selector(seeAllButtonTapped), for: .touchUpInside) return button }() private let cardOnFileStackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false stackView.alignment = .fill stackView.distribution = .equalSpacing stackView.axis = .vertical stackView.spacing = 12 return stackView }() private lazy var addMethodButton: AddPaymentMethodButton = { let button = AddPaymentMethodButton() button.translatesAutoresizingMaskIntoConstraints = false button.roundCorners() button.backgroundColor = .systemGray4 button.addTarget(self, action: #selector(addButtonDidTap), for: .touchUpInside) return button }() init() { super.init(nibName: nil, bundle: nil) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } func update(with viewModels: [PaymentMethodViewModel]) { cardOnFileStackView.arrangedSubviews.forEach { $0.removeFromSuperview() } let views = viewModels.map(PaymentMethodView.init) views.forEach { $0.roundCorners() cardOnFileStackView.addArrangedSubview($0) } cardOnFileStackView.addArrangedSubview(addMethodButton) let heightConstraints = views.map { $0.heightAnchor.constraint(equalToConstant: 60) } NSLayoutConstraint.activate(heightConstraints) } private func setupViews() { view.addSubview(headerStackView) view.addSubview(cardOnFileStackView) headerStackView.addArrangedSubview(titleLabel) headerStackView.addArrangedSubview(seeAllButton) NSLayoutConstraint.activate([ headerStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 10), headerStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), headerStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), cardOnFileStackView.topAnchor.constraint(equalTo: headerStackView.bottomAnchor, constant: 10), cardOnFileStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), cardOnFileStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), cardOnFileStackView.bottomAnchor.constraint(equalTo: view.bottomAnchor), addMethodButton.heightAnchor.constraint(equalToConstant: 60), ]) } @objc private func seeAllButtonTapped() { } @objc private func addButtonDidTap() { listener?.didTapAddPaymentMethod() } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceHome/CardOnFileDashboard/PaymentMethodViewModel.swift ================================================ import UIKit import FinanceEntity struct PaymentMethodViewModel { let name: String let digits: String let color: UIColor init(_ paymentMethod: PaymentMethod) { name = paymentMethod.name digits = "**** \(paymentMethod.digits)" color = UIColor(hex: paymentMethod.color) ?? .systemGray2 } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceHome/CardOnFileDashboard/Views/AddPaymentMethodButton.swift ================================================ import UIKit final class AddPaymentMethodButton: UIControl { private let plusIcon: UIImageView = { let imageView = UIImageView( image: UIImage( systemName: "plus", withConfiguration: UIImage.SymbolConfiguration(pointSize: 24, weight: .semibold) ) ) imageView.tintColor = .white imageView.translatesAutoresizingMaskIntoConstraints = false return imageView }() init() { super.init(frame: .zero) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private func setupViews() { addSubview(plusIcon) NSLayoutConstraint.activate([ plusIcon.centerXAnchor.constraint(equalTo: self.centerXAnchor), plusIcon.centerYAnchor.constraint(equalTo: self.centerYAnchor), ]) } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceHome/CardOnFileDashboard/Views/PaymentMethodView.swift ================================================ import UIKit final class PaymentMethodView: UIView { private let nameLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = .systemFont(ofSize: 18, weight: .semibold) label.textColor = .white label.text = "우리은행" return label }() private let subtitleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = .systemFont(ofSize: 15, weight: .regular) label.textColor = .white label.text = "**** 9999" return label }() init(viewModel: PaymentMethodViewModel) { super.init(frame: .zero) setupViews() nameLabel.text = viewModel.name subtitleLabel.text = viewModel.digits backgroundColor = viewModel.color } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private func setupViews() { addSubview(nameLabel) addSubview(subtitleLabel) backgroundColor = .systemIndigo NSLayoutConstraint.activate([ nameLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 24), nameLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), subtitleLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -24), subtitleLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor) ]) } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceHome/FinanceHomeBuilder.swift ================================================ import ModernRIBs import FinanceRepository import AddPaymentMethod import CombineUtil import Topup public protocol FinanceHomeDependency: Dependency { var cardOnFileRepository: CardOnFileRepository { get } var superPayRepository: SuperPayRepository { get } var topupBuildable: TopupBuildable { get } var addPaymentMethodBuildable: AddPaymentMethodBuildable { get } } final class FinanceHomeComponent: Component, SuperPayDashboardDependency, CardOnFileDashboardDependency { var cardOnFileRepository: CardOnFileRepository { dependency.cardOnFileRepository } var superPayRepository: SuperPayRepository { dependency.superPayRepository } var balance: ReadOnlyCurrentValuePublisher { superPayRepository.balance } var topupBuildable: TopupBuildable { dependency.topupBuildable } var addPaymentMethodBuildable: AddPaymentMethodBuildable { dependency.addPaymentMethodBuildable } } // MARK: - Builder public protocol FinanceHomeBuildable: Buildable { func build(withListener listener: FinanceHomeListener) -> ViewableRouting } public final class FinanceHomeBuilder: Builder, FinanceHomeBuildable { public override init(dependency: FinanceHomeDependency) { super.init(dependency: dependency) } public func build(withListener listener: FinanceHomeListener) -> ViewableRouting { let viewController = FinanceHomeViewController() let component = FinanceHomeComponent( dependency: dependency ) let interactor = FinanceHomeInteractor(presenter: viewController) interactor.listener = listener let superPayDashboardBuilder = SuperPayDashboardBuilder(dependency: component) let cardOnFileDashboardBuilder = CardOnFileDashboardBuilder(dependency: component) return FinanceHomeRouter( interactor: interactor, viewController: viewController, superPayDashboardBuildable: superPayDashboardBuilder, cardOnFileDashboardBuildable: cardOnFileDashboardBuilder, addPaymentMethodBuildable: component.addPaymentMethodBuildable, topupBuildable: component.topupBuildable ) } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceHome/FinanceHomeInteractor.swift ================================================ import ModernRIBs import UIKit import SuperUI import FinanceEntity protocol FinanceHomeRouting: ViewableRouting { func attachSuperPayDashboard() func attachCardOnFileDashboard() func attachAddPaymentMethod() func detachAddPaymentMethod() func attachTopup() func detachTopup() } protocol FinanceHomePresentable: Presentable { var listener: FinanceHomePresentableListener? { get set } } public protocol FinanceHomeListener: AnyObject { } final class FinanceHomeInteractor: PresentableInteractor, FinanceHomeInteractable, FinanceHomePresentableListener, AdaptivePresentationControllerDelegate { weak var router: FinanceHomeRouting? weak var listener: FinanceHomeListener? let presentationDelegateProxy: AdaptivePresentationControllerDelegateProxy override init(presenter: FinanceHomePresentable) { self.presentationDelegateProxy = AdaptivePresentationControllerDelegateProxy() super.init(presenter: presenter) presenter.listener = self self.presentationDelegateProxy.delegate = self } override func didBecomeActive() { super.didBecomeActive() router?.attachSuperPayDashboard() router?.attachCardOnFileDashboard() } override func willResignActive() { super.willResignActive() } func presentationControllerDidDismiss() { router?.detachAddPaymentMethod() } // MARK: - CardOnFileDashboardListener func cardOnFileDashboardDidTapAddPaymentMethod() { router?.attachAddPaymentMethod() } // MARK: - AddPaymentMethodListener func addPaymentMethodDidTapClose() { router?.detachAddPaymentMethod() } func addPaymentMethodDidAddCard(paymentMethod: PaymentMethod) { router?.detachAddPaymentMethod() } func superPayDashboardDidTapTopup() { router?.attachTopup() } func topupDidClose() { router?.detachTopup() } func topupDidFinish() { router?.detachTopup() } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceHome/FinanceHomeRouter.swift ================================================ import ModernRIBs import SuperUI import AddPaymentMethod import Topup import RIBsUtil protocol FinanceHomeInteractable: Interactable, SuperPayDashboardListener, CardOnFileDashboardListener, AddPaymentMethodListener, TopupListener { var router: FinanceHomeRouting? { get set } var listener: FinanceHomeListener? { get set } var presentationDelegateProxy: AdaptivePresentationControllerDelegateProxy { get } } protocol FinanceHomeViewControllable: ViewControllable { func addDashboard(_ view: ViewControllable) } final class FinanceHomeRouter: ViewableRouter, FinanceHomeRouting { private let superPayDashboardBuildable: SuperPayDashboardBuildable private var superPayRouting: Routing? private let cardOnFileDashboardBuildable: CardOnFileDashboardBuildable private var cardOnFileRouting: Routing? private let addPaymentMethodBuildable: AddPaymentMethodBuildable private var addPaymentMethodRouting: Routing? private let topupBuildable: TopupBuildable private var topupRouting: Routing? init( interactor: FinanceHomeInteractable, viewController: FinanceHomeViewControllable, superPayDashboardBuildable: SuperPayDashboardBuildable, cardOnFileDashboardBuildable: CardOnFileDashboardBuildable, addPaymentMethodBuildable: AddPaymentMethodBuildable, topupBuildable: TopupBuildable ) { self.superPayDashboardBuildable = superPayDashboardBuildable self.cardOnFileDashboardBuildable = cardOnFileDashboardBuildable self.addPaymentMethodBuildable = addPaymentMethodBuildable self.topupBuildable = topupBuildable super.init(interactor: interactor, viewController: viewController) interactor.router = self } func attachSuperPayDashboard() { if superPayRouting != nil { return } let router = superPayDashboardBuildable.build(withListener: interactor) let dashboard = router.viewControllable viewController.addDashboard(dashboard) self.superPayRouting = router attachChild(router) } func attachCardOnFileDashboard() { if cardOnFileRouting != nil { return } let router = cardOnFileDashboardBuildable.build(withListener: interactor) let dashboard = router.viewControllable viewController.addDashboard(dashboard) self.cardOnFileRouting = router attachChild(router) } func attachAddPaymentMethod() { if addPaymentMethodRouting != nil { return } let router = addPaymentMethodBuildable.build(withListener: interactor, closeButtonType: .close) let navigation = NavigationControllerable(root: router.viewControllable) navigation.navigationController.presentationController?.delegate = interactor.presentationDelegateProxy viewControllable.present(navigation, animated: true, completion: nil) addPaymentMethodRouting = router attachChild(router) } func detachAddPaymentMethod() { guard let router = addPaymentMethodRouting else { return } viewControllable.dismiss(completion: nil) detachChild(router) addPaymentMethodRouting = nil } func attachTopup() { if topupRouting != nil { return } let router = topupBuildable.build(withListener: interactor) topupRouting = router attachChild(router) } func detachTopup() { guard let router = topupRouting else { return } detachChild(router) self.topupRouting = nil } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceHome/FinanceHomeViewController.swift ================================================ import ModernRIBs import UIKit protocol FinanceHomePresentableListener: AnyObject { } final class FinanceHomeViewController: UIViewController, FinanceHomePresentable, FinanceHomeViewControllable { weak var listener: FinanceHomePresentableListener? private let stackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical stackView.alignment = .fill stackView.distribution = .equalSpacing stackView.spacing = 4 return stackView }() init() { super.init(nibName: nil, bundle: nil) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } func setupViews() { title = "슈퍼페이" tabBarItem = UITabBarItem(title: "슈퍼페이", image: UIImage(systemName: "creditcard"), selectedImage: UIImage(systemName: "creditcard.fill")) tabBarItem.accessibilityIdentifier = "superpay_home_tab_bar_item" view.backgroundColor = .white view.addSubview(stackView) NSLayoutConstraint.activate([ stackView.topAnchor.constraint(equalTo: view.topAnchor), stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor), stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor) ]) } func addDashboard(_ view: ViewControllable) { let vc = view.uiviewController addChild(vc) stackView.addArrangedSubview(vc.view) vc.didMove(toParent: self) } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceHome/SuperPayDashboard/Formatter.swift ================================================ import Foundation struct Formatter { static let balanceFormatter: NumberFormatter = { let formatter = NumberFormatter() formatter.numberStyle = .decimal return formatter }() } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceHome/SuperPayDashboard/SuperPayDashboardBuilder.swift ================================================ import ModernRIBs import Foundation import CombineUtil protocol SuperPayDashboardDependency: Dependency { var balance: ReadOnlyCurrentValuePublisher { get } } final class SuperPayDashboardComponent: Component, SuperPayDashboardInteractorDependency { var balanceFormatter: NumberFormatter { Formatter.balanceFormatter } var balance: ReadOnlyCurrentValuePublisher { dependency.balance } } // MARK: - Builder protocol SuperPayDashboardBuildable: Buildable { func build(withListener listener: SuperPayDashboardListener) -> SuperPayDashboardRouting } final class SuperPayDashboardBuilder: Builder, SuperPayDashboardBuildable { override init(dependency: SuperPayDashboardDependency) { super.init(dependency: dependency) } func build(withListener listener: SuperPayDashboardListener) -> SuperPayDashboardRouting { let component = SuperPayDashboardComponent(dependency: dependency) let viewController = SuperPayDashboardViewController() let interactor = SuperPayDashboardInteractor( presenter: viewController, dependency: component ) interactor.listener = listener return SuperPayDashboardRouter(interactor: interactor, viewController: viewController) } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceHome/SuperPayDashboard/SuperPayDashboardInteractor.swift ================================================ import ModernRIBs import Combine import Foundation import CombineUtil protocol SuperPayDashboardRouting: ViewableRouting { } protocol SuperPayDashboardPresentable: Presentable { var listener: SuperPayDashboardPresentableListener? { get set } func updateBalance(_ balance: String) } protocol SuperPayDashboardListener: AnyObject { func superPayDashboardDidTapTopup() } protocol SuperPayDashboardInteractorDependency { var balance: ReadOnlyCurrentValuePublisher { get } var balanceFormatter: NumberFormatter { get } } final class SuperPayDashboardInteractor: PresentableInteractor, SuperPayDashboardInteractable, SuperPayDashboardPresentableListener { weak var router: SuperPayDashboardRouting? weak var listener: SuperPayDashboardListener? private let dependency: SuperPayDashboardInteractorDependency private var cancellables: Set init( presenter: SuperPayDashboardPresentable, dependency: SuperPayDashboardInteractorDependency ) { self.dependency = dependency self.cancellables = .init() super.init(presenter: presenter) presenter.listener = self } override func didBecomeActive() { super.didBecomeActive() dependency.balance .receive(on: DispatchQueue.main) .sink { [weak self] balance in self?.dependency.balanceFormatter.string(from: NSNumber(value: balance)).map({ self?.presenter.updateBalance($0) }) }.store(in: &cancellables) } override func willResignActive() { super.willResignActive() } func topupButtonDidTap() { listener?.superPayDashboardDidTapTopup() } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceHome/SuperPayDashboard/SuperPayDashboardRouter.swift ================================================ import ModernRIBs protocol SuperPayDashboardInteractable: Interactable { var router: SuperPayDashboardRouting? { get set } var listener: SuperPayDashboardListener? { get set } } protocol SuperPayDashboardViewControllable: ViewControllable { } final class SuperPayDashboardRouter: ViewableRouter, SuperPayDashboardRouting { override init(interactor: SuperPayDashboardInteractable, viewController: SuperPayDashboardViewControllable) { super.init(interactor: interactor, viewController: viewController) interactor.router = self } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceHome/SuperPayDashboard/SuperPayDashboardViewController.swift ================================================ import ModernRIBs import UIKit protocol SuperPayDashboardPresentableListener: AnyObject { func topupButtonDidTap() } final class SuperPayDashboardViewController: UIViewController, SuperPayDashboardPresentable, SuperPayDashboardViewControllable { weak var listener: SuperPayDashboardPresentableListener? private let headerStackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false stackView.alignment = .fill stackView.distribution = .equalSpacing stackView.axis = .horizontal return stackView }() private let titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = .systemFont(ofSize: 22, weight: .semibold) label.text = "슈퍼페이 잔고" return label }() private lazy var topupButton: UIButton = { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false button.setTitle("충전하기", for: .normal) button.setTitleColor(.systemBlue, for: .normal) button.accessibilityIdentifier = "superpay_dashboard_topup_button" button.addTarget(self, action: #selector(topupButtonDidTap), for: .touchUpInside) return button }() private let cardView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.layer.cornerRadius = 16 view.layer.cornerCurve = .continuous view.backgroundColor = .systemIndigo return view }() private let currencyLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = .systemFont(ofSize: 22, weight: .semibold) label.text = "원" label.textColor = .white return label }() private let balanceAmountLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = .systemFont(ofSize: 22, weight: .semibold) label.accessibilityIdentifier = "superpay_dashboard_balance_label" label.textColor = .white return label }() private let balanceStackView: UIStackView = { let stack = UIStackView() stack.translatesAutoresizingMaskIntoConstraints = false stack.alignment = .fill stack.distribution = .equalSpacing stack.axis = .horizontal stack.spacing = 4 return stack }() init() { super.init(nibName: nil, bundle: nil) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private func setupViews() { view.addSubview(headerStackView) view.addSubview(cardView) headerStackView.addArrangedSubview(titleLabel) headerStackView.addArrangedSubview(topupButton) cardView.addSubview(balanceStackView) balanceStackView.addArrangedSubview(balanceAmountLabel) balanceStackView.addArrangedSubview(currencyLabel) NSLayoutConstraint.activate([ headerStackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 10), headerStackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), headerStackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), cardView.topAnchor.constraint(equalTo: headerStackView.bottomAnchor, constant: 10), cardView.heightAnchor.constraint(equalToConstant: 180), cardView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), cardView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), cardView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20), balanceStackView.centerXAnchor.constraint(equalTo: cardView.centerXAnchor), balanceStackView.centerYAnchor.constraint(equalTo: cardView.centerYAnchor) ]) } func updateBalance(_ balance: String) { balanceAmountLabel.text = balance } @objc private func topupButtonDidTap() { listener?.topupButtonDidTap() } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceRepository/AddCardRequest.swift ================================================ import Foundation import Network import FinanceEntity struct AddCardRequest: Request { typealias Output = AddCardResponse let endpoint: URL let method: HTTPMethod let query: QueryItems let header: HTTPHeader init(baseURL: URL, info: AddPaymentMethodInfo) { self.endpoint = baseURL.appendingPathComponent("/addCard") self.method = .post self.query = [ "number": info.number, "cvc": info.cvc, "expiry": info.expiration ] self.header = [:] } } struct AddCardResponse: Decodable { let card: PaymentMethod } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceRepository/CardOnFileRepository.swift ================================================ import Foundation import Combine import FinanceEntity import CombineUtil import Network public protocol CardOnFileRepository { var cardOnFile: ReadOnlyCurrentValuePublisher<[PaymentMethod]> { get } func addCard(info: AddPaymentMethodInfo) -> AnyPublisher func fetch() } public final class CardOnFileRepositoryImp: CardOnFileRepository { public var cardOnFile: ReadOnlyCurrentValuePublisher<[PaymentMethod]> { paymentMethodsSubject } private let paymentMethodsSubject = CurrentValuePublisher<[PaymentMethod]>([ // PaymentMethod(id: "0", name: "우리은행", digits: "0123", color: "#f19a38ff", isPrimary: false), // PaymentMethod(id: "1", name: "신한카드", digits: "0987", color: "#3478f6ff", isPrimary: false), // PaymentMethod(id: "2", name: "현대카드", digits: "8121", color: "#78c5f5ff", isPrimary: false), // PaymentMethod(id: "3", name: "국민은행", digits: "2812", color: "#65c466ff", isPrimary: false), // PaymentMethod(id: "4", name: "카카오뱅크", digits: "8751", color: "#ffcc00ff", isPrimary: false) ]) public func addCard(info: AddPaymentMethodInfo) -> AnyPublisher { let request = AddCardRequest(baseURL: baseURL, info: info) return network.send(request) .map(\.output.card) .handleEvents( receiveSubscription: nil, receiveOutput: { [weak self] method in guard let this = self else { return } this.paymentMethodsSubject.send(this.paymentMethodsSubject.value + [method]) }, receiveCompletion: nil, receiveCancel: nil, receiveRequest: nil ) .eraseToAnyPublisher() } public func fetch() { let request = CardOnFileRequest(baseURL: baseURL) network.send(request).map(\.output.cards) .sink( receiveCompletion: { _ in }, receiveValue: { [weak self] cards in self?.paymentMethodsSubject.send(cards) } ).store(in: &cancellables) } private let network: Network private let baseURL: URL private var cancellables: Set public init(network: Network, baseURL: URL) { self.network = network self.baseURL = baseURL self.cancellables = .init() } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceRepository/CardOnFileRequest.swift ================================================ import Foundation import Network import FinanceEntity struct CardOnFileRequest: Request { typealias Output = CardOnFileResponse let endpoint: URL let method: HTTPMethod let query: QueryItems let header: HTTPHeader init(baseURL: URL) { self.endpoint = baseURL.appendingPathComponent("/cards") self.method = .get self.query = [:] self.header = [:] } } struct CardOnFileResponse: Decodable { let cards: [PaymentMethod] } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceRepository/SuperPayRepository.swift ================================================ import Foundation import Combine import CombineUtil import Network public protocol SuperPayRepository { var balance: ReadOnlyCurrentValuePublisher { get } func topup(amount: Double, paymentMethodID: String) -> AnyPublisher } public final class SuperPayRepositoryImp: SuperPayRepository { public var balance: ReadOnlyCurrentValuePublisher { balanceSubject } private let balanceSubject = CurrentValuePublisher(0) public func topup(amount: Double, paymentMethodID: String) -> AnyPublisher { let request = TopupRequest(baseURL: baseURL, amount: amount, paymentMethodID: paymentMethodID) return network.send(request) .handleEvents( receiveSubscription: nil, receiveOutput: { [weak self] _ in let newBalance = (self?.balanceSubject.value).map { $0 + amount } newBalance.map { self?.balanceSubject.send($0) } }, receiveCompletion: nil, receiveCancel: nil, receiveRequest: nil ) .map({ _ in }) .eraseToAnyPublisher() } private let bgQueue = DispatchQueue(label: "topup.repository.queue") private let network: Network private let baseURL: URL public init(network: Network, baseURL: URL) { self.network = network self.baseURL = baseURL } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceRepository/TopupRequest.swift ================================================ import Foundation import Network struct TopupRequest: Request { typealias Output = TopupResponse let endpoint: URL let method: HTTPMethod let query: QueryItems let header: HTTPHeader init(baseURL: URL, amount: Double, paymentMethodID: String) { self.endpoint = baseURL.appendingPathComponent("/topup") self.method = .post self.query = [ "amount": amount, "paymentMethodID": paymentMethodID ] self.header = [:] } } struct TopupResponse: Decodable { let status: String } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceRepositoryTestSupport/CardOnFileRepositoryMock.swift ================================================ import Foundation import FinanceRepository import CombineUtil import Combine import FinanceEntity public final class CardOnFileRepositoryMock: CardOnFileRepository { public var cardOnFileSubject: CurrentValuePublisher<[PaymentMethod]> = .init([]) public var cardOnFile: ReadOnlyCurrentValuePublisher<[PaymentMethod]> { cardOnFileSubject } public var addCardCallCount = 0 public var addCardInfo: AddPaymentMethodInfo? public var addedPaymentMethod: PaymentMethod? public func addCard(info: AddPaymentMethodInfo) -> AnyPublisher { addCardCallCount += 1 addCardInfo = info if let addedPaymentMethod = addedPaymentMethod { return Just(addedPaymentMethod).setFailureType(to: Error.self).eraseToAnyPublisher() } else { return Fail(error: NSError(domain: "CardOnFileRepositoryMock", code: 0, userInfo: nil)).eraseToAnyPublisher() } } public var fetchCallCount = 0 public func fetch() { fetchCallCount += 1 } public init() { } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/FinanceRepositoryTestSupport/SuperPayRepositoryMock.swift ================================================ import Foundation import FinanceRepository import CombineUtil import Combine public final class SuperPayRepositoryMock: SuperPayRepository { public var balanceSubject = CurrentValuePublisher(0) public var balance: ReadOnlyCurrentValuePublisher { balanceSubject } public var topupCallCount = 0 public var topupAmount: Double? public var paymentMethodID: String? public var shouldTopupSucceed: Bool = true public func topup(amount: Double, paymentMethodID: String) -> AnyPublisher { topupCallCount += 1 topupAmount = amount self.paymentMethodID = paymentMethodID if shouldTopupSucceed { return Just(()).setFailureType(to: Error.self).eraseToAnyPublisher() } else { return Fail(error: NSError(domain: "SuperPayRepositoryMock", code: 0, userInfo: nil)).eraseToAnyPublisher() } } public init() { } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/Topup/TopupInterface.swift ================================================ import Foundation import ModernRIBs public protocol TopupBuildable: Buildable { func build(withListener listener: TopupListener) -> Routing } public protocol TopupListener: AnyObject { func topupDidClose() func topupDidFinish() } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/TopupImp/Array+Utils.swift ================================================ import Foundation extension Array { subscript(safe index: Int) -> Element? { return indices ~= index ? self[index] : nil } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/TopupImp/CardOnFile/CardOnFileBuilder.swift ================================================ import ModernRIBs import FinanceEntity protocol CardOnFileDependency: Dependency { } final class CardOnFileComponent: Component { } // MARK: - Builder protocol CardOnFileBuildable: Buildable { func build(withListener listener: CardOnFileListener, paymentMethods: [PaymentMethod]) -> CardOnFileRouting } final class CardOnFileBuilder: Builder, CardOnFileBuildable { override init(dependency: CardOnFileDependency) { super.init(dependency: dependency) } func build(withListener listener: CardOnFileListener, paymentMethods: [PaymentMethod]) -> CardOnFileRouting { _ = CardOnFileComponent(dependency: dependency) let viewController = CardOnFileViewController() let interactor = CardOnFileInteractor(presenter: viewController, paymentMethods: paymentMethods) interactor.listener = listener return CardOnFileRouter(interactor: interactor, viewController: viewController) } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/TopupImp/CardOnFile/CardOnFileCell.swift ================================================ import UIKit final class CardOnFileCell: UITableViewCell { func setImage(_ image: UIImage?) { thumbnailView.image = image } func setTitle(_ title: String) { titleLabel.text = title } private let thumbnailView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.contentMode = .scaleAspectFill imageView.clipsToBounds = true imageView.roundCorners(4) return imageView }() private let titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.numberOfLines = 1 return label }() required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupViews() } private func setupViews() { contentView.addSubview(thumbnailView) contentView.addSubview(titleLabel) NSLayoutConstraint.activate([ thumbnailView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), thumbnailView.widthAnchor.constraint(equalToConstant: 46), thumbnailView.heightAnchor.constraint(equalToConstant: 34), thumbnailView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), titleLabel.centerYAnchor.constraint(equalTo: thumbnailView.centerYAnchor), titleLabel.leadingAnchor.constraint(equalTo: thumbnailView.trailingAnchor, constant: 14) ]) } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/TopupImp/CardOnFile/CardOnFileInteractor.swift ================================================ import ModernRIBs import FinanceEntity protocol CardOnFileRouting: ViewableRouting { } protocol CardOnFilePresentable: Presentable { var listener: CardOnFilePresentableListener? { get set } func update(with viewModels: [PaymentMethodViewModel]) } protocol CardOnFileListener: AnyObject { func cardOnFileDidTapClose() func cardOnFileDidTapAddCard() func cardOnFileDidSelect(at index: Int) } final class CardOnFileInteractor: PresentableInteractor, CardOnFileInteractable, CardOnFilePresentableListener { weak var router: CardOnFileRouting? weak var listener: CardOnFileListener? private let paymentMethods: [PaymentMethod] init( presenter: CardOnFilePresentable, paymentMethods: [PaymentMethod] ) { self.paymentMethods = paymentMethods super.init(presenter: presenter) presenter.listener = self } override func didBecomeActive() { super.didBecomeActive() presenter.update(with: paymentMethods.map(PaymentMethodViewModel.init)) } override func willResignActive() { super.willResignActive() } func didTapClose() { listener?.cardOnFileDidTapClose() } func didSelectItem(at index: Int) { if index >= paymentMethods.count { listener?.cardOnFileDidTapAddCard() } else { listener?.cardOnFileDidSelect(at: index) } } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/TopupImp/CardOnFile/CardOnFileRouter.swift ================================================ import ModernRIBs protocol CardOnFileInteractable: Interactable { var router: CardOnFileRouting? { get set } var listener: CardOnFileListener? { get set } } protocol CardOnFileViewControllable: ViewControllable { } final class CardOnFileRouter: ViewableRouter, CardOnFileRouting { override init(interactor: CardOnFileInteractable, viewController: CardOnFileViewControllable) { super.init(interactor: interactor, viewController: viewController) interactor.router = self } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/TopupImp/CardOnFile/CardOnFileViewController.swift ================================================ import ModernRIBs import UIKit protocol CardOnFilePresentableListener: AnyObject { func didTapClose() func didSelectItem(at: Int) } final class CardOnFileViewController: UIViewController, CardOnFilePresentable, CardOnFileViewControllable, UITableViewDataSource, UITableViewDelegate { weak var listener: CardOnFilePresentableListener? func update(with viewModels: [PaymentMethodViewModel]) { self.viewModels = viewModels tableView.reloadData() } init() { super.init(nibName: nil, bundle: nil) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private var viewModels: [PaymentMethodViewModel] = [] private lazy var tableView: UITableView = { let tableView = UITableView() tableView.translatesAutoresizingMaskIntoConstraints = false tableView.dataSource = self tableView.delegate = self tableView.register(cellType: CardOnFileCell.self) tableView.tableFooterView = UIView() tableView.rowHeight = 60 tableView.separatorInset = .zero return tableView }() private func setupViews() { title = "카드 선택" view.backgroundColor = .white view.addSubview(tableView) setupNavigationItem(with: .back, target: self, action: #selector(didTapClose)) NSLayoutConstraint.activate([ tableView.topAnchor.constraint(equalTo: view.topAnchor), tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) } // MARK: - UITableView func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return viewModels.count + 1 } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: CardOnFileCell = tableView.dequeueReusableCell(for: indexPath) if let viewModel = viewModels[safe: indexPath.row] { cell.setImage(UIImage(color: viewModel.color)) cell.setTitle("\(viewModel.name) \(viewModel.digits)") } else { cell.setImage(UIImage(systemName: "plus.rectangle")) cell.setTitle("카드 추가") } return cell } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) listener?.didSelectItem(at: indexPath.row) } @objc private func didTapClose() { listener?.didTapClose() } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/TopupImp/EnterAmount/EnterAmountBuilder.swift ================================================ import ModernRIBs import CombineUtil import FinanceEntity import FinanceRepository import CombineSchedulers protocol EnterAmountDependency: Dependency { var selectedPaymentMethod: ReadOnlyCurrentValuePublisher { get } var superPayRepository: SuperPayRepository { get } var mainQueue: AnySchedulerOf { get } } final class EnterAmountComponent: Component, EnterAmountInteractorDependency { var selectedPaymentMethod: ReadOnlyCurrentValuePublisher { dependency.selectedPaymentMethod } var superPayRepository: SuperPayRepository { dependency.superPayRepository } var mainQueue: AnySchedulerOf { dependency.mainQueue } } // MARK: - Builder protocol EnterAmountBuildable: Buildable { func build(withListener listener: EnterAmountListener) -> EnterAmountRouting } final class EnterAmountBuilder: Builder, EnterAmountBuildable { override init(dependency: EnterAmountDependency) { super.init(dependency: dependency) } func build(withListener listener: EnterAmountListener) -> EnterAmountRouting { let component = EnterAmountComponent(dependency: dependency) let viewController = EnterAmountViewController() let interactor = EnterAmountInteractor(presenter: viewController, dependency: component) interactor.listener = listener return EnterAmountRouter(interactor: interactor, viewController: viewController) } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/TopupImp/EnterAmount/EnterAmountInteractor.swift ================================================ import ModernRIBs import Combine import Foundation import CombineUtil import FinanceEntity import FinanceRepository import CombineSchedulers protocol EnterAmountRouting: ViewableRouting { } protocol EnterAmountPresentable: Presentable { var listener: EnterAmountPresentableListener? { get set } func updateSelectedPaymentMethod(with viewModel: SelectedPaymentMethodViewModel) func startLoading() func stopLoading() } protocol EnterAmountListener: AnyObject { func enterAmountDidTapClose() func enterAmountDidTapPaymentMethod() func enterAmountDidFinishTopup() } protocol EnterAmountInteractorDependency { var selectedPaymentMethod: ReadOnlyCurrentValuePublisher { get } var superPayRepository: SuperPayRepository { get } var mainQueue: AnySchedulerOf { get } } final class EnterAmountInteractor: PresentableInteractor, EnterAmountInteractable, EnterAmountPresentableListener { weak var router: EnterAmountRouting? weak var listener: EnterAmountListener? private let dependency: EnterAmountInteractorDependency private var cancellables: Set init( presenter: EnterAmountPresentable, dependency: EnterAmountInteractorDependency ) { self.dependency = dependency self.cancellables = .init() super.init(presenter: presenter) presenter.listener = self } override func didBecomeActive() { super.didBecomeActive() dependency.selectedPaymentMethod.sink { [weak self] paymentMethod in self?.presenter.updateSelectedPaymentMethod(with: SelectedPaymentMethodViewModel(paymentMethod)) }.store(in: &cancellables) } override func willResignActive() { super.willResignActive() // TODO: Pause any business logic. } func didTapClose() { listener?.enterAmountDidTapClose() } func didTapPaymentMethod() { listener?.enterAmountDidTapPaymentMethod() } func didTapTopup(with amount: Double) { presenter.startLoading() dependency.superPayRepository.topup( amount: amount, paymentMethodID: dependency.selectedPaymentMethod.value.id ) .receive(on: dependency.mainQueue) .sink( receiveCompletion: { [weak self] _ in self?.presenter.stopLoading() }, receiveValue: { [weak self] in self?.listener?.enterAmountDidFinishTopup() } ).store(in: &cancellables) } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/TopupImp/EnterAmount/EnterAmountRouter.swift ================================================ import ModernRIBs protocol EnterAmountInteractable: Interactable { var router: EnterAmountRouting? { get set } var listener: EnterAmountListener? { get set } } protocol EnterAmountViewControllable: ViewControllable { } final class EnterAmountRouter: ViewableRouter, EnterAmountRouting { override init(interactor: EnterAmountInteractable, viewController: EnterAmountViewControllable) { super.init(interactor: interactor, viewController: viewController) interactor.router = self } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/TopupImp/EnterAmount/EnterAmountViewController.swift ================================================ import ModernRIBs import UIKit protocol EnterAmountPresentableListener: AnyObject { func didTapClose() func didTapPaymentMethod() func didTapTopup(with amount: Double) } final class EnterAmountViewController: UIViewController, EnterAmountPresentable, EnterAmountViewControllable { weak var listener: EnterAmountPresentableListener? func updateSelectedPaymentMethod(with viewModel: SelectedPaymentMethodViewModel) { selectedPaymentMethodView.update(with: viewModel) } func startLoading() { activityIndicator.startAnimating() ctaButton.isEnabled = false } func stopLoading() { activityIndicator.stopAnimating() ctaButton.isEnabled = true } private lazy var selectedPaymentMethodView: SelectedPaymentMethodView = { let view = SelectedPaymentMethodView() view.translatesAutoresizingMaskIntoConstraints = false view.addShadowWithRoundedCorners() let tap = UITapGestureRecognizer(target: self, action: #selector(didTapPaymentMethod)) view.addGestureRecognizer(tap) return view }() private let enterAmountWidget: EnterAmountWidget = { let widget = EnterAmountWidget() widget.translatesAutoresizingMaskIntoConstraints = false widget.addShadowWithRoundedCorners() return widget }() private lazy var ctaButton: UIButton = { let cta = UIButton(type: .system) cta.translatesAutoresizingMaskIntoConstraints = false cta.roundCorners() cta.setTitle("충전", for: .normal) cta.titleLabel?.font = UIFont.systemFont(ofSize: 20, weight: .semibold) cta.setBackgroundImage(UIImage(color: .primaryRed), for: .normal) cta.tintColor = .white cta.accessibilityIdentifier = "topup_enteramount_confirm_button" cta.addTarget(self, action: #selector(didTapCTAButton), for: .touchUpInside) return cta }() private let activityIndicator: UIActivityIndicatorView = { let activity = UIActivityIndicatorView(style: .medium) activity.translatesAutoresizingMaskIntoConstraints = false activity.hidesWhenStopped = true activity.stopAnimating() return activity }() init() { super.init(nibName: nil, bundle: nil) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private func setupViews() { title = "충전하기" view.backgroundColor = .backgroundColor setupNavigationItem(with: .close, target: self, action: #selector(didTapClose)) view.addSubview(selectedPaymentMethodView) view.addSubview(enterAmountWidget) view.addSubview(ctaButton) view.addSubview(activityIndicator) NSLayoutConstraint.activate([ selectedPaymentMethodView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), selectedPaymentMethodView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20), selectedPaymentMethodView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), selectedPaymentMethodView.heightAnchor.constraint(equalToConstant: 70), enterAmountWidget.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), enterAmountWidget.topAnchor.constraint(equalTo: selectedPaymentMethodView.bottomAnchor, constant: 20), enterAmountWidget.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), ctaButton.heightAnchor.constraint(equalToConstant: 60), ctaButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), ctaButton.topAnchor.constraint(equalTo: enterAmountWidget.bottomAnchor, constant: 40), ctaButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), activityIndicator.centerXAnchor.constraint(equalTo: ctaButton.centerXAnchor), activityIndicator.centerYAnchor.constraint(equalTo: ctaButton.centerYAnchor), ]) } @objc private func didTapClose() { listener?.didTapClose() } @objc private func didTapCTAButton() { if let amount = enterAmountWidget.text.flatMap(Double.init) { listener?.didTapTopup(with: amount) } } @objc private func didTapPaymentMethod() { listener?.didTapPaymentMethod() } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/TopupImp/EnterAmount/EnterAmountWidget.swift ================================================ import UIKit final class EnterAmountWidget: UIView { var text: String? { amountTextField.text } init() { super.init(frame: .zero) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private let titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.systemFont(ofSize: 18, weight: .semibold) label.text = "금액" label.numberOfLines = 1 return label }() private lazy var amountStackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false let button = UIButton() stackView.axis = .horizontal stackView.alignment = .fill stackView.distribution = .fill stackView.spacing = 5 stackView.addArrangedSubview(self.amountTextField) stackView.addArrangedSubview(self.currencyLabel) return stackView }() private let amountTextField: UITextField = { let textField = UITextField() textField.translatesAutoresizingMaskIntoConstraints = false textField.borderStyle = .none textField.font = UIFont.systemFont(ofSize: 18, weight: .semibold) textField.textAlignment = .right textField.keyboardType = .numberPad textField.accessibilityIdentifier = "topup_enteramount_textfield" return textField }() private let currencyLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.systemFont(ofSize: 18, weight: .semibold) label.text = "원" return label }() private func setupViews() { self.backgroundColor = .white self.addSubview(titleLabel) self.addSubview(amountStackView) NSLayoutConstraint.activate([ titleLabel.topAnchor.constraint(equalTo: self.topAnchor, constant: 16), titleLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16), titleLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16), amountStackView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 16), amountStackView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -16), amountStackView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 16), amountStackView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -16) ]) } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/TopupImp/EnterAmount/SelectedPaymentMethodView.swift ================================================ import UIKit import FinanceEntity struct SelectedPaymentMethodViewModel { let image: UIImage? let name: String init(_ paymentMethod: PaymentMethod) { image = UIColor(hex: paymentMethod.color).flatMap { UIImage(color: $0) } name = "\(paymentMethod.name) \(paymentMethod.digits)" } } final class SelectedPaymentMethodView: UIView { func update(with viewModel: SelectedPaymentMethodViewModel) { self.thumbnailView.image = viewModel.image self.nameLabel.text = viewModel.name } init() { super.init(frame: .zero) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private let thumbnailView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.contentMode = .scaleToFill imageView.roundCorners(4) imageView.backgroundColor = .systemGray3 return imageView }() private let nameLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.systemFont(ofSize: 16, weight: .semibold) label.numberOfLines = 1 return label }() private let rightChevronIcon: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.image = UIImage( systemName: "chevron.right", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .medium) ) imageView.tintColor = .systemGray3 return imageView }() private func setupViews() { self.backgroundColor = .white self.addSubview(thumbnailView) self.addSubview(nameLabel) self.addSubview(rightChevronIcon) NSLayoutConstraint.activate([ thumbnailView.centerYAnchor.constraint(equalTo: self.centerYAnchor), thumbnailView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 20), thumbnailView.widthAnchor.constraint(equalToConstant: 46), thumbnailView.heightAnchor.constraint(equalToConstant: 34), nameLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), nameLabel.leadingAnchor.constraint(equalTo: thumbnailView.trailingAnchor, constant: 22), rightChevronIcon.centerYAnchor.constraint(equalTo: self.centerYAnchor), rightChevronIcon.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -24) ]) } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/TopupImp/Models/PaymentMethodViewModel.swift ================================================ import UIKit import FinanceEntity struct PaymentMethodViewModel { let name: String let digits: String let color: UIColor init(_ paymentMethod: PaymentMethod) { name = paymentMethod.name digits = "**** \(paymentMethod.digits)" color = UIColor(hex: paymentMethod.color) ?? .systemGray2 } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/TopupImp/TopupBuilder.swift ================================================ import ModernRIBs import FinanceRepository import CombineUtil import AddPaymentMethod import FinanceEntity import Topup import CombineSchedulers public protocol TopupDependency: Dependency { var topupBaseViewController: ViewControllable { get } var cardOnFileRepository: CardOnFileRepository { get } var superPayRepository: SuperPayRepository { get } var addPaymentMethodBuildable: AddPaymentMethodBuildable { get } var mainQueue: AnySchedulerOf { get } } final class TopupComponent: Component, TopupInteractorDependency, EnterAmountDependency, CardOnFileDependency { var superPayRepository: SuperPayRepository { dependency.superPayRepository } var selectedPaymentMethod: ReadOnlyCurrentValuePublisher { paymentMethodStream } var cardOnFileRepository: CardOnFileRepository { dependency.cardOnFileRepository } var mainQueue: AnySchedulerOf { dependency.mainQueue } fileprivate var topupBaseViewController: ViewControllable { dependency.topupBaseViewController } let paymentMethodStream: CurrentValuePublisher var addPaymentMethodBuildable: AddPaymentMethodBuildable { dependency.addPaymentMethodBuildable } init( dependency: TopupDependency, paymentMethodStream: CurrentValuePublisher ) { self.paymentMethodStream = paymentMethodStream super.init(dependency: dependency) } } // MARK: - Builder public final class TopupBuilder: Builder, TopupBuildable { public override init(dependency: TopupDependency) { super.init(dependency: dependency) } public func build(withListener listener: TopupListener) -> Routing { let paymentMethodStream = CurrentValuePublisher(PaymentMethod(id: "", name: "", digits: "", color: "", isPrimary: false)) let component = TopupComponent(dependency: dependency, paymentMethodStream: paymentMethodStream) let interactor = TopupInteractor(dependency: component) interactor.listener = listener let enterAmountBuilder = EnterAmountBuilder(dependency: component) let cardOnFileBuilder = CardOnFileBuilder(dependency: component) return TopupRouter( interactor: interactor, viewController: component.topupBaseViewController, addPaymentMethodBuildable: component.addPaymentMethodBuildable, enterAmountBuildable: enterAmountBuilder, cardOnFileBuildable: cardOnFileBuilder ) } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/TopupImp/TopupInteractor.swift ================================================ import ModernRIBs import RIBsUtil import FinanceEntity import FinanceRepository import CombineUtil import AddPaymentMethod import SuperUI import Topup protocol TopupRouting: Routing { func cleanupViews() func attachAddPaymentMethod(closeButtonType: DismissButtonType) func detachAddPaymentMethod() func attachEnterAmount() func detachEnterAmount() func attachCardOnFile(paymentMethods: [PaymentMethod]) func detachCardOnFile() func popToRoot() } protocol TopupInteractorDependency { var cardOnFileRepository: CardOnFileRepository { get } var paymentMethodStream: CurrentValuePublisher { get } } final class TopupInteractor: Interactor, TopupInteractable, AddPaymentMethodListener, AdaptivePresentationControllerDelegate { weak var router: TopupRouting? weak var listener: TopupListener? let presentationDelegateProxy: AdaptivePresentationControllerDelegateProxy private var isEnterAmountRoot: Bool = false private var paymentMethods: [PaymentMethod] { dependency.cardOnFileRepository.cardOnFile.value } private let dependency: TopupInteractorDependency init( dependency: TopupInteractorDependency ) { self.presentationDelegateProxy = AdaptivePresentationControllerDelegateProxy() self.dependency = dependency super.init() self.presentationDelegateProxy.delegate = self } override func didBecomeActive() { super.didBecomeActive() if let card = dependency.cardOnFileRepository.cardOnFile.value.first { isEnterAmountRoot = true dependency.paymentMethodStream.send(card) router?.attachEnterAmount() } else { isEnterAmountRoot = false router?.attachAddPaymentMethod(closeButtonType: .close) } } override func willResignActive() { super.willResignActive() router?.cleanupViews() } func presentationControllerDidDismiss() { listener?.topupDidClose() } func addPaymentMethodDidTapClose() { router?.detachAddPaymentMethod() if isEnterAmountRoot == false { listener?.topupDidClose() } } func addPaymentMethodDidAddCard(paymentMethod: PaymentMethod) { dependency.paymentMethodStream.send(paymentMethod) if isEnterAmountRoot { router?.popToRoot() } else { isEnterAmountRoot = true router?.attachEnterAmount() } } func enterAmountDidTapClose() { router?.detachEnterAmount() listener?.topupDidClose() } func enterAmountDidTapPaymentMethod() { router?.attachCardOnFile(paymentMethods: paymentMethods) } func enterAmountDidFinishTopup() { listener?.topupDidFinish() } func cardOnFileDidTapClose() { router?.detachCardOnFile() } func cardOnFileDidTapAddCard() { router?.attachAddPaymentMethod(closeButtonType: .back) } func cardOnFileDidSelect(at index: Int) { if let selected = paymentMethods[safe: index] { dependency.paymentMethodStream.send(selected) } router?.detachCardOnFile() } } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/TopupImp/TopupRouter.swift ================================================ import ModernRIBs import AddPaymentMethod import SuperUI import RIBsUtil import FinanceEntity import Topup protocol TopupInteractable: Interactable, AddPaymentMethodListener, EnterAmountListener, CardOnFileListener { var router: TopupRouting? { get set } var listener: TopupListener? { get set } var presentationDelegateProxy: AdaptivePresentationControllerDelegateProxy { get } } protocol TopupViewControllable: ViewControllable { } final class TopupRouter: Router, TopupRouting { private var navigationControllable: NavigationControllerable? private let addPaymentMethodBuildable: AddPaymentMethodBuildable private var addPaymentMethodRouting: Routing? private let enterAmountBuildable: EnterAmountBuildable private var enterAmountRouting: Routing? private let cardOnFileBuildable: CardOnFileBuildable private var cardOnFileRouting: Routing? init( interactor: TopupInteractable, viewController: ViewControllable, addPaymentMethodBuildable: AddPaymentMethodBuildable, enterAmountBuildable: EnterAmountBuildable, cardOnFileBuildable: CardOnFileBuildable ) { self.viewController = viewController self.addPaymentMethodBuildable = addPaymentMethodBuildable self.enterAmountBuildable = enterAmountBuildable self.cardOnFileBuildable = cardOnFileBuildable super.init(interactor: interactor) interactor.router = self } func cleanupViews() { if viewController.uiviewController.presentedViewController != nil, navigationControllable != nil { navigationControllable?.dismiss(completion: nil) } } func attachAddPaymentMethod(closeButtonType: DismissButtonType) { if addPaymentMethodRouting != nil { return } let router = addPaymentMethodBuildable.build(withListener: interactor, closeButtonType: closeButtonType) if let navigationControllable = navigationControllable { navigationControllable.pushViewController(router.viewControllable, animated: true) } else { presentInsideNavigation(router.viewControllable) } attachChild(router) addPaymentMethodRouting = router } func detachAddPaymentMethod() { guard let router = addPaymentMethodRouting else { return } navigationControllable?.popViewController(animated: true) detachChild(router) addPaymentMethodRouting = nil } func attachEnterAmount() { if enterAmountRouting != nil { return } let router = enterAmountBuildable.build(withListener: interactor) if let navigation = navigationControllable { navigation.setViewControllers([router.viewControllable]) resetChildRouting() } else { presentInsideNavigation(router.viewControllable) } attachChild(router) enterAmountRouting = router } func detachEnterAmount() { guard let router = enterAmountRouting else { return } dismissPresentedNavigation(completion: nil) detachChild(router) enterAmountRouting = nil } func attachCardOnFile(paymentMethods: [PaymentMethod]) { if cardOnFileRouting != nil { return } let router = cardOnFileBuildable.build(withListener: interactor, paymentMethods: paymentMethods) navigationControllable?.pushViewController(router.viewControllable, animated: true) cardOnFileRouting = router attachChild(router) } func detachCardOnFile() { guard let router = cardOnFileRouting else { return } navigationControllable?.popViewController(animated: true) detachChild(router) cardOnFileRouting = nil } func popToRoot() { navigationControllable?.popToRoot(animated: true) resetChildRouting() } private func presentInsideNavigation(_ viewControllable: ViewControllable) { let navigation = NavigationControllerable(root: viewControllable) navigation.navigationController.presentationController?.delegate = interactor.presentationDelegateProxy self.navigationControllable = navigation viewController.present(navigation, animated: true, completion: nil) } private func dismissPresentedNavigation(completion: (() -> Void)?) { if self.navigationControllable == nil { return } viewController.dismiss(completion: nil) self.navigationControllable = nil } private func resetChildRouting() { if let cardOnFileRouting = cardOnFileRouting { detachChild(cardOnFileRouting) self.cardOnFileRouting = nil } if let addPaymentMethodRouting = addPaymentMethodRouting { detachChild(addPaymentMethodRouting) self.addPaymentMethodRouting = nil } } // MARK: - Private private let viewController: ViewControllable } ================================================ FILE: completed/MiniSuperApp/Finance/Sources/TopupTestSupport/TopupMock.swift ================================================ import Foundation import Topup public final class TopupListenerMock: TopupListener { public var topupDidCloseCallCount = 0 public func topupDidClose() { topupDidCloseCallCount += 1 } public var topupDidFinishCallCount = 0 public func topupDidFinish() { topupDidFinishCallCount += 1 } public init() { } } ================================================ FILE: completed/MiniSuperApp/Finance/Tests/TopupImpTests/CardOnFile/CardOnFileMock.swift ================================================ @testable import TopupImp import Foundation import RIBsTestSupport import FinanceEntity final class CardOnFileBuildableMock: CardOnFileBuildable { var buildHandler: ((_ listener: CardOnFileListener) -> CardOnFileRouting)? var buildCallCount = 0 var buildPaymentMethods: [PaymentMethod]? func build(withListener listener: CardOnFileListener, paymentMethods: [PaymentMethod]) -> CardOnFileRouting { buildCallCount += 1 buildPaymentMethods = paymentMethods if let buildHandler = buildHandler { return buildHandler(listener) } fatalError() } } final class CardOnFileRoutingMock: ViewableRoutingMock, CardOnFileRouting { } ================================================ FILE: completed/MiniSuperApp/Finance/Tests/TopupImpTests/CardOnFile/CardOnFileViewTests.swift ================================================ @testable import TopupImp import XCTest import Foundation import SnapshotTesting import FinanceEntity final class CardOnFileViewTests: XCTestCase { func testCardOnFile() { // given let viewModels = [ PaymentMethodViewModel(PaymentMethod(id: "0", name: "우리은행", digits: "1111", color: "#3478f6ff", isPrimary: false)), PaymentMethodViewModel(PaymentMethod(id: "1", name: "현대카드", digits: "2222", color: "#f19a38ff", isPrimary: false)), PaymentMethodViewModel(PaymentMethod(id: "2", name: "신한카드", digits: "3333", color: "#78c5f5ff", isPrimary: false)), ] // when let sut = CardOnFileViewController() sut.update(with: viewModels) // then assertSnapshot(matching: sut, as: .image(on: .iPhoneXsMax)) } } ================================================ FILE: completed/MiniSuperApp/Finance/Tests/TopupImpTests/EnterAmount/EnterAmountInteractorTests.swift ================================================ @testable import TopupImp import XCTest import FinanceEntity import FinanceRepositoryTestSupport final class EnterAmountInteractorTests: XCTestCase { private var sut: EnterAmountInteractor! private var presenter: EnterAmountPresentableMock! private var dependency: EnterAmountDependencyMock! private var listener: EnterAmountListenerMock! private var repository: SuperPayRepositoryMock! { dependency.superPayRepository as? SuperPayRepositoryMock } override func setUp() { super.setUp() self.presenter = EnterAmountPresentableMock() self.dependency = EnterAmountDependencyMock() self.listener = EnterAmountListenerMock() sut = EnterAmountInteractor( presenter: self.presenter, dependency: self.dependency ) sut.listener = self.listener } // MARK: - Tests func testActivate() { // given let paymentMethod = PaymentMethod( id: "id_0", name: "name_0", digits: "9999", color: "#13ABE8FF", isPrimary: false ) dependency.selectedPaymentMethodSubject.send(paymentMethod) // when sut.activate() // then XCTAssertEqual(presenter.updateSelectedPaymentMethodCallCount, 1) XCTAssertEqual(presenter.updateSelectedPaymentMethodViewModel?.name, "name_0 9999") XCTAssertNotNil(presenter.updateSelectedPaymentMethodViewModel?.image) } func testTopupWithValidAmount() { // given let paymentMethod = PaymentMethod( id: "id_0", name: "name_0", digits: "9999", color: "#13ABE8FF", isPrimary: false ) dependency.selectedPaymentMethodSubject.send(paymentMethod) // when sut.didTapTopup(with: 1_000_000) // then XCTAssertEqual(presenter.startLoadingCallCount, 1) XCTAssertEqual(presenter.stopLoadingCallCount, 1) XCTAssertEqual(repository.topupCallCount, 1) XCTAssertEqual(repository.paymentMethodID, "id_0") XCTAssertEqual(repository.topupAmount, 1_000_000) XCTAssertEqual(listener.enterAmountDidFinishTopupCallCount, 1) } func testTopupWithFailure() { // given let paymentMethod = PaymentMethod( id: "id_0", name: "name_0", digits: "9999", color: "#13ABE8FF", isPrimary: false ) dependency.selectedPaymentMethodSubject.send(paymentMethod) repository.shouldTopupSucceed = false // when sut.didTapTopup(with: 1_000_000) // then XCTAssertEqual(presenter.startLoadingCallCount, 1) XCTAssertEqual(presenter.stopLoadingCallCount, 1) XCTAssertEqual(listener.enterAmountDidFinishTopupCallCount, 0) } func testDidTapClose() { // given // when sut.didTapClose() // then XCTAssertEqual(listener.enterAmountDidTapCloseCallCount, 1) } func testDidTapPaymentMethod() { // given // when sut.didTapPaymentMethod() // then XCTAssertEqual(listener.enterAmountDidTapPaymentMethodCallCount, 1) } } ================================================ FILE: completed/MiniSuperApp/Finance/Tests/TopupImpTests/EnterAmount/EnterAmountMock.swift ================================================ @testable import TopupImp import Foundation import CombineUtil import FinanceEntity import FinanceRepository import FinanceRepositoryTestSupport import CombineSchedulers import RIBsTestSupport final class EnterAmountPresentableMock: EnterAmountPresentable { var listener: EnterAmountPresentableListener? var updateSelectedPaymentMethodCallCount = 0 var updateSelectedPaymentMethodViewModel: SelectedPaymentMethodViewModel? func updateSelectedPaymentMethod(with viewModel: SelectedPaymentMethodViewModel) { updateSelectedPaymentMethodCallCount += 1 updateSelectedPaymentMethodViewModel = viewModel } var startLoadingCallCount = 0 func startLoading() { startLoadingCallCount += 1 } var stopLoadingCallCount = 0 func stopLoading() { stopLoadingCallCount += 1 } init() { } } final class EnterAmountDependencyMock: EnterAmountInteractorDependency { var mainQueue: AnySchedulerOf { .immediate } var selectedPaymentMethodSubject = CurrentValuePublisher( PaymentMethod( id: "", name: "", digits: "", color: "", isPrimary: false ) ) var selectedPaymentMethod: ReadOnlyCurrentValuePublisher { selectedPaymentMethodSubject } var superPayRepository: SuperPayRepository = SuperPayRepositoryMock() } final class EnterAmountListenerMock: EnterAmountListener { var enterAmountDidTapCloseCallCount = 0 func enterAmountDidTapClose() { enterAmountDidTapCloseCallCount += 1 } var enterAmountDidTapPaymentMethodCallCount = 0 func enterAmountDidTapPaymentMethod() { enterAmountDidTapPaymentMethodCallCount += 1 } var enterAmountDidFinishTopupCallCount = 0 func enterAmountDidFinishTopup() { enterAmountDidFinishTopupCallCount += 1 } } final class EnterAmountBuildableMock: EnterAmountBuildable { var buildHandler: ((_ listener: EnterAmountListener) -> EnterAmountRouting)? var buildCallCount = 0 func build(withListener listener: EnterAmountListener) -> EnterAmountRouting { buildCallCount += 1 if let buildHandler = buildHandler { return buildHandler(listener) } fatalError() } } final class EnterAmountRoutingMock: ViewableRoutingMock, EnterAmountRouting { } ================================================ FILE: completed/MiniSuperApp/Finance/Tests/TopupImpTests/EnterAmount/EnterAmountRouterTests.swift ================================================ @testable import TopupImp import XCTest final class EnterAmountRouterTests: XCTestCase { private var router: EnterAmountRouter! override func setUp() { super.setUp() } } ================================================ FILE: completed/MiniSuperApp/Finance/Tests/TopupImpTests/EnterAmount/EnterAmountViewTests.swift ================================================ import XCTest import Foundation @testable import TopupImp import SnapshotTesting import FinanceEntity final class EnterAmountViewTests: XCTestCase { func testEnterAmount() { // given let paymentMethod = PaymentMethod( id: "0", name: "슈퍼은행", digits: "**** 9999", color: "#51AF80FF", isPrimary: false ) let viewModel = SelectedPaymentMethodViewModel(paymentMethod) // when let sut = EnterAmountViewController() sut.updateSelectedPaymentMethod(with: viewModel) // then assertSnapshot(matching: sut, as: .image(on: .iPhoneXsMax)) } func testEnterAmountLoading() { // given let paymentMethod = PaymentMethod( id: "0", name: "슈퍼은행", digits: "**** 9999", color: "#51AF80FF", isPrimary: false ) let viewModel = SelectedPaymentMethodViewModel(paymentMethod) // when let sut = EnterAmountViewController() sut.updateSelectedPaymentMethod(with: viewModel) sut.startLoading() // then assertSnapshot(matching: sut, as: .image(on: .iPhoneXsMax)) } func testEnterAmountStopLoading() { // given let paymentMethod = PaymentMethod( id: "0", name: "슈퍼은행", digits: "**** 9999", color: "#51AF80FF", isPrimary: false ) let viewModel = SelectedPaymentMethodViewModel(paymentMethod) // when let sut = EnterAmountViewController() sut.updateSelectedPaymentMethod(with: viewModel) sut.startLoading() sut.stopLoading() // then assertSnapshot(matching: sut, as: .image(on: .iPhoneXsMax)) } } ================================================ FILE: completed/MiniSuperApp/Finance/Tests/TopupImpTests/Topup/TopupInteractorTests.swift ================================================ @testable import TopupImp import XCTest import TopupTestSupport import FinanceEntity import FinanceRepositoryTestSupport final class TopupInteractorTests: XCTestCase { private var sut: TopupInteractor! private var dependency: TopupDependencyMock! private var listener: TopupListenerMock! private var router: TopupRoutingMock! private var cardOnFileRepository: CardOnFileRepositoryMock { dependency.cardOnFileRepository as! CardOnFileRepositoryMock } override func setUp() { super.setUp() self.dependency = TopupDependencyMock() self.listener = TopupListenerMock() let interactor = TopupInteractor(dependency: self.dependency) self.router = TopupRoutingMock(interactable: interactor) interactor.listener = self.listener interactor.router = self.router self.sut = interactor } // MARK: - Tests func testActivate() { // given let cards = [ PaymentMethod( id: "0", name: "Zero", digits: "0123", color: "", isPrimary: false ) ] cardOnFileRepository.cardOnFileSubject.send(cards) // when sut.activate() // then XCTAssertEqual(router.attachEnterAmountCallCount, 1) XCTAssertEqual(dependency.paymentMethodStream.value.name, "Zero") } func testActivateWithoutCard() { // given cardOnFileRepository.cardOnFileSubject.send([]) // when sut.activate() // then XCTAssertEqual(router.attachAddPaymentMethodCallCount, 1) XCTAssertEqual(router.attachAddPaymentMethodCloseButtonType, .close) } func testDidAddCardWithCard() { // given let cards = [ PaymentMethod( id: "0", name: "Zero", digits: "0123", color: "", isPrimary: false ) ] cardOnFileRepository.cardOnFileSubject.send(cards) let newCard = PaymentMethod( id: "new_card_id", name: "New Card", digits: "0000", color: "", isPrimary: false ) // when sut.activate() sut.addPaymentMethodDidAddCard(paymentMethod: newCard) // then XCTAssertEqual(router.popToRootCallCount, 1) XCTAssertEqual(dependency.paymentMethodStream.value.id, "new_card_id") } func testDidAddCardWithoutCard() { // given cardOnFileRepository.cardOnFileSubject.send([]) let newCard = PaymentMethod( id: "new_card_id", name: "New Card", digits: "0000", color: "", isPrimary: false ) // when sut.activate() sut.addPaymentMethodDidAddCard(paymentMethod: newCard) // then XCTAssertEqual(router.attachEnterAmountCallCount, 1) XCTAssertEqual(dependency.paymentMethodStream.value.id, "new_card_id") } func testAddPaymentMethodDidTapCloseFromEnterAmount() { // given let cards = [ PaymentMethod( id: "0", name: "Zero", digits: "0123", color: "", isPrimary: false ) ] cardOnFileRepository.cardOnFileSubject.send(cards) // when sut.activate() sut.addPaymentMethodDidTapClose() // then XCTAssertEqual(router.detachAddPaymentMethodCallCount, 1) } func testAddPaymentMethodDidTapClose() { // given cardOnFileRepository.cardOnFileSubject.send([]) // when sut.activate() sut.addPaymentMethodDidTapClose() // then XCTAssertEqual(router.detachAddPaymentMethodCallCount, 1) XCTAssertEqual(listener.topupDidCloseCallCount, 1) } func testDidSelectCard() { // given let cards = [ PaymentMethod( id: "0", name: "Zero", digits: "0123", color: "", isPrimary: false ), PaymentMethod( id: "1", name: "One", digits: "1234", color: "", isPrimary: false ) ] cardOnFileRepository.cardOnFileSubject.send(cards) // when sut.cardOnFileDidSelect(at: 0) // then XCTAssertEqual(dependency.paymentMethodStream.value.id, "0") XCTAssertEqual(router.detachCardOnFileCallCount, 1) } func testDidSelectCardWithInvalidIndex() { // given let cards = [ PaymentMethod( id: "0", name: "Zero", digits: "0123", color: "", isPrimary: false ), PaymentMethod( id: "1", name: "One", digits: "1234", color: "", isPrimary: false ) ] cardOnFileRepository.cardOnFileSubject.send(cards) // when sut.cardOnFileDidSelect(at: 2) // then XCTAssertEqual(dependency.paymentMethodStream.value.id, "") XCTAssertEqual(router.detachCardOnFileCallCount, 1) } } ================================================ FILE: completed/MiniSuperApp/Finance/Tests/TopupImpTests/Topup/TopupMock.swift ================================================ import Foundation import FinanceRepository import FinanceRepositoryTestSupport import CombineUtil import FinanceEntity import RIBsUtil import ModernRIBs import Combine import Topup import SuperUI @testable import TopupImp final class TopupDependencyMock: TopupInteractorDependency { var cardOnFileRepository: CardOnFileRepository = CardOnFileRepositoryMock() var paymentMethodStream: CurrentValuePublisher = .init( PaymentMethod(id: "", name: "", digits: "", color: "", isPrimary: false) ) } final class TopupRoutingMock: TopupRouting { var attachAddPaymentMethodCallCount = 0 var attachAddPaymentMethodCloseButtonType: DismissButtonType? func attachAddPaymentMethod(closeButtonType: DismissButtonType) { attachAddPaymentMethodCallCount += 1 attachAddPaymentMethodCloseButtonType = closeButtonType } var detachAddPaymentMethodCallCount = 0 func detachAddPaymentMethod() { detachAddPaymentMethodCallCount += 1 } var attachEnterAmountCallCount = 0 func attachEnterAmount() { attachEnterAmountCallCount += 1 } var detachEnterAmountCallCount = 0 func detachEnterAmount() { detachEnterAmountCallCount += 1 } var attachCardOnFileCallCount = 0 var attachCardOnFileCallCountPaymentMethods: [PaymentMethod]? func attachCardOnFile(paymentMethods: [PaymentMethod]) { attachCardOnFileCallCount += 1 } var detachCardOnFileCallCount = 0 func detachCardOnFile() { detachCardOnFileCallCount += 1 } var popToRootCallCount = 0 func popToRoot() { popToRootCallCount += 1 } // Variables var interactable: Interactable { didSet { interactableSetCallCount += 1 } } var interactableSetCallCount = 0 var children: [Routing] = [Routing]() { didSet { childrenSetCallCount += 1 } } var childrenSetCallCount = 0 var lifecycleSubject = PassthroughSubject() { didSet { lifecycleSubjectSetCallCount += 1 } } var lifecycleSubjectSetCallCount = 0 var lifecycle: AnyPublisher { return lifecycleSubject.eraseToAnyPublisher() } // Function Handlers var loadHandler: (() -> ())? var loadCallCount: Int = 0 var attachChildHandler: ((_ child: Routing) -> ())? var attachChildCallCount: Int = 0 var detachChildHandler: ((_ child: Routing) -> ())? var detachChildCallCount: Int = 0 init( interactable: Interactable ) { self.interactable = interactable } var cleanupViewsCallCount = 0 func cleanupViews() { cleanupViewsCallCount += 1 } func load() { loadCallCount += 1 if let loadHandler = loadHandler { return loadHandler() } } func attachChild(_ child: Routing) { attachChildCallCount += 1 if let attachChildHandler = attachChildHandler { return attachChildHandler(child) } } func detachChild(_ child: Routing) { detachChildCallCount += 1 if let detachChildHandler = detachChildHandler { return detachChildHandler(child) } } } final class TopupInteractableMock: TopupInteractable { var router: TopupRouting? var listener: TopupListener? var presentationDelegateProxy = AdaptivePresentationControllerDelegateProxy() var addPaymentMethodDidTapCloseCallCount = 0 func addPaymentMethodDidTapClose() { addPaymentMethodDidTapCloseCallCount += 1 } var addPaymentMethodDidAddCardCallCount = 0 var addPaymentMethodDidAddCardPaymentMethod: PaymentMethod? func addPaymentMethodDidAddCard(paymentMethod: PaymentMethod) { addPaymentMethodDidAddCardCallCount += 1 addPaymentMethodDidAddCardPaymentMethod = paymentMethod } var enterAmountDidTapCloseCallCount = 0 func enterAmountDidTapClose() { enterAmountDidTapCloseCallCount += 1 } var enterAmountDidTapPaymentMethodCallCount = 0 func enterAmountDidTapPaymentMethod() { enterAmountDidTapPaymentMethodCallCount += 1 } var enterAmountDidFinishTopupCallCount = 0 func enterAmountDidFinishTopup() { enterAmountDidFinishTopupCallCount += 1 } var cardOnFileDidTapCloseCallCount = 0 func cardOnFileDidTapClose() { cardOnFileDidTapCloseCallCount += 1 } var cardOnFileDidTapAddCardCallCount = 0 func cardOnFileDidTapAddCard() { cardOnFileDidTapAddCardCallCount += 1 } var cardOnFileDidSelectCallCount = 0 var cardOnFileDidSelectIndex: Int? func cardOnFileDidSelect(at index: Int) { cardOnFileDidSelectCallCount += 1 cardOnFileDidSelectIndex = index } func activate() { } func deactivate() { } var isActive: Bool { isActiveSubject.value } var isActiveStream: AnyPublisher { isActiveSubject.eraseToAnyPublisher() } private let isActiveSubject = CurrentValueSubject(false) } ================================================ FILE: completed/MiniSuperApp/Finance/Tests/TopupImpTests/Topup/TopupRouterTests.swift ================================================ @testable import TopupImp import XCTest import RIBsTestSupport import AddPaymentMethodTestSupport import ModernRIBs final class TopupRouterTests: XCTestCase { private var sut: TopupRouter! private var interactor: TopupInteractableMock! private var viewController: ViewControllableMock! private var addPaymentMethodBuildable: AddPaymentMethodBuildableMock! private var enterAmountBuildable: EnterAmountBuildableMock! private var cardOnFileBuildable: CardOnFileBuildableMock! override func setUp() { super.setUp() interactor = TopupInteractableMock() viewController = ViewControllableMock() addPaymentMethodBuildable = AddPaymentMethodBuildableMock() enterAmountBuildable = EnterAmountBuildableMock() cardOnFileBuildable = CardOnFileBuildableMock() sut = TopupRouter( interactor: interactor, viewController: viewController, addPaymentMethodBuildable: addPaymentMethodBuildable, enterAmountBuildable: enterAmountBuildable, cardOnFileBuildable: cardOnFileBuildable ) } // MARK: - Tests func testAttachAddPaymentMethod() { // given // when sut.attachAddPaymentMethod(closeButtonType: .close) // then XCTAssertEqual(addPaymentMethodBuildable.buildCallCount, 1) XCTAssertEqual(addPaymentMethodBuildable.closeButtonType, .close) XCTAssertEqual(viewController.presentCallCount, 1) } func testAttachEnterAmount() { // given let router = EnterAmountRoutingMock( interactable: Interactor(), viewControllable: ViewControllableMock() ) var assignedListener: EnterAmountListener? enterAmountBuildable.buildHandler = { listener in assignedListener = listener return router } // when sut.attachEnterAmount() // then XCTAssertTrue(assignedListener === interactor) XCTAssertEqual(enterAmountBuildable.buildCallCount, 1) } func testAttachEnterAmountOnNavigation() { // given let router = EnterAmountRoutingMock( interactable: Interactor(), viewControllable: ViewControllableMock() ) var assignedListener: EnterAmountListener? enterAmountBuildable.buildHandler = { listener in assignedListener = listener return router } // when sut.attachAddPaymentMethod(closeButtonType: .close) sut.attachEnterAmount() // then XCTAssertTrue(assignedListener === interactor) XCTAssertEqual(enterAmountBuildable.buildCallCount, 1) XCTAssertEqual(viewController.presentCallCount, 1) XCTAssertEqual(sut.children.count, 1) } } ================================================ FILE: completed/MiniSuperApp/MiniSuperApp/AppDelegate/AppComponent.swift ================================================ import Foundation import ModernRIBs final class AppComponent: Component, AppRootDependency { init() { super.init(dependency: EmptyComponent()) } } ================================================ FILE: completed/MiniSuperApp/MiniSuperApp/AppDelegate/AppDelegate.swift ================================================ import UIKit import ModernRIBs @main class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? private var launchRouter: LaunchRouting? private var urlHandler: URLHandler? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let window = UIWindow(frame: UIScreen.main.bounds) self.window = window let result = AppRootBuilder(dependency: AppComponent()).build() self.launchRouter = result.launchRouter self.urlHandler = result.urlHandler launchRouter?.launch(from: window) return true } } protocol URLHandler: AnyObject { func handle(_ url: URL) } ================================================ FILE: completed/MiniSuperApp/MiniSuperApp/AppRoot/AppRootBuilder.swift ================================================ import ModernRIBs import UIKit import FinanceRepository import FinanceHome import AppHome import ProfileHome protocol AppRootDependency: Dependency { } // MARK: - Builder protocol AppRootBuildable: Buildable { func build() -> (launchRouter: LaunchRouting, urlHandler: URLHandler) } final class AppRootBuilder: Builder, AppRootBuildable { override init(dependency: AppRootDependency) { super.init(dependency: dependency) } func build() -> (launchRouter: LaunchRouting, urlHandler: URLHandler) { let tabBar = RootTabBarController() let component = AppRootComponent( dependency: dependency, rootViewController: tabBar ) let interactor = AppRootInteractor(presenter: tabBar) let appHome = AppHomeBuilder(dependency: component) let financeHome = FinanceHomeBuilder(dependency: component) let profileHome = ProfileHomeBuilder(dependency: component) let router = AppRootRouter( interactor: interactor, viewController: tabBar, appHome: appHome, financeHome: financeHome, profileHome: profileHome ) return (router, interactor) } } ================================================ FILE: completed/MiniSuperApp/MiniSuperApp/AppRoot/AppRootComponent.swift ================================================ import Foundation import AppHome import FinanceHome import ProfileHome import FinanceRepository import ModernRIBs import TransportHome import TransportHomeImp import Topup import TopupImp import AddPaymentMethod import AddPaymentMethodImp import Network import NetworkImp import CombineSchedulers final class AppRootComponent: Component, AppHomeDependency, FinanceHomeDependency, ProfileHomeDependency, TransportHomeDependency, TopupDependency, AddPaymentMethodDependency { var cardOnFileRepository: CardOnFileRepository var superPayRepository: SuperPayRepository var mainQueue: AnySchedulerOf { .main } lazy var transportHomeBuildable: TransportHomeBuildable = { return TransportHomeBuilder(dependency: self) }() lazy var topupBuildable: TopupBuildable = { return TopupBuilder(dependency: self) }() lazy var addPaymentMethodBuildable: AddPaymentMethodBuildable = { return AddPaymentMethodBuilder(dependency: self) }() var topupBaseViewController: ViewControllable { rootViewController.topViewControllable } private let rootViewController: ViewControllable init( dependency: AppRootDependency, rootViewController: ViewControllable ) { #if UITESTING let config = URLSessionConfiguration.default #else let config = URLSessionConfiguration.ephemeral config.protocolClasses = [SuperAppURLProtocol.self] setupURLProtocol() #endif let network = NetworkImp(session: URLSession(configuration: config)) self.cardOnFileRepository = CardOnFileRepositoryImp(network: network, baseURL: BaseURL().financeBaseURL) self.cardOnFileRepository.fetch() self.superPayRepository = SuperPayRepositoryImp(network: network, baseURL: BaseURL().financeBaseURL) self.rootViewController = rootViewController super.init(dependency: dependency) } } ================================================ FILE: completed/MiniSuperApp/MiniSuperApp/AppRoot/AppRootInteractor.swift ================================================ import Foundation import ModernRIBs protocol AppRootRouting: ViewableRouting { func attachTabs() } protocol AppRootPresentable: Presentable { var listener: AppRootPresentableListener? { get set } } protocol AppRootListener: AnyObject { } final class AppRootInteractor: PresentableInteractor, AppRootInteractable, AppRootPresentableListener, URLHandler { weak var router: AppRootRouting? weak var listener: AppRootListener? override init(presenter: AppRootPresentable) { super.init(presenter: presenter) presenter.listener = self } override func didBecomeActive() { super.didBecomeActive() router?.attachTabs() } override func willResignActive() { super.willResignActive() } func handle(_ url: URL) { } } ================================================ FILE: completed/MiniSuperApp/MiniSuperApp/AppRoot/AppRootRouter.swift ================================================ import ModernRIBs import RIBsUtil import FinanceHome import AppHome import ProfileHome protocol AppRootInteractable: Interactable, AppHomeListener, FinanceHomeListener, ProfileHomeListener { var router: AppRootRouting? { get set } var listener: AppRootListener? { get set } } protocol AppRootViewControllable: ViewControllable { func setViewControllers(_ viewControllers: [ViewControllable]) } final class AppRootRouter: LaunchRouter, AppRootRouting { private let appHome: AppHomeBuildable private let financeHome: FinanceHomeBuildable private let profileHome: ProfileHomeBuildable private var appHomeRouting: ViewableRouting? private var financeHomeRouting: ViewableRouting? private var profileHomeRouting: ViewableRouting? init( interactor: AppRootInteractable, viewController: AppRootViewControllable, appHome: AppHomeBuildable, financeHome: FinanceHomeBuildable, profileHome: ProfileHomeBuildable ) { self.appHome = appHome self.financeHome = financeHome self.profileHome = profileHome super.init(interactor: interactor, viewController: viewController) interactor.router = self } func attachTabs() { let appHomeRouting = appHome.build(withListener: interactor) let financeHomeRouting = financeHome.build(withListener: interactor) let profileHomeRouting = profileHome.build(withListener: interactor) attachChild(appHomeRouting) attachChild(financeHomeRouting) attachChild(profileHomeRouting) let viewControllers = [ NavigationControllerable(root: appHomeRouting.viewControllable), NavigationControllerable(root: financeHomeRouting.viewControllable), profileHomeRouting.viewControllable ] viewController.setViewControllers(viewControllers) } } ================================================ FILE: completed/MiniSuperApp/MiniSuperApp/AppRoot/BaseURL.swift ================================================ import Foundation struct BaseURL { var financeBaseURL: URL { #if UITESTING return URL(string: "http://localhost:8080")! #else return URL(string: "https://finance.superapp.com/api/v1")! #endif } } ================================================ FILE: completed/MiniSuperApp/MiniSuperApp/AppRoot/RootTabBarController.swift ================================================ import UIKit import ModernRIBs protocol AppRootPresentableListener: AnyObject { } final class RootTabBarController: UITabBarController, AppRootViewControllable, AppRootPresentable { weak var listener: AppRootPresentableListener? override func viewDidLoad() { super.viewDidLoad() tabBar.isTranslucent = false tabBar.tintColor = .black tabBar.backgroundColor = .white } func setViewControllers(_ viewControllers: [ViewControllable]) { super.setViewControllers(viewControllers.map(\.uiviewController), animated: false) } } ================================================ FILE: completed/MiniSuperApp/MiniSuperApp/AppRoot/SetupURLProtocol.swift ================================================ import Foundation func setupURLProtocol() { // Topup let topupResponse: [String: Any] = [ "status": "success" ] let topupResponseData = try! JSONSerialization.data(withJSONObject: topupResponse, options: []) // AddCard let addCardResponse: [String: Any] = [ "card": [ "id": "999", "name": "새 카드", "digits": "**** 0101", "color": "", "isPrimary": false ] ] let addCardResponseData = try! JSONSerialization.data(withJSONObject: addCardResponse, options: []) // CardOnFile let cardOnFileResponse: [String: Any] = [ "cards": [ [ "id": "0", "name": "우리은행", "digits": "0123", "color": "#f19a38ff", "isPrimary": false ], [ "id": "1", "name": "신한카드", "digits": "0987", "color": "#3478f6ff", "isPrimary": false ], // [ // "id": "3", // "name": "국민은행", // "digits": "2812", // "color": "#65c466ff", // "isPrimary": false // ] ] ] let cardOnFileResponseData = try! JSONSerialization.data(withJSONObject: cardOnFileResponse, options: []) SuperAppURLProtocol.successMock = [ "/api/v1/topup": (200, topupResponseData), "/api/v1/addCard": (200, addCardResponseData), "/api/v1/cards": (200, cardOnFileResponseData) ] } ================================================ FILE: completed/MiniSuperApp/MiniSuperApp/AppRoot/SuperAppURLProtocol.swift ================================================ import Foundation typealias Path = String typealias MockResponse = (statusCode: Int, data: Data?) final class SuperAppURLProtocol: URLProtocol { static var successMock: [Path: MockResponse] = [:] static var failureErrors: [Path: Error] = [:] override class func canInit(with request: URLRequest) -> Bool { return true } override class func canonicalRequest(for request: URLRequest) -> URLRequest { return request } override func startLoading() { if let path = request.url?.path { if let mockResponse = SuperAppURLProtocol.successMock[path] { client?.urlProtocol(self, didReceive: HTTPURLResponse( url: request.url!, statusCode: mockResponse.statusCode, httpVersion: nil, headerFields: nil)!, cacheStoragePolicy: .notAllowed) mockResponse.data.map { client?.urlProtocol(self, didLoad: $0) } } else if let error = SuperAppURLProtocol.failureErrors[path] { client?.urlProtocol(self, didFailWithError: error) } else { client?.urlProtocol(self, didFailWithError: MockSessionError.notSupported) } } client?.urlProtocolDidFinishLoading(self) } override func stopLoading() { } } enum MockSessionError: Error { case notSupported } ================================================ FILE: completed/MiniSuperApp/MiniSuperApp/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: completed/MiniSuperApp/MiniSuperApp/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { "idiom" : "iphone", "scale" : "3x", "size" : "29x29" }, { "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { "idiom" : "iphone", "scale" : "3x", "size" : "60x60" }, { "idiom" : "ipad", "scale" : "1x", "size" : "20x20" }, { "idiom" : "ipad", "scale" : "2x", "size" : "20x20" }, { "idiom" : "ipad", "scale" : "1x", "size" : "29x29" }, { "idiom" : "ipad", "scale" : "2x", "size" : "29x29" }, { "idiom" : "ipad", "scale" : "1x", "size" : "40x40" }, { "idiom" : "ipad", "scale" : "2x", "size" : "40x40" }, { "idiom" : "ipad", "scale" : "1x", "size" : "76x76" }, { "idiom" : "ipad", "scale" : "2x", "size" : "76x76" }, { "idiom" : "ipad", "scale" : "2x", "size" : "83.5x83.5" }, { "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: completed/MiniSuperApp/MiniSuperApp/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: completed/MiniSuperApp/MiniSuperApp/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: completed/MiniSuperApp/MiniSuperApp/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString 1.0 CFBundleVersion 1 LSRequiresIPhoneOS NSAppTransportSecurity NSAllowsLocalNetworking UIApplicationSupportsIndirectInputEvents UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: completed/MiniSuperApp/MiniSuperApp/Utils/Array+Utils.swift ================================================ import Foundation extension Array { subscript(safe index: Int) -> Element? { return indices ~= index ? self[index] : nil } } ================================================ FILE: completed/MiniSuperApp/MiniSuperApp.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 52; objects = { /* Begin PBXBuildFile section */ F505938C27114F880087571C /* AddPaymentMethod in Frameworks */ = {isa = PBXBuildFile; productRef = F505938B27114F880087571C /* AddPaymentMethod */; }; F530665527117188007DAE42 /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = F530665427117188007DAE42 /* Network */; }; F530665727117188007DAE42 /* NetworkImp in Frameworks */ = {isa = PBXBuildFile; productRef = F530665627117188007DAE42 /* NetworkImp */; }; F5306659271171A6007DAE42 /* BaseURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5306658271171A6007DAE42 /* BaseURL.swift */; }; F530665B27117244007DAE42 /* SuperAppURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F530665A27117244007DAE42 /* SuperAppURLProtocol.swift */; }; F530665D271173B0007DAE42 /* SetupURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = F530665C271173B0007DAE42 /* SetupURLProtocol.swift */; }; F53FF07D271156E4004F3972 /* FinanceEntity in Frameworks */ = {isa = PBXBuildFile; productRef = F53FF07C271156E4004F3972 /* FinanceEntity */; }; F53FF07F271156E5004F3972 /* FinanceRepository in Frameworks */ = {isa = PBXBuildFile; productRef = F53FF07E271156E5004F3972 /* FinanceRepository */; }; F53FF081271156E5004F3972 /* CombineUtil in Frameworks */ = {isa = PBXBuildFile; productRef = F53FF080271156E5004F3972 /* CombineUtil */; }; F53FF083271156E5004F3972 /* RIBsUtil in Frameworks */ = {isa = PBXBuildFile; productRef = F53FF082271156E5004F3972 /* RIBsUtil */; }; F53FF085271156E5004F3972 /* SuperUI in Frameworks */ = {isa = PBXBuildFile; productRef = F53FF084271156E5004F3972 /* SuperUI */; }; F53FF087271158C1004F3972 /* Topup in Frameworks */ = {isa = PBXBuildFile; productRef = F53FF086271158C1004F3972 /* Topup */; }; F542F83227115B9500085E91 /* FinanceHome in Frameworks */ = {isa = PBXBuildFile; productRef = F542F83127115B9500085E91 /* FinanceHome */; }; F542F83627115C3200085E91 /* TransportHome in Frameworks */ = {isa = PBXBuildFile; productRef = F542F83527115C3200085E91 /* TransportHome */; }; F542F83827115D4D00085E91 /* AppHome in Frameworks */ = {isa = PBXBuildFile; productRef = F542F83727115D4D00085E91 /* AppHome */; }; F542F83A27115ED500085E91 /* ProfileHome in Frameworks */ = {isa = PBXBuildFile; productRef = F542F83927115ED500085E91 /* ProfileHome */; }; F542F83C271164F100085E91 /* AppRootComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = F542F83B271164F100085E91 /* AppRootComponent.swift */; }; F542F83E2711653900085E91 /* TransportHomeImp in Frameworks */ = {isa = PBXBuildFile; productRef = F542F83D2711653900085E91 /* TransportHomeImp */; }; F542F840271167EA00085E91 /* TopupImp in Frameworks */ = {isa = PBXBuildFile; productRef = F542F83F271167EA00085E91 /* TopupImp */; }; F54E9D9327116A42008D50BF /* AddPaymentMethodImp in Frameworks */ = {isa = PBXBuildFile; productRef = F54E9D9227116A42008D50BF /* AddPaymentMethodImp */; }; F55A6E0D27127BE300DD30F7 /* AddPaymentMethod in Frameworks */ = {isa = PBXBuildFile; productRef = F55A6E0C27127BE300DD30F7 /* AddPaymentMethod */; }; F55A6E0F27127BE300DD30F7 /* AddPaymentMethodImp in Frameworks */ = {isa = PBXBuildFile; productRef = F55A6E0E27127BE300DD30F7 /* AddPaymentMethodImp */; }; F55A6E1127127BE300DD30F7 /* AddPaymentMethodTestSupport in Frameworks */ = {isa = PBXBuildFile; productRef = F55A6E1027127BE300DD30F7 /* AddPaymentMethodTestSupport */; }; F55A6E1327127C3000DD30F7 /* FinanceRepository in Frameworks */ = {isa = PBXBuildFile; productRef = F55A6E1227127C3000DD30F7 /* FinanceRepository */; }; F55A6E1527127C3000DD30F7 /* FinanceRepositoryTestSupport in Frameworks */ = {isa = PBXBuildFile; productRef = F55A6E1427127C3000DD30F7 /* FinanceRepositoryTestSupport */; }; F57F6AFE26DB4CD700C0117D /* RootTabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F57F6AFD26DB4CD700C0117D /* RootTabBarController.swift */; }; F5829F5926DB2BC400BFA8CD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5829F5826DB2BC400BFA8CD /* AppDelegate.swift */; }; F5829F6226DB2BC500BFA8CD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F5829F6126DB2BC500BFA8CD /* Assets.xcassets */; }; F5829F6526DB2BC500BFA8CD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F5829F6326DB2BC500BFA8CD /* LaunchScreen.storyboard */; }; F5829F6E26DB2DE900BFA8CD /* ModernRIBs in Frameworks */ = {isa = PBXBuildFile; productRef = F5829F6D26DB2DE900BFA8CD /* ModernRIBs */; }; F5829F7426DB34AA00BFA8CD /* AppRootRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5829F7026DB34AA00BFA8CD /* AppRootRouter.swift */; }; F5829F7626DB34AA00BFA8CD /* AppRootBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5829F7226DB34AA00BFA8CD /* AppRootBuilder.swift */; }; F5829F7726DB34AA00BFA8CD /* AppRootInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5829F7326DB34AA00BFA8CD /* AppRootInteractor.swift */; }; F5829F7926DB397300BFA8CD /* AppComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5829F7826DB397300BFA8CD /* AppComponent.swift */; }; F58BACF2270BF21100E64072 /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = F58BACF1270BF21100E64072 /* CombineExt */; }; F58BAD60270C2C9F00E64072 /* Array+Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F58BAD5F270C2C9F00E64072 /* Array+Utils.swift */; }; F5E29F55271271F700A9EF5F /* TopupImpUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E29F54271271F700A9EF5F /* TopupImpUITests.swift */; }; F5E29F5A271273DB00A9EF5F /* PlatformTestSupport in Frameworks */ = {isa = PBXBuildFile; productRef = F5E29F59271273DB00A9EF5F /* PlatformTestSupport */; }; F5E29F5D2712744700A9EF5F /* cardOnFile.json in Resources */ = {isa = PBXBuildFile; fileRef = F5E29F5C2712744700A9EF5F /* cardOnFile.json */; }; F5E29F5F271274C100A9EF5F /* TestUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E29F5E271274C100A9EF5F /* TestUtil.swift */; }; F5E29F612712751B00A9EF5F /* topupSuccessResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = F5E29F602712751B00A9EF5F /* topupSuccessResponse.json */; }; F5E29FAB27127A9200A9EF5F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E29FAA27127A9200A9EF5F /* AppDelegate.swift */; }; F5E29FB427127A9400A9EF5F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F5E29FB327127A9400A9EF5F /* Assets.xcassets */; }; F5E29FB727127A9400A9EF5F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F5E29FB527127A9400A9EF5F /* LaunchScreen.storyboard */; }; F5E29FC427127AE700A9EF5F /* AddPaymentMethodIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5E29FC327127AE700A9EF5F /* AddPaymentMethodIntegrationTests.swift */; }; F5E29FCC27127B2800A9EF5F /* PlatformTestSupport in Frameworks */ = {isa = PBXBuildFile; productRef = F5E29FCB27127B2800A9EF5F /* PlatformTestSupport */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ F58CE267271271310033575A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F5829F4D26DB2BC400BFA8CD /* Project object */; proxyType = 1; remoteGlobalIDString = F5829F5426DB2BC400BFA8CD; remoteInfo = MiniSuperApp; }; F5E29FC527127AE700A9EF5F /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F5829F4D26DB2BC400BFA8CD /* Project object */; proxyType = 1; remoteGlobalIDString = F5E29FA727127A9100A9EF5F; remoteInfo = TestHost; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ F505938527114E3C0087571C /* CX */ = {isa = PBXFileReference; lastKnownFileType = folder; path = CX; sourceTree = ""; }; F505938627114E4A0087571C /* Finance */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Finance; sourceTree = ""; }; F505938727114E560087571C /* Transport */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Transport; sourceTree = ""; }; F505938827114E650087571C /* Profile */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Profile; sourceTree = ""; }; F505938927114E8F0087571C /* Platform */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Platform; sourceTree = ""; }; F5306658271171A6007DAE42 /* BaseURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseURL.swift; sourceTree = ""; }; F530665A27117244007DAE42 /* SuperAppURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuperAppURLProtocol.swift; sourceTree = ""; }; F530665C271173B0007DAE42 /* SetupURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupURLProtocol.swift; sourceTree = ""; }; F542F83B271164F100085E91 /* AppRootComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRootComponent.swift; sourceTree = ""; }; F57F6AFD26DB4CD700C0117D /* RootTabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootTabBarController.swift; sourceTree = ""; }; F5829F5526DB2BC400BFA8CD /* MiniSuperApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MiniSuperApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; F5829F5826DB2BC400BFA8CD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F5829F6126DB2BC500BFA8CD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F5829F6426DB2BC500BFA8CD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; F5829F6626DB2BC500BFA8CD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F5829F7026DB34AA00BFA8CD /* AppRootRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRootRouter.swift; sourceTree = ""; }; F5829F7226DB34AA00BFA8CD /* AppRootBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRootBuilder.swift; sourceTree = ""; }; F5829F7326DB34AA00BFA8CD /* AppRootInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRootInteractor.swift; sourceTree = ""; }; F5829F7826DB397300BFA8CD /* AppComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppComponent.swift; sourceTree = ""; }; F58BAD5F270C2C9F00E64072 /* Array+Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Utils.swift"; sourceTree = ""; }; F58CE261271271310033575A /* MiniSuperAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MiniSuperAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F5E29F54271271F700A9EF5F /* TopupImpUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopupImpUITests.swift; sourceTree = ""; }; F5E29F5C2712744700A9EF5F /* cardOnFile.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = cardOnFile.json; sourceTree = ""; }; F5E29F5E271274C100A9EF5F /* TestUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtil.swift; sourceTree = ""; }; F5E29F602712751B00A9EF5F /* topupSuccessResponse.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = topupSuccessResponse.json; sourceTree = ""; }; F5E29FA827127A9100A9EF5F /* TestHost.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestHost.app; sourceTree = BUILT_PRODUCTS_DIR; }; F5E29FAA27127A9200A9EF5F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; F5E29FB327127A9400A9EF5F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F5E29FB627127A9400A9EF5F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; F5E29FB827127A9400A9EF5F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F5E29FC127127AE700A9EF5F /* AddPaymentMethodIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AddPaymentMethodIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; F5E29FC327127AE700A9EF5F /* AddPaymentMethodIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddPaymentMethodIntegrationTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ F5829F5226DB2BC400BFA8CD /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( F58BACF2270BF21100E64072 /* CombineExt in Frameworks */, F54E9D9327116A42008D50BF /* AddPaymentMethodImp in Frameworks */, F505938C27114F880087571C /* AddPaymentMethod in Frameworks */, F53FF081271156E5004F3972 /* CombineUtil in Frameworks */, F530665727117188007DAE42 /* NetworkImp in Frameworks */, F542F83A27115ED500085E91 /* ProfileHome in Frameworks */, F542F83E2711653900085E91 /* TransportHomeImp in Frameworks */, F542F840271167EA00085E91 /* TopupImp in Frameworks */, F53FF087271158C1004F3972 /* Topup in Frameworks */, F53FF07D271156E4004F3972 /* FinanceEntity in Frameworks */, F53FF085271156E5004F3972 /* SuperUI in Frameworks */, F542F83627115C3200085E91 /* TransportHome in Frameworks */, F5829F6E26DB2DE900BFA8CD /* ModernRIBs in Frameworks */, F53FF083271156E5004F3972 /* RIBsUtil in Frameworks */, F530665527117188007DAE42 /* Network in Frameworks */, F542F83827115D4D00085E91 /* AppHome in Frameworks */, F542F83227115B9500085E91 /* FinanceHome in Frameworks */, F53FF07F271156E5004F3972 /* FinanceRepository in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; F58CE25E271271310033575A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( F5E29F5A271273DB00A9EF5F /* PlatformTestSupport in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; F5E29FA527127A9100A9EF5F /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; F5E29FBE27127AE700A9EF5F /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( F55A6E1127127BE300DD30F7 /* AddPaymentMethodTestSupport in Frameworks */, F55A6E1527127C3000DD30F7 /* FinanceRepositoryTestSupport in Frameworks */, F55A6E0F27127BE300DD30F7 /* AddPaymentMethodImp in Frameworks */, F55A6E0D27127BE300DD30F7 /* AddPaymentMethod in Frameworks */, F5E29FCC27127B2800A9EF5F /* PlatformTestSupport in Frameworks */, F55A6E1327127C3000DD30F7 /* FinanceRepository in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ F505938A27114F880087571C /* Frameworks */ = { isa = PBXGroup; children = ( ); name = Frameworks; sourceTree = ""; }; F57F6AFF26DB585100C0117D /* Utils */ = { isa = PBXGroup; children = ( F58BAD5F270C2C9F00E64072 /* Array+Utils.swift */, ); path = Utils; sourceTree = ""; }; F5829F4C26DB2BC400BFA8CD = { isa = PBXGroup; children = ( F505938927114E8F0087571C /* Platform */, F505938827114E650087571C /* Profile */, F505938727114E560087571C /* Transport */, F505938627114E4A0087571C /* Finance */, F505938527114E3C0087571C /* CX */, F5829F5726DB2BC400BFA8CD /* MiniSuperApp */, F58CE262271271310033575A /* MiniSuperAppUITests */, F5E29FA927127A9100A9EF5F /* TestHost */, F5E29FC227127AE700A9EF5F /* AddPaymentMethodIntegrationTests */, F5829F5626DB2BC400BFA8CD /* Products */, F505938A27114F880087571C /* Frameworks */, ); sourceTree = ""; }; F5829F5626DB2BC400BFA8CD /* Products */ = { isa = PBXGroup; children = ( F5829F5526DB2BC400BFA8CD /* MiniSuperApp.app */, F58CE261271271310033575A /* MiniSuperAppUITests.xctest */, F5E29FA827127A9100A9EF5F /* TestHost.app */, F5E29FC127127AE700A9EF5F /* AddPaymentMethodIntegrationTests.xctest */, ); name = Products; sourceTree = ""; }; F5829F5726DB2BC400BFA8CD /* MiniSuperApp */ = { isa = PBXGroup; children = ( F5829F7A26DB3AF900BFA8CD /* AppDelegate */, F5829F6F26DB33CF00BFA8CD /* AppRoot */, F57F6AFF26DB585100C0117D /* Utils */, F5829F6126DB2BC500BFA8CD /* Assets.xcassets */, F5829F6326DB2BC500BFA8CD /* LaunchScreen.storyboard */, F5829F6626DB2BC500BFA8CD /* Info.plist */, ); path = MiniSuperApp; sourceTree = ""; }; F5829F6F26DB33CF00BFA8CD /* AppRoot */ = { isa = PBXGroup; children = ( F5829F7026DB34AA00BFA8CD /* AppRootRouter.swift */, F5829F7226DB34AA00BFA8CD /* AppRootBuilder.swift */, F5829F7326DB34AA00BFA8CD /* AppRootInteractor.swift */, F57F6AFD26DB4CD700C0117D /* RootTabBarController.swift */, F542F83B271164F100085E91 /* AppRootComponent.swift */, F5306658271171A6007DAE42 /* BaseURL.swift */, F530665A27117244007DAE42 /* SuperAppURLProtocol.swift */, F530665C271173B0007DAE42 /* SetupURLProtocol.swift */, ); path = AppRoot; sourceTree = ""; }; F5829F7A26DB3AF900BFA8CD /* AppDelegate */ = { isa = PBXGroup; children = ( F5829F5826DB2BC400BFA8CD /* AppDelegate.swift */, F5829F7826DB397300BFA8CD /* AppComponent.swift */, ); path = AppDelegate; sourceTree = ""; }; F58CE262271271310033575A /* MiniSuperAppUITests */ = { isa = PBXGroup; children = ( F5E29F5B2712743300A9EF5F /* Response */, F5E29F54271271F700A9EF5F /* TopupImpUITests.swift */, F5E29F5E271274C100A9EF5F /* TestUtil.swift */, ); path = MiniSuperAppUITests; sourceTree = ""; }; F5E29F5B2712743300A9EF5F /* Response */ = { isa = PBXGroup; children = ( F5E29F5C2712744700A9EF5F /* cardOnFile.json */, F5E29F602712751B00A9EF5F /* topupSuccessResponse.json */, ); path = Response; sourceTree = ""; }; F5E29FA927127A9100A9EF5F /* TestHost */ = { isa = PBXGroup; children = ( F5E29FAA27127A9200A9EF5F /* AppDelegate.swift */, F5E29FB327127A9400A9EF5F /* Assets.xcassets */, F5E29FB527127A9400A9EF5F /* LaunchScreen.storyboard */, F5E29FB827127A9400A9EF5F /* Info.plist */, ); path = TestHost; sourceTree = ""; }; F5E29FC227127AE700A9EF5F /* AddPaymentMethodIntegrationTests */ = { isa = PBXGroup; children = ( F5E29FC327127AE700A9EF5F /* AddPaymentMethodIntegrationTests.swift */, ); path = AddPaymentMethodIntegrationTests; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ F5829F5426DB2BC400BFA8CD /* MiniSuperApp */ = { isa = PBXNativeTarget; buildConfigurationList = F5829F6926DB2BC500BFA8CD /* Build configuration list for PBXNativeTarget "MiniSuperApp" */; buildPhases = ( F5829F5126DB2BC400BFA8CD /* Sources */, F5829F5226DB2BC400BFA8CD /* Frameworks */, F5829F5326DB2BC400BFA8CD /* Resources */, ); buildRules = ( ); dependencies = ( ); name = MiniSuperApp; packageProductDependencies = ( F5829F6D26DB2DE900BFA8CD /* ModernRIBs */, F58BACF1270BF21100E64072 /* CombineExt */, F505938B27114F880087571C /* AddPaymentMethod */, F53FF07C271156E4004F3972 /* FinanceEntity */, F53FF07E271156E5004F3972 /* FinanceRepository */, F53FF080271156E5004F3972 /* CombineUtil */, F53FF082271156E5004F3972 /* RIBsUtil */, F53FF084271156E5004F3972 /* SuperUI */, F53FF086271158C1004F3972 /* Topup */, F542F83127115B9500085E91 /* FinanceHome */, F542F83527115C3200085E91 /* TransportHome */, F542F83727115D4D00085E91 /* AppHome */, F542F83927115ED500085E91 /* ProfileHome */, F542F83D2711653900085E91 /* TransportHomeImp */, F542F83F271167EA00085E91 /* TopupImp */, F54E9D9227116A42008D50BF /* AddPaymentMethodImp */, F530665427117188007DAE42 /* Network */, F530665627117188007DAE42 /* NetworkImp */, ); productName = SuperRedApp; productReference = F5829F5526DB2BC400BFA8CD /* MiniSuperApp.app */; productType = "com.apple.product-type.application"; }; F58CE260271271310033575A /* MiniSuperAppUITests */ = { isa = PBXNativeTarget; buildConfigurationList = F58CE26B271271310033575A /* Build configuration list for PBXNativeTarget "MiniSuperAppUITests" */; buildPhases = ( F58CE25D271271310033575A /* Sources */, F58CE25E271271310033575A /* Frameworks */, F58CE25F271271310033575A /* Resources */, ); buildRules = ( ); dependencies = ( F58CE268271271310033575A /* PBXTargetDependency */, ); name = MiniSuperAppUITests; packageProductDependencies = ( F5E29F59271273DB00A9EF5F /* PlatformTestSupport */, ); productName = MiniSuperAppUITests; productReference = F58CE261271271310033575A /* MiniSuperAppUITests.xctest */; productType = "com.apple.product-type.bundle.ui-testing"; }; F5E29FA727127A9100A9EF5F /* TestHost */ = { isa = PBXNativeTarget; buildConfigurationList = F5E29FB927127A9400A9EF5F /* Build configuration list for PBXNativeTarget "TestHost" */; buildPhases = ( F5E29FA427127A9100A9EF5F /* Sources */, F5E29FA527127A9100A9EF5F /* Frameworks */, F5E29FA627127A9100A9EF5F /* Resources */, ); buildRules = ( ); dependencies = ( ); name = TestHost; productName = TestHost; productReference = F5E29FA827127A9100A9EF5F /* TestHost.app */; productType = "com.apple.product-type.application"; }; F5E29FC027127AE700A9EF5F /* AddPaymentMethodIntegrationTests */ = { isa = PBXNativeTarget; buildConfigurationList = F5E29FC727127AE700A9EF5F /* Build configuration list for PBXNativeTarget "AddPaymentMethodIntegrationTests" */; buildPhases = ( F5E29FBD27127AE700A9EF5F /* Sources */, F5E29FBE27127AE700A9EF5F /* Frameworks */, F5E29FBF27127AE700A9EF5F /* Resources */, ); buildRules = ( ); dependencies = ( F5E29FC627127AE700A9EF5F /* PBXTargetDependency */, ); name = AddPaymentMethodIntegrationTests; packageProductDependencies = ( F5E29FCB27127B2800A9EF5F /* PlatformTestSupport */, F55A6E0C27127BE300DD30F7 /* AddPaymentMethod */, F55A6E0E27127BE300DD30F7 /* AddPaymentMethodImp */, F55A6E1027127BE300DD30F7 /* AddPaymentMethodTestSupport */, F55A6E1227127C3000DD30F7 /* FinanceRepository */, F55A6E1427127C3000DD30F7 /* FinanceRepositoryTestSupport */, ); productName = AddPaymentMethodIntegrationTests; productReference = F5E29FC127127AE700A9EF5F /* AddPaymentMethodIntegrationTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ F5829F4D26DB2BC400BFA8CD /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1300; LastUpgradeCheck = 1250; TargetAttributes = { F5829F5426DB2BC400BFA8CD = { CreatedOnToolsVersion = 12.5.1; }; F58CE260271271310033575A = { CreatedOnToolsVersion = 13.0; TestTargetID = F5829F5426DB2BC400BFA8CD; }; F5E29FA727127A9100A9EF5F = { CreatedOnToolsVersion = 13.0; }; F5E29FC027127AE700A9EF5F = { CreatedOnToolsVersion = 13.0; TestTargetID = F5E29FA727127A9100A9EF5F; }; }; }; buildConfigurationList = F5829F5026DB2BC400BFA8CD /* Build configuration list for PBXProject "MiniSuperApp" */; compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = F5829F4C26DB2BC400BFA8CD; packageReferences = ( F5829F6C26DB2DE900BFA8CD /* XCRemoteSwiftPackageReference "ModernRIBs" */, F58BACF0270BF21100E64072 /* XCRemoteSwiftPackageReference "CombineExt" */, ); productRefGroup = F5829F5626DB2BC400BFA8CD /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( F5829F5426DB2BC400BFA8CD /* MiniSuperApp */, F58CE260271271310033575A /* MiniSuperAppUITests */, F5E29FA727127A9100A9EF5F /* TestHost */, F5E29FC027127AE700A9EF5F /* AddPaymentMethodIntegrationTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ F5829F5326DB2BC400BFA8CD /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( F5829F6526DB2BC500BFA8CD /* LaunchScreen.storyboard in Resources */, F5829F6226DB2BC500BFA8CD /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; F58CE25F271271310033575A /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( F5E29F5D2712744700A9EF5F /* cardOnFile.json in Resources */, F5E29F612712751B00A9EF5F /* topupSuccessResponse.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; F5E29FA627127A9100A9EF5F /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( F5E29FB727127A9400A9EF5F /* LaunchScreen.storyboard in Resources */, F5E29FB427127A9400A9EF5F /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; F5E29FBF27127AE700A9EF5F /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ F5829F5126DB2BC400BFA8CD /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F5829F7626DB34AA00BFA8CD /* AppRootBuilder.swift in Sources */, F530665D271173B0007DAE42 /* SetupURLProtocol.swift in Sources */, F530665B27117244007DAE42 /* SuperAppURLProtocol.swift in Sources */, F542F83C271164F100085E91 /* AppRootComponent.swift in Sources */, F5829F7726DB34AA00BFA8CD /* AppRootInteractor.swift in Sources */, F57F6AFE26DB4CD700C0117D /* RootTabBarController.swift in Sources */, F5829F5926DB2BC400BFA8CD /* AppDelegate.swift in Sources */, F5829F7926DB397300BFA8CD /* AppComponent.swift in Sources */, F5829F7426DB34AA00BFA8CD /* AppRootRouter.swift in Sources */, F58BAD60270C2C9F00E64072 /* Array+Utils.swift in Sources */, F5306659271171A6007DAE42 /* BaseURL.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; F58CE25D271271310033575A /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F5E29F5F271274C100A9EF5F /* TestUtil.swift in Sources */, F5E29F55271271F700A9EF5F /* TopupImpUITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; F5E29FA427127A9100A9EF5F /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F5E29FAB27127A9200A9EF5F /* AppDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; F5E29FBD27127AE700A9EF5F /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F5E29FC427127AE700A9EF5F /* AddPaymentMethodIntegrationTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ F58CE268271271310033575A /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F5829F5426DB2BC400BFA8CD /* MiniSuperApp */; targetProxy = F58CE267271271310033575A /* PBXContainerItemProxy */; }; F5E29FC627127AE700A9EF5F /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F5E29FA727127A9100A9EF5F /* TestHost */; targetProxy = F5E29FC527127AE700A9EF5F /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ F5829F6326DB2BC500BFA8CD /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( F5829F6426DB2BC500BFA8CD /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; F5E29FB527127A9400A9EF5F /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( F5E29FB627127A9400A9EF5F /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ F5829F6726DB2BC500BFA8CD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.5; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; F5829F6826DB2BC500BFA8CD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.5; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; VALIDATE_PRODUCT = YES; }; name = Release; }; F5829F6A26DB2BC500BFA8CD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = MiniSuperApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.red.MiniSuperApp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; }; F5829F6B26DB2BC500BFA8CD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = MiniSuperApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.red.MiniSuperApp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; name = Release; }; F58CE269271271310033575A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.red.MiniSuperAppUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = MiniSuperApp; }; name = Debug; }; F58CE26A271271310033575A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.red.MiniSuperAppUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = MiniSuperApp; }; name = Release; }; F5E29F56271272CC00A9EF5F /* UITESTING */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 14.5; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = UITESTING; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = UITESTING; }; F5E29F57271272CC00A9EF5F /* UITESTING */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; INFOPLIST_FILE = MiniSuperApp/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); PRODUCT_BUNDLE_IDENTIFIER = com.red.MiniSuperApp; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; name = UITESTING; }; F5E29F58271272CC00A9EF5F /* UITESTING */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.red.MiniSuperAppUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_TARGET_NAME = MiniSuperApp; }; name = UITESTING; }; F5E29FBA27127A9400A9EF5F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TestHost/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.red.TestHost; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; F5E29FBB27127A9400A9EF5F /* UITESTING */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TestHost/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.red.TestHost; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = UITESTING; }; F5E29FBC27127A9400A9EF5F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TestHost/Info.plist; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.red.TestHost; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; F5E29FC827127AE700A9EF5F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.red.AddPaymentMethodIntegrationTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TestHost.app/TestHost"; }; name = Debug; }; F5E29FC927127AE700A9EF5F /* UITESTING */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.red.AddPaymentMethodIntegrationTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TestHost.app/TestHost"; }; name = UITESTING; }; F5E29FCA27127AE700A9EF5F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.red.AddPaymentMethodIntegrationTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TestHost.app/TestHost"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ F5829F5026DB2BC400BFA8CD /* Build configuration list for PBXProject "MiniSuperApp" */ = { isa = XCConfigurationList; buildConfigurations = ( F5829F6726DB2BC500BFA8CD /* Debug */, F5E29F56271272CC00A9EF5F /* UITESTING */, F5829F6826DB2BC500BFA8CD /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F5829F6926DB2BC500BFA8CD /* Build configuration list for PBXNativeTarget "MiniSuperApp" */ = { isa = XCConfigurationList; buildConfigurations = ( F5829F6A26DB2BC500BFA8CD /* Debug */, F5E29F57271272CC00A9EF5F /* UITESTING */, F5829F6B26DB2BC500BFA8CD /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F58CE26B271271310033575A /* Build configuration list for PBXNativeTarget "MiniSuperAppUITests" */ = { isa = XCConfigurationList; buildConfigurations = ( F58CE269271271310033575A /* Debug */, F5E29F58271272CC00A9EF5F /* UITESTING */, F58CE26A271271310033575A /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F5E29FB927127A9400A9EF5F /* Build configuration list for PBXNativeTarget "TestHost" */ = { isa = XCConfigurationList; buildConfigurations = ( F5E29FBA27127A9400A9EF5F /* Debug */, F5E29FBB27127A9400A9EF5F /* UITESTING */, F5E29FBC27127A9400A9EF5F /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F5E29FC727127AE700A9EF5F /* Build configuration list for PBXNativeTarget "AddPaymentMethodIntegrationTests" */ = { isa = XCConfigurationList; buildConfigurations = ( F5E29FC827127AE700A9EF5F /* Debug */, F5E29FC927127AE700A9EF5F /* UITESTING */, F5E29FCA27127AE700A9EF5F /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ F5829F6C26DB2DE900BFA8CD /* XCRemoteSwiftPackageReference "ModernRIBs" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/DevYeom/ModernRIBs"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.0.1; }; }; F58BACF0270BF21100E64072 /* XCRemoteSwiftPackageReference "CombineExt" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/CombineCommunity/CombineExt"; requirement = { kind = exactVersion; version = 1.5.1; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ F505938B27114F880087571C /* AddPaymentMethod */ = { isa = XCSwiftPackageProductDependency; productName = AddPaymentMethod; }; F530665427117188007DAE42 /* Network */ = { isa = XCSwiftPackageProductDependency; productName = Network; }; F530665627117188007DAE42 /* NetworkImp */ = { isa = XCSwiftPackageProductDependency; productName = NetworkImp; }; F53FF07C271156E4004F3972 /* FinanceEntity */ = { isa = XCSwiftPackageProductDependency; productName = FinanceEntity; }; F53FF07E271156E5004F3972 /* FinanceRepository */ = { isa = XCSwiftPackageProductDependency; productName = FinanceRepository; }; F53FF080271156E5004F3972 /* CombineUtil */ = { isa = XCSwiftPackageProductDependency; productName = CombineUtil; }; F53FF082271156E5004F3972 /* RIBsUtil */ = { isa = XCSwiftPackageProductDependency; productName = RIBsUtil; }; F53FF084271156E5004F3972 /* SuperUI */ = { isa = XCSwiftPackageProductDependency; productName = SuperUI; }; F53FF086271158C1004F3972 /* Topup */ = { isa = XCSwiftPackageProductDependency; productName = Topup; }; F542F83127115B9500085E91 /* FinanceHome */ = { isa = XCSwiftPackageProductDependency; productName = FinanceHome; }; F542F83527115C3200085E91 /* TransportHome */ = { isa = XCSwiftPackageProductDependency; productName = TransportHome; }; F542F83727115D4D00085E91 /* AppHome */ = { isa = XCSwiftPackageProductDependency; productName = AppHome; }; F542F83927115ED500085E91 /* ProfileHome */ = { isa = XCSwiftPackageProductDependency; productName = ProfileHome; }; F542F83D2711653900085E91 /* TransportHomeImp */ = { isa = XCSwiftPackageProductDependency; productName = TransportHomeImp; }; F542F83F271167EA00085E91 /* TopupImp */ = { isa = XCSwiftPackageProductDependency; productName = TopupImp; }; F54E9D9227116A42008D50BF /* AddPaymentMethodImp */ = { isa = XCSwiftPackageProductDependency; productName = AddPaymentMethodImp; }; F55A6E0C27127BE300DD30F7 /* AddPaymentMethod */ = { isa = XCSwiftPackageProductDependency; productName = AddPaymentMethod; }; F55A6E0E27127BE300DD30F7 /* AddPaymentMethodImp */ = { isa = XCSwiftPackageProductDependency; productName = AddPaymentMethodImp; }; F55A6E1027127BE300DD30F7 /* AddPaymentMethodTestSupport */ = { isa = XCSwiftPackageProductDependency; productName = AddPaymentMethodTestSupport; }; F55A6E1227127C3000DD30F7 /* FinanceRepository */ = { isa = XCSwiftPackageProductDependency; productName = FinanceRepository; }; F55A6E1427127C3000DD30F7 /* FinanceRepositoryTestSupport */ = { isa = XCSwiftPackageProductDependency; productName = FinanceRepositoryTestSupport; }; F5829F6D26DB2DE900BFA8CD /* ModernRIBs */ = { isa = XCSwiftPackageProductDependency; package = F5829F6C26DB2DE900BFA8CD /* XCRemoteSwiftPackageReference "ModernRIBs" */; productName = ModernRIBs; }; F58BACF1270BF21100E64072 /* CombineExt */ = { isa = XCSwiftPackageProductDependency; package = F58BACF0270BF21100E64072 /* XCRemoteSwiftPackageReference "CombineExt" */; productName = CombineExt; }; F5E29F59271273DB00A9EF5F /* PlatformTestSupport */ = { isa = XCSwiftPackageProductDependency; productName = PlatformTestSupport; }; F5E29FCB27127B2800A9EF5F /* PlatformTestSupport */ = { isa = XCSwiftPackageProductDependency; productName = PlatformTestSupport; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = F5829F4D26DB2BC400BFA8CD /* Project object */; } ================================================ FILE: completed/MiniSuperApp/MiniSuperApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: completed/MiniSuperApp/MiniSuperApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: completed/MiniSuperApp/MiniSuperApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved ================================================ { "object": { "pins": [ { "package": "combine-schedulers", "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", "state": { "branch": null, "revision": "4cf088c29a20f52be0f2ca54992b492c54e0076b", "version": "0.5.3" } }, { "package": "CombineExt", "repositoryURL": "https://github.com/CombineCommunity/CombineExt", "state": { "branch": null, "revision": "0880829102152185190064fd17847a7c681d2127", "version": "1.5.1" } }, { "package": "Hammer", "repositoryURL": "https://github.com/lyft/Hammer", "state": { "branch": null, "revision": "1042e3f7b5d7ee30a727a3cd9e90945a1f6daed5", "version": "0.13.0" } }, { "package": "ModernRIBs", "repositoryURL": "https://github.com/DevYeom/ModernRIBs", "state": { "branch": null, "revision": "5e0a67365a1fb18ca06b919dbf53608843ddc284", "version": "1.0.1" } }, { "package": "SnapshotTesting", "repositoryURL": "https://github.com/pointfreeco/swift-snapshot-testing", "state": { "branch": null, "revision": "f8a9c997c3c1dab4e216a8ec9014e23144cbab37", "version": "1.9.0" } }, { "package": "Swifter", "repositoryURL": "https://github.com/httpswift/swifter", "state": { "branch": null, "revision": "9483a5d459b45c3ffd059f7b55f9638e268632fd", "version": "1.5.0" } }, { "package": "xctest-dynamic-overlay", "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", "state": { "branch": null, "revision": "50a70a9d3583fe228ce672e8923010c8df2deddd", "version": "0.2.1" } } ] }, "version": 1 } ================================================ FILE: completed/MiniSuperApp/MiniSuperApp.xcodeproj/xcshareddata/xcschemes/AddPaymentMethodIntegrationTests.xcscheme ================================================ ================================================ FILE: completed/MiniSuperApp/MiniSuperApp.xcodeproj/xcshareddata/xcschemes/MiniSuperApp.xcscheme ================================================ ================================================ FILE: completed/MiniSuperApp/MiniSuperApp.xcodeproj/xcshareddata/xcschemes/MiniSuperAppUITests.xcscheme ================================================ ================================================ FILE: completed/MiniSuperApp/MiniSuperApp.xcodeproj/xcshareddata/xcschemes/TestHost.xcscheme ================================================ ================================================ FILE: completed/MiniSuperApp/MiniSuperAppUITests/Response/cardOnFile.json ================================================ { "cards": [ { "id": "0", "name": "우리은행", "digits": "0123", "color": "#f19a38ff", "isPrimary": false }, { "id": "1", "name": "신한카드", "digits": "0987", "color": "#3478f6ff", "isPrimary": false }, { "id": "2", "name": "현대카드", "digits": "8765", "color": "#78c5f5ff", "isPrimary": false } ] } ================================================ FILE: completed/MiniSuperApp/MiniSuperAppUITests/Response/topupSuccessResponse.json ================================================ { "status": "success" } ================================================ FILE: completed/MiniSuperApp/MiniSuperAppUITests/TestUtil.swift ================================================ import Foundation enum TestUtilError: Error { case fileNotFound } final class TestUtil { static func path(for fileName: String, in bundleClass: AnyClass) throws -> String { if let path = Bundle(for: bundleClass).path(forResource: fileName, ofType: nil) { return path } else { throw TestUtilError.fileNotFound } } } ================================================ FILE: completed/MiniSuperApp/MiniSuperAppUITests/TopupImpUITests.swift ================================================ import XCTest import Swifter final class TopupImpUITests: XCTestCase { private var app: XCUIApplication! private var server: HttpServer! override func setUpWithError() throws { continueAfterFailure = false server = HttpServer() app = XCUIApplication() } func testTopupSuccess() throws { // given let cardOnFileJSONPath = try TestUtil.path(for: "cardOnFile.json", in: type(of: self)) server["/cards"] = shareFile(cardOnFileJSONPath) let topupResponse = try TestUtil.path(for: "topupSuccessResponse.json", in: type(of: self)) server["/topup"] = shareFile(topupResponse) // when try server.start() app.launch() // then app.tabBars.buttons["superpay_home_tab_bar_item"].tap() app.buttons["superpay_dashboard_topup_button"].tap() let textField = app.textFields["topup_enteramount_textfield"] textField.tap() textField.typeText("10000") app.buttons["topup_enteramount_confirm_button"].tap() XCTAssertEqual(app.staticTexts.element(matching: .any, identifier: "superpay_dashboard_balance_label").label, "10,000") } } ================================================ FILE: completed/MiniSuperApp/Platform/.gitignore ================================================ .DS_Store /.build /Packages /*.xcodeproj xcuserdata/ DerivedData/ .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata ================================================ FILE: completed/MiniSuperApp/Platform/Package.swift ================================================ // swift-tools-version:5.5 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Platform", platforms: [.iOS(.v14)], products: [ .library( name: "CombineUtil", targets: ["CombineUtil"] ), .library( name: "RIBsUtil", targets: ["RIBsUtil"] ), .library( name: "RIBsTestSupport", targets: ["RIBsTestSupport"] ), .library( name: "PlatformTestSupport", targets: ["PlatformTestSupport"] ), .library( name: "SuperUI", targets: ["SuperUI"] ), .library( name: "DefaultsStore", targets: ["DefaultsStore"] ), .library( name: "Network", targets: ["Network"] ), .library( name: "NetworkImp", targets: ["NetworkImp"] ), ], dependencies: [ .package(url: "https://github.com/CombineCommunity/CombineExt", from: "1.0.0"), .package(name: "ModernRIBs", url: "https://github.com/DevYeom/ModernRIBs", .exact("1.0.1")), .package(url: "https://github.com/pointfreeco/combine-schedulers", from: "0.5.3"), .package(name: "SnapshotTesting", url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.9.0"), .package(name: "Swifter", url: "https://github.com/httpswift/swifter", from: "1.5.0"), .package(name: "Hammer", url: "https://github.com/lyft/Hammer", .exact("0.13.0")), ], targets: [ .target( name: "CombineUtil", dependencies: [ "CombineExt", .product(name: "CombineSchedulers", package: "combine-schedulers") ] ), .target( name: "RIBsUtil", dependencies: [ "ModernRIBs" ] ), .target( name: "RIBsTestSupport", dependencies: [ "ModernRIBs" ] ), .target( name: "PlatformTestSupport", dependencies: [ "SnapshotTesting", "Swifter", "Hammer" ] ), .target( name: "SuperUI", dependencies: [ "RIBsUtil" ] ), .target( name: "DefaultsStore", dependencies: [ ] ), .target( name: "Network", dependencies: [ ] ), .target( name: "NetworkImp", dependencies: [ "Network" ] ), ] ) ================================================ FILE: completed/MiniSuperApp/Platform/README.md ================================================ # Platform A description of this package. ================================================ FILE: completed/MiniSuperApp/Platform/Sources/CombineUtil/Combine+Utils.swift ================================================ import Combine import CombineExt import Foundation public class ReadOnlyCurrentValuePublisher: Publisher { public typealias Output = Element public typealias Failure = Never public var value: Element { currentValueRelay.value } fileprivate let currentValueRelay: CurrentValueRelay fileprivate init(_ initialValue: Element) { currentValueRelay = CurrentValueRelay(initialValue) } public func receive(subscriber: S) where S : Subscriber, Never == S.Failure, Element == S.Input { currentValueRelay.receive(subscriber: subscriber) } } public final class CurrentValuePublisher: ReadOnlyCurrentValuePublisher { typealias Output = Element typealias Failure = Never public override init(_ initialValue: Element) { super.init(initialValue) } public func send(_ value: Element) { currentValueRelay.accept(value) } } ================================================ FILE: completed/MiniSuperApp/Platform/Sources/DefaultsStore/DefaultsStore.swift ================================================ import Foundation public protocol DefaultsStore { var isInitialLaunch: Bool { get set } var lastNoticeDate: Double { get set } } public struct DefaultsStoreImp: DefaultsStore { public var isInitialLaunch: Bool { get { userDefaults.bool(forKey: kIsInitialLaunch) } set { userDefaults.set(newValue, forKey: kIsInitialLaunch) } } public var lastNoticeDate: Double { get { userDefaults.double(forKey: kLastNoticeDate) } set { userDefaults.set(newValue, forKey: kLastNoticeDate) } } private let userDefaults: UserDefaults private let kIsInitialLaunch = "kIsInitialLaunch" private let kLastNoticeDate = "kLastNoticeDate" public init(defaults: UserDefaults) { self.userDefaults = defaults } } ================================================ FILE: completed/MiniSuperApp/Platform/Sources/Network/HTTPMethod.swift ================================================ import Foundation public enum HTTPMethod: String, Encodable { case get = "GET" case post = "POST" case put = "PUT" } ================================================ FILE: completed/MiniSuperApp/Platform/Sources/Network/Network.swift ================================================ import Combine import Foundation public typealias QueryItems = [String: AnyHashable] public typealias HTTPHeader = [String: String] public protocol Request: Hashable { associatedtype Output: Decodable var endpoint: URL { get } var method: HTTPMethod { get } var query: QueryItems { get } var header: HTTPHeader { get } } public protocol Network { func send(_ request: T) -> AnyPublisher, Error> } public struct Response { public let output: T public let statusCode: Int public init(output: T, statusCode: Int) { self.output = output self.statusCode = statusCode } } ================================================ FILE: completed/MiniSuperApp/Platform/Sources/Network/NetworkError.swift ================================================ import Foundation public enum NetworkError: Error { case invalidURL(url: String?) } ================================================ FILE: completed/MiniSuperApp/Platform/Sources/NetworkImp/NetworkImp.swift ================================================ import Foundation import Network import Combine public final class NetworkImp: Network { private let session: URLSession public init( session: URLSession ) { self.session = session } public func send(_ request: T) -> AnyPublisher, Error> where T: Request { do { let urlRequest = try RequestFactory(request: request).urlRequestRepresentation() return session.dataTaskPublisher(for: urlRequest) .tryMap { data, response in let output = try JSONDecoder().decode(T.Output.self, from: data) return Response(output: output, statusCode: (response as? HTTPURLResponse)?.statusCode ?? 0) } .eraseToAnyPublisher() } catch { return Fail(error: error).eraseToAnyPublisher() } } } private final class RequestFactory { let request: T private var urlComponents: URLComponents? init(request: T) { self.request = request self.urlComponents = URLComponents(url: request.endpoint, resolvingAgainstBaseURL: true) } func urlRequestRepresentation() throws -> URLRequest { switch request.method { case .get: return try makeGetRequest() case .post: return try makePostRequest() case .put: return try makePutRequest() } } private func makeGetRequest() throws -> URLRequest { if request.query.isEmpty == false { urlComponents?.queryItems = request.query.map { URLQueryItem(name: $0.key, value: "\($0.value)") } } return try makeURLRequest() } private func makePostRequest() throws -> URLRequest { let body = try JSONSerialization.data(withJSONObject: request.query, options: []) return try makeURLRequest(httpBody: body) } private func makePutRequest() throws -> URLRequest { if request.query.isEmpty == false { urlComponents?.queryItems = request.query.map { URLQueryItem(name: $0.key, value: "\($0.value)") } } return try makeURLRequest() } private func makeURLRequest(httpBody: Data? = nil) throws -> URLRequest { guard let url = urlComponents?.url else { throw NetworkError.invalidURL(url: request.endpoint.absoluteString) } var urlRequest = URLRequest(url: url) request.header.forEach { urlRequest.setValue($0.value, forHTTPHeaderField: $0.key) } urlRequest.httpMethod = request.method.rawValue urlRequest.httpBody = httpBody return urlRequest } } ================================================ FILE: completed/MiniSuperApp/Platform/Sources/PlatformTestSupport/PlatformTestSupport.swift ================================================ import Foundation ================================================ FILE: completed/MiniSuperApp/Platform/Sources/RIBsTestSupport/RoutingMock.swift ================================================ import Foundation import ModernRIBs import Combine public final class RoutingMock: Routing { public var loadHandler: (() -> ())? public var loadCallCount: Int = 0 public var attachChildHandler: ((_ child: Routing) -> ())? public var attachChildCallCount: Int = 0 public var detachChildHandler: ((_ child: Routing) -> ())? public var detachChildCallCount: Int = 0 public var interactable: Interactable public var children: [Routing] = [Routing]() { didSet { childrenSetCallCount += 1 } } public var childrenSetCallCount = 0 public init( interactable: Interactable ) { self.interactable = interactable } public func load() { loadCallCount += 1 if let loadHandler = loadHandler { return loadHandler() } } public func attachChild(_ child: Routing) { attachChildCallCount += 1 if let attachChildHandler = attachChildHandler { return attachChildHandler(child) } } public func detachChild(_ child: Routing) { detachChildCallCount += 1 if let detachChildHandler = detachChildHandler { return detachChildHandler(child) } } public var lifecycleSubject = PassthroughSubject() { didSet { lifecycleSubjectSetCallCount += 1 } } public var lifecycleSubjectSetCallCount = 0 public var lifecycle: AnyPublisher { return lifecycleSubject.eraseToAnyPublisher() } } ================================================ FILE: completed/MiniSuperApp/Platform/Sources/RIBsTestSupport/ViewControllableMock.swift ================================================ import Foundation import ModernRIBs import UIKit public final class ViewControllableMock: UIViewController, ViewControllable { public init() { super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } public var presentCallCount = 0 public override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) { presentCallCount += 1 } public var dismissCallCount = 0 public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { dismissCallCount += 1 } } ================================================ FILE: completed/MiniSuperApp/Platform/Sources/RIBsTestSupport/ViewableRoutingMock.swift ================================================ import Foundation import ModernRIBs import Combine open class ViewableRoutingMock: ViewableRouting { // Variables public var viewControllable: ViewControllable public var interactable: Interactable { didSet { interactableSetCallCount += 1 } } public var interactableSetCallCount = 0 public var children: [Routing] = [Routing]() { didSet { childrenSetCallCount += 1 } } public var childrenSetCallCount = 0 public var lifecycleSubject = PassthroughSubject() { didSet { lifecycleSubjectSetCallCount += 1 } } public var lifecycleSubjectSetCallCount = 0 public var lifecycle: AnyPublisher { return lifecycleSubject.eraseToAnyPublisher() } // Function Handlers public var loadHandler: (() -> ())? public var loadCallCount: Int = 0 public var attachChildHandler: ((_ child: Routing) -> ())? public var attachChildCallCount: Int = 0 public var detachChildHandler: ((_ child: Routing) -> ())? public var detachChildCallCount: Int = 0 public init( interactable: Interactable, viewControllable: ViewControllable ) { self.interactable = interactable self.viewControllable = viewControllable } public func load() { loadCallCount += 1 if let loadHandler = loadHandler { return loadHandler() } } public func attachChild(_ child: Routing) { attachChildCallCount += 1 if let attachChildHandler = attachChildHandler { return attachChildHandler(child) } } public func detachChild(_ child: Routing) { detachChildCallCount += 1 if let detachChildHandler = detachChildHandler { return detachChildHandler(child) } } } ================================================ FILE: completed/MiniSuperApp/Platform/Sources/RIBsUtil/RIBs+Util.swift ================================================ import Foundation import ModernRIBs import UIKit public enum DismissButtonType { case back, close public var iconSystemName: String { switch self { case .back: return "chevron.backward" case .close: return "xmark" } } } public final class NavigationControllerable: ViewControllable { public var uiviewController: UIViewController { self.navigationController } public let navigationController: UINavigationController public init(root: ViewControllable) { let navigation = UINavigationController(rootViewController: root.uiviewController) navigation.navigationBar.isTranslucent = false navigation.navigationBar.backgroundColor = .white navigation.navigationBar.scrollEdgeAppearance = navigation.navigationBar.standardAppearance self.navigationController = navigation } } public extension ViewControllable { func present(_ viewControllable: ViewControllable, animated: Bool, completion: (() -> Void)?) { self.uiviewController.present(viewControllable.uiviewController, animated: animated, completion: completion) } func dismiss(completion: (() -> Void)?) { self.uiviewController.dismiss(animated: true, completion: completion) } func pushViewController(_ viewControllable: ViewControllable, animated: Bool) { if let nav = self.uiviewController as? UINavigationController { nav.pushViewController(viewControllable.uiviewController, animated: animated) } else { self.uiviewController.navigationController?.pushViewController(viewControllable.uiviewController, animated: animated) } } func popViewController(animated: Bool) { if let nav = self.uiviewController as? UINavigationController { nav.popViewController(animated: animated) } else { self.uiviewController.navigationController?.popViewController(animated: animated) } } func popToRoot(animated: Bool) { if let nav = self.uiviewController as? UINavigationController { nav.popToRootViewController(animated: animated) } else { self.uiviewController.navigationController?.popToRootViewController(animated: animated) } } func setViewControllers(_ viewControllerables: [ViewControllable]) { if let nav = self.uiviewController as? UINavigationController { nav.setViewControllers(viewControllerables.map(\.uiviewController), animated: true) } else { self.uiviewController.navigationController?.setViewControllers(viewControllerables.map(\.uiviewController), animated: true) } } var topViewControllable: ViewControllable { var top: ViewControllable = self while let presented = top.uiviewController.presentedViewController as? ViewControllable { top = presented } return top } } ================================================ FILE: completed/MiniSuperApp/Platform/Sources/SuperUI/AdaptivePresentationControllerDelegate.swift ================================================ import UIKit public protocol AdaptivePresentationControllerDelegate: AnyObject { func presentationControllerDidDismiss() } public final class AdaptivePresentationControllerDelegateProxy: NSObject, UIAdaptivePresentationControllerDelegate { public weak var delegate: AdaptivePresentationControllerDelegate? public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { delegate?.presentationControllerDidDismiss() } } ================================================ FILE: completed/MiniSuperApp/Platform/Sources/SuperUI/PushModalPresentationController.swift ================================================ import UIKit public final class PushModalPresentationController: NSObject, UIViewControllerTransitioningDelegate { public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { return PushModalPresentTransitioning() } public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { return PushModalDismissTransitioning() } } private final class PushModalPresentTransitioning: NSObject, UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return 0.25 } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let toViewController = transitionContext.viewController(forKey: .to) else { return } let containerView = transitionContext.containerView let toView = transitionContext.view(forKey: .to) var toViewInitialFrame = transitionContext.initialFrame(for: toViewController) let toViewFinalFrame = transitionContext.finalFrame(for: toViewController) toView.map(containerView.addSubview) toViewInitialFrame.origin = CGPoint(x: containerView.bounds.maxX, y: containerView.bounds.minY) toViewInitialFrame.size = toViewFinalFrame.size toView?.frame = toViewInitialFrame UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.allowUserInteraction, .curveEaseOut], animations: { toView?.frame = toViewFinalFrame }) { _ in let isCompleted = !transitionContext.transitionWasCancelled transitionContext.completeTransition(isCompleted) } } } private final class PushModalDismissTransitioning: NSObject, UIViewControllerAnimatedTransitioning { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return 0.25 } func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let fromViewController = transitionContext.viewController(forKey: .from) else { return } let containerView = transitionContext.containerView let toView = transitionContext.view(forKey: .to) let fromView = transitionContext.view(forKey: .from) var fromViewFinalFrame = transitionContext.finalFrame(for: fromViewController) toView.map(containerView.addSubview) if let fromView = fromView { fromViewFinalFrame = fromView.frame.offsetBy(dx: fromView.frame.width, dy: 0) } UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0, options: [.allowUserInteraction, .curveEaseOut], animations: { fromView?.frame = fromViewFinalFrame }) { _ in let isCompleted = !transitionContext.transitionWasCancelled transitionContext.completeTransition(isCompleted) } } } ================================================ FILE: completed/MiniSuperApp/Platform/Sources/SuperUI/UIColor+Super.swift ================================================ import UIKit public extension UIColor { static let backgroundColor = UIColor(hex: "#F1F5F9FF")! static let primaryRed = UIColor(hex: "#eb445aff")! } ================================================ FILE: completed/MiniSuperApp/Platform/Sources/SuperUI/UIColor+Utils.swift ================================================ import UIKit public extension UIColor { convenience init?(hex: String) { let r, g, b, a: CGFloat if hex.hasPrefix("#") { let start = hex.index(hex.startIndex, offsetBy: 1) let hexColor = String(hex[start...]) if hexColor.count == 8 { let scanner = Scanner(string: hexColor) var hexNumber: UInt64 = 0 if scanner.scanHexInt64(&hexNumber) { r = CGFloat((hexNumber & 0xff000000) >> 24) / 255 g = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255 b = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255 a = CGFloat(hexNumber & 0x000000ff) / 255 self.init(red: r, green: g, blue: b, alpha: a) return } } } return nil } } ================================================ FILE: completed/MiniSuperApp/Platform/Sources/SuperUI/UIImage+Utils.swift ================================================ import UIKit public extension UIImage { convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) { let rect = CGRect(origin: .zero, size: size) UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0) color.setFill() UIRectFill(rect) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() guard let cgImage = image?.cgImage else { return nil } self.init(cgImage: cgImage) } } ================================================ FILE: completed/MiniSuperApp/Platform/Sources/SuperUI/UITableView+Utils.swift ================================================ import UIKit public protocol Reusable: AnyObject { static var reuseIdentifier: String { get } } public extension Reusable { static var reuseIdentifier: String { return String(describing: self) } } extension UITableViewCell: Reusable {} public extension UITableView { func register(cellType: T.Type) { self.register(cellType, forCellReuseIdentifier: T.reuseIdentifier) } func dequeueReusableCell(for indexPath: IndexPath, cellType: T.Type = T.self) -> T { guard let cell = self.dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else { fatalError("Failed to dequeue reusable cell") } return cell } } ================================================ FILE: completed/MiniSuperApp/Platform/Sources/SuperUI/UIView+Utils.swift ================================================ import UIKit public extension UIView { func addShadowWithRoundedCorners( _ radius: CGFloat = 16, shadowColor: CGColor = UIColor.black.cgColor, opacity: Float = 0.1 ) { self.layer.cornerCurve = .continuous self.layer.masksToBounds = false self.layer.shadowColor = shadowColor self.layer.shadowOffset = CGSize(width: 0, height: 0) self.layer.shadowOpacity = opacity self.layer.shadowRadius = 2.5 self.layer.cornerRadius = radius } func roundCorners( _ radius: CGFloat = 16 ) { self.layer.cornerCurve = .continuous self.layer.cornerRadius = radius self.clipsToBounds = true } } ================================================ FILE: completed/MiniSuperApp/Platform/Sources/SuperUI/UIViewController+Utils.swift ================================================ import UIKit import RIBsUtil public extension UIViewController { func setupNavigationItem(with buttonType: DismissButtonType, target: Any?, action: Selector?) { navigationItem.leftBarButtonItem = UIBarButtonItem( image: UIImage( systemName: buttonType.iconSystemName, withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .semibold) ), style: .plain, target: target, action: action ) } } ================================================ FILE: completed/MiniSuperApp/Profile/.gitignore ================================================ .DS_Store /.build /Packages /*.xcodeproj xcuserdata/ DerivedData/ .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata ================================================ FILE: completed/MiniSuperApp/Profile/Package.swift ================================================ // swift-tools-version:5.5 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Profile", platforms: [.iOS(.v14)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( name: "ProfileHome", targets: ["ProfileHome"] ), ], dependencies: [ .package(name: "ModernRIBs", url: "https://github.com/DevYeom/ModernRIBs", .exact("1.0.1")), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "ProfileHome", dependencies: [ "ModernRIBs" ] ), ] ) ================================================ FILE: completed/MiniSuperApp/Profile/README.md ================================================ # Profile A description of this package. ================================================ FILE: completed/MiniSuperApp/Profile/Sources/ProfileHome/ProfileHomeBuilder.swift ================================================ import ModernRIBs public protocol ProfileHomeDependency: Dependency { } final class ProfileHomeComponent: Component { } // MARK: - Builder public protocol ProfileHomeBuildable: Buildable { func build(withListener listener: ProfileHomeListener) -> ViewableRouting } public final class ProfileHomeBuilder: Builder, ProfileHomeBuildable { public override init(dependency: ProfileHomeDependency) { super.init(dependency: dependency) } public func build(withListener listener: ProfileHomeListener) -> ViewableRouting { let _ = ProfileHomeComponent(dependency: dependency) let viewController = ProfileHomeViewController() let interactor = ProfileHomeInteractor(presenter: viewController) interactor.listener = listener return ProfileHomeRouter(interactor: interactor, viewController: viewController) } } ================================================ FILE: completed/MiniSuperApp/Profile/Sources/ProfileHome/ProfileHomeInteractor.swift ================================================ import ModernRIBs protocol ProfileHomeRouting: ViewableRouting { } protocol ProfileHomePresentable: Presentable { var listener: ProfileHomePresentableListener? { get set } } public protocol ProfileHomeListener: AnyObject { } final class ProfileHomeInteractor: PresentableInteractor, ProfileHomeInteractable, ProfileHomePresentableListener { weak var router: ProfileHomeRouting? weak var listener: ProfileHomeListener? override init(presenter: ProfileHomePresentable) { super.init(presenter: presenter) presenter.listener = self } override func didBecomeActive() { super.didBecomeActive() } override func willResignActive() { super.willResignActive() } } ================================================ FILE: completed/MiniSuperApp/Profile/Sources/ProfileHome/ProfileHomeRouter.swift ================================================ import ModernRIBs protocol ProfileHomeInteractable: Interactable { var router: ProfileHomeRouting? { get set } var listener: ProfileHomeListener? { get set } } protocol ProfileHomeViewControllable: ViewControllable { } final class ProfileHomeRouter: ViewableRouter, ProfileHomeRouting { override init(interactor: ProfileHomeInteractable, viewController: ProfileHomeViewControllable) { super.init(interactor: interactor, viewController: viewController) interactor.router = self } } ================================================ FILE: completed/MiniSuperApp/Profile/Sources/ProfileHome/ProfileHomeViewController.swift ================================================ import ModernRIBs import UIKit protocol ProfileHomePresentableListener: AnyObject { } final class ProfileHomeViewController: UIViewController, ProfileHomePresentable, ProfileHomeViewControllable { weak var listener: ProfileHomePresentableListener? init() { super.init(nibName: nil, bundle: nil) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private let label: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false return label }() func setupViews() { tabBarItem = UITabBarItem(title: "프로필", image: UIImage(systemName: "person"), selectedImage: UIImage(systemName: "person.fill")) label.text = "Profile Home" view.backgroundColor = .systemTeal view.addSubview(label) NSLayoutConstraint.activate([ label.centerXAnchor.constraint(equalTo: view.centerXAnchor), label.centerYAnchor.constraint(equalTo: view.centerYAnchor) ]) } } ================================================ FILE: completed/MiniSuperApp/Samples/TestUtil.swift ================================================ import Foundation enum TestUtilError: Error { case fileNotFound } class TestUtil { static func path(for fileName: String, in bundleClass: AnyClass) throws -> String { if let path = Bundle(for: bundleClass).path(forResource: fileName, ofType: nil) { return path } else { throw TestUtilError.fileNotFound } } } ================================================ FILE: completed/MiniSuperApp/Samples/TopupDependencyMock.swift ================================================ // // File.swift // // // Created by Soojin Ro on 2021/10/04. // @testable import TopupImp import Foundation import CombineUtil import FinanceEntity import FinanceRepositoryTestSupport import FinanceRepository import Combine import ModernRIBs import RIBsUtil import Topup import SuperUI final class TopupDependencyMock: TopupInteractorDependency { var cardOnFileRepository: CardOnFileRepository = CardOnFileRepositoryMock() var paymentMethodStream: CurrentValuePublisher = .init( PaymentMethod(id: "", name: "", digits: "", color: "", isPrimary: false) ) } final class TopupRoutingMock: TopupRouting { var attachAddPaymentMethodCallCount = 0 var attachAddPaymentMethodCloseButtonType: DismissButtonType? func attachAddPaymentMethod(closeButtonType: DismissButtonType) { attachAddPaymentMethodCallCount += 1 attachAddPaymentMethodCloseButtonType = closeButtonType } var detachAddPaymentMethodCallCount = 0 func detachAddPaymentMethod() { detachAddPaymentMethodCallCount += 1 } var attachEnterAmountCallCount = 0 func attachEnterAmount() { attachEnterAmountCallCount += 1 } var detachEnterAmountCallCount = 0 func detachEnterAmount() { detachEnterAmountCallCount += 1 } var attachCardOnFileCallCount = 0 var attachCardOnFileCallCountPaymentMethods: [PaymentMethod]? func attachCardOnFile(paymentMethods: [PaymentMethod]) { attachCardOnFileCallCount += 1 } var detachCardOnFileCallCount = 0 func detachCardOnFile() { detachCardOnFileCallCount += 1 } var popToRootCallCount = 0 func popToRoot() { popToRootCallCount += 1 } // Variables var interactable: Interactable { didSet { interactableSetCallCount += 1 } } var interactableSetCallCount = 0 var children: [Routing] = [Routing]() { didSet { childrenSetCallCount += 1 } } var childrenSetCallCount = 0 var lifecycleSubject = PassthroughSubject() { didSet { lifecycleSubjectSetCallCount += 1 } } var lifecycleSubjectSetCallCount = 0 var lifecycle: AnyPublisher { return lifecycleSubject.eraseToAnyPublisher() } // Function Handlers var loadHandler: (() -> ())? var loadCallCount: Int = 0 var attachChildHandler: ((_ child: Routing) -> ())? var attachChildCallCount: Int = 0 var detachChildHandler: ((_ child: Routing) -> ())? var detachChildCallCount: Int = 0 init( interactable: Interactable ) { self.interactable = interactable } var cleanupViewsCallCount = 0 func cleanupViews() { cleanupViewsCallCount += 1 } func load() { loadCallCount += 1 if let loadHandler = loadHandler { return loadHandler() } } func attachChild(_ child: Routing) { attachChildCallCount += 1 if let attachChildHandler = attachChildHandler { return attachChildHandler(child) } } func detachChild(_ child: Routing) { detachChildCallCount += 1 if let detachChildHandler = detachChildHandler { return detachChildHandler(child) } } } final class TopupInteractableMock: TopupInteractable { var router: TopupRouting? var listener: TopupListener? var presentationDelegateProxy = AdaptivePresentationControllerDelegateProxy() var addPaymentMethodDidTapCloseCallCount = 0 func addPaymentMethodDidTapClose() { addPaymentMethodDidTapCloseCallCount += 1 } var addPaymentMethodDidAddCardCallCount = 0 var addPaymentMethodDidAddCardPaymentMethod: PaymentMethod? func addPaymentMethodDidAddCard(paymentMethod: PaymentMethod) { addPaymentMethodDidAddCardCallCount += 1 addPaymentMethodDidAddCardPaymentMethod = paymentMethod } var enterAmountDidTapCloseCallCount = 0 func enterAmountDidTapClose() { enterAmountDidTapCloseCallCount += 1 } var enterAmountDidTapPaymentMethodCallCount = 0 func enterAmountDidTapPaymentMethod() { enterAmountDidTapPaymentMethodCallCount += 1 } var enterAmountDidFinishTopupCallCount = 0 func enterAmountDidFinishTopup() { enterAmountDidFinishTopupCallCount += 1 } var cardOnFileDidTapCloseCallCount = 0 func cardOnFileDidTapClose() { cardOnFileDidTapCloseCallCount += 1 } var cardOnFileDidTapAddCardCallCount = 0 func cardOnFileDidTapAddCard() { cardOnFileDidTapAddCardCallCount += 1 } var cardOnFileDidSelectCardCallCount = 0 var cardOnFileDidSelectCardIndex: Int? func cardOnFileDidSelectCard(at index: Int) { cardOnFileDidSelectCardCallCount += 1 cardOnFileDidSelectCardIndex = index } func activate() { } func deactivate() { } var isActive: Bool { isActiveSubject.value } var isActiveStream: AnyPublisher { isActiveSubject.eraseToAnyPublisher() } private let isActiveSubject = CurrentValueSubject(false) } ================================================ FILE: completed/MiniSuperApp/TestHost/AppDelegate.swift ================================================ import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { public var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { let window = UIWindow(frame: UIScreen.main.bounds) self.window = window return true } } ================================================ FILE: completed/MiniSuperApp/TestHost/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: completed/MiniSuperApp/TestHost/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { "idiom" : "iphone", "scale" : "3x", "size" : "29x29" }, { "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { "idiom" : "iphone", "scale" : "3x", "size" : "60x60" }, { "idiom" : "ipad", "scale" : "1x", "size" : "20x20" }, { "idiom" : "ipad", "scale" : "2x", "size" : "20x20" }, { "idiom" : "ipad", "scale" : "1x", "size" : "29x29" }, { "idiom" : "ipad", "scale" : "2x", "size" : "29x29" }, { "idiom" : "ipad", "scale" : "1x", "size" : "40x40" }, { "idiom" : "ipad", "scale" : "2x", "size" : "40x40" }, { "idiom" : "ipad", "scale" : "1x", "size" : "76x76" }, { "idiom" : "ipad", "scale" : "2x", "size" : "76x76" }, { "idiom" : "ipad", "scale" : "2x", "size" : "83.5x83.5" }, { "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: completed/MiniSuperApp/TestHost/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: completed/MiniSuperApp/TestHost/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: completed/MiniSuperApp/TestHost/Info.plist ================================================ ================================================ FILE: completed/MiniSuperApp/Transport/.gitignore ================================================ .DS_Store /.build /Packages /*.xcodeproj xcuserdata/ DerivedData/ .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata ================================================ FILE: completed/MiniSuperApp/Transport/Package.swift ================================================ // swift-tools-version:5.5 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Transport", platforms: [.iOS(.v14)], products: [ .library( name: "TransportHome", targets: ["TransportHome"] ), .library( name: "TransportHomeImp", targets: ["TransportHomeImp"] ), ], dependencies: [ .package(name: "ModernRIBs", url: "https://github.com/DevYeom/ModernRIBs", .exact("1.0.1")), .package(path: "../Finance"), .package(path: "../Platform") ], targets: [ .target( name: "TransportHome", dependencies: [ "ModernRIBs", ] ), .target( name: "TransportHomeImp", dependencies: [ "ModernRIBs", "TransportHome", .product(name: "FinanceRepository", package: "Finance"), .product(name: "Topup", package: "Finance"), .product(name: "SuperUI", package: "Platform"), ], resources: [ .process("Resources"), ] ), ] ) ================================================ FILE: completed/MiniSuperApp/Transport/README.md ================================================ # Transport A description of this package. ================================================ FILE: completed/MiniSuperApp/Transport/Sources/TransportHome/TransportHomeInterface.swift ================================================ import Foundation import ModernRIBs public protocol TransportHomeBuildable: Buildable { func build(withListener listener: TransportHomeListener) -> ViewableRouting } public protocol TransportHomeListener: AnyObject { func transportHomeDidTapClose() } ================================================ FILE: completed/MiniSuperApp/Transport/Sources/TransportHomeImp/Formatter.swift ================================================ import Foundation struct Formatter { static let balanceFormatter: NumberFormatter = { let formatter = NumberFormatter() formatter.numberStyle = .decimal return formatter }() } ================================================ FILE: completed/MiniSuperApp/Transport/Sources/TransportHomeImp/TransportHomeBuilder.swift ================================================ import ModernRIBs import FinanceRepository import CombineUtil import Topup import TransportHome public protocol TransportHomeDependency: Dependency { var cardOnFileRepository: CardOnFileRepository { get } var superPayRepository: SuperPayRepository { get } var topupBuildable: TopupBuildable { get } } final class TransportHomeComponent: Component, TransportHomeInteractorDependency { let topupBaseViewController: ViewControllable var cardOnFileRepository: CardOnFileRepository { dependency.cardOnFileRepository } var superPayRepository: SuperPayRepository { dependency.superPayRepository } var superPayBalance: ReadOnlyCurrentValuePublisher { superPayRepository.balance } var topupBuildable: TopupBuildable { dependency.topupBuildable } init( dependency: TransportHomeDependency, topupBaseViewController: ViewControllable ) { self.topupBaseViewController = topupBaseViewController super.init(dependency: dependency) } } // MARK: - Builder public final class TransportHomeBuilder: Builder, TransportHomeBuildable { override public init(dependency: TransportHomeDependency) { super.init(dependency: dependency) } public func build(withListener listener: TransportHomeListener) -> ViewableRouting { let viewController = TransportHomeViewController() let component = TransportHomeComponent(dependency: dependency, topupBaseViewController: viewController) let interactor = TransportHomeInteractor(presenter: viewController, dependency: component) interactor.listener = listener return TransportHomeRouter( interactor: interactor, viewController: viewController, topupBuildable: component.topupBuildable ) } } ================================================ FILE: completed/MiniSuperApp/Transport/Sources/TransportHomeImp/TransportHomeInteractor.swift ================================================ import ModernRIBs import Combine import Foundation import CombineUtil import TransportHome protocol TransportHomeRouting: ViewableRouting { func attachTopup() func detachTopup() } protocol TransportHomePresentable: Presentable { var listener: TransportHomePresentableListener? { get set } func setSuperPayBalance(_ balance: String) } protocol TransportHomeInteractorDependency { var superPayBalance: ReadOnlyCurrentValuePublisher { get } } final class TransportHomeInteractor: PresentableInteractor, TransportHomeInteractable, TransportHomePresentableListener { weak var router: TransportHomeRouting? weak var listener: TransportHomeListener? private var cancellables: Set private let ridePrice: Double = 18000 private let dependency: TransportHomeInteractorDependency init( presenter: TransportHomePresentable, dependency: TransportHomeInteractorDependency ) { self.dependency = dependency self.cancellables = .init() super.init(presenter: presenter) presenter.listener = self } override func didBecomeActive() { super.didBecomeActive() dependency.superPayBalance .receive(on: DispatchQueue.main) .sink { [weak self] balance in if let balanceText = Formatter.balanceFormatter.string(from: NSNumber(value: balance)) { self?.presenter.setSuperPayBalance(balanceText) } } .store(in: &cancellables) } override func willResignActive() { super.willResignActive() // TODO: Pause any business logic. } func didTapBack() { listener?.transportHomeDidTapClose() } func didTapRideConfirmButton() { if dependency.superPayBalance.value < ridePrice { router?.attachTopup() } else { print("Success") } } func topupDidClose() { router?.detachTopup() } func topupDidFinish() { router?.detachTopup() } } ================================================ FILE: completed/MiniSuperApp/Transport/Sources/TransportHomeImp/TransportHomeRouter.swift ================================================ import ModernRIBs import Topup import TransportHome protocol TransportHomeInteractable: Interactable, TopupListener { var router: TransportHomeRouting? { get set } var listener: TransportHomeListener? { get set } } protocol TransportHomeViewControllable: ViewControllable { } final class TransportHomeRouter: ViewableRouter, TransportHomeRouting { private let topupBuildable: TopupBuildable private var topupRouting: Routing? init( interactor: TransportHomeInteractable, viewController: TransportHomeViewControllable, topupBuildable: TopupBuildable ) { self.topupBuildable = topupBuildable super.init(interactor: interactor, viewController: viewController) interactor.router = self } func attachTopup() { if topupRouting != nil { return } let router = topupBuildable.build(withListener: interactor) self.topupRouting = router attachChild(router) } func detachTopup() { guard let router = topupRouting else { return } detachChild(router) self.topupRouting = nil } } ================================================ FILE: completed/MiniSuperApp/Transport/Sources/TransportHomeImp/TransportHomeViewController.swift ================================================ import ModernRIBs import SuperUI import UIKit protocol TransportHomePresentableListener: AnyObject { func didTapBack() func didTapRideConfirmButton() } final class TransportHomeViewController: UIViewController, TransportHomePresentable, TransportHomeViewControllable { weak var listener: TransportHomePresentableListener? private let mapView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.contentMode = .scaleAspectFill imageView.image = UIImage(named: "map_seoul", in: .module, with: nil) return imageView }() private let searchView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.addShadowWithRoundedCorners(8) view.backgroundColor = .white return view }() private let departureLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.systemFont(ofSize: 18, weight: .medium) label.text = "우리집" return label }() private let destinationLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.systemFont(ofSize: 18, weight: .medium) label.text = "회사" return label }() private let arrowImageView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.tintColor = .black imageView.image = UIImage( systemName: "arrow.right", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .semibold) ) return imageView }() private let rideTypeView: RideTypeView = { let view = RideTypeView() view.translatesAutoresizingMaskIntoConstraints = false return view }() private let superPayView: SuperPayView = { let view = SuperPayView() view.translatesAutoresizingMaskIntoConstraints = false return view }() private lazy var backButton: UIButton = { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false button.backgroundColor = .white button.roundCorners(25) button.tintColor = .black button.setImage( UIImage( systemName: "chevron.backward", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .semibold) ), for: .normal ) button.addTarget(self, action: #selector(backButtonDidTap), for: .touchUpInside) return button }() private let rideTypeStackView: UIStackView = { let stack = UIStackView() return stack }() private let paymentStackView: UIStackView = { let stack = UIStackView() return stack }() private let rideInfoPane: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = .white view.addShadowWithRoundedCorners() return view }() private lazy var rideConfirmButton: UIButton = { let button = UIButton(type: .system) button.translatesAutoresizingMaskIntoConstraints = false button.setTitle("슈퍼택시 호출하기", for: .normal) button.backgroundColor = .primaryRed button.tintColor = .white button.addTarget(self, action: #selector(didTapRideConfirmButton), for: .touchUpInside) button.titleLabel?.font = UIFont.systemFont(ofSize: 18, weight: .semibold) return button }() private let separatorView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = .systemGray6 return view }() func setSuperPayBalance(_ balanceText: String) { superPayView.setBalanceText("잔고: \(balanceText)원") } init() { super.init(nibName: nil, bundle: nil) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private func setupViews() { view.addSubview(mapView) view.addSubview(searchView) searchView.addSubview(arrowImageView) searchView.addSubview(departureLabel) searchView.addSubview(destinationLabel) view.addSubview(backButton) view.addSubview(rideInfoPane) rideInfoPane.addSubview(rideTypeView) rideInfoPane.addSubview(superPayView) rideInfoPane.addSubview(separatorView) rideInfoPane.addSubview(rideConfirmButton) NSLayoutConstraint.activate([ mapView.topAnchor.constraint(equalTo: view.topAnchor), mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor), mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor), mapView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.7), searchView.leadingAnchor.constraint(equalTo: backButton.trailingAnchor, constant: 10), searchView.centerYAnchor.constraint(equalTo: backButton.centerYAnchor), searchView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), searchView.heightAnchor.constraint(equalToConstant: 50), departureLabel.leadingAnchor.constraint(equalTo: searchView.leadingAnchor, constant: 60), departureLabel.centerYAnchor.constraint(equalTo: searchView.centerYAnchor), destinationLabel.trailingAnchor.constraint(equalTo: searchView.trailingAnchor, constant: -60), destinationLabel.centerYAnchor.constraint(equalTo: searchView.centerYAnchor), rideInfoPane.bottomAnchor.constraint(equalTo: view.bottomAnchor), rideInfoPane.leadingAnchor.constraint(equalTo: view.leadingAnchor), rideInfoPane.trailingAnchor.constraint(equalTo: view.trailingAnchor), rideInfoPane.topAnchor.constraint(equalTo: mapView.bottomAnchor), backButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), backButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20), backButton.widthAnchor.constraint(equalToConstant: 50), backButton.heightAnchor.constraint(equalToConstant: 50), arrowImageView.centerXAnchor.constraint(equalTo: searchView.centerXAnchor), arrowImageView.centerYAnchor.constraint(equalTo: searchView.centerYAnchor), rideTypeView.leadingAnchor.constraint(equalTo: rideInfoPane.leadingAnchor, constant: 30), rideTypeView.trailingAnchor.constraint(equalTo: rideInfoPane.trailingAnchor, constant: -30), rideTypeView.topAnchor.constraint(equalTo: rideInfoPane.topAnchor, constant: 10), rideTypeView.heightAnchor.constraint(equalToConstant: 70), separatorView.topAnchor.constraint(equalTo: rideTypeView.bottomAnchor), separatorView.leadingAnchor.constraint(equalTo: rideInfoPane.leadingAnchor), separatorView.trailingAnchor.constraint(equalTo: rideInfoPane.trailingAnchor), separatorView.heightAnchor.constraint(equalToConstant: 1), superPayView.leadingAnchor.constraint(equalTo: rideInfoPane.leadingAnchor, constant: 30), superPayView.trailingAnchor.constraint(equalTo: rideInfoPane.trailingAnchor, constant: -30), superPayView.topAnchor.constraint(equalTo: separatorView.bottomAnchor, constant: 0), superPayView.bottomAnchor.constraint(equalTo: rideConfirmButton.topAnchor), rideConfirmButton.leadingAnchor.constraint(equalTo: rideInfoPane.leadingAnchor, constant: 30), rideConfirmButton.trailingAnchor.constraint(equalTo: rideInfoPane.trailingAnchor, constant: -30), rideConfirmButton.bottomAnchor.constraint(equalTo: rideInfoPane.safeAreaLayoutGuide.bottomAnchor, constant: -20), rideConfirmButton.heightAnchor.constraint(equalToConstant: 60) ]) } @objc private func backButtonDidTap() { listener?.didTapBack() } @objc private func didTapRideConfirmButton() { listener?.didTapRideConfirmButton() } } ================================================ FILE: completed/MiniSuperApp/Transport/Sources/TransportHomeImp/Views/RideTypeView.swift ================================================ import UIKit final class RideTypeView: UIView { private let thumbnailView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.contentMode = .scaleAspectFit imageView.tintColor = .black imageView.image = UIImage( systemName: "bolt.car", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .semibold) ) return imageView }() private let rideTypeNameLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.systemFont(ofSize: 18, weight: .medium) label.text = "슈퍼전기차 택시" return label }() private let priceLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 18, weight: .medium) label.translatesAutoresizingMaskIntoConstraints = false label.text = "18,000원" return label }() init() { super.init(frame: .zero) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private func setupViews() { addSubview(thumbnailView) addSubview(priceLabel) addSubview(rideTypeNameLabel) NSLayoutConstraint.activate([ thumbnailView.leadingAnchor.constraint(equalTo: self.leadingAnchor), thumbnailView.centerYAnchor.constraint(equalTo: self.centerYAnchor), thumbnailView.widthAnchor.constraint(equalToConstant: 40), thumbnailView.heightAnchor.constraint(equalToConstant: 40), rideTypeNameLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), rideTypeNameLabel.leadingAnchor.constraint(equalTo: thumbnailView.trailingAnchor, constant: 10), priceLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor), priceLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor) ]) } } ================================================ FILE: completed/MiniSuperApp/Transport/Sources/TransportHomeImp/Views/SuperPayView.swift ================================================ import UIKit final class SuperPayView: UIView { private let thumbnailView: UIImageView = { let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.backgroundColor = .systemBlue imageView.roundCorners(4) return imageView }() private let nameLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.systemFont(ofSize: 18, weight: .medium) label.text = "슈퍼페이" return label }() private let balanceLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = UIFont.systemFont(ofSize: 18, weight: .medium) label.text = "---원" return label }() func setBalanceText(_ text: String) { balanceLabel.text = text } init() { super.init(frame: .zero) setupViews() } required init?(coder: NSCoder) { super.init(coder: coder) setupViews() } private func setupViews() { addSubview(thumbnailView) addSubview(nameLabel) addSubview(balanceLabel) NSLayoutConstraint.activate([ thumbnailView.widthAnchor.constraint(equalToConstant: 46), thumbnailView.heightAnchor.constraint(equalToConstant: 34), thumbnailView.centerYAnchor.constraint(equalTo: self.centerYAnchor), nameLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), nameLabel.leadingAnchor.constraint(equalTo: thumbnailView.trailingAnchor, constant: 10), balanceLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor), balanceLabel.centerYAnchor.constraint(equalTo: self.centerYAnchor), ]) } }