├── .gitignore
├── .swiftformat
├── .swiftlint.yml
├── App
├── App.project.yml
└── Sources
│ ├── AppDelegate.swift
│ ├── Info.plist
│ ├── Resolver
│ ├── Resolver+InfrastructureServices.swift
│ ├── Resolver+RepositoryServices.swift
│ ├── Resolver+ResolverRegistering.swift
│ ├── Resolver+UIServices.swift
│ └── Views
│ │ ├── Resolver+AppSalesView.swift
│ │ ├── Resolver+FeedLanguageSettingView.swift
│ │ ├── Resolver+FeedListView.swift
│ │ ├── Resolver+FeedView.swift
│ │ ├── Resolver+FullScreenModalView.swift
│ │ ├── Resolver+KeyboardTestView.swift
│ │ ├── Resolver+LicensesView.swift
│ │ ├── Resolver+MainView.swift
│ │ ├── Resolver+ModalTransitionTestView.swift
│ │ ├── Resolver+NoticeAlertView.swift
│ │ ├── Resolver+PageSheetModalView.swift
│ │ ├── Resolver+PushTransitionTestView.swift
│ │ ├── Resolver+RootView.swift
│ │ ├── Resolver+SettingsMenuView.swift
│ │ ├── Resolver+SettingsView.swift
│ │ ├── Resolver+ToolbarTestView.swift
│ │ ├── Resolver+UserNameSettingView.swift
│ │ ├── Resolver+WalkthroughFinishView.swift
│ │ ├── Resolver+WalkthroughIntroView.swift
│ │ ├── Resolver+WalkthroughSettingsView.swift
│ │ └── Resolver+WalkthroughView.swift
│ └── SceneDelegate.swift
├── Entities
├── Entities.project.yml
└── Sources
│ ├── Data
│ ├── AppInformation.swift
│ ├── AppSegment.swift
│ ├── FeedLanguage.swift
│ ├── Navigation.swift
│ ├── News.swift
│ └── SettingsMenu.swift
│ ├── Errors
│ └── FeedClientError.swift
│ └── PreviewContents
│ ├── AppInformation+PreviewContents.swift
│ └── News+PreviewContents.swift
├── Infrastructures
├── FeedClient
│ ├── FeedClient.project.yml
│ ├── Sources
│ │ ├── Extensions
│ │ │ ├── Date+Formatter.swift
│ │ │ └── URL+EndPoints.swift
│ │ └── FeedClient.swift
│ └── Tests
│ │ ├── FetchFreeAppRankingTests.swift
│ │ ├── FetchNewsTests.swift
│ │ ├── FetchPaidAppRankingTests.swift
│ │ └── Resources
│ │ ├── stub_free_app_ranking_feed_15_en.xml
│ │ ├── stub_free_app_ranking_feed_15_jp.xml
│ │ ├── stub_free_app_ranking_feed_1_en.xml
│ │ ├── stub_free_app_ranking_feed_1_jp.xml
│ │ ├── stub_news_feed_1_en.xml
│ │ ├── stub_news_feed_1_jp.xml
│ │ ├── stub_news_feed_20_en.xml
│ │ ├── stub_news_feed_20_jp.xml
│ │ ├── stub_paid_app_ranking_feed_15_en.xml
│ │ ├── stub_paid_app_ranking_feed_15_jp.xml
│ │ ├── stub_paid_app_ranking_feed_1_en.xml
│ │ └── stub_paid_app_ranking_feed_1_jp.xml
├── InfrastructureServices
│ ├── InfrastructureServices.project.yml
│ └── Sources
│ │ ├── FeedClientService.swift
│ │ └── UserDefaultsManagerService.swift
└── UserDefaultsManager
│ ├── Sources
│ ├── UserDefaultsKey.swift
│ └── UserDefaultsManager.swift
│ └── UserDefaultsManager.project.yml
├── LICENSE
├── Makefile
├── MockingbirdSupport
├── AppKit
│ ├── NSResponder.swift
│ ├── NSView.swift
│ └── NSViewController.swift
├── Foundation
│ └── ObjectiveC
│ │ └── NSObject.swift
├── LICENSE.md
├── Swift
│ ├── Codable.swift
│ ├── Comparable.swift
│ ├── Equatable.swift
│ ├── Hashable.swift
│ └── Misc.swift
└── UIKit
│ ├── UIControl.swift
│ ├── UILabel.swift
│ ├── UIResponder.swift
│ ├── UIView.swift
│ └── UIViewController.swift
├── README.md
├── Repositories
├── AppStateRepository
│ ├── AppStateRepository.project.yml
│ ├── Sources
│ │ └── AppStateRepository.swift
│ └── Tests
│ │ ├── AppStateRepositoryTests.swift
│ │ └── Generated
│ │ └── .gitkeep
├── FeedRepository
│ ├── FeedRepository.project.yml
│ └── Sources
│ │ └── FeedRepository.swift
├── PreferencesRepository
│ ├── PreferencesRepository.project.yml
│ └── Sources
│ │ └── PreferencesRepository.swift
└── RepositoryServices
│ ├── RepositoryServices.project.yml
│ └── Sources
│ ├── AppStateRepositoryService.swift
│ ├── FeedRepositoryService.swift
│ ├── PreferencesRepositoryService.swift
│ └── PreviewContents
│ ├── PreviewFeedRepository.swift
│ └── PreviewPreferencesRepository.swift
├── Resources
├── Resources.project.yml
├── Resources.swiftgen.yml
└── Sources
│ ├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ ├── Colors
│ │ ├── Contents.json
│ │ └── background.colorset
│ │ │ └── Contents.json
│ └── Contents.json
│ ├── Licenses.md
│ └── Settings.bundle
│ ├── Root.plist
│ └── en.lproj
│ └── Root.strings
├── UI
├── UIComponents
│ ├── Sources
│ │ ├── AppIcon.swift
│ │ ├── AppRankingButton.swift
│ │ ├── AppRankingStack.swift
│ │ ├── BulletPointText.swift
│ │ ├── NavigationLinkButton.swift
│ │ ├── NewsLargeCard.swift
│ │ ├── NewsSmallCard.swift
│ │ ├── RadioButton.swift
│ │ └── RadioButtonGroup.swift
│ └── UIComponents.project.yml
├── UIServices
│ ├── Sources
│ │ ├── AppSales
│ │ │ └── AppSalesFlowControllerService.swift
│ │ ├── Feed
│ │ │ └── FeedFlowControllerService.swift
│ │ ├── FeedLanguageSetting
│ │ │ └── FeedLanguageSettingFlowControllerService.swift
│ │ ├── FeedList
│ │ │ ├── FeedListFlowControllerDelegate.swift
│ │ │ └── FeedListFlowControllerService.swift
│ │ ├── FullScreenModal
│ │ │ ├── FullScreenModalFlowControllerDelegate.swift
│ │ │ └── FullScreenModalFlowControllerService.swift
│ │ ├── KeyboardTest
│ │ │ └── KeyboardTestFlowControllerService.swift
│ │ ├── Licenses
│ │ │ └── LicensesFlowControllerService.swift
│ │ ├── Main
│ │ │ └── MainFlowControllerService.swift
│ │ ├── ModalTransitionTest
│ │ │ └── ModalTransitionTestFlowControllerService.swift
│ │ ├── NoticeAlert
│ │ │ ├── NoticeAlertFlowControllerDelegate.swift
│ │ │ └── NoticeAlertFlowControllerService.swift
│ │ ├── PageSheetModal
│ │ │ ├── PageSheetModalFlowControllerDelegate.swift
│ │ │ └── PageSheetModalFlowControllerService.swift
│ │ ├── PushTransitionTest
│ │ │ ├── PushTransitionTestFlowControllerDelegate.swift
│ │ │ └── PushTransitionTestFlowControllerService.swift
│ │ ├── Root
│ │ │ └── RootFlowControllerService.swift
│ │ ├── Settings
│ │ │ └── SettingsFlowControllerService.swift
│ │ ├── SettingsMenu
│ │ │ ├── SettingsMenuFlowControllerDelegate.swift
│ │ │ └── SettingsMenuFlowControllerService.swift
│ │ ├── ToolbarTest
│ │ │ └── ToolbarTestFlowControllerService.swift
│ │ ├── UIViewController+Content.swift
│ │ ├── UIViewController+OpenLink.swift
│ │ ├── UserNameSetting
│ │ │ └── UserNameSettingFlowControllerService.swift
│ │ ├── Walkthrough
│ │ │ └── WalkthroughFlowControllerService.swift
│ │ ├── WalkthroughFinish
│ │ │ ├── WalkthroughFinishFlowControllerDelegate.swift
│ │ │ └── WalkthroughFinishFlowControllerService.swift
│ │ ├── WalkthroughIntro
│ │ │ └── WalkthroughIntroFlowControllerService.swift
│ │ └── WalkthroughSettings
│ │ │ ├── WalkthroughSettingsFlowControllerDelegate.swift
│ │ │ └── WalkthroughSettingsFlowControllerService.swift
│ └── UIServices.project.yml
└── Views
│ ├── AppSalesView
│ ├── AppSalesView.project.yml
│ ├── Sources
│ │ ├── AppSalesFlowController.swift
│ │ ├── View
│ │ │ └── AppSalesView.swift
│ │ └── ViewModel
│ │ │ ├── AppSalesViewModel+Navigation.swift
│ │ │ └── AppSalesViewModel.swift
│ └── Tests
│ │ ├── AppSalesFlowControllerTests.swift
│ │ ├── AppSalesViewModelTests.swift
│ │ └── Generated
│ │ └── .gitkeep
│ ├── FeedLanguageSettingView
│ ├── FeedLanguageSettingView.project.yml
│ └── Sources
│ │ ├── FeedLanguageSettingFlowController.swift
│ │ ├── View
│ │ └── FeedLanguageSettingView.swift
│ │ └── ViewModel
│ │ └── FeedLanguageSettingViewModel.swift
│ ├── FeedListView
│ ├── FeedListView.project.yml
│ └── Sources
│ │ ├── FeedListFlowController.swift
│ │ ├── View
│ │ ├── FeedListView.swift
│ │ └── Section
│ │ │ ├── AllNewsSection.swift
│ │ │ ├── AppRankingSection.swift
│ │ │ └── LatestNewsSection.swift
│ │ └── ViewModel
│ │ ├── FeedListViewModel+Navigation.swift
│ │ └── FeedListViewModel.swift
│ ├── FeedView
│ ├── FeedView.project.yml
│ └── Sources
│ │ ├── FeedFlowController.swift
│ │ └── ViewModel
│ │ └── FeedViewModel.swift
│ ├── FullScreenModalView
│ ├── FullScreenModalView.project.yml
│ └── Sources
│ │ ├── FullScreenModalFlowController.swift
│ │ ├── View
│ │ └── FullScreenModalView.swift
│ │ └── ViewModel
│ │ ├── FullScreenModalViewModel+Navigation.swift
│ │ └── FullScreenModalViewModel.swift
│ ├── KeyboardTestView
│ ├── KeyboardTestView.project.yml
│ └── Sources
│ │ ├── KeyboardTestFlowController.swift
│ │ ├── View
│ │ └── KeyboardTestView.swift
│ │ └── ViewModel
│ │ └── KeyboardTestViewModel.swift
│ ├── LicensesView
│ ├── LicensesView.project.yml
│ └── Sources
│ │ ├── LicensesFlowController.swift
│ │ ├── View
│ │ └── LicensesView.swift
│ │ └── ViewModel
│ │ └── LicensesViewModel.swift
│ ├── MainView
│ ├── MainView.project.yml
│ └── Sources
│ │ ├── MainFlowController.swift
│ │ └── ViewModel
│ │ └── MainViewModel.swift
│ ├── ModalTransitionTestView
│ ├── ModalTransitionTestView.project.yml
│ └── Sources
│ │ ├── ModalTransitionTestFlowController.swift
│ │ ├── View
│ │ └── ModalTransitionTestView.swift
│ │ └── ViewModel
│ │ ├── ModalTransitionTestViewModel+Navigation.swift
│ │ └── ModalTransitionTestViewModel.swift
│ ├── NoticeAlertView
│ ├── NoticeAlertView.project.yml
│ └── Sources
│ │ └── NoticeAlertFlowController.swift
│ ├── PageSheetModalView
│ ├── PageSheetModalView.project.yml
│ └── Sources
│ │ ├── PageSheetModalFlowController.swift
│ │ ├── View
│ │ └── PageSheetModalView.swift
│ │ └── ViewModel
│ │ ├── PageSheetModalViewModel+Navigation.swift
│ │ └── PageSheetModalViewModel.swift
│ ├── PushTransitionTestView
│ ├── PushTransitionTestView.project.yml
│ └── Sources
│ │ ├── PushTransitionTestFlowController.swift
│ │ ├── View
│ │ └── PushTransitionTestView.swift
│ │ └── ViewModel
│ │ ├── PushTransitionTestViewModel+Navigation.swift
│ │ └── PushTransitionTestViewModel.swift
│ ├── RootView
│ ├── RootView.project.yml
│ ├── Sources
│ │ ├── RootFlowController.swift
│ │ └── ViewModel
│ │ │ ├── RootViewModel+Navigation.swift
│ │ │ └── RootViewModel.swift
│ └── Tests
│ │ ├── Generated
│ │ └── .gitkeep
│ │ ├── RootViewControllerTests.swift
│ │ └── RootViewModelTests.swift
│ ├── SettingsMenuView
│ ├── SettingsMenuView.project.yml
│ └── Sources
│ │ ├── SettingsMenuFlowController.swift
│ │ ├── View
│ │ └── SettingsMenuView.swift
│ │ └── ViewModel
│ │ ├── SettingsMenuViewModel+Navigation.swift
│ │ └── SettingsMenuViewModel.swift
│ ├── SettingsView
│ ├── SettingsView.project.yml
│ └── Sources
│ │ ├── SettingsFlowController.swift
│ │ └── ViewModel
│ │ └── SettingsViewModel.swift
│ ├── ToolbarTestView
│ ├── Sources
│ │ ├── ToolbarTestFlowController.swift
│ │ ├── View
│ │ │ └── ToolbarTestView.swift
│ │ └── ViewModel
│ │ │ └── ToolbarTestViewModel.swift
│ └── ToolbarTestView.project.yml
│ ├── UserNameSettingView
│ ├── Sources
│ │ ├── UserNameSettingFlowController.swift
│ │ ├── View
│ │ │ └── UserNameSettingView.swift
│ │ └── ViewModel
│ │ │ └── UserNameSettingViewModel.swift
│ └── UserNameSettingView.project.yml
│ ├── WalkthroughFinishView
│ ├── Sources
│ │ ├── View
│ │ │ └── WalkthroughFinishView.swift
│ │ ├── ViewModel
│ │ │ ├── WalkthroughFinishViewModel+Navigation.swift
│ │ │ └── WalkthroughFinishViewModel.swift
│ │ └── WalkthroughFinishFlowController.swift
│ └── WalkthroughFinishView.project.yml
│ ├── WalkthroughIntroView
│ ├── Sources
│ │ ├── View
│ │ │ └── WalkthroughIntroView.swift
│ │ ├── ViewModel
│ │ │ └── WalkthroughIntroViewModel.swift
│ │ └── WalkthroughIntroFlowController.swift
│ └── WalkthroughIntroView.project.yml
│ ├── WalkthroughSettingsView
│ ├── Sources
│ │ ├── View
│ │ │ └── WalkthroughSettingsView.swift
│ │ ├── ViewModel
│ │ │ └── WalkthroughSettingsViewModel.swift
│ │ └── WalkthroughSettingsFlowController.swift
│ └── WalkthroughSettingsView.project.yml
│ └── WalkthroughView
│ ├── Sources
│ ├── ViewModel
│ │ └── WalkthroughViewModel.swift
│ └── WalkthroughFlowController.swift
│ └── WalkthroughView.project.yml
├── bin
└── .gitkeep
├── images
├── architecture
│ ├── concept.png
│ ├── data_flow.png
│ ├── data_flow2.png
│ ├── event_flow.png
│ └── transition.png
└── screenshots
│ ├── app_sales.png
│ ├── feed_language_setting.png
│ ├── feed_list_1.png
│ ├── feed_list_2.png
│ ├── feed_list_3.png
│ ├── keyboard_test.png
│ ├── licenses.png
│ ├── modal_transition_test.png
│ ├── push_transition_test.png
│ ├── settings_menu.png
│ ├── toolbar_test.png
│ ├── user_name_setting.png
│ ├── walkthrough_finish.png
│ ├── walkthrough_intro.png
│ └── walkthrough_settings.png
├── lib
└── .gitkeep
├── project.yml
├── scripts
├── local-install-licenseplist.sh
├── local-install-swiftformat.sh
├── local-install-swiftgen.sh
├── local-install-swiftlint.sh
└── local-install-xcodegen.sh
└── swiftui-flowcontroller-pattern.xcodeproj
├── project.xcworkspace
└── xcshareddata
│ ├── IDEWorkspaceChecks.plist
│ ├── WorkspaceSettings.xcsettings
│ └── swiftpm
│ └── Package.resolved
└── xcshareddata
└── IDETemplateMacros.plist
/App/App.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | App:
3 | type: application
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontrollerpattern
8 | TARGETED_DEVICE_FAMILY: 1
9 | MARKETING_VERSION: "1.0.0"
10 | CURRENT_PROJECT_VERSION: 1
11 | PRODUCT_NAME: "SwiftUI-FlowController Pattern"
12 | PRODUCT_MODULE_NAME: App
13 | GENERATE_INFOPLIST_FILE: YES
14 | INFOPLIST_FILE: "App/Sources/Info.plist"
15 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents: YES
16 | INFOPLIST_KEY_UILaunchStoryboardName: LaunchScreen
17 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone:
18 | UIInterfaceOrientationPortrait
19 | sources:
20 | - path: Sources
21 | group: App
22 | preBuildScripts:
23 | - name: Format and linting
24 | script: |
25 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 App/Sources
26 | ${SRCROOT}/bin/swiftlint App/Sources
27 | dependencies:
28 | - target: Entities
29 | - target: InfrastructureServices
30 | - target: FeedClient
31 | - target: UserDefaultsManager
32 | - target: RepositoryServices
33 | - target: AppStateRepository
34 | - target: FeedRepository
35 | - target: PreferencesRepository
36 | - target: Resources
37 | - target: UIServices
38 | - target: UIComponents
39 | - target: AppSalesView
40 | - target: FeedLanguageSettingView
41 | - target: FeedListView
42 | - target: FeedView
43 | - target: FullScreenModalView
44 | - target: KeyboardTestView
45 | - target: LicensesView
46 | - target: MainView
47 | - target: ModalTransitionTestView
48 | - target: NoticeAlertView
49 | - target: PageSheetModalView
50 | - target: PushTransitionTestView
51 | - target: RootView
52 | - target: SettingsMenuView
53 | - target: SettingsView
54 | - target: ToolbarTestView
55 | - target: UserNameSettingView
56 | - target: WalkthroughFinishView
57 | - target: WalkthroughIntroView
58 | - target: WalkthroughSettingsView
59 | - target: WalkthroughView
60 | - package: Resolver
61 |
--------------------------------------------------------------------------------
/App/Sources/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | @main
4 | class AppDelegate: UIResponder, UIApplicationDelegate {
5 | func application(
6 | _: UIApplication,
7 | // swiftlint:disable:next discouraged_optional_collection
8 | didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?
9 | ) -> Bool {
10 | true
11 | }
12 |
13 | func application(
14 | _: UIApplication,
15 | configurationForConnecting connectingSceneSession: UISceneSession,
16 | options _: UIScene.ConnectionOptions
17 | ) -> UISceneConfiguration {
18 | UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/App/Sources/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | UIApplicationSceneManifest
6 |
7 | UIApplicationSupportsMultipleScenes
8 |
9 | UISceneConfigurations
10 |
11 | UIWindowSceneSessionRoleApplication
12 |
13 |
14 | UISceneConfigurationName
15 | Default Configuration
16 | UISceneDelegateClassName
17 | $(PRODUCT_MODULE_NAME).SceneDelegate
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Resolver+InfrastructureServices.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | import Resolver
4 |
5 | import FeedClient
6 | import InfrastructureServices
7 | import UserDefaultsManager
8 |
9 | extension Resolver {
10 | static func registerInfrastructureServices() {
11 | register { FeedClient() as FeedClientService }
12 | .scope(.application)
13 | register { UserDefaultsManager(userDefaults: UserDefaults.standard) as UserDefaultsManagerService }
14 | .scope(.application)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Resolver+RepositoryServices.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import AppStateRepository
4 | import FeedRepository
5 | import PreferencesRepository
6 | import RepositoryServices
7 |
8 | extension Resolver {
9 | static func registerRepositoryServices() {
10 | register { AppStateRepository(userDefaultsManager: resolve()) as AppStateRepositoryService }
11 | .scope(.application)
12 | register { FeedRepository(feedClient: resolve()) as FeedRepositoryService }
13 | .scope(.application)
14 | register { PreferencesRepository(userDefaultsManager: resolve()) as PreferencesRepositoryService }
15 | .scope(.application)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Resolver+ResolverRegistering.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | import Resolver
4 |
5 | extension Resolver: ResolverRegistering {
6 | public static func registerAllServices() {
7 | registerInfrastructureServices()
8 | registerRepositoryServices()
9 | registerUIServices()
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Resolver+UIServices.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import AppSalesView
4 | import FeedLanguageSettingView
5 | import FeedListView
6 | import UIServices
7 |
8 | extension Resolver {
9 | static func registerUIServices() {
10 | registerAppSalesView()
11 | registerFeedLanguageSettingView()
12 | registerFeedListView()
13 | registerFeedView()
14 | registerFullScreenModalView()
15 | registerKeyboardTestView()
16 | registerLicensesView()
17 | registerMainView()
18 | registerModalTransitionTestView()
19 | registerNoticeAlertView()
20 | registerPageSheetModalView()
21 | registerPushTransitionTestView()
22 | registerRootView()
23 | registerSettingsMenuView()
24 | registerSettingsView()
25 | registerToolbarTestView()
26 | registerUserNameSettingView()
27 | registerWalkthroughFinishView()
28 | registerWalkthroughIntroView()
29 | registerWalkthroughSettingsView()
30 | registerWalkthroughView()
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+AppSalesView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import AppSalesView
4 | import UIServices
5 |
6 | extension Resolver {
7 | static func registerAppSalesView() {
8 | register { AppSalesViewModel(feedRepository: resolve(), preferencesRepository: resolve()) }
9 | register { AppSalesView(viewModel: resolve()) }
10 | register { AppSalesFlowController(rootView: resolve()) as AppSalesFlowControllerService }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+FeedLanguageSettingView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import FeedLanguageSettingView
4 | import UIServices
5 |
6 | extension Resolver {
7 | static func registerFeedLanguageSettingView() {
8 | register { FeedLanguageSettingViewModel(preferencesRepository: resolve()) }
9 | register { FeedLanguageSettingView(viewModel: resolve()) }
10 | register { FeedLanguageSettingFlowController(rootView: resolve()) as FeedLanguageSettingFlowControllerService }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+FeedListView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import FeedListView
4 | import UIServices
5 |
6 | extension Resolver {
7 | static func registerFeedListView() {
8 | register { FeedListViewModel(feedRepository: resolve(), preferencesRepository: resolve()) }
9 | register { FeedListView(viewModel: resolve()) }
10 | register { FeedListFlowController(rootView: resolve()) as FeedListFlowControllerService }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+FeedView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import FeedView
4 | import UIServices
5 |
6 | extension Resolver {
7 | static func registerFeedView() {
8 | register { FeedViewModel() }
9 | register {
10 | FeedFlowController(
11 | viewModel: resolve(),
12 | appSalesProvider: resolve(),
13 | feedListProvider: resolve()
14 | ) as FeedFlowControllerService
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+FullScreenModalView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import FullScreenModalView
4 | import UIServices
5 |
6 | extension Resolver {
7 | static func registerFullScreenModalView() {
8 | register { FullScreenModalViewModel() }
9 | register { FullScreenModalView(viewModel: resolve()) }
10 | register { FullScreenModalFlowController(rootView: resolve()) as FullScreenModalFlowControllerService }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+KeyboardTestView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import KeyboardTestView
4 | import UIServices
5 |
6 | extension Resolver {
7 | static func registerKeyboardTestView() {
8 | register { KeyboardTestViewModel() }
9 | register { KeyboardTestView(viewModel: resolve()) }
10 | register { KeyboardTestFlowController(rootView: resolve()) as KeyboardTestFlowControllerService }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+LicensesView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import LicensesView
4 | import UIServices
5 |
6 | extension Resolver {
7 | static func registerLicensesView() {
8 | register { LicensesViewModel() }
9 | register { LicensesView(viewModel: resolve()) }
10 | register { LicensesFlowController(rootView: resolve()) as LicensesFlowControllerService }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+MainView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import MainView
4 | import UIServices
5 |
6 | extension Resolver {
7 | static func registerMainView() {
8 | register { MainViewModel() }
9 | register {
10 | MainFlowController(
11 | viewModel: resolve(),
12 | feedProvider: resolve(),
13 | settingsProvider: resolve()
14 | ) as MainFlowControllerService
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+ModalTransitionTestView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import ModalTransitionTestView
4 | import UIServices
5 |
6 | extension Resolver {
7 | static func registerModalTransitionTestView() {
8 | register { ModalTransitionTestViewModel() }
9 | register { ModalTransitionTestView(viewModel: resolve()) }
10 | register {
11 | ModalTransitionTestFlowController(
12 | rootView: resolve(),
13 | fullScreenModalProvider: resolve(),
14 | noticeAlertProvider: resolve(),
15 | pageSheetModalProvider: resolve()
16 | ) as ModalTransitionTestFlowControllerService
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+NoticeAlertView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import NoticeAlertView
4 | import UIServices
5 |
6 | extension Resolver {
7 | static func registerNoticeAlertView() {
8 | register { NoticeAlertFlowController.instantiate() as NoticeAlertFlowControllerService }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+PageSheetModalView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import PageSheetModalView
4 | import UIServices
5 |
6 | extension Resolver {
7 | static func registerPageSheetModalView() {
8 | register { PageSheetModalViewModel() }
9 | register { PageSheetModalView(viewModel: resolve()) }
10 | register { PageSheetModalFlowController(rootView: resolve()) as PageSheetModalFlowControllerService }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+PushTransitionTestView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import PushTransitionTestView
4 | import UIServices
5 |
6 | extension Resolver {
7 | static func registerPushTransitionTestView() {
8 | register { PushTransitionTestViewModel() }
9 | register { PushTransitionTestView(viewModel: resolve()) }
10 | register { PushTransitionTestFlowController(rootView: resolve()) as PushTransitionTestFlowControllerService }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+RootView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import RootView
4 | import UIServices
5 |
6 | extension Resolver {
7 | static func registerRootView() {
8 | register { RootViewModel(appStateRepository: resolve()) }
9 | register {
10 | RootFlowController(
11 | rootViewModel: resolve(),
12 | walkthroughProvider: resolve(),
13 | mainProvider: resolve()
14 | ) as RootFlowControllerService
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+SettingsMenuView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import SettingsMenuView
4 | import UIServices
5 |
6 | extension Resolver {
7 | static func registerSettingsMenuView() {
8 | register { SettingsMenuViewModel(preferenceRepository: resolve()) }
9 | register { SettingsMenuView(viewModel: resolve()) }
10 | register { SettingsMenuFlowController(rootView: resolve()) as SettingsMenuFlowControllerService }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+SettingsView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import SettingsView
4 | import UIServices
5 |
6 | extension Resolver {
7 | static func registerSettingsView() {
8 | register { SettingsViewModel() }
9 | register {
10 | SettingsFlowController(
11 | viewModel: resolve(),
12 | settingsMenuProvider: resolve(),
13 | userNameSettingProvider: resolve(),
14 | feedLanguageSettingProvider: resolve(),
15 | keyboardTestProvider: resolve(),
16 | toolbarTestProvider: resolve(),
17 | pushTransitionTestProvider: resolve(),
18 | modalTransitionTestProvider: resolve(),
19 | licensesProvider: resolve()
20 | ) as SettingsFlowControllerService
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+ToolbarTestView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import ToolbarTestView
4 | import UIServices
5 |
6 | extension Resolver {
7 | static func registerToolbarTestView() {
8 | register { ToolbarTestViewModel() }
9 | register { ToolbarTestView(viewModel: resolve()) }
10 | register { ToolbarTestFlowController(rootView: resolve()) as ToolbarTestFlowControllerService }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+UserNameSettingView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import UIServices
4 | import UserNameSettingView
5 |
6 | extension Resolver {
7 | static func registerUserNameSettingView() {
8 | register { UserNameSettingViewModel(preferencesRepository: resolve()) }
9 | register { UserNameSettingView(viewModel: resolve()) }
10 | register { UserNameSettingFlowController(rootView: resolve()) as UserNameSettingFlowControllerService }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+WalkthroughFinishView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import UIServices
4 | import WalkthroughFinishView
5 |
6 | extension Resolver {
7 | static func registerWalkthroughFinishView() {
8 | register { WalkthroughFinishViewModel() }
9 | register { WalkthroughFinishView(viewModel: resolve()) }
10 | register { WalkthroughFinishFlowController(rootView: resolve()) as WalkthroughFinishFlowControllerService }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+WalkthroughIntroView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import UIServices
4 | import WalkthroughIntroView
5 |
6 | extension Resolver {
7 | static func registerWalkthroughIntroView() {
8 | register { WalkthroughIntroViewModel() }
9 | register { WalkthroughIntroView(viewModel: resolve()) }
10 | register { WalkthroughIntroFlowController(rootView: resolve()) as WalkthroughIntroFlowControllerService }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+WalkthroughSettingsView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import UIServices
4 | import WalkthroughSettingsView
5 |
6 | extension Resolver {
7 | static func registerWalkthroughSettingsView() {
8 | register { WalkthroughSettingsViewModel() }
9 | register { WalkthroughSettingsView(viewModel: resolve()) }
10 | register { WalkthroughSettingsFlowController(rootView: resolve()) as WalkthroughSettingsFlowControllerService }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/App/Sources/Resolver/Views/Resolver+WalkthroughView.swift:
--------------------------------------------------------------------------------
1 | import Resolver
2 |
3 | import UIServices
4 | import WalkthroughView
5 |
6 | extension Resolver {
7 | static func registerWalkthroughView() {
8 | register { WalkthroughViewModel(appStateRepository: resolve(), preferencesRepository: resolve()) }
9 | register {
10 | WalkthroughFlowController(
11 | viewModel: resolve(),
12 | walkthroughIntroProvider: resolve(),
13 | walkthroughSettingsProvider: resolve(),
14 | walkthroughFinishProvider: resolve()
15 | ) as WalkthroughFlowControllerService
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/App/Sources/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | import Resolver
4 |
5 | import UIServices
6 |
7 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
8 | var window: UIWindow?
9 |
10 | func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) {
11 | if let scene = scene as? UIWindowScene {
12 | let rootViewController = Resolver.resolve(RootFlowControllerService.self)
13 |
14 | let window = UIWindow(windowScene: scene)
15 | window.rootViewController = rootViewController
16 | window.makeKeyAndVisible()
17 |
18 | rootViewController.start()
19 |
20 | self.window = window
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Entities/Entities.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | Entities:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.entities
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | DEVELOPMENT_ASSET_PATHS: Entities/Sources/PreviewContents
11 | sources:
12 | - path: Sources
13 | group: Entities
14 | preBuildScripts:
15 | - name: Format and linting
16 | script: |
17 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 Entities/Sources
18 | ${SRCROOT}/bin/swiftlint Entities/Sources
--------------------------------------------------------------------------------
/Entities/Sources/Data/AppInformation.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct AppInformation: Hashable {
4 | public let name: String
5 | public let seller: String
6 | public let iconURL: URL
7 | public let storeURL: URL
8 |
9 | public init(name: String, seller: String, iconURL: URL, storeURL: URL) {
10 | self.name = name
11 | self.seller = seller
12 | self.iconURL = iconURL
13 | self.storeURL = storeURL
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Entities/Sources/Data/AppSegment.swift:
--------------------------------------------------------------------------------
1 | public enum AppSegment {
2 | case freeApp
3 | case paidApp
4 |
5 | public var title: String {
6 | switch self {
7 | case .freeApp:
8 | return "Free App"
9 |
10 | case .paidApp:
11 | return "Paid App"
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Entities/Sources/Data/FeedLanguage.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum FeedLanguage: String {
4 | case english
5 | case japanese
6 |
7 | public var title: String {
8 | switch self {
9 | case .english:
10 | return "English"
11 |
12 | case .japanese:
13 | return "Japanese"
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Entities/Sources/Data/Navigation.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum Navigation: Hashable {
4 | public enum AppSalesView: Hashable {
5 | case appStore(url: URL)
6 | }
7 |
8 | public enum FeedListView: Hashable {
9 | case news(url: URL)
10 | case appStore(url: URL)
11 | case freeAppRanking
12 | case paidAppRanking
13 | }
14 |
15 | public enum FullScreenModalView {
16 | case dismiss
17 | }
18 |
19 | public enum ModalTransitionTestView {
20 | case alert
21 | case fullScreen
22 | case pageSheet
23 | }
24 |
25 | public enum PageSheetModalView {
26 | case dismiss
27 | }
28 |
29 | public enum PushTransitionTestView {
30 | case next
31 | case popToRoot
32 | }
33 |
34 | public enum RootView {
35 | case main
36 | case walkthrough
37 | }
38 |
39 | public enum SettingsMenuView: Hashable {
40 | case menu(row: SettingsMenu.Row)
41 | }
42 |
43 | public enum WalkthroughFinishView {
44 | case finish
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Entities/Sources/Data/News.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct News: Hashable {
4 | public let title: String
5 | public let category: String
6 | public let date: Date
7 | public let imageURL: URL
8 | public let linkURL: URL
9 |
10 | public init(
11 | title: String,
12 | category: String,
13 | date: Date,
14 | imageURL: URL,
15 | linkURL: URL
16 | ) {
17 | self.title = title
18 | self.category = category
19 | self.date = date
20 | self.imageURL = imageURL
21 | self.linkURL = linkURL
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Entities/Sources/Data/SettingsMenu.swift:
--------------------------------------------------------------------------------
1 | public enum SettingsMenu {
2 | public enum Section: CaseIterable {
3 | case user
4 | case preferences
5 | case test
6 | case app
7 |
8 | public var title: String? {
9 | switch self {
10 | case .user:
11 | return nil
12 |
13 | case .preferences:
14 | return "Preferences"
15 |
16 | case .test:
17 | return "Test"
18 |
19 | case .app:
20 | return "App"
21 | }
22 | }
23 |
24 | public var rows: [Row] {
25 | switch self {
26 | case .user:
27 | return [.userName, .userNameSetting]
28 |
29 | case .preferences:
30 | return [.feedLanguageSetting]
31 |
32 | case .test:
33 | return [.keyboardTest, .toolbarTest, .pushTransitionTest, .modalTransitionTest]
34 |
35 | case .app:
36 | return [.licenses, .version]
37 | }
38 | }
39 | }
40 |
41 | public enum RowType {
42 | case user
43 | case transition
44 | case version
45 | }
46 |
47 | public enum Row {
48 | case userName
49 | case userNameSetting
50 | case feedLanguageSetting
51 | case keyboardTest
52 | case toolbarTest
53 | case pushTransitionTest
54 | case modalTransitionTest
55 | case licenses
56 | case version
57 |
58 | public var title: String? {
59 | switch self {
60 | case .userName:
61 | return nil
62 |
63 | case .userNameSetting:
64 | return "Edit"
65 |
66 | case .feedLanguageSetting:
67 | return "Feed language"
68 |
69 | case .keyboardTest:
70 | return "Keyboard"
71 |
72 | case .toolbarTest:
73 | return "Toolbar"
74 |
75 | case .pushTransitionTest:
76 | return "Push transition"
77 |
78 | case .modalTransitionTest:
79 | return "Modal transition"
80 |
81 | case .licenses:
82 | return "Licenses"
83 |
84 | case .version:
85 | return "Version"
86 | }
87 | }
88 |
89 | public var type: RowType {
90 | switch self {
91 | case .userName:
92 | return .user
93 |
94 | case .userNameSetting,
95 | .feedLanguageSetting,
96 | .keyboardTest,
97 | .toolbarTest,
98 | .pushTransitionTest,
99 | .modalTransitionTest,
100 | .licenses:
101 | return .transition
102 |
103 | case .version:
104 | return .version
105 | }
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/Entities/Sources/Errors/FeedClientError.swift:
--------------------------------------------------------------------------------
1 | public enum FeedClientError: Error {
2 | case connection
3 | case httpStatus
4 | case format
5 | }
6 |
--------------------------------------------------------------------------------
/Entities/Sources/PreviewContents/AppInformation+PreviewContents.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension AppInformation { // swiftlint:disable:this extension_access_modifier
4 | // swiftlint:disable force_unwrapping line_length
5 | public static let youtube = AppInformation(
6 | name: "YouTube: Watch, Listen, Stream",
7 | seller: "Google LLC",
8 | iconURL: URL(
9 | string: "https://is3-ssl.mzstatic.com/image/thumb/Purple116/v4/d0/8d/34/d08d342b-c581-bc37-e6d5-79ba90a8d0de/logo_youtube_color-0-0-1x_U007emarketing-0-0-0-6-0-0-sRGB-0-0-0-GLES2_U002c0-512MB-85-220-0-0.png/100x100bb.png"
10 | )!,
11 | storeURL: URL(string: "https://apps.apple.com/us/app/youtube-watch-listen-stream/id544007664?uo=2")!
12 | )
13 | public static let facebook = AppInformation(
14 | name: "Facebook",
15 | seller: "Meta Platforms, Inc.",
16 | iconURL: URL(
17 | string: "https://is4-ssl.mzstatic.com/image/thumb/Purple116/v4/8b/f6/6f/8bf66f1e-9de5-583e-27b6-58f3180ccf6b/Icon-Production-0-0-1x_U007emarketing-0-0-0-7-0-0-sRGB-0-0-0-GLES2_U002c0-512MB-85-220-0-0.png/100x100bb.png"
18 | )!,
19 | storeURL: URL(string: "https://apps.apple.com/us/app/facebook/id284882215?uo=2")!
20 | )
21 | public static let tiktok = AppInformation(
22 | name: "TikTok",
23 | seller: "TikTok Ltd.",
24 | iconURL: URL(
25 | string: "https://is5-ssl.mzstatic.com/image/thumb/Purple126/v4/3f/68/f2/3f68f20b-fd7d-f518-0fd0-4d20a6e1c8d2/AppIcon_TikTok-0-0-1x_U007emarketing-0-0-0-7-0-0-sRGB-0-0-0-GLES2_U002c0-512MB-85-220-0-0.png/100x100bb.png"
26 | )!,
27 | storeURL: URL(string: "https://apps.apple.com/us/app/tiktok/id835599320?uo=2")!
28 | )
29 | public static let snapchat = AppInformation(
30 | name: "Snapchat",
31 | seller: "Snap, Inc.",
32 | iconURL: URL(
33 | string: "https://is2-ssl.mzstatic.com/image/thumb/Purple116/v4/a8/94/16/a8941680-17cb-e9ff-df64-2b3341608cf4/AppIcon-0-0-1x_U007emarketing-0-0-0-5-0-0-sRGB-0-0-0-GLES2_U002c0-512MB-85-220-0-0.png/100x100bb.png"
34 | )!,
35 | storeURL: URL(string: "https://apps.apple.com/us/app/snapchat/id447188370?uo=2")!
36 | )
37 | public static let zoom = AppInformation(
38 | name: "ZOOM Cloud Meetings",
39 | seller: "Zoom Video Communications, Inc.",
40 | iconURL: URL(
41 | string: "https://is5-ssl.mzstatic.com/image/thumb/Purple126/v4/e2/2d/57/e22d57ae-015e-1247-9d19-d421effdad91/AppIcon-0-1x_U007emarketing-0-9-0-85-220.png/100x100bb.png"
42 | )!,
43 | storeURL: URL(string: "https://apps.apple.com/us/app/zoom-cloud-meetings/id546505307?uo=2")!
44 | )
45 | // swiftlint:enable force_unwrapping line_length
46 |
47 | public static let previewContentList: [AppInformation] = [
48 | youtube,
49 | facebook,
50 | tiktok,
51 | snapchat,
52 | zoom,
53 | ]
54 | }
55 |
--------------------------------------------------------------------------------
/Infrastructures/FeedClient/FeedClient.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | FeedClient:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.feedclient
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: Infrastructures/FeedClient
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 Infrastructures/FeedClient/Sources
17 | ${SRCROOT}/bin/swiftlint Infrastructures/FeedClient/Sources
18 | dependencies:
19 | - target: InfrastructureServices
20 | - target: Entities
21 | - package: Kanna
22 | FeedClientTests:
23 | type: bundle.unit-test
24 | platform: iOS
25 | sources:
26 | - path: Tests
27 | group: Infrastructures/FeedClient
28 | preBuildScripts:
29 | - name: Format and linting
30 | script: |
31 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 Infrastructures/FeedClient/Tests
32 | ${SRCROOT}/bin/swiftlint Infrastructures/FeedClient/Tests
33 | dependencies:
34 | - target: FeedClient
35 | - package: OHHTTPStubs
36 | product: OHHTTPStubs
37 | - package: OHHTTPStubs
38 | product: OHHTTPStubsSwift
39 |
--------------------------------------------------------------------------------
/Infrastructures/FeedClient/Sources/Extensions/Date+Formatter.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Date {
4 | init?(fromNewsFeedFormat string: String) {
5 | let formatter = ISO8601DateFormatter()
6 | formatter.formatOptions.insert(.withFractionalSeconds)
7 |
8 | guard let date = formatter.date(from: string) else { return nil }
9 |
10 | self = date
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Infrastructures/FeedClient/Sources/Extensions/URL+EndPoints.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | import Entities
4 |
5 | extension URL {
6 | static func newsFeed(language: FeedLanguage) -> URL {
7 | switch language {
8 | case .english:
9 | // swiftlint:disable:next force_unwrapping
10 | return URL(string: "https://www.apple.com/newsroom/rss-feed.rss")!
11 |
12 | case .japanese:
13 | // swiftlint:disable:next force_unwrapping
14 | return URL(string: "https://www.apple.com/jp/newsroom/rss-feed.rss")!
15 | }
16 | }
17 |
18 | static func freeAppRankingFeed(language: FeedLanguage, limit: Int) -> URL {
19 | switch language {
20 | case .english:
21 | // swiftlint:disable:next force_unwrapping
22 | return URL(string: "https://itunes.apple.com/rss/topfreeapplications/limit=\(limit)/xml")!
23 |
24 | case .japanese:
25 | // swiftlint:disable:next force_unwrapping
26 | return URL(string: "https://itunes.apple.com/jp/rss/topfreeapplications/limit=\(limit)/xml")!
27 | }
28 | }
29 |
30 | static func paidAppRankingFeed(language: FeedLanguage, limit: Int) -> URL {
31 | switch language {
32 | case .english:
33 | // swiftlint:disable:next force_unwrapping
34 | return URL(string: "https://itunes.apple.com/rss/toppaidapplications/limit=\(limit)/xml")!
35 |
36 | case .japanese:
37 | // swiftlint:disable:next force_unwrapping
38 | return URL(string: "https://itunes.apple.com/jp/rss/toppaidapplications/limit=\(limit)/xml")!
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Infrastructures/FeedClient/Tests/Resources/stub_news_feed_1_en.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Infrastructures/FeedClient/Tests/Resources/stub_news_feed_1_jp.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Infrastructures/InfrastructureServices/InfrastructureServices.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | InfrastructureServices:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.infrastructureservices
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: Infrastructures/InfrastructureServices
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 Infrastructures/InfrastructureServices/Sources
17 | ${SRCROOT}/bin/swiftlint Infrastructures/InfrastructureServices/Sources
18 | dependencies:
19 | - target: Entities
20 |
--------------------------------------------------------------------------------
/Infrastructures/InfrastructureServices/Sources/FeedClientService.swift:
--------------------------------------------------------------------------------
1 | import Entities
2 |
3 | public protocol FeedClientService {
4 | func fetchNews(language: FeedLanguage) async throws -> [News]
5 | func fetchFreeAppRanking(language: FeedLanguage, limit: Int) async throws -> [AppInformation]
6 | func fetchPaidAppRanking(language: FeedLanguage, limit: Int) async throws -> [AppInformation]
7 | }
8 |
--------------------------------------------------------------------------------
/Infrastructures/InfrastructureServices/Sources/UserDefaultsManagerService.swift:
--------------------------------------------------------------------------------
1 | import Entities
2 |
3 | public protocol UserDefaultsManagerService {
4 | func fetchFeedLanguage() async -> FeedLanguage?
5 | func fetchUserName() async -> String?
6 | func fetchIsWalkthroughFinished() async -> Bool
7 | func saveFeedLanguage(_ language: FeedLanguage) async
8 | func saveUserName(_ name: String) async
9 | func saveIsWalkthroughFinished(_ flag: Bool) async
10 | }
11 |
--------------------------------------------------------------------------------
/Infrastructures/UserDefaultsManager/Sources/UserDefaultsKey.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum UserDefaultsKey: String {
4 | case isWalkthroughFinished
5 | case feedLanguage
6 | case userName
7 | }
8 |
--------------------------------------------------------------------------------
/Infrastructures/UserDefaultsManager/Sources/UserDefaultsManager.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | import Entities
4 | import InfrastructureServices
5 |
6 | public final class UserDefaultsManager: UserDefaultsManagerService {
7 | private let _userDefaults: UserDefaults
8 |
9 | public init(userDefaults: UserDefaults) {
10 | self._userDefaults = userDefaults
11 | }
12 |
13 | public func fetchFeedLanguage() async -> FeedLanguage? {
14 | await withCheckedContinuation { continuation in
15 | let rawValue = _userDefaults.value(forKey: UserDefaultsKey.feedLanguage.rawValue) as? String
16 | let language = rawValue.flatMap { FeedLanguage(rawValue: $0) }
17 | continuation.resume(returning: language)
18 | }
19 | }
20 |
21 | public func fetchUserName() async -> String? {
22 | await withCheckedContinuation { continuation in
23 | let value = _userDefaults.value(forKey: UserDefaultsKey.userName.rawValue) as? String
24 | continuation.resume(returning: value)
25 | }
26 | }
27 |
28 | public func fetchIsWalkthroughFinished() async -> Bool {
29 | await withCheckedContinuation { continuation in
30 | let value = _userDefaults.bool(forKey: UserDefaultsKey.isWalkthroughFinished.rawValue)
31 | continuation.resume(returning: value)
32 | }
33 | }
34 |
35 | public func saveFeedLanguage(_ language: FeedLanguage) async {
36 | await withCheckedContinuation { (continuation: CheckedContinuation) in
37 | _userDefaults.set(language.rawValue, forKey: UserDefaultsKey.feedLanguage.rawValue)
38 | continuation.resume()
39 | }
40 | }
41 |
42 | public func saveUserName(_ name: String) async {
43 | await withCheckedContinuation { (continuation: CheckedContinuation) in
44 | _userDefaults.set(name, forKey: UserDefaultsKey.userName.rawValue)
45 | continuation.resume()
46 | }
47 | }
48 |
49 | public func saveIsWalkthroughFinished(_ flag: Bool) async {
50 | await withCheckedContinuation { (continuation: CheckedContinuation) in
51 | _userDefaults.set(flag, forKey: UserDefaultsKey.isWalkthroughFinished.rawValue)
52 | continuation.resume()
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/Infrastructures/UserDefaultsManager/UserDefaultsManager.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | UserDefaultsManager:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.userdefaultsmanager
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: Infrastructures/UserDefaultsManager
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 Infrastructures/UserDefaultsManager/Sources
17 | ${SRCROOT}/bin/swiftlint Infrastructures/UserDefaultsManager/Sources
18 | dependencies:
19 | - target: InfrastructureServices
20 | - target: Entities
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 hugehoge
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SHELL = bash
2 |
3 | MAKEFILE_DIR := $(dir $(realpath $(firstword ${MAKEFILE_LIST})))
4 | BIN_DIR := ${MAKEFILE_DIR}/bin
5 | LIB_DIR := ${MAKEFILE_DIR}/lib
6 |
7 | .PHONY: open \
8 | bootstrap \
9 | xcodeproj \
10 | lint \
11 | format \
12 | local-install-xcodegen \
13 | local-install-swiftgen \
14 | local-install-swiftlint \
15 | local-install-swiftformat \
16 | local-install-licenseplist
17 |
18 | open:
19 | @open swiftui-flowcontroller-pattern.xcodeproj
20 |
21 | bootstrap:
22 | @echo "*** Install build tools ***"
23 | @$(MAKE) local-install-xcodegen
24 | @$(MAKE) local-install-swiftgen
25 | @$(MAKE) local-install-swiftlint
26 | @$(MAKE) local-install-swiftformat
27 | @${MAKE} local-install-licenseplist
28 | @echo ""
29 | @echo "*** Generate Xcode project ***"
30 | @$(MAKE) xcodeproj
31 | @echo ""
32 | @echo "Complete bootstrap."
33 |
34 | xcodeproj: local-install-xcodegen
35 | @echo -n "Xcode project generating..."
36 | @${XCODEGEN_BIN_PATH} -q
37 | @echo "OK!"
38 |
39 | lint: local-install-swiftlint
40 | @echo "Linting..."
41 | @${BIN_DIR}/swiftlint --quiet
42 |
43 | format: local-install-swiftformat
44 | @echo "Formatting..."
45 | @${BIN_DIR}/swiftformat --swiftversion 5.5 App Infrastructures Repositories
46 |
47 | local-install-xcodegen:
48 | @echo -n "XcodeGen..."
49 | # It won't work properly with symbolic link.
50 | # see https://github.com/yonaskolb/XcodeGen/issues/247, https://github.com/yonaskolb/XcodeGen/pull/248
51 | $(eval XCODEGEN_BIN_PATH := $(shell ${MAKEFILE_DIR}/scripts/local-install-xcodegen.sh -s "${LIB_DIR}/xcodegen"))
52 | @echo "OK!"
53 |
54 | local-install-swiftgen:
55 | @echo -n "SwiftGen..."
56 | @SWIFTGEN_PATH=$$(${MAKEFILE_DIR}/scripts/local-install-swiftgen.sh -s "${LIB_DIR}/swiftgen"); \
57 | ln -sf "$${SWIFTGEN_PATH}" "${BIN_DIR}/swiftgen"
58 | @echo "OK!"
59 |
60 | local-install-swiftlint:
61 | @echo -n "SwiftLint..."
62 | @SWIFTLINT_PATH=$$(${MAKEFILE_DIR}/scripts/local-install-swiftlint.sh -s "${LIB_DIR}/swiftlint"); \
63 | ln -sf "$${SWIFTLINT_PATH}" "${BIN_DIR}/swiftlint"
64 | @echo "OK!"
65 |
66 | local-install-swiftformat:
67 | @echo -n "SwiftFormat..."
68 | @SWIFTFORMAT_PATH=$$(${MAKEFILE_DIR}/scripts/local-install-swiftformat.sh -s "${LIB_DIR}/swiftformat"); \
69 | ln -sf "$${SWIFTFORMAT_PATH}" "${BIN_DIR}/swiftformat"
70 | @echo "OK!"
71 |
72 | local-install-licenseplist:
73 | @echo -n "LicensePlist..."
74 | @LICENSEPLIST_PATH=$$(${MAKEFILE_DIR}/scripts/local-install-licenseplist.sh -s "${LIB_DIR}/licenseplist"); \
75 | ln -sf "$${LICENSEPLIST_PATH}" "${BIN_DIR}/license-plist"
76 | @echo "OK!"
77 |
--------------------------------------------------------------------------------
/MockingbirdSupport/AppKit/NSResponder.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Foundation
3 |
4 | open class NSResponder : NSObject, NSCoding {
5 | public init()
6 | public required init?(coder: NSCoder)
7 | }
8 |
--------------------------------------------------------------------------------
/MockingbirdSupport/AppKit/NSView.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Foundation
3 |
4 | open class NSView : NSResponder {
5 | public init(frame frameRect: NSRect)
6 | public required init?(coder: NSCoder)
7 | }
8 |
--------------------------------------------------------------------------------
/MockingbirdSupport/AppKit/NSViewController.swift:
--------------------------------------------------------------------------------
1 | import AppKit
2 | import Foundation
3 |
4 | @available(OSX 10.5, *)
5 | open class NSViewController : NSResponder {
6 | public init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?)
7 | public required init?(coder: NSCoder)
8 | }
9 |
--------------------------------------------------------------------------------
/MockingbirdSupport/Foundation/ObjectiveC/NSObject.swift:
--------------------------------------------------------------------------------
1 | import ObjectiveC
2 |
3 | public protocol NSObjectProtocol {}
4 |
5 | open class NSObject : NSObjectProtocol {
6 | public init() {}
7 | }
8 |
9 | public protocol NSCopying {
10 | func copy(with zone: NSZone?) -> Any
11 | }
12 |
13 | public protocol NSMutableCopying {
14 | func mutableCopy(with zone: NSZone?) -> Any
15 | }
16 |
17 | public protocol NSCoding {
18 | func encode(with coder: NSCoder)
19 | init?(coder: NSCoder)
20 | }
21 |
--------------------------------------------------------------------------------
/MockingbirdSupport/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Bird Rides, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/MockingbirdSupport/Swift/Codable.swift:
--------------------------------------------------------------------------------
1 | public protocol Encodable {
2 | func encode(to encoder: Encoder) throws
3 | }
4 |
5 | public protocol Decodable {
6 | @objc(mkb_implicit) init(from decoder: Decoder) throws
7 | }
8 |
9 | public typealias Codable = Decodable & Encodable
10 |
--------------------------------------------------------------------------------
/MockingbirdSupport/Swift/Comparable.swift:
--------------------------------------------------------------------------------
1 | public protocol Comparable : Equatable {
2 | static func < (lhs: Self, rhs: Self) -> Bool
3 | static func <= (lhs: Self, rhs: Self) -> Bool
4 | static func >= (lhs: Self, rhs: Self) -> Bool
5 | static func > (lhs: Self, rhs: Self) -> Bool
6 | }
7 |
--------------------------------------------------------------------------------
/MockingbirdSupport/Swift/Equatable.swift:
--------------------------------------------------------------------------------
1 | public protocol Equatable {
2 | static func == (lhs: Self, rhs: Self) -> Bool
3 | }
4 |
--------------------------------------------------------------------------------
/MockingbirdSupport/Swift/Hashable.swift:
--------------------------------------------------------------------------------
1 | public protocol Hashable : Equatable {
2 | var hashValue: Int { get }
3 | func hash(into hasher: inout Hasher)
4 | }
5 |
--------------------------------------------------------------------------------
/MockingbirdSupport/Swift/Misc.swift:
--------------------------------------------------------------------------------
1 | public protocol AnyObject {}
2 |
3 | public typealias `class` = AnyObject
4 |
5 | public typealias AnyClass = AnyObject.Type
6 |
7 | public protocol CustomStringConvertible {
8 | var description: String { get }
9 | }
10 |
11 | public protocol CustomDebugStringConvertible {
12 | var debugDescription: String { get }
13 | }
14 |
--------------------------------------------------------------------------------
/MockingbirdSupport/UIKit/UIControl.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit
3 |
4 | @available(iOS 2.0, *)
5 | open class UIControl : UIView {}
6 |
--------------------------------------------------------------------------------
/MockingbirdSupport/UIKit/UILabel.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit
3 |
4 | @available(iOS 2.0, *)
5 | open class UILabel : UIView, NSCoding {}
6 |
--------------------------------------------------------------------------------
/MockingbirdSupport/UIKit/UIResponder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit
3 |
4 | @available(iOS 2.0, *)
5 | open class UIResponder : NSObject, NSCoding {
6 | public init()
7 | public required init?(coder: NSCoder)
8 | }
9 |
--------------------------------------------------------------------------------
/MockingbirdSupport/UIKit/UIView.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit
3 |
4 | @available(iOS 2.0, *)
5 | open class UIView : NSCoding {
6 | public init(frame: CGRect)
7 | public required init?(coder: NSCoder)
8 | }
9 |
--------------------------------------------------------------------------------
/MockingbirdSupport/UIKit/UIViewController.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import UIKit
3 |
4 | @available(iOS 2.0, *)
5 | open class UIViewController : UIResponder, NSCoding {
6 | public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?)
7 | public required init?(coder: NSCoder)
8 | }
9 |
--------------------------------------------------------------------------------
/Repositories/AppStateRepository/AppStateRepository.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | AppStateRepository:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.appstaterepository
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: Repositories/AppStateRepository
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 Repositories/AppStateRepository/Sources
17 | ${SRCROOT}/bin/swiftlint Repositories/AppStateRepository/Sources
18 | dependencies:
19 | - target: RepositoryServices
20 | - target: InfrastructureServices
21 | - target: Entities
22 | AppStateRepositoryTests:
23 | type: bundle.unit-test
24 | platform: iOS
25 | sources:
26 | - path: Tests
27 | group: Repositories/AppStateRepository
28 | excludes:
29 | - "**/.gitkeep"
30 | - path: Tests/Generated/InfrastructureServicesMocks.generated.swift
31 | group: Repositories/AppStateRepository/Tests/Generated
32 | optional: true
33 | preBuildScripts:
34 | - name: Mockingbird
35 | script: |
36 | DERIVED_DATA="$(xcodebuild -showBuildSettings | sed -n 's|.*BUILD_ROOT = \(.*\)/Build/.*|\1|p')"
37 | "${DERIVED_DATA}/SourcePackages/checkouts/mockingbird/mockingbird" generate \
38 | --targets InfrastructureServices \
39 | --testbundle AppStateRepositoryTests \
40 | --output-dir Repositories/AppStateRepository/Tests/Generated \
41 | --only-protocols \
42 | --disable-swiftlint
43 | outputFiles:
44 | - ${SRCROOT}/Repositories/AppStateRepository/Tests/Generated/InfrastructureServicesMocks.generated.swift
45 | basedOnDependencyAnalysis: no
46 | - name: Format and linting
47 | script: |
48 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 Repositories/AppStateRepository/Tests
49 | ${SRCROOT}/bin/swiftlint Repositories/AppStateRepository/Tests
50 | dependencies:
51 | - target: AppStateRepository
52 | - package: CombineExpectations
53 | - package: Mockingbird
54 |
--------------------------------------------------------------------------------
/Repositories/AppStateRepository/Sources/AppStateRepository.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 |
4 | import InfrastructureServices
5 | import RepositoryServices
6 |
7 | public final class AppStateRepository: AppStateRepositoryService {
8 | private let _userDefaultsManager: UserDefaultsManagerService
9 |
10 | // swiftlint:disable:next discouraged_optional_boolean
11 | private let _isWalkthroughFinishedSubject = CurrentValueSubject(nil)
12 |
13 | public init(userDefaultsManager: UserDefaultsManagerService) {
14 | self._userDefaultsManager = userDefaultsManager
15 |
16 | Task {
17 | let isWalkthroughFinished = await userDefaultsManager.fetchIsWalkthroughFinished()
18 | _isWalkthroughFinishedSubject.send(isWalkthroughFinished)
19 | }
20 | }
21 |
22 | public func isWalkthroughFinishedStream() -> AnyPublisher {
23 | _isWalkthroughFinishedSubject
24 | .compactMap { $0 }
25 | .removeDuplicates()
26 | .eraseToAnyPublisher()
27 | }
28 |
29 | public func finishWalkthrough() async {
30 | await _userDefaultsManager.saveIsWalkthroughFinished(true)
31 | _isWalkthroughFinishedSubject.send(true)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Repositories/AppStateRepository/Tests/AppStateRepositoryTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import CombineExpectations
4 | import Mockingbird
5 |
6 | @testable import AppStateRepository
7 | @testable import InfrastructureServices
8 |
9 | final class AppStateRepositoryTests: XCTestCase {
10 | override func setUpWithError() throws {
11 | try super.setUpWithError()
12 | }
13 |
14 | override func tearDownWithError() throws {
15 | try super.tearDownWithError()
16 | }
17 |
18 | func test_isWalkthroughFinishedStream_PersistedAsFalse_InitialOutputIsFalse() async throws {
19 | // Arrange
20 | let mock = mock(UserDefaultsManagerService.self)
21 | given(await mock.fetchIsWalkthroughFinished()).willReturn(false)
22 |
23 | // Act
24 | let repository = AppStateRepository(userDefaultsManager: mock)
25 | let recorder = repository.isWalkthroughFinishedStream().record()
26 |
27 | let first = try wait(for: recorder.next(), timeout: 0.1)
28 |
29 | // Assert
30 | XCTAssertFalse(first)
31 | }
32 |
33 | func test_isWalkthroughFinishedStream_PersistedAsTrue_InitialOutputIsTrue() async throws {
34 | // Arrange
35 | let mock = mock(UserDefaultsManagerService.self)
36 | given(await mock.fetchIsWalkthroughFinished()).willReturn(true)
37 |
38 | // Act
39 | let repository = AppStateRepository(userDefaultsManager: mock)
40 | let recorder = repository.isWalkthroughFinishedStream().record()
41 |
42 | let first = try wait(for: recorder.next(), timeout: 0.1)
43 |
44 | // Assert
45 | XCTAssertTrue(first)
46 | }
47 |
48 | func test_finishWalkthrough_PersistedAsFalse_PersistTrueAndOutputTrue() async throws {
49 | // Arrange
50 | let mock = mock(UserDefaultsManagerService.self)
51 | given(await mock.fetchIsWalkthroughFinished()).willReturn(false)
52 |
53 | // Act
54 | let repository = AppStateRepository(userDefaultsManager: mock)
55 | let recorder = repository.isWalkthroughFinishedStream().record()
56 |
57 | await repository.finishWalkthrough()
58 |
59 | let output = try wait(for: recorder.availableElements, timeout: 0.1)
60 |
61 | // Assert
62 | XCTAssertEqual([false, true], output)
63 | verify(await mock.saveIsWalkthroughFinished(true)).wasCalled()
64 | }
65 |
66 | func test_finishWalkthrough_PersistedAsTrue_PersistTrueAndOutputTrueOnce() async throws {
67 | // Arrange
68 | let mock = mock(UserDefaultsManagerService.self)
69 | given(await mock.fetchIsWalkthroughFinished()).willReturn(true)
70 |
71 | // Act
72 | let repository = AppStateRepository(userDefaultsManager: mock)
73 | let recorder = repository.isWalkthroughFinishedStream().record()
74 |
75 | await repository.finishWalkthrough()
76 |
77 | let output = try wait(for: recorder.availableElements, timeout: 0.1)
78 |
79 | // Assert
80 | XCTAssertEqual([true], output)
81 | verify(await mock.saveIsWalkthroughFinished(true)).wasCalled()
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Repositories/AppStateRepository/Tests/Generated/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/Repositories/AppStateRepository/Tests/Generated/.gitkeep
--------------------------------------------------------------------------------
/Repositories/FeedRepository/FeedRepository.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | FeedRepository:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.feedrepository
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: Repositories/FeedRepository
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 Repositories/FeedRepository/Sources
17 | ${SRCROOT}/bin/swiftlint Repositories/FeedRepository/Sources
18 | dependencies:
19 | - target: RepositoryServices
20 | - target: InfrastructureServices
21 | - target: Entities
22 |
--------------------------------------------------------------------------------
/Repositories/FeedRepository/Sources/FeedRepository.swift:
--------------------------------------------------------------------------------
1 | import Entities
2 | import InfrastructureServices
3 | import RepositoryServices
4 |
5 | public final class FeedRepository: FeedRepositoryService {
6 | private let _feedClient: FeedClientService
7 |
8 | public init(feedClient: FeedClientService) {
9 | self._feedClient = feedClient
10 | }
11 |
12 | public func fetchNews(language: FeedLanguage) async throws -> [News] {
13 | try await _feedClient.fetchNews(language: language)
14 | }
15 |
16 | public func fetchFreeAppTop15Ranking(language: FeedLanguage) async throws -> [AppInformation] {
17 | try await _feedClient.fetchFreeAppRanking(language: language, limit: 15)
18 | }
19 |
20 | public func fetchPaidAppTop15Ranking(language: FeedLanguage) async throws -> [AppInformation] {
21 | try await _feedClient.fetchPaidAppRanking(language: language, limit: 15)
22 | }
23 |
24 | public func fetchFreeAppAllRanking(language: FeedLanguage) async throws -> [AppInformation] {
25 | try await _feedClient.fetchFreeAppRanking(language: language, limit: 50)
26 | }
27 |
28 | public func fetchPaidAppAllRanking(language: FeedLanguage) async throws -> [AppInformation] {
29 | try await _feedClient.fetchPaidAppRanking(language: language, limit: 50)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Repositories/PreferencesRepository/PreferencesRepository.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | PreferencesRepository:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.preferencesrepository
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: Repositories/PreferencesRepository
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 Repositories/PreferencesRepository/Sources
17 | ${SRCROOT}/bin/swiftlint Repositories/PreferencesRepository/Sources
18 | dependencies:
19 | - target: RepositoryServices
20 | - target: InfrastructureServices
21 | - target: Entities
22 |
--------------------------------------------------------------------------------
/Repositories/PreferencesRepository/Sources/PreferencesRepository.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 |
4 | import Entities
5 | import InfrastructureServices
6 | import RepositoryServices
7 |
8 | public final class PreferencesRepository: PreferencesRepositoryService {
9 | private let _userDefaultsManager: UserDefaultsManagerService
10 |
11 | private let _feedLanguageSubject = CurrentValueSubject(nil)
12 | private let _userNameSubject = CurrentValueSubject(nil)
13 |
14 | public init(userDefaultsManager: UserDefaultsManagerService) {
15 | self._userDefaultsManager = userDefaultsManager
16 |
17 | Task {
18 | _feedLanguageSubject.send(await userDefaultsManager.fetchFeedLanguage())
19 | }
20 | Task {
21 | _userNameSubject.send(await userDefaultsManager.fetchUserName())
22 | }
23 | }
24 |
25 | public func feedLanguageStream() -> AnyPublisher {
26 | _feedLanguageSubject.compactMap { $0 }
27 | .removeDuplicates()
28 | .eraseToAnyPublisher()
29 | }
30 |
31 | public func userNameStream() -> AnyPublisher {
32 | _userNameSubject.compactMap { $0 }
33 | .removeDuplicates()
34 | .eraseToAnyPublisher()
35 | }
36 |
37 | public func fetchFeedLanguage() async -> FeedLanguage? {
38 | await withCheckedContinuation { continuation in
39 | continuation.resume(returning: _feedLanguageSubject.value)
40 | }
41 | }
42 |
43 | public func fetchUserName() async -> String? {
44 | await withCheckedContinuation { continuation in
45 | continuation.resume(returning: _userNameSubject.value)
46 | }
47 | }
48 |
49 | public func saveFeedLanguage(_ language: FeedLanguage) async {
50 | await _userDefaultsManager.saveFeedLanguage(language)
51 | _feedLanguageSubject.send(language)
52 | }
53 |
54 | public func saveUserName(_ name: String) async {
55 | await _userDefaultsManager.saveUserName(name)
56 | _userNameSubject.send(name)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Repositories/RepositoryServices/RepositoryServices.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | RepositoryServices:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.repositoryservices
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | DEVELOPMENT_ASSET_PATHS: Repositories/RepositoryServices/Sources/PreviewContents
11 | sources:
12 | - path: Sources
13 | group: Repositories/RepositoryServices
14 | preBuildScripts:
15 | - name: Format and linting
16 | script: |
17 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 Repositories/RepositoryServices/Sources
18 | ${SRCROOT}/bin/swiftlint Repositories/RepositoryServices/Sources
19 | dependencies:
20 | - target: Entities
21 |
--------------------------------------------------------------------------------
/Repositories/RepositoryServices/Sources/AppStateRepositoryService.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | public protocol AppStateRepositoryService {
4 | func isWalkthroughFinishedStream() -> AnyPublisher
5 | func finishWalkthrough() async
6 | }
7 |
--------------------------------------------------------------------------------
/Repositories/RepositoryServices/Sources/FeedRepositoryService.swift:
--------------------------------------------------------------------------------
1 | import Entities
2 |
3 | public protocol FeedRepositoryService {
4 | func fetchNews(language: FeedLanguage) async throws -> [News]
5 | func fetchFreeAppTop15Ranking(language: FeedLanguage) async throws -> [AppInformation]
6 | func fetchPaidAppTop15Ranking(language: FeedLanguage) async throws -> [AppInformation]
7 | func fetchFreeAppAllRanking(language: FeedLanguage) async throws -> [AppInformation]
8 | func fetchPaidAppAllRanking(language: FeedLanguage) async throws -> [AppInformation]
9 | }
10 |
--------------------------------------------------------------------------------
/Repositories/RepositoryServices/Sources/PreferencesRepositoryService.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | import Entities
4 |
5 | public protocol PreferencesRepositoryService {
6 | func feedLanguageStream() -> AnyPublisher
7 | func userNameStream() -> AnyPublisher
8 | func fetchFeedLanguage() async -> FeedLanguage?
9 | func fetchUserName() async -> String?
10 | func saveFeedLanguage(_ language: FeedLanguage) async
11 | func saveUserName(_ name: String) async
12 | }
13 |
--------------------------------------------------------------------------------
/Repositories/RepositoryServices/Sources/PreviewContents/PreviewFeedRepository.swift:
--------------------------------------------------------------------------------
1 | import Entities
2 |
3 | public final class PreviewFeedRepository: FeedRepositoryService {
4 | public init() { }
5 |
6 | public func fetchNews(language _: FeedLanguage) async throws -> [News] {
7 | News.previewContentList
8 | }
9 |
10 | public func fetchFreeAppTop15Ranking(language _: FeedLanguage) async throws -> [AppInformation] {
11 | AppInformation.previewContentList
12 | }
13 |
14 | public func fetchPaidAppTop15Ranking(language _: FeedLanguage) async throws -> [AppInformation] {
15 | AppInformation.previewContentList
16 | }
17 |
18 | public func fetchFreeAppAllRanking(language _: FeedLanguage) async throws -> [AppInformation] {
19 | AppInformation.previewContentList
20 | }
21 |
22 | public func fetchPaidAppAllRanking(language _: FeedLanguage) async throws -> [AppInformation] {
23 | AppInformation.previewContentList
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Repositories/RepositoryServices/Sources/PreviewContents/PreviewPreferencesRepository.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | import Entities
4 |
5 | public final class PreviewPreferencesRepository: PreferencesRepositoryService {
6 | public init() { }
7 |
8 | public func feedLanguageStream() -> AnyPublisher {
9 | Just(.english).eraseToAnyPublisher()
10 | }
11 |
12 | public func userNameStream() -> AnyPublisher {
13 | Just("John Doe").eraseToAnyPublisher()
14 | }
15 |
16 | public func fetchFeedLanguage() async -> FeedLanguage? {
17 | .english
18 | }
19 |
20 | public func fetchUserName() async -> String? {
21 | "John Doe"
22 | }
23 |
24 | public func saveFeedLanguage(_ language: FeedLanguage) async {
25 | print("Call PreferencesRepository.saveFeedLanguage(\(language)")
26 | }
27 |
28 | public func saveUserName(_ name: String) async {
29 | print("Call PreferencesRepository.saveUserName(\(name))")
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Resources/Resources.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | Resources:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.resources
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: Resources
13 | - path: Sources/Generated/Assets+Generated.swift
14 | group: Resources/Sources/Generated
15 | optional: true
16 | - path: Sources/Generated/Files+Generated.swift
17 | group: Resources/Sources/Generated
18 | optional: true
19 | preBuildScripts:
20 | - name: SwiftGen
21 | script: |
22 | if [ $ACTION != "indexbuild" ]; then
23 | ${SRCROOT}/bin/swiftgen config run --config Resources/Resources.swiftgen.yml
24 | fi
25 | outputFiles:
26 | - ${SRCROOT}/Resources/Sources/Generated/Assets+Generated.swift
27 | - ${SRCROOT}/Resources/Sources/Generated/Files+Generated.swift
28 | basedOnDependencyAnalysis: no
29 | - name: LicensePlist
30 | script: |
31 | ${SRCROOT}/bin/license-plist \
32 | --suppress-opening-directory \
33 | --fail-if-missing-license \
34 | --output-path "${SRCROOT}/Resources/Sources/Settings.bundle" \
35 | --markdown-path "${SRCROOT}/Resources/Sources/Licenses.md"
36 |
--------------------------------------------------------------------------------
/Resources/Resources.swiftgen.yml:
--------------------------------------------------------------------------------
1 | input_dir: Sources
2 | output_dir: Sources/Generated
3 |
4 | files:
5 | inputs:
6 | - Licenses.md
7 | outputs:
8 | - templateName: structured-swift5
9 | output: Files+Generated.swift
10 | params:
11 | publicAccess: true
12 |
13 | xcassets:
14 | inputs:
15 | - Assets.xcassets
16 | outputs:
17 | - templateName: swift5
18 | output: Assets+Generated.swift
19 | params:
20 | publicAccess: true
21 |
--------------------------------------------------------------------------------
/Resources/Sources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Resources/Sources/Assets.xcassets/Colors/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Resources/Sources/Assets.xcassets/Colors/background.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0xF7",
9 | "green" : "0xF2",
10 | "red" : "0xF2"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0x00",
27 | "green" : "0x00",
28 | "red" : "0x00"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Resources/Sources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Resources/Sources/Settings.bundle/Root.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | StringsTable
6 | Root
7 | PreferenceSpecifiers
8 |
9 |
10 | Type
11 | PSChildPaneSpecifier
12 | Title
13 | Licenses
14 | File
15 | com.mono0926.LicensePlist
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/Resources/Sources/Settings.bundle/en.lproj/Root.strings:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/Resources/Sources/Settings.bundle/en.lproj/Root.strings
--------------------------------------------------------------------------------
/UI/UIComponents/Sources/AppIcon.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import Kingfisher
4 |
5 | import Entities
6 |
7 | // MARK: - AppIcon
8 |
9 | public struct AppIcon: View {
10 | private let _url: URL
11 |
12 | public var body: some View {
13 | KFImage(_url)
14 | .cacheMemoryOnly()
15 | .placeholder {
16 | Color.gray
17 | }
18 | .resizable()
19 | .frame(width: 72, height: 72)
20 | .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
21 | .fixedSize()
22 | }
23 |
24 | public init(url: URL) {
25 | self._url = url
26 | }
27 | }
28 |
29 | // MARK: - AppIcon_Previews
30 |
31 | struct AppIcon_Previews: PreviewProvider {
32 | static var previews: some View {
33 | Group {
34 | ForEach(AppInformation.previewContentList, id: \.self) { content in
35 | AppIcon(url: content.iconURL)
36 | .previewLayout(.fixed(width: 200, height: 100))
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/UI/UIComponents/Sources/AppRankingButton.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import Entities
4 |
5 | // MARK: - _AppRankingButtonStyle
6 |
7 | private struct _AppRankingButtonStyle: ButtonStyle {
8 | func makeBody(configuration: Configuration) -> some View {
9 | configuration.label
10 | .scaleEffect(configuration.isPressed ? 0.95 : 1.0)
11 | .animation(.easeInOut, value: configuration.isPressed ? 0.95 : 1.0)
12 | }
13 | }
14 |
15 | // MARK: - AppRankingButton
16 |
17 | public struct AppRankingButton: View {
18 | private let _app: AppInformation
19 | private let _rank: Int
20 | private let _action: (() -> Void)?
21 |
22 | public var body: some View {
23 | Button {
24 | _action?()
25 | } label: {
26 | _buttonLabel
27 | }
28 | .buttonStyle(_AppRankingButtonStyle())
29 | }
30 |
31 | private var _buttonLabel: some View {
32 | HStack {
33 | AppIcon(url: _app.iconURL)
34 |
35 | HStack(alignment: .firstTextBaseline) {
36 | Text("\(_rank)")
37 | .font(.headline)
38 | .foregroundColor(.primary)
39 |
40 | VStack(alignment: .leading) {
41 | Text(_app.name)
42 | .font(.headline)
43 | .foregroundColor(.primary)
44 | .lineLimit(2)
45 | Text(_app.seller)
46 | .font(.body)
47 | .foregroundColor(.secondary)
48 | .lineLimit(1)
49 | }
50 | }
51 |
52 | Spacer(minLength: 0)
53 | }
54 | .contentShape(Rectangle()) // For tap area
55 | }
56 |
57 | public init(
58 | _ app: AppInformation,
59 | rank: Int,
60 | action: (() -> Void)? = nil
61 | ) {
62 | self._app = app
63 | self._rank = rank
64 | self._action = action
65 | }
66 | }
67 |
68 | // MARK: - AppRankingButton_Previews
69 |
70 | struct AppRankingButton_Previews: PreviewProvider {
71 | static var previews: some View {
72 | Group {
73 | ForEach(0.. Void)?
13 |
14 | public var body: some View {
15 | VStack(alignment: .leading) {
16 | AppRankingButton(
17 | _firstApp,
18 | rank: _rankOffset + 0
19 | ) {
20 | _openStoreAction?(_firstApp.storeURL)
21 | }
22 | if let secondApp = _secondApp {
23 | Divider().padding(.leading, 72)
24 | AppRankingButton(
25 | secondApp,
26 | rank: _rankOffset + 1
27 | ) {
28 | _openStoreAction?(secondApp.storeURL)
29 | }
30 | }
31 | if let thirdApp = _thirdApp {
32 | Divider().padding(.leading, 72)
33 | AppRankingButton(
34 | thirdApp,
35 | rank: _rankOffset + 2
36 | ) {
37 | _openStoreAction?(thirdApp.storeURL)
38 | }
39 | }
40 | Spacer(minLength: 0)
41 | }
42 | }
43 |
44 | public init(
45 | rankOffset: Int,
46 | firstApp: AppInformation,
47 | secondApp: AppInformation? = nil,
48 | thirdApp: AppInformation? = nil,
49 | openStoreAction: ((URL) -> Void)? = nil
50 | ) {
51 | self._rankOffset = rankOffset
52 | self._firstApp = firstApp
53 | self._secondApp = secondApp
54 | self._thirdApp = thirdApp
55 | self._openStoreAction = openStoreAction
56 | }
57 | }
58 |
59 | // MARK: - AppRankingStack_Previews
60 |
61 | struct AppRankingStack_Previews: PreviewProvider {
62 | static var previews: some View {
63 | AppRankingStack(
64 | rankOffset: 1,
65 | firstApp: AppInformation.previewContentList[0],
66 | secondApp: AppInformation.previewContentList[1],
67 | thirdApp: AppInformation.previewContentList[2]
68 | )
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/UI/UIComponents/Sources/BulletPointText.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - BulletPointText
4 |
5 | public struct BulletPointText: View {
6 | private let _text: String
7 | private let _bulletPointMark: String
8 |
9 | public var body: some View {
10 | HStack(alignment: .top) {
11 | Text(_bulletPointMark)
12 | .padding(.trailing, 4)
13 |
14 | Text(_text)
15 | .multilineTextAlignment(.leading)
16 | }
17 | }
18 |
19 | public init(_ text: String, bulletPointMark: String = "-") {
20 | self._bulletPointMark = bulletPointMark
21 | self._text = text
22 | }
23 | }
24 |
25 | // MARK: - BulletPointText_Previews
26 |
27 | struct BulletPointText_Previews: PreviewProvider {
28 | static var previews: some View {
29 | Group {
30 | BulletPointText("-Test Text-")
31 | .padding()
32 | .previewLayout(.fixed(width: 400, height: 50))
33 | .previewDisplayName("Default")
34 |
35 | BulletPointText("*Test Text*", bulletPointMark: "*")
36 | .padding()
37 | .previewLayout(.fixed(width: 400, height: 50))
38 | .previewDisplayName("Other bullet point mark")
39 |
40 | VStack(alignment: .leading) {
41 | BulletPointText("Test Text")
42 | BulletPointText("")
43 | BulletPointText("Long long long long test text")
44 | BulletPointText("A")
45 | }
46 | .padding()
47 | .previewLayout(.fixed(width: 400, height: 100))
48 | .previewDisplayName("Multi length")
49 |
50 | BulletPointText(
51 | // swiftlint:disable line_length
52 | """
53 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
54 | """
55 | // swiftlint:enable line_length
56 | )
57 | .padding()
58 | .previewLayout(.fixed(width: 400, height: 100))
59 | .previewDisplayName("Line break")
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/UI/UIComponents/Sources/NavigationLinkButton.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - NavigationLinkButton
4 |
5 | /// A Button in NavigationView simulating NavigationLink
6 | ///
7 | /// - seealso: https://ideal-reality.com/programing/swiftui-list-navlink-design/
8 | public struct NavigationLinkButton: View {
9 | private let _title: String?
10 | private let _action: () -> Void
11 |
12 | public var body: some View {
13 | Button(action: _action) {
14 | HStack {
15 | if let title = _title {
16 | Text(title)
17 | .foregroundColor(.primary)
18 | }
19 | Spacer()
20 | Image(systemName: "chevron.right")
21 | .font(Font.system(size: 14, weight: .semibold))
22 | .foregroundColor(.secondary)
23 | .opacity(0.5)
24 | }
25 | }
26 | }
27 |
28 | public init(_ title: String?, action: @escaping () -> Void) {
29 | self._title = title
30 | self._action = action
31 | }
32 | }
33 |
34 | // MARK: - NavigationLinkButton_Previews
35 |
36 | struct NavigationLinkButton_Previews: PreviewProvider {
37 | static var previews: some View {
38 | Group {
39 | NavigationView {
40 | List {
41 | NavigationLink("Original", destination: EmptyView())
42 | NavigationLinkButton("NavigationLinkButton") { }
43 | }
44 | .listStyle(.plain)
45 | }
46 | .preferredColorScheme(.light)
47 | .previewLayout(.fixed(width: 300, height: 200))
48 | .previewDisplayName("Light plain")
49 |
50 | NavigationView {
51 | List {
52 | NavigationLink("Original", destination: EmptyView())
53 | NavigationLinkButton("NavigationLinkButton") { }
54 | }
55 | .listStyle(.insetGrouped)
56 | }
57 | .preferredColorScheme(.light)
58 | .previewLayout(.fixed(width: 300, height: 200))
59 | .previewDisplayName("Light inset group")
60 |
61 | NavigationView {
62 | List {
63 | NavigationLink("Original", destination: EmptyView())
64 | NavigationLinkButton("NavigationLinkButton") { }
65 | }
66 | .listStyle(.plain)
67 | }
68 | .preferredColorScheme(.dark)
69 | .previewLayout(.fixed(width: 300, height: 200))
70 | .previewDisplayName("Dark plain")
71 |
72 | NavigationView {
73 | List {
74 | NavigationLink("Original", destination: EmptyView())
75 | NavigationLinkButton("NavigationLinkButton") { }
76 | }
77 | .listStyle(.insetGrouped)
78 | }
79 | .preferredColorScheme(.dark)
80 | .previewLayout(.fixed(width: 300, height: 200))
81 | .previewDisplayName("Dark inset group")
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/UI/UIComponents/Sources/NewsLargeCard.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import Kingfisher
4 |
5 | import Entities
6 |
7 | // MARK: - _NewsLargeCardButtonStyle
8 |
9 | private struct _NewsLargeCardButtonStyle: ButtonStyle {
10 | func makeBody(configuration: Configuration) -> some View {
11 | configuration.label
12 | .scaleEffect(configuration.isPressed ? 0.95 : 1.0)
13 | .animation(.easeInOut, value: configuration.isPressed ? 0.95 : 1.0)
14 | }
15 | }
16 |
17 | // MARK: - NewsLargeCard
18 |
19 | public struct NewsLargeCard: View {
20 | private let _news: News
21 | private let _action: (() -> Void)?
22 |
23 | public var body: some View {
24 | Button {
25 | _action?()
26 | } label: {
27 | _buttonLabel
28 | }
29 | .buttonStyle(_NewsLargeCardButtonStyle())
30 | }
31 |
32 | private var _buttonLabel: some View {
33 | VStack(spacing: 8) {
34 | KFImage(_news.imageURL)
35 | .cacheMemoryOnly()
36 | .placeholder {
37 | Color.gray
38 | }
39 | .resizable()
40 | .aspectRatio(1192 / 626, contentMode: .fit)
41 |
42 | HStack {
43 | Text(_news.category)
44 | .foregroundColor(.secondary)
45 | .font(.footnote)
46 | .fontWeight(.bold)
47 | .lineLimit(1)
48 | Spacer(minLength: 0)
49 | }
50 | .padding([.leading, .trailing], 16)
51 |
52 | HStack {
53 | Text(_news.title)
54 | .font(.headline)
55 | .lineLimit(3)
56 | Spacer(minLength: 0)
57 | }
58 | .padding([.leading, .trailing], 16)
59 |
60 | HStack {
61 | Spacer(minLength: 0)
62 | Text(_dateString)
63 | .foregroundColor(.secondary)
64 | .font(.footnote)
65 | .lineLimit(1)
66 | }
67 | .padding([.leading, .trailing], 16)
68 | .padding(.bottom, 8)
69 | }
70 | .background(Color(uiColor: .secondarySystemGroupedBackground))
71 | .cornerRadius(16)
72 | }
73 |
74 | private var _dateString: String {
75 | let formatter = DateFormatter()
76 | formatter.dateStyle = .medium
77 | formatter.timeStyle = .none
78 |
79 | return formatter.string(from: _news.date)
80 | }
81 |
82 | public init(_ news: News, action: (() -> Void)? = nil) {
83 | self._news = news
84 | self._action = action
85 | }
86 | }
87 |
88 | // MARK: - NewsLargeCard_Previews
89 |
90 | struct NewsLargeCard_Previews: PreviewProvider {
91 | static var previews: some View {
92 | ScrollView {
93 | VStack {
94 | NewsLargeCard(News.appStoreAward)
95 | .padding()
96 |
97 | NewsLargeCard(News.appleMusicAward)
98 | .padding()
99 | }
100 | .listStyle(.plain)
101 | }
102 | .background(Color.gray)
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/UI/UIComponents/Sources/RadioButtonGroup.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - RadioButtonGroup
4 |
5 | public struct RadioButtonGroup: View {
6 | private let _titles: [String]
7 |
8 | @Binding private var _selectedIndex: Int?
9 |
10 | public var body: some View {
11 | VStack(alignment: .leading, spacing: 24) {
12 | ForEach(0..<_titles.count) { index in
13 | RadioButton(
14 | _titles[index],
15 | isSelected: Binding(get: {
16 | index == _selectedIndex
17 | }, set: { newValue in
18 | _selectedIndex = newValue ? index : nil
19 | })
20 | )
21 | }
22 | }
23 | .padding(4)
24 | }
25 |
26 | public init(_ titles: [String], selectedIndex: Binding) {
27 | self._titles = titles
28 | self.__selectedIndex = selectedIndex
29 | }
30 | }
31 |
32 | // MARK: - RadioButtonGroup_Previews
33 |
34 | struct RadioButtonGroup_Previews: PreviewProvider {
35 | struct RadioButtonGroupBindingHolder: View {
36 | let titles: [String]
37 |
38 | @State var selectedIndex: Int?
39 |
40 | var body: some View {
41 | RadioButtonGroup(titles, selectedIndex: $selectedIndex)
42 | }
43 | }
44 |
45 | static var previews: some View {
46 | Group {
47 | RadioButtonGroupBindingHolder(
48 | titles: [
49 | "Item 1",
50 | "Item 22",
51 | "Item 333",
52 | "Item 4444",
53 | "Item 55555",
54 | "Item 1",
55 | ],
56 | selectedIndex: nil
57 | )
58 | .previewLayout(.sizeThatFits)
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/UI/UIComponents/UIComponents.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | UIComponents:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.uicomponents
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/UIComponents
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/UIComponents/Sources
17 | ${SRCROOT}/bin/swiftlint UI/UIComponents/Sources
18 | dependencies:
19 | - target: Entities
20 | - package: Kingfisher
21 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/AppSales/AppSalesFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | import Entities
4 |
5 | public protocol AppSalesFlowControllerService: UIViewController {
6 | func start(segment: AppSegment)
7 | }
8 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/Feed/FeedFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol FeedFlowControllerService: UIViewController {
4 | func start()
5 | }
6 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/FeedLanguageSetting/FeedLanguageSettingFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol FeedLanguageSettingFlowControllerService: UIViewController {
4 | func start()
5 | }
6 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/FeedList/FeedListFlowControllerDelegate.swift:
--------------------------------------------------------------------------------
1 | import Entities
2 |
3 | public protocol FeedListFlowControllerDelegate: AnyObject {
4 | func feedListFlowController(_ flowController: FeedListFlowControllerService, didSelect appSales: AppSegment)
5 | }
6 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/FeedList/FeedListFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol FeedListFlowControllerService: UIViewController {
4 | var delegate: FeedListFlowControllerDelegate? { get set }
5 |
6 | func start()
7 | }
8 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/FullScreenModal/FullScreenModalFlowControllerDelegate.swift:
--------------------------------------------------------------------------------
1 | public protocol FullScreenModalFlowControllerDelegate: AnyObject {
2 | func fullScreenModalFlowControllerShouldDismiss(_ flowController: FullScreenModalFlowControllerService)
3 | }
4 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/FullScreenModal/FullScreenModalFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol FullScreenModalFlowControllerService: UIViewController {
4 | var delegate: FullScreenModalFlowControllerDelegate? { get set }
5 |
6 | func start()
7 | }
8 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/KeyboardTest/KeyboardTestFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol KeyboardTestFlowControllerService: UIViewController {
4 | func start()
5 | }
6 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/Licenses/LicensesFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol LicensesFlowControllerService: UIViewController {
4 | func start()
5 | }
6 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/Main/MainFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol MainFlowControllerService: UIViewController {
4 | func start()
5 | }
6 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/ModalTransitionTest/ModalTransitionTestFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol ModalTransitionTestFlowControllerService: UIViewController {
4 | func start()
5 | }
6 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/NoticeAlert/NoticeAlertFlowControllerDelegate.swift:
--------------------------------------------------------------------------------
1 | public protocol NoticeAlertFlowControllerDelegate: AnyObject {
2 | func noticeAlertFlowControllerShouldDismiss(_ flowController: NoticeAlertFlowControllerService)
3 | }
4 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/NoticeAlert/NoticeAlertFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol NoticeAlertFlowControllerService: UIViewController {
4 | var delegate: NoticeAlertFlowControllerDelegate? { get set }
5 |
6 | func start(title: String?, message: String?)
7 | }
8 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/PageSheetModal/PageSheetModalFlowControllerDelegate.swift:
--------------------------------------------------------------------------------
1 | public protocol PageSheetModalFlowControllerDelegate: AnyObject {
2 | func pageSheetModalFlowControllerShouldDismiss(_ flowController: PageSheetModalFlowControllerService)
3 | }
4 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/PageSheetModal/PageSheetModalFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol PageSheetModalFlowControllerService: UIViewController {
4 | var delegate: PageSheetModalFlowControllerDelegate? { get set }
5 |
6 | func start()
7 | }
8 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/PushTransitionTest/PushTransitionTestFlowControllerDelegate.swift:
--------------------------------------------------------------------------------
1 | public protocol PushTransitionTestFlowControllerDelegate: AnyObject {
2 | func pushTransitionTestFlowController(
3 | _ flowController: PushTransitionTestFlowControllerService,
4 | nextWith forwardText: String
5 | )
6 | func pushTransitionTestFlowControllerPopToRoot(_ flowController: PushTransitionTestFlowControllerService)
7 | }
8 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/PushTransitionTest/PushTransitionTestFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol PushTransitionTestFlowControllerService: UIViewController {
4 | var delegate: PushTransitionTestFlowControllerDelegate? { get set }
5 |
6 | func start(forwardedText: String)
7 | }
8 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/Root/RootFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol RootFlowControllerService: UIViewController {
4 | func start()
5 | }
6 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/Settings/SettingsFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol SettingsFlowControllerService: UIViewController {
4 | func start()
5 | }
6 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/SettingsMenu/SettingsMenuFlowControllerDelegate.swift:
--------------------------------------------------------------------------------
1 | import Entities
2 |
3 | public protocol SettingsMenuFlowControllerDelegate: AnyObject {
4 | func settingsMenuFlowController(
5 | _ flowController: SettingsMenuFlowControllerService,
6 | didSelect menuRow: SettingsMenu.Row
7 | )
8 | }
9 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/SettingsMenu/SettingsMenuFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol SettingsMenuFlowControllerService: UIViewController {
4 | var delegate: SettingsMenuFlowControllerDelegate? { get set }
5 |
6 | func start()
7 | }
8 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/ToolbarTest/ToolbarTestFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol ToolbarTestFlowControllerService: UIViewController {
4 | func start()
5 | }
6 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/UIViewController+Content.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | extension UIViewController {
4 | public func addContent(_ viewController: UIViewController) {
5 | addChild(viewController)
6 | view.addSubview(viewController.view)
7 | viewController.view.translatesAutoresizingMaskIntoConstraints = false
8 | NSLayoutConstraint.activate([
9 | viewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
10 | viewController.view.topAnchor.constraint(equalTo: view.topAnchor),
11 | viewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
12 | viewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
13 | ])
14 | viewController.didMove(toParent: self)
15 | }
16 |
17 | public func removeContent(_ viewController: UIViewController) {
18 | viewController.willMove(toParent: nil)
19 | viewController.view.removeFromSuperview()
20 | viewController.removeFromParent()
21 | }
22 |
23 | public func removeAllContentViewController() {
24 | for child in children.reversed() {
25 | removeContent(child)
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/UIViewController+OpenLink.swift:
--------------------------------------------------------------------------------
1 | import SafariServices
2 |
3 | extension UIViewController {
4 | public func openInternally(link url: URL) {
5 | let vc = SFSafariViewController(url: url)
6 |
7 | present(vc, animated: true)
8 | }
9 |
10 | public func openExternally(link url: URL) {
11 | UIApplication.shared.open(url, options: [:])
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/UserNameSetting/UserNameSettingFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol UserNameSettingFlowControllerService: UIViewController {
4 | func start()
5 | }
6 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/Walkthrough/WalkthroughFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol WalkthroughFlowControllerService: UIViewController {
4 | func start()
5 | }
6 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/WalkthroughFinish/WalkthroughFinishFlowControllerDelegate.swift:
--------------------------------------------------------------------------------
1 | public protocol WalkthroughFinishFlowControllerDelegate: AnyObject {
2 | func walkthroughFinishFlowControllerFinishWalkthrough(_ flowController: WalkthroughFinishFlowControllerService)
3 | }
4 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/WalkthroughFinish/WalkthroughFinishFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol WalkthroughFinishFlowControllerService: UIViewController {
4 | var delegate: WalkthroughFinishFlowControllerDelegate? { get set }
5 |
6 | func start()
7 | }
8 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/WalkthroughIntro/WalkthroughIntroFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol WalkthroughIntroFlowControllerService: UIViewController {
4 | func start()
5 | }
6 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/WalkthroughSettings/WalkthroughSettingsFlowControllerDelegate.swift:
--------------------------------------------------------------------------------
1 | import Entities
2 |
3 | public protocol WalkthroughSettingsFlowControllerDelegate: AnyObject {
4 | func walkthroughSettingsFlowController(
5 | _ flowController: WalkthroughSettingsFlowControllerService,
6 | update feedLanguage: FeedLanguage
7 | )
8 | func walkthroughSettingsFlowController(
9 | _ flowController: WalkthroughSettingsFlowControllerService,
10 | update userName: String
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/UI/UIServices/Sources/WalkthroughSettings/WalkthroughSettingsFlowControllerService.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public protocol WalkthroughSettingsFlowControllerService: UIViewController {
4 | var delegate: WalkthroughSettingsFlowControllerDelegate? { get set }
5 |
6 | func start()
7 | }
8 |
--------------------------------------------------------------------------------
/UI/UIServices/UIServices.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | UIServices:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.uiservices
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/UIServices
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/UIServices
17 | ${SRCROOT}/bin/swiftlint UI/UIServices
18 | dependencies:
19 | - target: Entities
20 |
--------------------------------------------------------------------------------
/UI/Views/AppSalesView/AppSalesView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | AppSalesView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.appsalesview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/AppSalesView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/AppSalesView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/AppSalesView/Sources
18 | dependencies:
19 | - target: Entities
20 | - target: RepositoryServices
21 | - target: Resources
22 | - target: UIServices
23 | - target: UIComponents
24 | AppSalesViewTests:
25 | type: bundle.unit-test
26 | platform: iOS
27 | sources:
28 | - path: Tests
29 | group: UI/Views/AppSalesView
30 | excludes:
31 | - "**/.gitkeep"
32 | - path: Tests/Generated/AppSalesViewMocks.generated.swift
33 | group: UI/Views/AppSalesView/Tests/Generated
34 | optional: true
35 | - path: Tests/Generated/RepositoryServicesMocks.generated.swift
36 | group: UI/Views/AppSalesView/Tests/Generated
37 | optional: true
38 | preBuildScripts:
39 | - name: Mockingbird
40 | script: |
41 | DERIVED_DATA="$(xcodebuild -showBuildSettings | sed -n 's|.*BUILD_ROOT = \(.*\)/Build/.*|\1|p')"
42 | "${DERIVED_DATA}/SourcePackages/checkouts/mockingbird/mockingbird" generate \
43 | --targets AppSalesView RepositoryServices \
44 | --testbundle AppSalesViewTests \
45 | --output-dir UI/Views/AppSalesView/Tests/Generated \
46 | --disable-swiftlint
47 | outputFiles:
48 | - ${SRCROOT}/UI/Views/AppSalesView/Tests/Generated/AppSalesViewMocks.generated.swift
49 | - ${SRCROOT}/UI/Views/AppSalesView/Tests/Generated/RepositoryServicesMocks.generated.swift
50 | basedOnDependencyAnalysis: no
51 | - name: Format and linting
52 | script: |
53 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/AppSalesView/Tests
54 | ${SRCROOT}/bin/swiftlint UI/Views/AppSalesView/Tests
55 | dependencies:
56 | - target: AppSalesView
57 | - package: CombineExpectations
58 | - package: Mockingbird
59 |
--------------------------------------------------------------------------------
/UI/Views/AppSalesView/Sources/AppSalesFlowController.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import SwiftUI
3 |
4 | import Entities
5 | import UIServices
6 |
7 | public final class AppSalesFlowController: UIHostingController, AppSalesFlowControllerService {
8 | private var _cancellable = Set()
9 |
10 | private var _viewModel: AppSalesViewModel {
11 | rootView.viewModel
12 | }
13 |
14 | override public init(rootView: AppSalesView) {
15 | super.init(rootView: rootView)
16 |
17 | // FIXME: Using only `View.navigationBarTitleDisplayMode(.inline)` does not be a smooth transition
18 | navigationItem.largeTitleDisplayMode = .never
19 | }
20 |
21 | @available(*, unavailable)
22 | required init?(coder _: NSCoder) {
23 | fatalError("init(coder:) has not been implemented")
24 | }
25 |
26 | public func start(segment: AppSegment) {
27 | _cancellable = Set()
28 |
29 | _viewModel.segment = segment
30 |
31 | _viewModel.navigationSignal
32 | .receive(on: DispatchQueue.main)
33 | .sink { [weak self] navigation in
34 | guard let self = self else { return }
35 |
36 | switch navigation {
37 | case let .appStore(url):
38 | self.openExternally(link: url)
39 | }
40 | }
41 | .store(in: &_cancellable)
42 |
43 | _viewModel.fetch()
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/UI/Views/AppSalesView/Sources/ViewModel/AppSalesViewModel+Navigation.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension AppSalesViewModel {
4 | enum Navigation: Hashable {
5 | case appStore(url: URL)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/UI/Views/AppSalesView/Sources/ViewModel/AppSalesViewModel.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | import Entities
4 | import RepositoryServices
5 |
6 | public class AppSalesViewModel: ObservableObject {
7 | private let _feedRepository: FeedRepositoryService
8 | private let _preferencesRepository: PreferencesRepositoryService
9 |
10 | private let _navigationSubject = PassthroughSubject()
11 |
12 | @Published public var segment: AppSegment?
13 |
14 | @Published private(set) var isInLoading = false
15 | @Published private(set) var hasError = false
16 | @Published private(set) var freeApps: [AppInformation] = []
17 | @Published private(set) var paidApps: [AppInformation] = []
18 |
19 | var navigationSignal: AnyPublisher {
20 | _navigationSubject.eraseToAnyPublisher()
21 | }
22 |
23 | public init(feedRepository: FeedRepositoryService, preferencesRepository: PreferencesRepositoryService) {
24 | self._feedRepository = feedRepository
25 | self._preferencesRepository = preferencesRepository
26 | }
27 |
28 | func fetch() {
29 | Task { @MainActor in
30 | isInLoading = true
31 |
32 | hasError = false
33 | freeApps = []
34 | paidApps = []
35 |
36 | let language = await _preferencesRepository.fetchFeedLanguage() ?? .english
37 | do {
38 | (freeApps, paidApps) = try await (
39 | _feedRepository.fetchFreeAppAllRanking(language: language),
40 | _feedRepository.fetchPaidAppAllRanking(language: language)
41 | )
42 | } catch {
43 | hasError = true
44 | }
45 |
46 | isInLoading = false
47 | }
48 | }
49 |
50 | func navigate(_ navigation: Navigation) {
51 | _navigationSubject.send(navigation)
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/UI/Views/AppSalesView/Tests/AppSalesFlowControllerTests.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import XCTest
3 |
4 | import Mockingbird
5 |
6 | @testable import AppSalesView
7 | @testable import Entities
8 | @testable import RepositoryServices
9 |
10 | class AppSalesFlowControllerTests: XCTestCase {
11 | override func setUpWithError() throws {
12 | try super.setUpWithError()
13 | }
14 |
15 | override func tearDownWithError() throws {
16 | try super.tearDownWithError()
17 | }
18 |
19 | func test_start_FreeApp_SetSegmentAndCallFetch() throws {
20 | // Arrange
21 | let mockFeedRepository = mock(FeedRepositoryService.self)
22 | let mockPreferencesRepository = mock(PreferencesRepositoryService.self)
23 |
24 | let mockViewModel = mock(AppSalesViewModel.self)
25 | .initialize(feedRepository: mockFeedRepository, preferencesRepository: mockPreferencesRepository)
26 | given(mockViewModel.navigationSignal).willReturn(Empty(completeImmediately: false).eraseToAnyPublisher())
27 |
28 | // Act
29 | let viewController = AppSalesFlowController(rootView: AppSalesView(viewModel: mockViewModel))
30 |
31 | viewController.start(segment: .freeApp)
32 |
33 | // Assert
34 | verify(mockViewModel.setSegment(.freeApp)).wasCalled()
35 | verify(mockViewModel.fetch()).wasCalled()
36 | }
37 |
38 | func test_start_PaidApp_SetSegmentAndCallFetch() throws {
39 | // Arrange
40 | let mockFeedRepository = mock(FeedRepositoryService.self)
41 | let mockPreferencesRepository = mock(PreferencesRepositoryService.self)
42 |
43 | let mockViewModel = mock(AppSalesViewModel.self)
44 | .initialize(feedRepository: mockFeedRepository, preferencesRepository: mockPreferencesRepository)
45 | given(mockViewModel.navigationSignal).willReturn(Empty(completeImmediately: false).eraseToAnyPublisher())
46 |
47 | // Act
48 | let viewController = AppSalesFlowController(rootView: AppSalesView(viewModel: mockViewModel))
49 |
50 | viewController.start(segment: .paidApp)
51 |
52 | // Assert
53 | verify(mockViewModel.setSegment(.paidApp)).wasCalled()
54 | verify(mockViewModel.fetch()).wasCalled()
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/UI/Views/AppSalesView/Tests/Generated/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/UI/Views/AppSalesView/Tests/Generated/.gitkeep
--------------------------------------------------------------------------------
/UI/Views/FeedLanguageSettingView/FeedLanguageSettingView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | FeedLanguageSettingView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.feedlanguagesettingview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/FeedLanguageSettingView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/FeedLanguageSettingView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/FeedLanguageSettingView/Sources
18 | dependencies:
19 | - target: Entities
20 | - target: RepositoryServices
21 | - target: UIServices
22 | - target: UIComponents
23 |
--------------------------------------------------------------------------------
/UI/Views/FeedLanguageSettingView/Sources/FeedLanguageSettingFlowController.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import UIServices
4 |
5 | // swiftformat:disable indent
6 | public final class FeedLanguageSettingFlowController: UIHostingController,
7 | FeedLanguageSettingFlowControllerService {
8 | // swiftformat:enable indent
9 | private var _viewModel: FeedLanguageSettingViewModel {
10 | rootView.viewModel
11 | }
12 |
13 | override public init(rootView: FeedLanguageSettingView) {
14 | super.init(rootView: rootView)
15 |
16 | // FIXME: Using only `View.navigationBarTitleDisplayMode(.inline)` does not be a smooth transition
17 | navigationItem.largeTitleDisplayMode = .never
18 | }
19 |
20 | @available(*, unavailable)
21 | required init?(coder _: NSCoder) {
22 | fatalError("init(coder:) has not been implemented")
23 | }
24 |
25 | public func start() { }
26 | }
27 |
--------------------------------------------------------------------------------
/UI/Views/FeedLanguageSettingView/Sources/View/FeedLanguageSettingView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import Entities
4 | import RepositoryServices
5 | import UIComponents
6 |
7 | // MARK: - FeedLanguageSettingView
8 |
9 | public struct FeedLanguageSettingView: View {
10 | @ObservedObject private(set) var viewModel: FeedLanguageSettingViewModel
11 |
12 | private let _languages: [FeedLanguage] = [.english, .japanese]
13 |
14 | public var body: some View {
15 | List {
16 | Section {
17 | RadioButtonGroup(
18 | _languages.map(\.title),
19 | selectedIndex: Binding(get: {
20 | _languages.firstIndex(of: viewModel.language)
21 | }, set: { selectedIndex in
22 | if let selectedIndex = selectedIndex {
23 | viewModel.updateFeedLanguage(_languages[selectedIndex])
24 | }
25 | })
26 | )
27 | .padding([.top, .bottom], 4)
28 | } header: {
29 | Text("Feed language")
30 | } footer: {
31 | Text("Language setting in the feed source.")
32 | }
33 | .textCase(nil)
34 | }
35 | .navigationBarTitleDisplayMode(.inline)
36 | .navigationTitle("Feed language")
37 | }
38 |
39 | public init(viewModel: FeedLanguageSettingViewModel) {
40 | self.viewModel = viewModel
41 | }
42 | }
43 |
44 | // MARK: - FeedLanguageSettingView_Previews
45 |
46 | struct FeedLanguageSettingView_Previews: PreviewProvider {
47 | private class PreviewViewModel: FeedLanguageSettingViewModel {
48 | init() {
49 | super.init(preferencesRepository: PreviewPreferencesRepository())
50 | }
51 | }
52 |
53 | static var previews: some View {
54 | Group {
55 | NavigationView {
56 | FeedLanguageSettingView(viewModel: PreviewViewModel())
57 | }
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/UI/Views/FeedLanguageSettingView/Sources/ViewModel/FeedLanguageSettingViewModel.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 |
4 | import Entities
5 | import RepositoryServices
6 |
7 | public class FeedLanguageSettingViewModel: ObservableObject {
8 | private let _preferencesRepository: PreferencesRepositoryService
9 |
10 | @Published private(set) var language: FeedLanguage = .english
11 |
12 | public init(preferencesRepository: PreferencesRepositoryService) {
13 | self._preferencesRepository = preferencesRepository
14 |
15 | _preferencesRepository.feedLanguageStream()
16 | .receive(on: DispatchQueue.main)
17 | .assign(to: &$language)
18 | }
19 |
20 | func updateFeedLanguage(_ language: FeedLanguage) {
21 | Task {
22 | await self._preferencesRepository.saveFeedLanguage(language)
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/UI/Views/FeedListView/FeedListView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | FeedListView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.feedlistview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/FeedListView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/FeedListView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/FeedListView/Sources
18 | dependencies:
19 | - target: Entities
20 | - target: RepositoryServices
21 | - target: Resources
22 | - target: UIServices
23 | - target: UIComponents
24 | - package: Snappable
25 |
--------------------------------------------------------------------------------
/UI/Views/FeedListView/Sources/FeedListFlowController.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import SwiftUI
3 |
4 | import Entities
5 | import UIServices
6 |
7 | public final class FeedListFlowController: UIHostingController, FeedListFlowControllerService {
8 | public weak var delegate: FeedListFlowControllerDelegate?
9 |
10 | private var _cancellable = Set()
11 |
12 | private var _viewModel: FeedListViewModel {
13 | rootView.viewModel
14 | }
15 |
16 | override public init(rootView: FeedListView) {
17 | super.init(rootView: rootView)
18 | }
19 |
20 | @available(*, unavailable)
21 | required init?(coder _: NSCoder) {
22 | fatalError("init(coder:) has not been implemented")
23 | }
24 |
25 | public func start() {
26 | _cancellable = Set()
27 |
28 | _viewModel.navigationSignal
29 | .receive(on: DispatchQueue.main)
30 | .sink { [weak self] navigation in
31 | guard let self = self else { return }
32 |
33 | switch navigation {
34 | case let .news(url):
35 | // self._showBrowseView(url: url)
36 | self.openInternally(link: url)
37 |
38 | case let .appStore(url):
39 | self.openExternally(link: url)
40 |
41 | case .freeAppRanking:
42 | self.delegate?.feedListFlowController(self, didSelect: .freeApp)
43 |
44 | case .paidAppRanking:
45 | self.delegate?.feedListFlowController(self, didSelect: .paidApp)
46 | }
47 | }
48 | .store(in: &_cancellable)
49 |
50 | _viewModel.fetch()
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/UI/Views/FeedListView/Sources/View/Section/AllNewsSection.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import Snappable
4 |
5 | import Entities
6 | import UIComponents
7 |
8 | // MARK: - AllNewsSection
9 |
10 | struct AllNewsSection: View {
11 | let newsList: [News]
12 | let action: ((News) -> Void)?
13 |
14 | var body: some View {
15 | VStack(alignment: .leading) {
16 | Text("All News")
17 | .font(.headline)
18 | .padding([.leading, .trailing], 16)
19 | ScrollView(.horizontal, showsIndicators: false) {
20 | HStack(spacing: 0) {
21 | ForEach(0.. Void)? = nil) {
40 | self.newsList = newsList
41 | self.action = action
42 | }
43 | }
44 |
45 | // MARK: - AllNewsSection_Previews
46 |
47 | struct AllNewsSection_Previews: PreviewProvider {
48 | static var previews: some View {
49 | AllNewsSection(News.previewContentList)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/UI/Views/FeedListView/Sources/View/Section/AppRankingSection.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import Snappable
4 |
5 | import Entities
6 | import UIComponents
7 |
8 | // MARK: - AppRankingSection
9 |
10 | struct AppRankingSection: View {
11 | let rankingName: String
12 | let apps: [AppInformation]
13 | private let showAllAction: (() -> Void)?
14 | private let openStoreAction: ((URL) -> Void)?
15 |
16 | var body: some View {
17 | VStack(alignment: .leading) {
18 | HStack {
19 | Text(rankingName)
20 | .font(.headline)
21 | .padding([.leading, .trailing], 16)
22 | Spacer()
23 | Button {
24 | showAllAction?()
25 | } label: {
26 | Text("Show All")
27 | .font(.body)
28 | .padding([.leading, .trailing], 16)
29 | }
30 | }
31 |
32 | _carousel
33 | .padding([.top, .bottom], 8)
34 | }
35 | }
36 |
37 | private var _carousel: some View {
38 | ScrollView(.horizontal, showsIndicators: false) {
39 | if !apps.isEmpty {
40 | HStack(spacing: 0) {
41 | ForEach(0..<(apps.count + 2) / 3) { index in
42 | let offset = index * 3
43 |
44 | HStack(spacing: 0) {
45 | Spacer(minLength: 16)
46 | AppRankingStack(
47 | rankOffset: offset + 1,
48 | firstApp: apps[offset],
49 | secondApp: apps.indices.contains(offset + 1) ? apps[offset + 1] : nil,
50 | thirdApp: apps.indices.contains(offset + 2) ? apps[offset + 2] : nil
51 | ) { url in
52 | openStoreAction?(url)
53 | }
54 | .frame(width: UIScreen.main.bounds.width - 48) // FIXME: Avoid using UIScreen.main.bounds
55 | }
56 | .snapID(index)
57 | }
58 |
59 | Spacer(minLength: 32)
60 | }
61 | }
62 | }
63 | .snappable(alignment: .leading)
64 | }
65 |
66 | init(
67 | rankingName: String,
68 | apps: [AppInformation],
69 | showAll: (() -> Void)? = nil,
70 | openLink: ((URL) -> Void)? = nil
71 | ) {
72 | self.rankingName = rankingName
73 | self.apps = apps
74 | self.showAllAction = showAll
75 | self.openStoreAction = openLink
76 | }
77 | }
78 |
79 | // MARK: - AppRankingSection_Previews
80 |
81 | struct AppRankingSection_Previews: PreviewProvider {
82 | static var previews: some View {
83 | AppRankingSection(rankingName: "Free App", apps: AppInformation.previewContentList)
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/UI/Views/FeedListView/Sources/View/Section/LatestNewsSection.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import Entities
4 | import UIComponents
5 |
6 | // MARK: - LatestNewsSection
7 |
8 | struct LatestNewsSection: View {
9 | let newsList: [News]
10 | private let action: ((News) -> Void)?
11 |
12 | var body: some View {
13 | VStack(alignment: .leading) {
14 | Text("Latest News")
15 | .font(.headline)
16 | .padding([.leading, .trailing], 16)
17 | ForEach(0.. Void)? = nil) {
29 | self.newsList = newsList
30 | self.action = action
31 | }
32 | }
33 |
34 | // MARK: - LatestNewsSection_Previews
35 |
36 | struct LatestNewsSection_Previews: PreviewProvider {
37 | static var previews: some View {
38 | ScrollView {
39 | LatestNewsSection(News.previewContentList)
40 | }
41 | .background(Color.gray)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/UI/Views/FeedListView/Sources/ViewModel/FeedListViewModel+Navigation.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension FeedListViewModel {
4 | enum Navigation: Hashable {
5 | case news(url: URL)
6 | case appStore(url: URL)
7 | case freeAppRanking
8 | case paidAppRanking
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/UI/Views/FeedListView/Sources/ViewModel/FeedListViewModel.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | import Entities
4 | import RepositoryServices
5 |
6 | public class FeedListViewModel: ObservableObject {
7 | private let _feedRepository: FeedRepositoryService
8 | private let _preferencesRepository: PreferencesRepositoryService
9 |
10 | private let _navigationSubject = PassthroughSubject()
11 |
12 | @Published private(set) var isInLoading = false
13 | @Published private(set) var hasError = false
14 | @Published private(set) var newsList: [News] = []
15 | @Published private(set) var freeAppRanking: [AppInformation] = []
16 | @Published private(set) var paidAppRanking: [AppInformation] = []
17 |
18 | var navigationSignal: AnyPublisher {
19 | _navigationSubject.eraseToAnyPublisher()
20 | }
21 |
22 | public init(feedRepository: FeedRepositoryService, preferencesRepository: PreferencesRepositoryService) {
23 | self._feedRepository = feedRepository
24 | self._preferencesRepository = preferencesRepository
25 | }
26 |
27 | func fetch() {
28 | Task { @MainActor in
29 | guard !isInLoading else { return }
30 |
31 | isInLoading = true
32 |
33 | hasError = false
34 | newsList = []
35 | freeAppRanking = []
36 | paidAppRanking = []
37 |
38 | do {
39 | let language = await _preferencesRepository.fetchFeedLanguage() ?? .english
40 |
41 | (newsList, freeAppRanking, paidAppRanking) = try await (
42 | _feedRepository.fetchNews(language: language),
43 | _feedRepository.fetchFreeAppTop15Ranking(language: language),
44 | _feedRepository.fetchPaidAppTop15Ranking(language: language)
45 | )
46 | } catch {
47 | hasError = true
48 | }
49 | isInLoading = false
50 | }
51 | }
52 |
53 | func navigate(_ navigation: Navigation) {
54 | _navigationSubject.send(navigation)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/UI/Views/FeedView/FeedView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | FeedView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.feedview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/FeedView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/FeedView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/FeedView/Sources
18 | dependencies:
19 | - target: Entities
20 | - target: UIServices
21 |
--------------------------------------------------------------------------------
/UI/Views/FeedView/Sources/FeedFlowController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | import Entities
4 | import UIServices
5 |
6 | public final class FeedFlowController: UIViewController, FeedFlowControllerService, FeedListFlowControllerDelegate {
7 | private let _viewModel: FeedViewModel
8 | private let _appSalesProvider: () -> AppSalesFlowControllerService
9 | private let _feedListProvider: () -> FeedListFlowControllerService
10 |
11 | private let _embeddedNavigationController = UINavigationController()
12 |
13 | public init(
14 | viewModel: FeedViewModel,
15 | appSalesProvider: @autoclosure @escaping () -> AppSalesFlowControllerService,
16 | feedListProvider: @autoclosure @escaping () -> FeedListFlowControllerService
17 | ) {
18 | self._viewModel = viewModel
19 | self._appSalesProvider = appSalesProvider
20 | self._feedListProvider = feedListProvider
21 |
22 | super.init(nibName: nil, bundle: nil)
23 |
24 | _embeddedNavigationController.navigationBar.prefersLargeTitles = true
25 |
26 | addContent(_embeddedNavigationController)
27 | }
28 |
29 | @available(*, unavailable)
30 | required init?(coder _: NSCoder) {
31 | fatalError("init(coder:) has not been implemented")
32 | }
33 |
34 | public func start() {
35 | let feedList = _feedListProvider()
36 | feedList.delegate = self
37 |
38 | _embeddedNavigationController.setViewControllers([feedList], animated: false)
39 |
40 | feedList.start()
41 | }
42 |
43 | public func feedListFlowController(_: FeedListFlowControllerService, didSelect appSales: AppSegment) {
44 | let sales = _appSalesProvider()
45 |
46 | _embeddedNavigationController.pushViewController(sales, animated: true)
47 |
48 | sales.start(segment: appSales)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/UI/Views/FeedView/Sources/ViewModel/FeedViewModel.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public class FeedViewModel: ObservableObject {
4 | public init() { }
5 | }
6 |
--------------------------------------------------------------------------------
/UI/Views/FullScreenModalView/FullScreenModalView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | FullScreenModalView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.fullscreenmodalview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/FullScreenModalView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/FullScreenModalView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/FullScreenModalView/Sources
18 | dependencies:
19 | - target: UIServices
20 |
--------------------------------------------------------------------------------
/UI/Views/FullScreenModalView/Sources/FullScreenModalFlowController.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import SwiftUI
3 |
4 | import UIServices
5 |
6 | // swiftformat:disable indent
7 | public final class FullScreenModalFlowController: UIHostingController,
8 | FullScreenModalFlowControllerService {
9 | // swiftformat:enable indent
10 | public weak var delegate: FullScreenModalFlowControllerDelegate?
11 |
12 | private var _cancellable = Set()
13 |
14 | private var _viewModel: FullScreenModalViewModel {
15 | rootView.viewModel
16 | }
17 |
18 | override public init(rootView: FullScreenModalView) {
19 | super.init(rootView: rootView)
20 |
21 | modalPresentationStyle = .fullScreen
22 | }
23 |
24 | @available(*, unavailable)
25 | required init?(coder _: NSCoder) {
26 | fatalError("init(coder:) has not been implemented")
27 | }
28 |
29 | public func start() {
30 | _cancellable = Set()
31 |
32 | _viewModel.navigationSignal
33 | .receive(on: DispatchQueue.main)
34 | .sink { [weak self] navigation in
35 | guard let self = self else { return }
36 |
37 | switch navigation {
38 | case .dismiss:
39 | self.delegate?.fullScreenModalFlowControllerShouldDismiss(self)
40 | }
41 | }
42 | .store(in: &_cancellable)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/UI/Views/FullScreenModalView/Sources/View/FullScreenModalView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - FullScreenModalView
4 |
5 | public struct FullScreenModalView: View {
6 | @ObservedObject private(set) var viewModel: FullScreenModalViewModel
7 |
8 | public var body: some View {
9 | NavigationView { // Just for the layout
10 | Button {
11 | viewModel.navigate(.dismiss)
12 | } label: {
13 | Text("Dismiss")
14 | }
15 | .navigationBarTitleDisplayMode(.inline)
16 | .navigationTitle("Modal .fullScreen")
17 | }
18 | }
19 |
20 | public init(viewModel: FullScreenModalViewModel) {
21 | self.viewModel = viewModel
22 | }
23 | }
24 |
25 | // MARK: - FullScreenModalView_Previews
26 |
27 | struct FullScreenModalView_Previews: PreviewProvider {
28 | private class PreviewViewModel: FullScreenModalViewModel { }
29 |
30 | static var previews: some View {
31 | FullScreenModalView(viewModel: PreviewViewModel())
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/UI/Views/FullScreenModalView/Sources/ViewModel/FullScreenModalViewModel+Navigation.swift:
--------------------------------------------------------------------------------
1 | extension FullScreenModalViewModel {
2 | enum Navigation {
3 | case dismiss
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/UI/Views/FullScreenModalView/Sources/ViewModel/FullScreenModalViewModel.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | public class FullScreenModalViewModel: ObservableObject {
4 | private let _navigationSubject = PassthroughSubject()
5 |
6 | var navigationSignal: AnyPublisher {
7 | _navigationSubject.eraseToAnyPublisher()
8 | }
9 |
10 | public init() { }
11 |
12 | func navigate(_ navigation: Navigation) {
13 | _navigationSubject.send(navigation)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/UI/Views/KeyboardTestView/KeyboardTestView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | KeyboardTestView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.keyboardtestview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/KeyboardTestView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/KeyboardTestView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/KeyboardTestView/Sources
18 | dependencies:
19 | - target: UIServices
20 |
--------------------------------------------------------------------------------
/UI/Views/KeyboardTestView/Sources/KeyboardTestFlowController.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import UIServices
4 |
5 | // swiftformat:disable indent
6 | public final class KeyboardTestFlowController: UIHostingController,
7 | KeyboardTestFlowControllerService {
8 | // swiftformat:enable indent
9 | private var _viewModel: KeyboardTestViewModel {
10 | rootView.viewModel
11 | }
12 |
13 | override public init(rootView: KeyboardTestView) {
14 | super.init(rootView: rootView)
15 | }
16 |
17 | @available(*, unavailable)
18 | required init?(coder _: NSCoder) {
19 | fatalError("init(coder:) has not been implemented")
20 | }
21 |
22 | public func start() { }
23 | }
24 |
--------------------------------------------------------------------------------
/UI/Views/KeyboardTestView/Sources/View/KeyboardTestView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - KeyboardTestView
4 |
5 | public struct KeyboardTestView: View {
6 | @ObservedObject private(set) var viewModel: KeyboardTestViewModel
7 |
8 | public var body: some View {
9 | List {
10 | Section("TextField 1") {
11 | TextField("TextField 1", text: $viewModel.text1)
12 | }
13 | Section("TextField 2") {
14 | TextField("TextField 2", text: $viewModel.text2)
15 | }
16 | Section("TextField 3") {
17 | TextField("TextField 3", text: $viewModel.text3)
18 | }
19 | Section("TextField 4") {
20 | TextField("TextField 4", text: $viewModel.text4)
21 | }
22 | Section("TextField 5") {
23 | TextField("TextField 5", text: $viewModel.text5)
24 | }
25 | Section("TextField 6") {
26 | TextField("TextField 6", text: $viewModel.text6)
27 | }
28 | Section("TextField 7") {
29 | TextField("TextField 7", text: $viewModel.text7)
30 | }
31 | Section("TextField 8") {
32 | TextField("TextField 8", text: $viewModel.text8)
33 | }
34 | Section("TextField 9") {
35 | TextField("TextField 9", text: $viewModel.text9)
36 | }
37 | Section("TextField 10") {
38 | TextField("TextField 10", text: $viewModel.text10)
39 | }
40 | }
41 | .onTapGesture {
42 | UIApplication.shared.sendAction(
43 | #selector(UIResponder.resignFirstResponder),
44 | to: nil,
45 | from: nil,
46 | for: nil
47 | )
48 | }
49 | .navigationBarTitleDisplayMode(.inline)
50 | .navigationTitle("Keyboard test")
51 | }
52 |
53 | public init(viewModel: KeyboardTestViewModel) {
54 | self.viewModel = viewModel
55 | }
56 | }
57 |
58 | // MARK: - KeyboardTestView_Previews
59 |
60 | struct KeyboardTestView_Previews: PreviewProvider {
61 | private class PreviewViewModel: KeyboardTestViewModel { }
62 |
63 | static var previews: some View {
64 | NavigationView {
65 | KeyboardTestView(viewModel: PreviewViewModel())
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/UI/Views/KeyboardTestView/Sources/ViewModel/KeyboardTestViewModel.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public class KeyboardTestViewModel: ObservableObject {
4 | @Published var text1 = ""
5 | @Published var text2 = ""
6 | @Published var text3 = ""
7 | @Published var text4 = ""
8 | @Published var text5 = ""
9 | @Published var text6 = ""
10 | @Published var text7 = ""
11 | @Published var text8 = ""
12 | @Published var text9 = ""
13 | @Published var text10 = ""
14 |
15 | public init() { }
16 | }
17 |
--------------------------------------------------------------------------------
/UI/Views/LicensesView/LicensesView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | LicensesView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.licensesview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/LicensesView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/LicensesView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/LicensesView/Sources
18 | dependencies:
19 | - target: Resources
20 | - target: UIServices
21 | - package: MarkdownView
22 |
--------------------------------------------------------------------------------
/UI/Views/LicensesView/Sources/LicensesFlowController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | import Resources
4 | import UIServices
5 |
6 | public final class LicensesFlowController: UIViewController, LicensesFlowControllerService {
7 | private let _rootView: LicensesView
8 |
9 | private var _viewModel: LicensesViewModel {
10 | _rootView.viewModel
11 | }
12 |
13 | public init(rootView: LicensesView) {
14 | self._rootView = rootView
15 |
16 | super.init(nibName: nil, bundle: nil)
17 |
18 | navigationItem.largeTitleDisplayMode = .never
19 | view.backgroundColor = Asset.background.color
20 |
21 | addContent(rootView)
22 | }
23 |
24 | @available(*, unavailable)
25 | required init?(coder _: NSCoder) {
26 | fatalError("init(coder:) has not been implemented")
27 | }
28 |
29 | public func start() {
30 | _viewModel.markdownText = (try? String(contentsOf: Files.licensesMd.url)) ?? ""
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/UI/Views/LicensesView/Sources/View/LicensesView.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import UIKit
3 |
4 | import MarkdownView
5 |
6 | public final class LicensesView: UIViewController {
7 | private let _markdownView = MarkdownView()
8 |
9 | private var _cancellable = Set()
10 |
11 | private(set) var viewModel: LicensesViewModel
12 |
13 | public init(viewModel: LicensesViewModel) {
14 | self.viewModel = viewModel
15 |
16 | super.init(nibName: nil, bundle: nil)
17 |
18 | view.addSubview(_markdownView)
19 | _markdownView.translatesAutoresizingMaskIntoConstraints = false
20 | NSLayoutConstraint.activate([
21 | _markdownView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
22 | _markdownView.topAnchor.constraint(equalTo: view.topAnchor),
23 | _markdownView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
24 | _markdownView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
25 | ])
26 |
27 | updateView()
28 | viewModel.objectWillChange
29 | .receive(on: DispatchQueue.main)
30 | .sink { [weak self] _ in
31 | self?.updateView()
32 | }
33 | .store(in: &_cancellable)
34 | }
35 |
36 | @available(*, unavailable)
37 | required init?(coder _: NSCoder) {
38 | fatalError("init(coder:) has not been implemented")
39 | }
40 |
41 | private func updateView() {
42 | _markdownView.load(markdown: viewModel.markdownText)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/UI/Views/LicensesView/Sources/ViewModel/LicensesViewModel.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public class LicensesViewModel: ObservableObject {
4 | @Published var markdownText = ""
5 |
6 | public init() { }
7 | }
8 |
--------------------------------------------------------------------------------
/UI/Views/MainView/MainView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | MainView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.mainview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/MainView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/MainView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/MainView/Sources
18 | dependencies:
19 | - target: RepositoryServices
20 | - target: UIServices
21 |
--------------------------------------------------------------------------------
/UI/Views/MainView/Sources/MainFlowController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | import RepositoryServices
4 | import UIServices
5 |
6 | public final class MainFlowController: UIViewController, MainFlowControllerService {
7 | private let _viewModel: MainViewModel
8 | private let _feedProvider: () -> FeedFlowControllerService
9 | private let _settingsProvider: () -> SettingsFlowControllerService
10 |
11 | private let _embeddedTabBarController = UITabBarController()
12 |
13 | public init(
14 | viewModel: MainViewModel,
15 | feedProvider: @autoclosure @escaping () -> FeedFlowControllerService,
16 | settingsProvider: @autoclosure @escaping () -> SettingsFlowControllerService
17 | ) {
18 | self._viewModel = viewModel
19 | self._feedProvider = feedProvider
20 | self._settingsProvider = settingsProvider
21 |
22 | super.init(nibName: nil, bundle: nil)
23 |
24 | addContent(_embeddedTabBarController)
25 | }
26 |
27 | @available(*, unavailable)
28 | required init?(coder _: NSCoder) {
29 | fatalError("init(coder:) has not been implemented")
30 | }
31 |
32 | public func start() {
33 | let feed = _feedProvider()
34 | let settings = _settingsProvider()
35 |
36 | feed.tabBarItem = UITabBarItem(
37 | title: "Feed",
38 | image: UIImage(systemName: "doc.text.image"), // SFSymbol has no Feed icon
39 | tag: 0
40 | )
41 | settings.tabBarItem = UITabBarItem(
42 | title: "Settings",
43 | image: UIImage(systemName: "wrench.and.screwdriver"),
44 | tag: 1
45 | )
46 |
47 | _embeddedTabBarController.setViewControllers([feed, settings], animated: false)
48 | _embeddedTabBarController.selectedIndex = 0
49 |
50 | feed.start()
51 | settings.start()
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/UI/Views/MainView/Sources/ViewModel/MainViewModel.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public class MainViewModel: ObservableObject {
4 | public init() { }
5 | }
6 |
--------------------------------------------------------------------------------
/UI/Views/ModalTransitionTestView/ModalTransitionTestView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | ModalTransitionTestView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.modaltransitiontestview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/ModalTransitionTestView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/ModalTransitionTestView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/ModalTransitionTestView/Sources
18 | dependencies:
19 | - target: Resources
20 | - target: UIServices
21 |
--------------------------------------------------------------------------------
/UI/Views/ModalTransitionTestView/Sources/View/ModalTransitionTestView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import Resources
4 |
5 | // MARK: - ModalTransitionTestView
6 |
7 | public struct ModalTransitionTestView: View {
8 | @ObservedObject private(set) var viewModel: ModalTransitionTestViewModel
9 |
10 | public var body: some View {
11 | VStack {
12 | Button {
13 | viewModel.navigate(.alert)
14 | } label: {
15 | Text("Alert")
16 | }
17 | .padding()
18 |
19 | Button {
20 | viewModel.navigate(.fullScreen)
21 | } label: {
22 | Text("Modal .fullScreen")
23 | }
24 | .padding()
25 |
26 | Button {
27 | viewModel.navigate(.pageSheet)
28 | } label: {
29 | Text("Modal .pageSheet")
30 | }
31 | .padding()
32 | }
33 | .frame(maxWidth: .infinity, maxHeight: .infinity)
34 | .background(Color(uiColor: Asset.background.color))
35 | .navigationBarTitleDisplayMode(.inline)
36 | .navigationTitle("Modal transition")
37 | }
38 |
39 | public init(viewModel: ModalTransitionTestViewModel) {
40 | self.viewModel = viewModel
41 | }
42 | }
43 |
44 | // MARK: - ModalTransitionTestView_Previews
45 |
46 | struct ModalTransitionTestView_Previews: PreviewProvider {
47 | private class PreviewViewModel: ModalTransitionTestViewModel { }
48 |
49 | static var previews: some View {
50 | NavigationView {
51 | ModalTransitionTestView(viewModel: PreviewViewModel())
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/UI/Views/ModalTransitionTestView/Sources/ViewModel/ModalTransitionTestViewModel+Navigation.swift:
--------------------------------------------------------------------------------
1 | extension ModalTransitionTestViewModel {
2 | enum Navigation {
3 | case alert
4 | case fullScreen
5 | case pageSheet
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/UI/Views/ModalTransitionTestView/Sources/ViewModel/ModalTransitionTestViewModel.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | public class ModalTransitionTestViewModel: ObservableObject {
4 | private let _navigationSubject = PassthroughSubject()
5 |
6 | var navigationSignal: AnyPublisher {
7 | _navigationSubject.eraseToAnyPublisher()
8 | }
9 |
10 | public init() { }
11 |
12 | func navigate(_ navigation: Navigation) {
13 | _navigationSubject.send(navigation)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/UI/Views/NoticeAlertView/NoticeAlertView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | NoticeAlertView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.noticealertview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/NoticeAlertView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/NoticeAlertView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/NoticeAlertView/Sources
18 | dependencies:
19 | - target: UIServices
20 |
--------------------------------------------------------------------------------
/UI/Views/NoticeAlertView/Sources/NoticeAlertFlowController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | import UIServices
4 |
5 | public final class NoticeAlertFlowController: UIAlertController, NoticeAlertFlowControllerService {
6 | // This is an experimental implementation for using UIAlertController based on the FlowController pattern.
7 | // UIAlertController subclassing is known as a bad practice.
8 | // See also https://developer.apple.com/documentation/uikit/uialertcontroller
9 |
10 | public static func instantiate() -> NoticeAlertFlowController {
11 | let alert = NoticeAlertFlowController(title: nil, message: nil, preferredStyle: .alert)
12 |
13 | alert.addAction(
14 | UIAlertAction(title: "OK", style: .default) { _ in
15 | alert.delegate?.noticeAlertFlowControllerShouldDismiss(alert)
16 | }
17 | )
18 |
19 | return alert
20 | }
21 |
22 | public weak var delegate: NoticeAlertFlowControllerDelegate?
23 |
24 | public func start(title: String?, message: String?) {
25 | self.title = title
26 | self.message = message
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/UI/Views/PageSheetModalView/PageSheetModalView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | PageSheetModalView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.pagesheetmodalview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/PageSheetModalView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/PageSheetModalView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/PageSheetModalView/Sources
18 | dependencies:
19 | - target: UIServices
20 |
--------------------------------------------------------------------------------
/UI/Views/PageSheetModalView/Sources/PageSheetModalFlowController.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import SwiftUI
3 |
4 | import UIServices
5 |
6 | // swiftformat:disable indent
7 | public final class PageSheetModalFlowController: UIHostingController,
8 | PageSheetModalFlowControllerService {
9 | // swiftformat:enable indent
10 | public weak var delegate: PageSheetModalFlowControllerDelegate?
11 |
12 | private var _cancellable = Set()
13 |
14 | private var _viewModel: PageSheetModalViewModel {
15 | rootView.viewModel
16 | }
17 |
18 | override public init(rootView: PageSheetModalView) {
19 | super.init(rootView: rootView)
20 |
21 | modalPresentationStyle = .pageSheet
22 | }
23 |
24 | @available(*, unavailable)
25 | required init?(coder _: NSCoder) {
26 | fatalError("init(coder:) has not been implemented")
27 | }
28 |
29 | public func start() {
30 | _cancellable = Set()
31 |
32 | _viewModel.navigationSignal
33 | .receive(on: DispatchQueue.main)
34 | .sink { [weak self] navigation in
35 | guard let self = self else { return }
36 |
37 | switch navigation {
38 | case .dismiss:
39 | self.delegate?.pageSheetModalFlowControllerShouldDismiss(self)
40 | }
41 | }
42 | .store(in: &_cancellable)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/UI/Views/PageSheetModalView/Sources/View/PageSheetModalView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - PageSheetModalView
4 |
5 | public struct PageSheetModalView: View {
6 | @ObservedObject private(set) var viewModel: PageSheetModalViewModel
7 |
8 | public var body: some View {
9 | NavigationView { // Just for the layout
10 | Button {
11 | viewModel.navigate(.dismiss)
12 | } label: {
13 | Text("Dismiss")
14 | }
15 | .navigationBarTitleDisplayMode(.inline)
16 | .navigationTitle("Modal .pageSheet")
17 | }
18 | }
19 |
20 | public init(viewModel: PageSheetModalViewModel) {
21 | self.viewModel = viewModel
22 | }
23 | }
24 |
25 | // MARK: - PageSheetModalView_Previews
26 |
27 | struct PageSheetModalView_Previews: PreviewProvider {
28 | private class PreviewViewModel: PageSheetModalViewModel { }
29 |
30 | static var previews: some View {
31 | PageSheetModalView(viewModel: PreviewViewModel())
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/UI/Views/PageSheetModalView/Sources/ViewModel/PageSheetModalViewModel+Navigation.swift:
--------------------------------------------------------------------------------
1 | extension PageSheetModalViewModel {
2 | enum Navigation {
3 | case dismiss
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/UI/Views/PageSheetModalView/Sources/ViewModel/PageSheetModalViewModel.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | public class PageSheetModalViewModel: ObservableObject {
4 | private let _navigationSubject = PassthroughSubject()
5 |
6 | var navigationSignal: AnyPublisher {
7 | _navigationSubject.eraseToAnyPublisher()
8 | }
9 |
10 | public init() { }
11 |
12 | func navigate(_ navigation: Navigation) {
13 | _navigationSubject.send(navigation)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/UI/Views/PushTransitionTestView/PushTransitionTestView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | PushTransitionTestView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.pushtransitiontestview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/PushTransitionTestView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/PushTransitionTestView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/PushTransitionTestView/Sources
18 | dependencies:
19 | - target: UIServices
20 |
--------------------------------------------------------------------------------
/UI/Views/PushTransitionTestView/Sources/PushTransitionTestFlowController.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import SwiftUI
3 |
4 | import UIServices
5 |
6 | // swiftformat:disable indent
7 | public final class PushTransitionTestFlowController: UIHostingController,
8 | PushTransitionTestFlowControllerService {
9 | // swiftformat:enable indent
10 | public weak var delegate: PushTransitionTestFlowControllerDelegate?
11 |
12 | private var _cancellable = Set()
13 |
14 | private var _viewModel: PushTransitionTestViewModel {
15 | rootView.viewModel
16 | }
17 |
18 | override public init(rootView: PushTransitionTestView) {
19 | super.init(rootView: rootView)
20 | }
21 |
22 | @available(*, unavailable)
23 | required init?(coder _: NSCoder) {
24 | fatalError("init(coder:) has not been implemented")
25 | }
26 |
27 | public func start(forwardedText: String) {
28 | _cancellable = Set()
29 |
30 | _viewModel.forwardText = forwardedText
31 |
32 | _viewModel.navigationSignal
33 | .receive(on: DispatchQueue.main)
34 | .sink { [weak self] navigation in
35 | guard let self = self else { return }
36 |
37 | switch navigation {
38 | case .next:
39 | self.delegate?.pushTransitionTestFlowController(self, nextWith: self._viewModel.forwardText)
40 |
41 | case .popToRoot:
42 | self.delegate?.pushTransitionTestFlowControllerPopToRoot(self)
43 | }
44 | }
45 | .store(in: &_cancellable)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/UI/Views/PushTransitionTestView/Sources/View/PushTransitionTestView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - PushTransitionTestView
4 |
5 | public struct PushTransitionTestView: View {
6 | @ObservedObject private(set) var viewModel: PushTransitionTestViewModel
7 |
8 | public var body: some View {
9 | List {
10 | Section {
11 | TextField("Forward text", text: $viewModel.forwardText)
12 | } header: {
13 | Text("Forward text")
14 | }
15 | .textCase(nil)
16 | }
17 | .navigationBarTitleDisplayMode(.inline)
18 | .navigationTitle("Push transition")
19 | .toolbar {
20 | ToolbarItemGroup(placement: .bottomBar) {
21 | Button {
22 | viewModel.navigate(.popToRoot)
23 | } label: {
24 | Text("Pop to root")
25 | }
26 | Spacer()
27 | Button {
28 | viewModel.navigate(.next)
29 | } label: {
30 | Text("Next")
31 | }
32 | Spacer()
33 | }
34 | }
35 | }
36 |
37 | public init(viewModel: PushTransitionTestViewModel) {
38 | self.viewModel = viewModel
39 | }
40 | }
41 |
42 | // MARK: - PushTransitionTestView_Previews
43 |
44 | struct PushTransitionTestView_Previews: PreviewProvider {
45 | private class PreviewViewModel: PushTransitionTestViewModel { }
46 |
47 | static var previews: some View {
48 | NavigationView {
49 | PushTransitionTestView(viewModel: PreviewViewModel())
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/UI/Views/PushTransitionTestView/Sources/ViewModel/PushTransitionTestViewModel+Navigation.swift:
--------------------------------------------------------------------------------
1 | extension PushTransitionTestViewModel {
2 | enum Navigation {
3 | case next
4 | case popToRoot
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/UI/Views/PushTransitionTestView/Sources/ViewModel/PushTransitionTestViewModel.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | public class PushTransitionTestViewModel: ObservableObject {
4 | private let _navigationSubject = PassthroughSubject()
5 |
6 | @Published var forwardText = ""
7 |
8 | var navigationSignal: AnyPublisher {
9 | _navigationSubject.eraseToAnyPublisher()
10 | }
11 |
12 | public init() { }
13 |
14 | func navigate(_ navigation: Navigation) {
15 | _navigationSubject.send(navigation)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/UI/Views/RootView/RootView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | RootView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.rootview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/RootView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/RootView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/RootView/Sources
18 | dependencies:
19 | - target: RepositoryServices
20 | - target: UIServices
21 | RootViewTests:
22 | type: bundle.unit-test
23 | platform: iOS
24 | sources:
25 | - path: Tests
26 | group: UI/Views/RootView
27 | excludes:
28 | - "**/.gitkeep"
29 | - path: Tests/Generated/RepositoryServicesMocks.generated.swift
30 | group: UI/Views/RootView/Tests/Generated
31 | optional: true
32 | - path: Tests/Generated/RootViewMocks.generated.swift
33 | group: UI/Views/RootView/Tests/Generated
34 | optional: true
35 | - path: Tests/Generated/UIServicesMocks.generated.swift
36 | group: UI/Views/RootView/Tests/Generated
37 | optional: true
38 | preBuildScripts:
39 | - name: Mockingbird
40 | script: |
41 | DERIVED_DATA="$(xcodebuild -showBuildSettings | sed -n 's|.*BUILD_ROOT = \(.*\)/Build/.*|\1|p')"
42 | "${DERIVED_DATA}/SourcePackages/checkouts/mockingbird/mockingbird" generate \
43 | --targets RepositoryServices RootView UIServices \
44 | --testbundle RootViewTests \
45 | --output-dir UI/Views/RootView/Tests/Generated \
46 | --disable-swiftlint
47 | outputFiles:
48 | - ${SRCROOT}/UI/Views/RootView/Tests/Generated/RepositoryServicesMocks.generated.swift
49 | - ${SRCROOT}/UI/Views/RootView/Tests/Generated/RootViewMocks.generated.swift
50 | - ${SRCROOT}/UI/Views/RootView/Tests/Generated/UIServicesMocks.generated.swift
51 | basedOnDependencyAnalysis: no
52 | - name: Format and linting
53 | script: |
54 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/RootView/Tests
55 | ${SRCROOT}/bin/swiftlint UI/Views/RootView/Tests
56 | dependencies:
57 | - target: RootView
58 | - package: CombineExpectations
59 | - package: Mockingbird
60 |
--------------------------------------------------------------------------------
/UI/Views/RootView/Sources/RootFlowController.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 | import SwiftUI
4 | import UIKit
5 |
6 | import UIServices
7 |
8 | public final class RootFlowController: UIViewController, RootFlowControllerService {
9 | private let _rootViewModel: RootViewModel
10 | private let _walkthroughProvider: () -> WalkthroughFlowControllerService
11 | private let _mainProvider: () -> MainFlowControllerService
12 |
13 | private var _cancellable = Set()
14 |
15 | public init(
16 | rootViewModel: RootViewModel,
17 | walkthroughProvider: @autoclosure @escaping () -> WalkthroughFlowControllerService,
18 | mainProvider: @autoclosure @escaping () -> MainFlowControllerService
19 | ) {
20 | self._rootViewModel = rootViewModel
21 | self._walkthroughProvider = walkthroughProvider
22 | self._mainProvider = mainProvider
23 |
24 | super.init(nibName: nil, bundle: nil)
25 | }
26 |
27 | @available(*, unavailable)
28 | required init?(coder _: NSCoder) {
29 | fatalError("init(coder:) has not been implemented")
30 | }
31 |
32 | public func start() {
33 | _cancellable = Set()
34 |
35 | _rootViewModel.navigationStream
36 | .receive(on: DispatchQueue.main)
37 | .sink { [weak self] navigation in
38 | guard let self = self else { return }
39 |
40 | switch navigation {
41 | case .main:
42 | self.startMain()
43 |
44 | case .walkthrough:
45 | self.startWalkthrough()
46 | }
47 | }
48 | .store(in: &_cancellable)
49 | }
50 |
51 | private func startWalkthrough() {
52 | dismiss(animated: false) { [weak self] in
53 | guard let self = self else { return }
54 |
55 | let walkthrough = self._walkthroughProvider()
56 |
57 | self.removeAllContentViewController()
58 | self.addContent(walkthrough)
59 |
60 | walkthrough.start()
61 | }
62 | }
63 |
64 | private func startMain() {
65 | dismiss(animated: false) { [weak self] in
66 | guard let self = self else { return }
67 |
68 | let main = self._mainProvider()
69 |
70 | self.removeAllContentViewController()
71 | self.addContent(main)
72 |
73 | main.start()
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/UI/Views/RootView/Sources/ViewModel/RootViewModel+Navigation.swift:
--------------------------------------------------------------------------------
1 | extension RootViewModel {
2 | enum Navigation {
3 | case main
4 | case walkthrough
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/UI/Views/RootView/Sources/ViewModel/RootViewModel.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | import RepositoryServices
4 |
5 | public class RootViewModel: ObservableObject {
6 | private let _appStateRepository: AppStateRepositoryService
7 |
8 | private let _navigationSubject = CurrentValueSubject(nil)
9 |
10 | var navigationStream: AnyPublisher {
11 | _navigationSubject
12 | .compactMap { $0 }
13 | .eraseToAnyPublisher()
14 | }
15 |
16 | private var _cancellable = Set()
17 |
18 | public init(appStateRepository: AppStateRepositoryService) {
19 | self._appStateRepository = appStateRepository
20 |
21 | _appStateRepository.isWalkthroughFinishedStream()
22 | .sink { [weak self] isWalkthroughFinished in
23 | guard let self = self else { return }
24 |
25 | self._navigationSubject.send(isWalkthroughFinished ? .main : .walkthrough)
26 | }
27 | .store(in: &_cancellable)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/UI/Views/RootView/Tests/Generated/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/UI/Views/RootView/Tests/Generated/.gitkeep
--------------------------------------------------------------------------------
/UI/Views/RootView/Tests/RootViewModelTests.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import XCTest
3 |
4 | import CombineExpectations
5 | import Mockingbird
6 |
7 | @testable import Entities
8 | @testable import RepositoryServices
9 | @testable import RootView
10 |
11 | class RootViewModelTests: XCTestCase {
12 | override func setUpWithError() throws {
13 | try super.setUpWithError()
14 | }
15 |
16 | override func tearDownWithError() throws {
17 | try super.tearDownWithError()
18 | }
19 |
20 | func test_navigationStream_IsWalkthroughFinishedNever_OutputNever() throws {
21 | // Arrange
22 | let mockAppStateRepository = mock(AppStateRepositoryService.self)
23 | given(mockAppStateRepository.isWalkthroughFinishedStream())
24 | .willReturn(Empty(completeImmediately: false).eraseToAnyPublisher())
25 |
26 | // Act
27 | let viewModel = RootViewModel(appStateRepository: mockAppStateRepository)
28 | let recorder = viewModel.navigationStream.record()
29 |
30 | let output = try wait(for: recorder.availableElements, timeout: 0.1)
31 |
32 | // Assert
33 | XCTAssertEqual([], output)
34 | }
35 |
36 | func test_navigationStream_IsWalkthroughFinishedFalse_OutputWalkthrough() throws {
37 | // Arrange
38 | let mockAppStateRepository = mock(AppStateRepositoryService.self)
39 | given(mockAppStateRepository.isWalkthroughFinishedStream())
40 | .willReturn(Just(false).append(Empty(completeImmediately: false)).eraseToAnyPublisher())
41 |
42 | // Act
43 | let viewModel = RootViewModel(appStateRepository: mockAppStateRepository)
44 | let recorder = viewModel.navigationStream.record()
45 |
46 | let output = try wait(for: recorder.availableElements, timeout: 0.1)
47 |
48 | // Assert
49 | XCTAssertEqual([.walkthrough], output)
50 | }
51 |
52 | func test_navigationStream_IsWalkthroughFinishedTrue_OutputMain() throws {
53 | // Arrange
54 | let mockAppStateRepository = mock(AppStateRepositoryService.self)
55 | given(mockAppStateRepository.isWalkthroughFinishedStream())
56 | .willReturn(Just(true).append(Empty(completeImmediately: false)).eraseToAnyPublisher())
57 |
58 | // Act
59 | let viewModel = RootViewModel(appStateRepository: mockAppStateRepository)
60 | let recorder = viewModel.navigationStream.record()
61 |
62 | let output = try wait(for: recorder.availableElements, timeout: 0.1)
63 |
64 | // Assert
65 | XCTAssertEqual([.main], output)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/UI/Views/SettingsMenuView/SettingsMenuView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | SettingsMenuView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.settingsmenuview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/SettingsMenuView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/SettingsMenuView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/SettingsMenuView/Sources
18 | dependencies:
19 | - target: Entities
20 | - target: RepositoryServices
21 | - target: UIServices
22 | - target: UIComponents
23 |
--------------------------------------------------------------------------------
/UI/Views/SettingsMenuView/Sources/SettingsMenuFlowController.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import SwiftUI
3 |
4 | import UIServices
5 |
6 | // swiftformat:disable indent
7 | public final class SettingsMenuFlowController: UIHostingController,
8 | SettingsMenuFlowControllerService {
9 | // swiftformat:enable indent
10 | public weak var delegate: SettingsMenuFlowControllerDelegate?
11 |
12 | private var _cancellable = Set()
13 |
14 | private var _viewModel: SettingsMenuViewModel {
15 | rootView.viewModel
16 | }
17 |
18 | override public init(rootView: SettingsMenuView) {
19 | super.init(rootView: rootView)
20 | }
21 |
22 | @available(*, unavailable)
23 | required init?(coder _: NSCoder) {
24 | fatalError("init(coder:) has not been implemented")
25 | }
26 |
27 | public func start() {
28 | _cancellable = Set()
29 |
30 | _viewModel.navigationSignal
31 | .receive(on: DispatchQueue.main)
32 | .sink { [weak self] navigation in
33 | guard let self = self else { return }
34 |
35 | switch navigation {
36 | case let .menu(row):
37 | self.delegate?.settingsMenuFlowController(self, didSelect: row)
38 | }
39 | }
40 | .store(in: &_cancellable)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/UI/Views/SettingsMenuView/Sources/View/SettingsMenuView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import Entities
4 | import RepositoryServices
5 | import UIComponents
6 |
7 | // MARK: - SettingsMenuView
8 |
9 | public struct SettingsMenuView: View {
10 | @ObservedObject private(set) var viewModel: SettingsMenuViewModel
11 |
12 | public var body: some View {
13 | List {
14 | ForEach(SettingsMenu.Section.allCases, id: \.self) { section in
15 | Section {
16 | ForEach(section.rows, id: \.self) { row in
17 | switch row.type {
18 | case .user:
19 | HStack {
20 | Image(systemName: "person.circle.fill")
21 | .resizable()
22 | .foregroundColor(.secondary)
23 | .frame(width: 48, height: 48)
24 | .padding([.top, .trailing, .bottom], 8)
25 | Text(viewModel.userName)
26 | }
27 |
28 | case .transition:
29 | NavigationLinkButton(row.title) {
30 | viewModel.navigate(.menu(row: row))
31 | }
32 |
33 | case .version:
34 | HStack {
35 | Text("Version")
36 | Spacer()
37 | Text("1.0.0").foregroundColor(.secondary)
38 | }
39 | }
40 | }
41 | } header: {
42 | if let title = section.title {
43 | Text(title)
44 | }
45 | }
46 | .textCase(nil)
47 | }
48 | }
49 | .listStyle(.insetGrouped)
50 | .navigationTitle("Settings")
51 | }
52 |
53 | public init(viewModel: SettingsMenuViewModel) {
54 | self.viewModel = viewModel
55 | }
56 | }
57 |
58 | // MARK: - SettingsMenuView_Previews
59 |
60 | struct SettingsMenuView_Previews: PreviewProvider {
61 | private class PreviewModel: SettingsMenuViewModel {
62 | private let _userName: String
63 |
64 | override var userName: String { _userName }
65 |
66 | init(userName: String = "") {
67 | self._userName = userName
68 |
69 | super.init(preferenceRepository: PreviewPreferencesRepository())
70 | }
71 | }
72 |
73 | static var previews: some View {
74 | NavigationView {
75 | SettingsMenuView(viewModel: PreviewModel(userName: "John Doe"))
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/UI/Views/SettingsMenuView/Sources/ViewModel/SettingsMenuViewModel+Navigation.swift:
--------------------------------------------------------------------------------
1 | import Entities
2 |
3 | extension SettingsMenuViewModel {
4 | enum Navigation: Hashable {
5 | case menu(row: SettingsMenu.Row)
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/UI/Views/SettingsMenuView/Sources/ViewModel/SettingsMenuViewModel.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | import RepositoryServices
4 |
5 | public class SettingsMenuViewModel: ObservableObject {
6 | private let _preferencesRepository: PreferencesRepositoryService
7 |
8 | private let _navigationSubject = PassthroughSubject()
9 |
10 | @Published private(set) var userName = ""
11 |
12 | var navigationSignal: AnyPublisher {
13 | _navigationSubject.eraseToAnyPublisher()
14 | }
15 |
16 | public init(preferenceRepository: PreferencesRepositoryService) {
17 | self._preferencesRepository = preferenceRepository
18 |
19 | _preferencesRepository.userNameStream()
20 | .assign(to: &$userName)
21 | }
22 |
23 | func navigate(_ navigation: Navigation) {
24 | _navigationSubject.send(navigation)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/UI/Views/SettingsView/SettingsView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | SettingsView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.settingsview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/SettingsView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/SettingsView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/SettingsView/Sources
18 | dependencies:
19 | - target: Entities
20 | - target: UIServices
21 |
--------------------------------------------------------------------------------
/UI/Views/SettingsView/Sources/ViewModel/SettingsViewModel.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public class SettingsViewModel: ObservableObject {
4 | public init() { }
5 | }
6 |
--------------------------------------------------------------------------------
/UI/Views/ToolbarTestView/Sources/ToolbarTestFlowController.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import UIServices
4 |
5 | public final class ToolbarTestFlowController: UIHostingController, ToolbarTestFlowControllerService {
6 | private var _viewModel: ToolbarTestViewModel {
7 | rootView.viewModel
8 | }
9 |
10 | override public init(rootView: ToolbarTestView) {
11 | super.init(rootView: rootView)
12 | }
13 |
14 | @available(*, unavailable)
15 | required init?(coder _: NSCoder) {
16 | fatalError("init(coder:) has not been implemented")
17 | }
18 |
19 | public func start() { }
20 | }
21 |
--------------------------------------------------------------------------------
/UI/Views/ToolbarTestView/Sources/View/ToolbarTestView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | // MARK: - ToolbarTestView
4 |
5 | public struct ToolbarTestView: View {
6 | @ObservedObject private(set) var viewModel: ToolbarTestViewModel
7 |
8 | public var body: some View {
9 | List {
10 | Section("Action") {
11 | Text(viewModel.actionText)
12 | }
13 | }
14 | .toolbar {
15 | ToolbarItem(placement: .navigationBarTrailing) {
16 | Button {
17 | viewModel.printNavigation1Text()
18 | } label: {
19 | Image(systemName: "1.circle")
20 | }
21 | }
22 |
23 | ToolbarItem(placement: .navigationBarTrailing) {
24 | Button {
25 | viewModel.printNavigation2Text()
26 | } label: {
27 | Image(systemName: "2.circle")
28 | }
29 | }
30 |
31 | ToolbarItemGroup(placement: .bottomBar) {
32 | Button("Left") {
33 | viewModel.printBottomLeftText()
34 | }
35 | Spacer()
36 | Button("Center") {
37 | viewModel.printBottomCenterText()
38 | }
39 | Spacer()
40 | Button("Right") {
41 | viewModel.printBottomRightText()
42 | }
43 | }
44 | }
45 | .navigationBarTitleDisplayMode(.inline)
46 | .navigationTitle("Toolbar test")
47 | }
48 |
49 | public init(viewModel: ToolbarTestViewModel) {
50 | self.viewModel = viewModel
51 | }
52 | }
53 |
54 | // MARK: - ToolbarTestView_Previews
55 |
56 | struct ToolbarTestView_Previews: PreviewProvider {
57 | private class PreviewViewModel: ToolbarTestViewModel { }
58 |
59 | static var previews: some View {
60 | NavigationView {
61 | ToolbarTestView(viewModel: PreviewViewModel())
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/UI/Views/ToolbarTestView/Sources/ViewModel/ToolbarTestViewModel.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | public class ToolbarTestViewModel: ObservableObject {
4 | @Published var actionText = ""
5 |
6 | public init() { }
7 |
8 | func printNavigation1Text() {
9 | actionText = "Navigation 1 tapped."
10 | }
11 |
12 | func printNavigation2Text() {
13 | actionText = "Navigation 2 tapped."
14 | }
15 |
16 | func printBottomLeftText() {
17 | actionText = "Bottom left tapped."
18 | }
19 |
20 | func printBottomCenterText() {
21 | actionText = "Bottom center tapped."
22 | }
23 |
24 | func printBottomRightText() {
25 | actionText = "Bottom right tapped."
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/UI/Views/ToolbarTestView/ToolbarTestView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | ToolbarTestView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.toolbartestview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/ToolbarTestView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/ToolbarTestView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/ToolbarTestView/Sources
18 | dependencies:
19 | - target: UIServices
20 |
--------------------------------------------------------------------------------
/UI/Views/UserNameSettingView/Sources/UserNameSettingFlowController.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import UIServices
4 |
5 | // swiftformat:disable indent
6 | public final class UserNameSettingFlowController: UIHostingController,
7 | UserNameSettingFlowControllerService {
8 | // swiftformat:enable indent
9 | private var _viewModel: UserNameSettingViewModel {
10 | rootView.viewModel
11 | }
12 |
13 | override public init(rootView: UserNameSettingView) {
14 | super.init(rootView: rootView)
15 |
16 | // FIXME: Using only `View.navigationBarTitleDisplayMode(.inline)` does not be a smooth transition
17 | navigationItem.largeTitleDisplayMode = .never
18 | }
19 |
20 | @available(*, unavailable)
21 | required init?(coder _: NSCoder) {
22 | fatalError("init(coder:) has not been implemented")
23 | }
24 |
25 | public func start() { }
26 | }
27 |
--------------------------------------------------------------------------------
/UI/Views/UserNameSettingView/Sources/View/UserNameSettingView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import RepositoryServices
4 |
5 | // MARK: - UserNameSettingView
6 |
7 | public struct UserNameSettingView: View {
8 | @ObservedObject private(set) var viewModel: UserNameSettingViewModel
9 |
10 | public var body: some View {
11 | List {
12 | Section {
13 | TextField(
14 | "Put your name",
15 | text: Binding(get: {
16 | viewModel.userName
17 | }, set: { text in
18 | viewModel.updateUserName(text)
19 | })
20 | )
21 | .padding(4)
22 | } header: {
23 | Text("User name")
24 | } footer: {
25 | Text("For testing keyboard behavior and local data stores.")
26 | }
27 | .textCase(nil)
28 | }
29 | .navigationBarTitleDisplayMode(.inline)
30 | .navigationTitle("User name")
31 | }
32 |
33 | public init(viewModel: UserNameSettingViewModel) {
34 | self.viewModel = viewModel
35 | }
36 | }
37 |
38 | // MARK: - UserNameSettingView_Previews
39 |
40 | struct UserNameSettingView_Previews: PreviewProvider {
41 | private class PreviewViewModel: UserNameSettingViewModel {
42 | private let _userName: String
43 |
44 | init(userName: String = "") {
45 | self._userName = userName
46 |
47 | super.init(preferencesRepository: PreviewPreferencesRepository())
48 | }
49 |
50 | override var userName: String { _userName }
51 | }
52 |
53 | static var previews: some View {
54 | Group {
55 | NavigationView {
56 | UserNameSettingView(
57 | viewModel: PreviewViewModel()
58 | )
59 | }
60 |
61 | NavigationView {
62 | UserNameSettingView(
63 | viewModel: PreviewViewModel(userName: "Typical Name")
64 | )
65 | }
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/UI/Views/UserNameSettingView/Sources/ViewModel/UserNameSettingViewModel.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 |
4 | import RepositoryServices
5 |
6 | public class UserNameSettingViewModel: ObservableObject {
7 | private let _preferencesRepository: PreferencesRepositoryService
8 |
9 | @Published private(set) var userName = ""
10 |
11 | public init(preferencesRepository: PreferencesRepositoryService) {
12 | self._preferencesRepository = preferencesRepository
13 |
14 | _preferencesRepository.userNameStream()
15 | .receive(on: DispatchQueue.main)
16 | .assign(to: &$userName)
17 | }
18 |
19 | func updateUserName(_ name: String) {
20 | Task {
21 | await _preferencesRepository.saveUserName(name)
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/UI/Views/UserNameSettingView/UserNameSettingView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | UserNameSettingView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.usernamesettingview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/UserNameSettingView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/UserNameSettingView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/UserNameSettingView/Sources
18 | dependencies:
19 | - target: RepositoryServices
20 | - target: UIServices
21 |
--------------------------------------------------------------------------------
/UI/Views/WalkthroughFinishView/Sources/View/WalkthroughFinishView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import Resources
4 |
5 | // MARK: - WalkthroughFinishView
6 |
7 | public struct WalkthroughFinishView: View {
8 | @ObservedObject private(set) var viewModel: WalkthroughFinishViewModel
9 |
10 | public var body: some View {
11 | NavigationView { // For layout purpose only
12 | ZStack {
13 | Color(uiColor: Asset.background.color)
14 | .ignoresSafeArea()
15 |
16 | Button {
17 | viewModel.navigate(.finish)
18 | } label: {
19 | Text("Start")
20 | }
21 | }
22 | .navigationBarTitleDisplayMode(.large)
23 | .navigationTitle("Start App")
24 | }
25 | }
26 |
27 | public init(viewModel: WalkthroughFinishViewModel) {
28 | self.viewModel = viewModel
29 | }
30 | }
31 |
32 | // MARK: - WalkthroughFinishView_Previews
33 |
34 | struct WalkthroughFinishView_Previews: PreviewProvider {
35 | private class PreviewModel: WalkthroughFinishViewModel { }
36 |
37 | static var previews: some View {
38 | WalkthroughFinishView(viewModel: PreviewModel())
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/UI/Views/WalkthroughFinishView/Sources/ViewModel/WalkthroughFinishViewModel+Navigation.swift:
--------------------------------------------------------------------------------
1 | extension WalkthroughFinishViewModel {
2 | enum Navigation {
3 | case finish
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/UI/Views/WalkthroughFinishView/Sources/ViewModel/WalkthroughFinishViewModel.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | public class WalkthroughFinishViewModel: ObservableObject {
4 | private let _navigationSubject = PassthroughSubject()
5 |
6 | var navigationSignal: AnyPublisher {
7 | _navigationSubject.eraseToAnyPublisher()
8 | }
9 |
10 | public init() { }
11 |
12 | func navigate(_ navigation: Navigation) {
13 | _navigationSubject.send(navigation)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/UI/Views/WalkthroughFinishView/Sources/WalkthroughFinishFlowController.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import SwiftUI
3 |
4 | import UIServices
5 |
6 | // swiftformat:disable indent
7 | public final class WalkthroughFinishFlowController: UIHostingController,
8 | WalkthroughFinishFlowControllerService {
9 | // swiftformat:enable indent
10 | public weak var delegate: WalkthroughFinishFlowControllerDelegate?
11 |
12 | private var _cancellable = Set()
13 |
14 | private var _viewModel: WalkthroughFinishViewModel {
15 | rootView.viewModel
16 | }
17 |
18 | override public init(rootView: WalkthroughFinishView) {
19 | super.init(rootView: rootView)
20 | }
21 |
22 | @available(*, unavailable)
23 | required init?(coder _: NSCoder) {
24 | fatalError("init(coder:) has not been implemented")
25 | }
26 |
27 | public func start() {
28 | _cancellable = Set()
29 |
30 | _viewModel.navigationSignal
31 | .receive(on: DispatchQueue.main)
32 | .sink { [weak self] navigation in
33 | guard let self = self else { return }
34 |
35 | switch navigation {
36 | case .finish:
37 | self.delegate?.walkthroughFinishFlowControllerFinishWalkthrough(self)
38 | }
39 | }
40 | .store(in: &_cancellable)
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/UI/Views/WalkthroughFinishView/WalkthroughFinishView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | WalkthroughFinishView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.walkthroughfinishview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/WalkthroughFinishView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/WalkthroughFinishView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/WalkthroughFinishView/Sources
18 | dependencies:
19 | - target: Resources
20 | - target: UIServices
21 |
--------------------------------------------------------------------------------
/UI/Views/WalkthroughIntroView/Sources/View/WalkthroughIntroView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import Resources
4 | import UIComponents
5 |
6 | // MARK: - WalkthroughIntroView
7 |
8 | public struct WalkthroughIntroView: View {
9 | @ObservedObject private(set) var viewModel: WalkthroughIntroViewModel
10 |
11 | public var body: some View {
12 | NavigationView { // For layout purpose only
13 | VStack {
14 | HStack {
15 | Text(
16 | """
17 | This is a Proof-of-Concept app for iOS app architecture using SwiftUI + FlowController pattern.
18 | """
19 | )
20 | Spacer(minLength: 0)
21 | }
22 | .padding()
23 |
24 | HStack {
25 | Text("App has features below:")
26 | Spacer(minLength: 0)
27 | }
28 | .padding([.leading, .trailing])
29 | .padding([.bottom], 8)
30 |
31 | HStack {
32 | VStack(alignment: .leading, spacing: 4) {
33 | BulletPointText("Walkthrough with UIPageViewController")
34 | BulletPointText("Main view with UITabBarController")
35 | BulletPointText("Feed view and Settings view with UINavigationController")
36 | BulletPointText("Feed view shows the feed for Apple Newsroom and App sales")
37 | BulletPointText("Settings view has some views for feature tests")
38 | }
39 | Spacer(minLength: 0)
40 | }
41 | .padding([.leading, .trailing])
42 |
43 | Spacer()
44 | }
45 | .background(Color(uiColor: Asset.background.color))
46 | .navigationBarTitleDisplayMode(.large)
47 | .navigationTitle("About")
48 | }
49 | }
50 |
51 | public init(viewModel: WalkthroughIntroViewModel) {
52 | self.viewModel = viewModel
53 | }
54 | }
55 |
56 | // MARK: - WalkthroughIntroView_Previews
57 |
58 | struct WalkthroughIntroView_Previews: PreviewProvider {
59 | private class PreviewModel: WalkthroughIntroViewModel { }
60 |
61 | static var previews: some View {
62 | WalkthroughIntroView(viewModel: PreviewModel())
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/UI/Views/WalkthroughIntroView/Sources/ViewModel/WalkthroughIntroViewModel.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public class WalkthroughIntroViewModel: ObservableObject {
4 | public init() { }
5 | }
6 |
--------------------------------------------------------------------------------
/UI/Views/WalkthroughIntroView/Sources/WalkthroughIntroFlowController.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import UIServices
4 |
5 | // swiftformat:disable indent
6 | public final class WalkthroughIntroFlowController: UIHostingController,
7 | WalkthroughIntroFlowControllerService {
8 | // swiftformat:enable indent
9 | private var _viewModel: WalkthroughIntroViewModel {
10 | rootView.viewModel
11 | }
12 |
13 | override public init(rootView: WalkthroughIntroView) {
14 | super.init(rootView: rootView)
15 | }
16 |
17 | @available(*, unavailable)
18 | required init?(coder _: NSCoder) {
19 | fatalError("init(coder:) has not been implemented")
20 | }
21 |
22 | public func start() { }
23 | }
24 |
--------------------------------------------------------------------------------
/UI/Views/WalkthroughIntroView/WalkthroughIntroView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | WalkthroughIntroView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.walkthroughintroview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/WalkthroughIntroView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/WalkthroughIntroView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/WalkthroughIntroView/Sources
18 | dependencies:
19 | - target: Resources
20 | - target: UIComponents
21 | - target: UIServices
22 |
--------------------------------------------------------------------------------
/UI/Views/WalkthroughSettingsView/Sources/View/WalkthroughSettingsView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | import Entities
4 | import Resources
5 | import UIComponents
6 |
7 | // MARK: - WalkthroughSettingsView
8 |
9 | public struct WalkthroughSettingsView: View {
10 | @ObservedObject private(set) var viewModel: WalkthroughSettingsViewModel
11 |
12 | private let _languages: [FeedLanguage] = [.english, .japanese]
13 |
14 | public var body: some View {
15 | NavigationView { // For layout purpose only
16 | List {
17 | Section {
18 | RadioButtonGroup(
19 | _languages.map(\.title),
20 | selectedIndex: Binding(get: {
21 | _languages.firstIndex(of: viewModel.feedLanguage)
22 | }, set: { selectedIndex in
23 | if let selectedIndex = selectedIndex {
24 | viewModel.feedLanguage = _languages[selectedIndex]
25 | }
26 | })
27 | )
28 | .padding([.top, .bottom], 4)
29 | } header: {
30 | Text("Feed language")
31 | } footer: {
32 | Text("Language setting in the feed source.")
33 | }
34 | .textCase(nil)
35 |
36 | Section {
37 | TextField(
38 | "Put your name",
39 | text: $viewModel.userName
40 | )
41 | .padding(4)
42 | } header: {
43 | Text("User name")
44 | } footer: {
45 | Text("For testing keyboard behavior and local data stores.")
46 | }
47 | .textCase(nil)
48 | }
49 | .listStyle(.insetGrouped)
50 | .navigationBarTitleDisplayMode(.large)
51 | .navigationTitle("Settings")
52 | }
53 | }
54 |
55 | public init(viewModel: WalkthroughSettingsViewModel) {
56 | self.viewModel = viewModel
57 | }
58 | }
59 |
60 | // MARK: - WalkthroughSettingsView_Previews
61 |
62 | struct WalkthroughSettingsView_Previews: PreviewProvider {
63 | private class PreviewModel: WalkthroughSettingsViewModel {
64 | init(feedLanguage: FeedLanguage, userName: String) {
65 | super.init()
66 |
67 | self.feedLanguage = feedLanguage
68 | self.userName = userName
69 | }
70 | }
71 |
72 | static var previews: some View {
73 | WalkthroughSettingsView(
74 | viewModel: PreviewModel(
75 | feedLanguage: .english,
76 | userName: ""
77 | )
78 | )
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/UI/Views/WalkthroughSettingsView/Sources/ViewModel/WalkthroughSettingsViewModel.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | import Entities
4 |
5 | public class WalkthroughSettingsViewModel: ObservableObject {
6 | @Published var feedLanguage: FeedLanguage = .english
7 | @Published var userName = ""
8 |
9 | var feedLanguageStream: AnyPublisher {
10 | $feedLanguage.eraseToAnyPublisher()
11 | }
12 |
13 | var userNameStream: AnyPublisher {
14 | $userName.eraseToAnyPublisher()
15 | }
16 |
17 | public init() { }
18 | }
19 |
--------------------------------------------------------------------------------
/UI/Views/WalkthroughSettingsView/Sources/WalkthroughSettingsFlowController.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import SwiftUI
3 |
4 | import UIServices
5 |
6 | // swiftformat:disable indent
7 | public final class WalkthroughSettingsFlowController: UIHostingController,
8 | WalkthroughSettingsFlowControllerService {
9 | // swiftformat:enable indent
10 | public weak var delegate: WalkthroughSettingsFlowControllerDelegate?
11 |
12 | private var _cancellable = Set()
13 |
14 | private var _viewModel: WalkthroughSettingsViewModel {
15 | rootView.viewModel
16 | }
17 |
18 | override public init(rootView: WalkthroughSettingsView) {
19 | super.init(rootView: rootView)
20 | }
21 |
22 | @available(*, unavailable)
23 | required init?(coder _: NSCoder) {
24 | fatalError("init(coder:) has not been implemented")
25 | }
26 |
27 | public func start() {
28 | _cancellable = Set()
29 |
30 | _viewModel.feedLanguageStream
31 | .receive(on: DispatchQueue.main)
32 | .sink { [weak self] language in
33 | guard let self = self else { return }
34 | self.delegate?.walkthroughSettingsFlowController(self, update: language)
35 | }
36 | .store(in: &_cancellable)
37 |
38 | _viewModel.userNameStream
39 | .receive(on: DispatchQueue.main)
40 | .sink { [weak self] name in
41 | guard let self = self else { return }
42 | self.delegate?.walkthroughSettingsFlowController(self, update: name)
43 | }
44 | .store(in: &_cancellable)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/UI/Views/WalkthroughSettingsView/WalkthroughSettingsView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | WalkthroughSettingsView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.walkthroughsettingsview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/WalkthroughSettingsView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/WalkthroughSettingsView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/WalkthroughSettingsView/Sources
18 | dependencies:
19 | - target: Entities
20 | - target: Resources
21 | - target: UIServices
22 | - target: UIComponents
23 |
--------------------------------------------------------------------------------
/UI/Views/WalkthroughView/Sources/ViewModel/WalkthroughViewModel.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 |
3 | import Entities
4 | import RepositoryServices
5 |
6 | public class WalkthroughViewModel: ObservableObject {
7 | private let _appStateRepository: AppStateRepositoryService
8 | private let _preferencesRepository: PreferencesRepositoryService
9 |
10 | @Published var feedLanguage: FeedLanguage?
11 | @Published var userName: String?
12 |
13 | public init(appStateRepository: AppStateRepositoryService, preferencesRepository: PreferencesRepositoryService) {
14 | self._appStateRepository = appStateRepository
15 | self._preferencesRepository = preferencesRepository
16 | }
17 |
18 | func finishWalkthrough() {
19 | Task {
20 | if let feedLanguage = feedLanguage {
21 | await _preferencesRepository.saveFeedLanguage(feedLanguage)
22 | }
23 | if let userName = userName {
24 | await _preferencesRepository.saveUserName(userName)
25 | }
26 | await _appStateRepository.finishWalkthrough()
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/UI/Views/WalkthroughView/WalkthroughView.project.yml:
--------------------------------------------------------------------------------
1 | targets:
2 | WalkthroughView:
3 | type: framework
4 | platform: iOS
5 | settings:
6 | base:
7 | PRODUCT_BUNDLE_IDENTIFIER: io.github.hugehoge.swiftuiflowcontroller.walkthroughview
8 | TARGETED_DEVICE_FAMILY: 1
9 | GENERATE_INFOPLIST_FILE: YES
10 | sources:
11 | - path: Sources
12 | group: UI/Views/WalkthroughView
13 | preBuildScripts:
14 | - name: Format and linting
15 | script: |
16 | ${SRCROOT}/bin/swiftformat --swiftversion 5.5 UI/Views/WalkthroughView/Sources
17 | ${SRCROOT}/bin/swiftlint UI/Views/WalkthroughView/Sources
18 | dependencies:
19 | - target: Entities
20 | - target: RepositoryServices
21 | - target: Resources
22 | - target: UIServices
23 |
--------------------------------------------------------------------------------
/bin/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/bin/.gitkeep
--------------------------------------------------------------------------------
/images/architecture/concept.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/images/architecture/concept.png
--------------------------------------------------------------------------------
/images/architecture/data_flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/images/architecture/data_flow.png
--------------------------------------------------------------------------------
/images/architecture/data_flow2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/images/architecture/data_flow2.png
--------------------------------------------------------------------------------
/images/architecture/event_flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/images/architecture/event_flow.png
--------------------------------------------------------------------------------
/images/architecture/transition.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/images/architecture/transition.png
--------------------------------------------------------------------------------
/images/screenshots/app_sales.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/images/screenshots/app_sales.png
--------------------------------------------------------------------------------
/images/screenshots/feed_language_setting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/images/screenshots/feed_language_setting.png
--------------------------------------------------------------------------------
/images/screenshots/feed_list_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/images/screenshots/feed_list_1.png
--------------------------------------------------------------------------------
/images/screenshots/feed_list_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/images/screenshots/feed_list_2.png
--------------------------------------------------------------------------------
/images/screenshots/feed_list_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/images/screenshots/feed_list_3.png
--------------------------------------------------------------------------------
/images/screenshots/keyboard_test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/images/screenshots/keyboard_test.png
--------------------------------------------------------------------------------
/images/screenshots/licenses.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/images/screenshots/licenses.png
--------------------------------------------------------------------------------
/images/screenshots/modal_transition_test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/images/screenshots/modal_transition_test.png
--------------------------------------------------------------------------------
/images/screenshots/push_transition_test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/images/screenshots/push_transition_test.png
--------------------------------------------------------------------------------
/images/screenshots/settings_menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/images/screenshots/settings_menu.png
--------------------------------------------------------------------------------
/images/screenshots/toolbar_test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/images/screenshots/toolbar_test.png
--------------------------------------------------------------------------------
/images/screenshots/user_name_setting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/images/screenshots/user_name_setting.png
--------------------------------------------------------------------------------
/images/screenshots/walkthrough_finish.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/images/screenshots/walkthrough_finish.png
--------------------------------------------------------------------------------
/images/screenshots/walkthrough_intro.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/images/screenshots/walkthrough_intro.png
--------------------------------------------------------------------------------
/images/screenshots/walkthrough_settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/images/screenshots/walkthrough_settings.png
--------------------------------------------------------------------------------
/lib/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hugehoge/swiftui-flowcontroller-pattern/5b6baccbcb0690418fe62cfb50761bc38a83255d/lib/.gitkeep
--------------------------------------------------------------------------------
/scripts/local-install-licenseplist.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Function
4 |
5 | function print_verbose() {
6 | if [ ${S_FLAG} -ne 1 ]; then
7 | echo "$1"
8 | fi
9 | }
10 |
11 | # Usage Text
12 |
13 | USAGE=$(cat <] [install path]
16 |
17 | Options:
18 | -h help text
19 | -s silent mode
20 | EOF
21 | )
22 |
23 | # Arguments parse
24 |
25 | S_FLAG=0
26 | while getopts :hs OPT
27 | do
28 | case $OPT in
29 | h) echo "${USAGE}"
30 | exit 0
31 | ;;
32 | s) S_FLAG=1
33 | ;;
34 | \?) echo "${USAGE}" 1>&2
35 | exit 1
36 | ;;
37 | esac
38 | done
39 |
40 | shift $((OPTIND - 1))
41 |
42 | if [ $# != 1 ]; then
43 | echo "${USAGE}" 1>&2
44 | exit 1
45 | fi
46 | INSTALL_PATH=$1
47 |
48 | # Install process
49 |
50 | mkdir -p "${INSTALL_PATH}"
51 |
52 | NAME="LicensePlist"
53 | VERSION="3.17.0"
54 | ZIP_URL="https://github.com/mono0926/LicensePlist/releases/download/${VERSION}/portable_licenseplist.zip"
55 | BIN_PATH="${INSTALL_PATH}/license-plist"
56 |
57 | VERSION_CMD="${BIN_PATH} --version"
58 | EXPECTED_VERSION_FMT="${VERSION}"
59 |
60 | if [ "$(${VERSION_CMD} 2>/dev/null)" != "${EXPECTED_VERSION_FMT}" ]; then
61 | print_verbose "${NAME} ${VERSION} not installed. Download and installing..."
62 |
63 | curl -fsSL "${ZIP_URL}" | bsdtar xf - -C "${INSTALL_PATH}"
64 | chmod 755 "${BIN_PATH}"
65 |
66 | print_verbose "${NAME} ${VERSION} installed."
67 | else
68 | print_verbose "${NAME} ${VERSION} already installed."
69 | fi
70 |
71 | echo "${BIN_PATH}"
72 |
--------------------------------------------------------------------------------
/scripts/local-install-swiftformat.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Function
4 |
5 | function print_verbose() {
6 | if [ ${S_FLAG} -ne 1 ]; then
7 | echo "$1"
8 | fi
9 | }
10 |
11 | # Usage Text
12 |
13 | USAGE=$(cat <] [install path]
16 |
17 | Options:
18 | -h help text
19 | -s silent mode
20 | EOF
21 | )
22 |
23 | # Arguments parse
24 |
25 | S_FLAG=0
26 | while getopts :hs OPT
27 | do
28 | case $OPT in
29 | h) echo "${USAGE}"
30 | exit 0
31 | ;;
32 | s) S_FLAG=1
33 | ;;
34 | \?) echo "${USAGE}" 1>&2
35 | exit 1
36 | ;;
37 | esac
38 | done
39 |
40 | shift $((OPTIND - 1))
41 |
42 | if [ $# != 1 ]; then
43 | echo "${USAGE}" 1>&2
44 | exit 1
45 | fi
46 | INSTALL_PATH=$1
47 |
48 | # Install process
49 |
50 | mkdir -p "${INSTALL_PATH}"
51 |
52 | NAME="SwiftFormat"
53 | VERSION="0.49.3"
54 | ZIP_URL="https://github.com/nicklockwood/SwiftFormat/releases/download/${VERSION}/swiftformat.zip"
55 | BIN_PATH="${INSTALL_PATH}/swiftformat"
56 |
57 | VERSION_CMD="${BIN_PATH} --version"
58 | EXPECTED_VERSION_FMT="${VERSION}"
59 |
60 | if [ "$(${VERSION_CMD} 2>/dev/null)" != "${EXPECTED_VERSION_FMT}" ]; then
61 | print_verbose "${NAME} ${VERSION} not installed. Download and installing..."
62 |
63 | curl -fsSL "${ZIP_URL}" | bsdtar xf - -C "${INSTALL_PATH}"
64 | chmod 755 "${BIN_PATH}"
65 |
66 | print_verbose "${NAME} ${VERSION} installed."
67 | else
68 | print_verbose "${NAME} ${VERSION} already installed."
69 | fi
70 |
71 | echo "${BIN_PATH}"
72 |
--------------------------------------------------------------------------------
/scripts/local-install-swiftgen.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Function
4 |
5 | function print_verbose() {
6 | if [ ${S_FLAG} -ne 1 ]; then
7 | echo "$1"
8 | fi
9 | }
10 |
11 | # Usage Text
12 |
13 | USAGE=$(cat <] [install path]
16 |
17 | Options:
18 | -h help text
19 | -s silent mode
20 | EOF
21 | )
22 |
23 | # Arguments parse
24 |
25 | S_FLAG=0
26 | while getopts :hs OPT
27 | do
28 | case $OPT in
29 | h) echo "${USAGE}"
30 | exit 0
31 | ;;
32 | s) S_FLAG=1
33 | ;;
34 | \?) echo "${USAGE}" 1>&2
35 | exit 1
36 | ;;
37 | esac
38 | done
39 |
40 | shift $((OPTIND - 1))
41 |
42 | if [ $# != 1 ]; then
43 | echo "${USAGE}" 1>&2
44 | exit 1
45 | fi
46 | INSTALL_PATH=$1
47 |
48 | # Install process
49 |
50 | mkdir -p "${INSTALL_PATH}"
51 |
52 | NAME="SwiftGen"
53 | VERSION="6.5.1"
54 | ZIP_URL="https://github.com/SwiftGen/SwiftGen/releases/download/${VERSION}/swiftgen-${VERSION}.zip"
55 | BIN_PATH="${INSTALL_PATH}/bin/swiftgen"
56 |
57 | STENCIL_VERSION="0.14.1"
58 | STENCILSWIFTKIT_VERSION="2.8.0"
59 | SWIFTGENKIT_VERSION="6.5.1"
60 |
61 | VERSION_CMD="${BIN_PATH} --version"
62 | EXPECTED_VERSION_FMT="SwiftGen v${VERSION} (Stencil v${STENCIL_VERSION}, StencilSwiftKit v${STENCILSWIFTKIT_VERSION}, SwiftGenKit v${SWIFTGENKIT_VERSION})"
63 |
64 | if [ "$(${VERSION_CMD} 2>/dev/null)" != "${EXPECTED_VERSION_FMT}" ]; then
65 | print_verbose "${NAME} ${VERSION} not installed. Download and installing..."
66 |
67 | curl -fsSL "${ZIP_URL}" | bsdtar xf - -C "${INSTALL_PATH}"
68 | chmod 755 "${BIN_PATH}"
69 |
70 | print_verbose "${NAME} ${VERSION} installed."
71 | else
72 | print_verbose "${NAME} ${VERSION} already installed."
73 | fi
74 |
75 | echo "${BIN_PATH}"
76 |
--------------------------------------------------------------------------------
/scripts/local-install-swiftlint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Function
4 |
5 | function print_verbose() {
6 | if [ ${S_FLAG} -ne 1 ]; then
7 | echo "$1"
8 | fi
9 | }
10 |
11 | # Usage Text
12 |
13 | USAGE=$(cat <] [install path]
16 |
17 | Options:
18 | -h help text
19 | -s silent mode
20 | EOF
21 | )
22 |
23 | # Arguments parse
24 |
25 | S_FLAG=0
26 | while getopts :hs OPT
27 | do
28 | case $OPT in
29 | h) echo "${USAGE}"
30 | exit 0
31 | ;;
32 | s) S_FLAG=1
33 | ;;
34 | \?) echo "${USAGE}" 1>&2
35 | exit 1
36 | ;;
37 | esac
38 | done
39 |
40 | shift $((OPTIND - 1))
41 |
42 | if [ $# != 1 ]; then
43 | echo "${USAGE}" 1>&2
44 | exit 1
45 | fi
46 | INSTALL_PATH=$1
47 |
48 | # Install process
49 |
50 | mkdir -p "${INSTALL_PATH}"
51 |
52 | NAME="SwiftLint"
53 | VERSION="0.46.2"
54 | ZIP_URL="https://github.com/realm/SwiftLint/releases/download/${VERSION}/portable_swiftlint.zip"
55 | BIN_PATH="${INSTALL_PATH}/swiftlint"
56 |
57 | VERSION_CMD="${BIN_PATH} version"
58 | EXPECTED_VERSION_FMT="${VERSION}"
59 |
60 | if [ "$(${VERSION_CMD} 2>/dev/null)" != "${EXPECTED_VERSION_FMT}" ]; then
61 | print_verbose "${NAME} ${VERSION} not installed. Download and installing..."
62 |
63 | curl -fsSL "${ZIP_URL}" | bsdtar xf - -C "${INSTALL_PATH}"
64 | chmod 755 "${BIN_PATH}"
65 |
66 | print_verbose "${NAME} ${VERSION} installed."
67 | else
68 | print_verbose "${NAME} ${VERSION} already installed."
69 | fi
70 |
71 | echo "${BIN_PATH}"
72 |
--------------------------------------------------------------------------------
/scripts/local-install-xcodegen.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Function
4 |
5 | function print_verbose() {
6 | if [ ${S_FLAG} -ne 1 ]; then
7 | echo "$1"
8 | fi
9 | }
10 |
11 | # Usage Text
12 |
13 | USAGE=$(cat <] [install path]
16 |
17 | Options:
18 | -h help text
19 | -s silent mode
20 | EOF
21 | )
22 |
23 | # Arguments parse
24 |
25 | S_FLAG=0
26 | while getopts :hs OPT
27 | do
28 | case $OPT in
29 | h) echo "${USAGE}"
30 | exit 0
31 | ;;
32 | s) S_FLAG=1
33 | ;;
34 | \?) echo "${USAGE}" 1>&2
35 | exit 1
36 | ;;
37 | esac
38 | done
39 |
40 | shift $((OPTIND - 1))
41 |
42 | if [ $# != 1 ]; then
43 | echo "${USAGE}" 1>&2
44 | exit 1
45 | fi
46 | INSTALL_PATH=$1
47 |
48 | # Install process
49 |
50 | mkdir -p "${INSTALL_PATH}"
51 |
52 | NAME="XcodeGen"
53 | VERSION="2.25.0"
54 | ZIP_URL="https://github.com/yonaskolb/XcodeGen/releases/download/${VERSION}/xcodegen.zip"
55 | BIN_PATH="${INSTALL_PATH}/xcodegen/bin/xcodegen"
56 |
57 | VERSION_CMD="${BIN_PATH} version"
58 | EXPECTED_VERSION_FMT="Version: ${VERSION}"
59 |
60 | if [ "$(${VERSION_CMD} 2>/dev/null)" != "${EXPECTED_VERSION_FMT}" ]; then
61 | print_verbose "${NAME} ${VERSION} not installed. Download and installing..."
62 |
63 | curl -fsSL "${ZIP_URL}" | bsdtar xf - -C "${INSTALL_PATH}"
64 | chmod 755 "${BIN_PATH}"
65 |
66 | print_verbose "${NAME} ${VERSION} installed."
67 | else
68 | print_verbose "${NAME} ${VERSION} already installed."
69 | fi
70 |
71 | echo "${BIN_PATH}"
72 |
--------------------------------------------------------------------------------
/swiftui-flowcontroller-pattern.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/swiftui-flowcontroller-pattern.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/swiftui-flowcontroller-pattern.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "CombineExpectations",
6 | "repositoryURL": "https://github.com/groue/CombineExpectations.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "04d4e4b21c9e8361925f03f64a7ecda89ef9974f",
10 | "version": "0.10.0"
11 | }
12 | },
13 | {
14 | "package": "Kanna",
15 | "repositoryURL": "https://github.com/tid-kijyun/Kanna.git",
16 | "state": {
17 | "branch": null,
18 | "revision": "f9e4922223dd0d3dfbf02ca70812cf5531fc0593",
19 | "version": "5.2.7"
20 | }
21 | },
22 | {
23 | "package": "Kingfisher",
24 | "repositoryURL": "https://github.com/onevcat/Kingfisher.git",
25 | "state": {
26 | "branch": null,
27 | "revision": "0c02c46cfdc0656ce74fd0963a75e5000a0b7f23",
28 | "version": "7.1.2"
29 | }
30 | },
31 | {
32 | "package": "MarkdownView",
33 | "repositoryURL": "https://github.com/keitaoouchi/MarkdownView.git",
34 | "state": {
35 | "branch": null,
36 | "revision": "35964c602ecfccc359180224a9d7d318d03b304b",
37 | "version": "1.8.3"
38 | }
39 | },
40 | {
41 | "package": "Mockingbird",
42 | "repositoryURL": "https://github.com/birdrides/mockingbird.git",
43 | "state": {
44 | "branch": null,
45 | "revision": "6e0d473df6b975719e145f6aebf90990c634700e",
46 | "version": "0.20.0"
47 | }
48 | },
49 | {
50 | "package": "OHHTTPStubs",
51 | "repositoryURL": "https://github.com/AliSoftware/OHHTTPStubs.git",
52 | "state": {
53 | "branch": null,
54 | "revision": "12f19662426d0434d6c330c6974d53e2eb10ecd9",
55 | "version": "9.1.0"
56 | }
57 | },
58 | {
59 | "package": "Resolver",
60 | "repositoryURL": "https://github.com/hmlongco/Resolver.git",
61 | "state": {
62 | "branch": null,
63 | "revision": "97de0b0320036607564af4a60025b48f8d041221",
64 | "version": "1.5.0"
65 | }
66 | },
67 | {
68 | "package": "Snappable",
69 | "repositoryURL": "https://github.com/hugehoge/Snappable.git",
70 | "state": {
71 | "branch": null,
72 | "revision": "5b7ac7b5bbb0d7e83bfa6b7273f29247724df55e",
73 | "version": "0.2.0"
74 | }
75 | },
76 | {
77 | "package": "Introspect",
78 | "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect.git",
79 | "state": {
80 | "branch": null,
81 | "revision": "2e09be8af614401bc9f87d40093ec19ce56ccaf2",
82 | "version": "0.1.3"
83 | }
84 | }
85 | ]
86 | },
87 | "version": 1
88 | }
89 |
--------------------------------------------------------------------------------
/swiftui-flowcontroller-pattern.xcodeproj/xcshareddata/IDETemplateMacros.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | FILEHEADER
6 |
7 | // This header comment will be stripped by SwiftFormat.
8 | //
9 |
10 |
11 |
--------------------------------------------------------------------------------