├── .gitignore
├── README.md
├── SwiftUIAndReduxExample
├── SwiftUIAndReduxExample.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── swiftpm
│ │ │ └── Package.resolved
│ └── xcshareddata
│ │ └── xcschemes
│ │ ├── SwiftUIAndReduxExample.xcscheme
│ │ └── SwiftUIAndReduxExampleMockApi.xcscheme
├── SwiftUIAndReduxExample
│ ├── App
│ │ ├── AppDelegate.swift
│ │ └── SwiftUIAndReduxExampleApp.swift
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ ├── Archive
│ │ │ ├── Contents.json
│ │ │ └── archive_sample_image.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── archive_sample_image.jpg
│ │ ├── Contents.json
│ │ ├── Onboarding
│ │ │ ├── Contents.json
│ │ │ ├── onboarding1.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── onboarding1.jpg
│ │ │ ├── onboarding2.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── onboarding2.jpg
│ │ │ └── onboarding3.imageset
│ │ │ │ ├── Contents.json
│ │ │ │ └── onboarding3.jpg
│ │ └── Profile
│ │ │ ├── Contents.json
│ │ │ ├── profile_avatar_sample.imageset
│ │ │ ├── Contents.json
│ │ │ └── profile_avatar_sample.jpg
│ │ │ └── profile_header_sample.imageset
│ │ │ ├── Contents.json
│ │ │ └── profile_background_sample.jpg
│ ├── Extension
│ │ ├── FirstAppearExtension.swift
│ │ ├── StringExtension.swift
│ │ ├── SwiftUIColorExtension.swift
│ │ └── UIColorExtension.swift
│ ├── Info.plist
│ ├── Infrastructure
│ │ ├── APIClientManager.swift
│ │ ├── RealmAccessManager.swift
│ │ └── UserDefaultManager.swift
│ ├── InternalData
│ │ └── Testing
│ │ │ ├── achive_images.json
│ │ │ ├── campaign_banners.json
│ │ │ ├── favorite_scenes.json
│ │ │ ├── featured_topics.json
│ │ │ ├── pickup_photos.json
│ │ │ ├── profile_announcement.json
│ │ │ ├── profile_comment.json
│ │ │ ├── profile_personal.json
│ │ │ ├── profile_recent_favorite.json
│ │ │ ├── recent_news.json
│ │ │ └── trend_articles.json
│ ├── Model
│ │ ├── DataTransfer
│ │ │ └── Response
│ │ │ │ ├── Archive
│ │ │ │ └── ArchiveSceneResponse.swift
│ │ │ │ ├── ArchiveResponse.swift
│ │ │ │ ├── Favorite
│ │ │ │ └── FavoriteSceneResponse.swift
│ │ │ │ ├── FavoriteResponse.swift
│ │ │ │ ├── Home
│ │ │ │ ├── CampaignBannersResponse.swift
│ │ │ │ ├── FeaturedTopicsResponse.swift
│ │ │ │ ├── PickupPhotoResponse.swift
│ │ │ │ ├── RecentNewsResponse.swift
│ │ │ │ └── TrendArticleResponse.swift
│ │ │ │ ├── HomeResponse.swift
│ │ │ │ ├── Profile
│ │ │ │ ├── ProfileAnnoucementResponse.swift
│ │ │ │ ├── ProfileCommentResponse.swift
│ │ │ │ ├── ProfilePersonalResponse.swift
│ │ │ │ └── ProfileRecentFavoriteResponse.swift
│ │ │ │ └── ProfileResponse.swift
│ │ ├── Entity
│ │ │ ├── Archive
│ │ │ │ └── ArchiveSceneEntity.swift
│ │ │ ├── Favorite
│ │ │ │ └── FavoriteSceneEntity.swift
│ │ │ ├── Home
│ │ │ │ ├── CampaignBannerEntity.swift
│ │ │ │ ├── FeaturedTopicEntity.swift
│ │ │ │ ├── PickupPhotoEntity.swift
│ │ │ │ ├── RecentNewsEntity.swift
│ │ │ │ └── TrendArticleEntity.swift
│ │ │ └── Profile
│ │ │ │ ├── ProfileAnnoucementEntity.swift
│ │ │ │ ├── ProfileCommentEntity.swift
│ │ │ │ ├── ProfilePersonalEntity.swift
│ │ │ │ └── ProfileRecentFavoriteEntity.swift
│ │ └── RealmObject
│ │ │ └── StockArchiveRealmEntity.swift
│ ├── Preview Content
│ │ └── Preview Assets.xcassets
│ │ │ └── Contents.json
│ ├── Repository
│ │ ├── Archive
│ │ │ ├── RequestArchiveRepository.swift
│ │ │ └── StoredArchiveDataRepository.swift
│ │ ├── Favorite
│ │ │ └── FavioriteRepository.swift
│ │ ├── Home
│ │ │ └── HomeRepository.swift
│ │ ├── Onboarding
│ │ │ └── OnboardingRepository.swift
│ │ └── Profile
│ │ │ └── ProfileRepository.swift
│ ├── Store
│ │ ├── Actions
│ │ │ ├── ArchiveActions.swift
│ │ │ ├── FavoriteActions.swift
│ │ │ ├── HomeActions.swift
│ │ │ ├── OnboardingActions.swift
│ │ │ └── ProfileActions.swift
│ │ ├── Middlewares
│ │ │ ├── ArchiveMiddleware.swift
│ │ │ ├── FavoriteMiddleware.swift
│ │ │ ├── HomeMiddleware.swift
│ │ │ ├── OnboardingMiddleware.swift
│ │ │ └── ProfileMiddleware.swift
│ │ ├── Reducers
│ │ │ ├── AppReducer.swift
│ │ │ ├── ArchiveReducer.swift
│ │ │ ├── FavoriteReducer.swift
│ │ │ ├── HomeReducer.swift
│ │ │ ├── OnboardingReducer.swift
│ │ │ └── ProfileReducer.swift
│ │ ├── States
│ │ │ ├── AppState.swift
│ │ │ ├── ArchiveState.swift
│ │ │ ├── FavoriteState.swift
│ │ │ ├── HomeState.swift
│ │ │ ├── OnboardingState.swift
│ │ │ └── ProfileState.swift
│ │ └── Store.swift
│ ├── UsefulFunction
│ │ └── DateLabelFormatter.swift
│ ├── View
│ │ ├── Components
│ │ │ ├── Archive
│ │ │ │ ├── ArchiveContentsView.swift
│ │ │ │ └── Section
│ │ │ │ │ ├── ArchiveCategoryView.swift
│ │ │ │ │ ├── ArchiveCellView.swift
│ │ │ │ │ ├── ArchiveCurrentCountView.swift
│ │ │ │ │ ├── ArchiveEmptyView.swift
│ │ │ │ │ └── ArchiveFreewordView.swift
│ │ │ ├── Common
│ │ │ │ ├── ConnectionErrorView.swift
│ │ │ │ └── ExecutingConnectionView.swift
│ │ │ ├── Favorite
│ │ │ │ ├── FavoriteCommonSectionView.swift
│ │ │ │ ├── FavoriteContentsView.swift
│ │ │ │ └── Section
│ │ │ │ │ └── FavoriteSwipePaging
│ │ │ │ │ └── FavoriteSwipePagingView.swift
│ │ │ ├── Home
│ │ │ │ ├── HomeCommonSectionView.swift
│ │ │ │ ├── HomeContentsView.swift
│ │ │ │ └── Section
│ │ │ │ │ ├── CampaignBannerCarousel
│ │ │ │ │ └── CampaignBannerCarouselView.swift
│ │ │ │ │ ├── FeaturedTopicsCarousel
│ │ │ │ │ └── FeaturedTopicsCarouselView.swift
│ │ │ │ │ ├── PickupPhotosGrid
│ │ │ │ │ └── PickupPhotosGridView.swift
│ │ │ │ │ ├── RecentNewsCarousel
│ │ │ │ │ └── RecentNewsCarouselView.swift
│ │ │ │ │ └── TrendArticlesGrid
│ │ │ │ │ └── TrendArticlesGridView.swift
│ │ │ ├── Onboarding
│ │ │ │ ├── OnboardingContentsView.swift
│ │ │ │ └── Section
│ │ │ │ │ └── OnboardingItemView.swift
│ │ │ └── Profile
│ │ │ │ ├── ProfileCommonSectionView.swift
│ │ │ │ ├── ProfileContentsView.swift
│ │ │ │ └── Section
│ │ │ │ ├── ProfileInformation
│ │ │ │ ├── ProfileInformationTabComponent
│ │ │ │ │ ├── ProfileInformationAnnouncementView.swift
│ │ │ │ │ ├── ProfileInformationCommentView.swift
│ │ │ │ │ ├── ProfileInformationRecentView.swift
│ │ │ │ │ └── ProfileInformationTabSwitcher.swift
│ │ │ │ └── ProfileInformationView.swift
│ │ │ │ ├── ProfilePersonal
│ │ │ │ └── ProfilePersonalView.swift
│ │ │ │ ├── ProfilePointAndHistory
│ │ │ │ └── ProfilePointsAndHistoryView.swift
│ │ │ │ ├── ProfileSelfIntroduction
│ │ │ │ └── ProfileSelfIntroductionView.swift
│ │ │ │ ├── ProfileSocialMediaLink
│ │ │ │ └── ProfileSocialMediaLinkView.swift
│ │ │ │ └── ProfileSpecialContents
│ │ │ │ └── ProfileSpecialContentsView.swift
│ │ ├── ContentView.swift
│ │ ├── Representable
│ │ │ ├── LoadingIndicatorViewRepresentable.swift
│ │ │ └── RatingViewRepresentable.swift
│ │ └── Screens
│ │ │ ├── ArchiveScreenView.swift
│ │ │ ├── FavoriteScreenView.swift
│ │ │ ├── HomeScreenView.swift
│ │ │ └── ProfileScreenView.swift
│ └── ViewObject
│ │ ├── Archive
│ │ └── ArchiveCellViewObject.swift
│ │ ├── Favorite
│ │ └── FavoritePhotosCardViewObject.swift
│ │ ├── Home
│ │ ├── CampaignBannerCarouselViewObject.swift
│ │ ├── FeaturedTopicsCarouselViewObject.swift
│ │ ├── PickupPhotosGridViewObject.swift
│ │ ├── RecentNewsCarouselViewObject.swift
│ │ └── TrendArticlesGridViewObject.swift
│ │ └── Profile
│ │ ├── ProfileInformationViewObject.swift
│ │ ├── ProfilePersonalViewObject.swift
│ │ ├── ProfilePointsAndHistoryViewObject.swift
│ │ ├── ProfileSelfIntroductionViewObject.swift
│ │ └── ProfileSocialMediaViewObject.swift
├── SwiftUIAndReduxExampleMockApi-Info.plist
├── SwiftUIAndReduxExampleTests
│ ├── ArchiveStateTest.swift
│ ├── FavoriteStateTest.swift
│ ├── HomeStateTest.swift
│ ├── Info.plist
│ ├── OnboardingStateTest.swift
│ └── ProfileStateTest.swift
└── SwiftUIAndReduxExampleUITests
│ ├── Info.plist
│ └── SwiftUIAndReduxExampleUITests.swift
├── images
├── 3-1-fundamental_of_redux.png
├── 3-2-example_of_middleware.png
├── 4-1-1-3d_carousel_example.png
├── 4-1-2-drag_carousel_example.png
├── 4-1-3-simple_horizontal_carousel_example.png
├── 4-1-4-simple_2column_grid_example.png
├── 4-1-5-waterfall_grid_example.png
├── 4-1-6-swipe_paging_example.png
├── 4-2-profile_ui_example.png
├── 4-3-archive_ui_example.png
├── build-target-setting.png
├── design_memo.png
├── sample_screen1.png
├── sample_screen2.png
├── sample_screen3.png
└── sample_screen4.png
└── mock_server
├── db
└── db.json
├── package.json
├── server.ts
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode (from gitignore.io)
2 | build/
3 | *.pbxuser
4 | !default.pbxuser
5 | *.mode1v3
6 | !default.mode1v3
7 | *.mode2v3
8 | !default.mode2v3
9 | *.perspectivev3
10 | !default.perspectivev3
11 | xcuserdata
12 | *.xccheckout
13 | *.moved-aside
14 | DerivedData
15 | *.hmap
16 | *.ipa
17 | *.xcuserstate
18 |
19 | # Others
20 | *.swp
21 | !.gitkeep
22 | .DS_Store
23 |
24 | # このプロジェクト独自のもの
25 | /SwiftUIAndReduxExample/Pods/*
26 | /mock_server/node_modules/*
27 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "f4651f291e85801e4e6a23041f9989e94c2021626b070930f2da77b8c504f828",
3 | "pins" : [
4 | {
5 | "identity" : "collectionviewpaginglayout",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/amirdew/CollectionViewPagingLayout.git",
8 | "state" : {
9 | "revision" : "4bdec327535470c8d9765d190625b13e99b87db5",
10 | "version" : "1.1.0"
11 | }
12 | },
13 | {
14 | "identity" : "combineexpectations",
15 | "kind" : "remoteSourceControl",
16 | "location" : "https://github.com/groue/CombineExpectations",
17 | "state" : {
18 | "branch" : "master",
19 | "revision" : "04d4e4b21c9e8361925f03f64a7ecda89ef9974f"
20 | }
21 | },
22 | {
23 | "identity" : "cosmos",
24 | "kind" : "remoteSourceControl",
25 | "location" : "https://github.com/evgenyneu/Cosmos.git",
26 | "state" : {
27 | "branch" : "master",
28 | "revision" : "cce521e734d0090494bd4afab5fd2e2f2465c1af"
29 | }
30 | },
31 | {
32 | "identity" : "cwlcatchexception",
33 | "kind" : "remoteSourceControl",
34 | "location" : "https://github.com/mattgallagher/CwlCatchException.git",
35 | "state" : {
36 | "revision" : "3b123999de19bf04905bc1dfdb76f817b0f2cc00",
37 | "version" : "2.1.2"
38 | }
39 | },
40 | {
41 | "identity" : "cwlpreconditiontesting",
42 | "kind" : "remoteSourceControl",
43 | "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git",
44 | "state" : {
45 | "revision" : "2ef56b2caf25f55fa7eef8784c30d5a767550f54",
46 | "version" : "2.2.1"
47 | }
48 | },
49 | {
50 | "identity" : "kingfisher",
51 | "kind" : "remoteSourceControl",
52 | "location" : "https://github.com/onevcat/Kingfisher.git",
53 | "state" : {
54 | "revision" : "5b92f029fab2cce44386d28588098b5be0824ef5",
55 | "version" : "7.11.0"
56 | }
57 | },
58 | {
59 | "identity" : "nimble",
60 | "kind" : "remoteSourceControl",
61 | "location" : "https://github.com/Quick/Nimble.git",
62 | "state" : {
63 | "revision" : "efe11bbca024b57115260709b5c05e01131470d0",
64 | "version" : "13.2.1"
65 | }
66 | },
67 | {
68 | "identity" : "quick",
69 | "kind" : "remoteSourceControl",
70 | "location" : "https://github.com/Quick/Quick.git",
71 | "state" : {
72 | "revision" : "6d01974d236f598633cac58280372c0c8cfea5bc",
73 | "version" : "7.4.1"
74 | }
75 | },
76 | {
77 | "identity" : "realm-core",
78 | "kind" : "remoteSourceControl",
79 | "location" : "https://github.com/realm/realm-core",
80 | "state" : {
81 | "revision" : "a5e87a39cffdcc591f3203c11cfca68100d0b9a6",
82 | "version" : "13.26.0"
83 | }
84 | },
85 | {
86 | "identity" : "realm-swift",
87 | "kind" : "remoteSourceControl",
88 | "location" : "https://github.com/realm/realm-swift",
89 | "state" : {
90 | "revision" : "e7f82d1721d99c7149a0af3d0424df9070a1a646",
91 | "version" : "10.48.1"
92 | }
93 | },
94 | {
95 | "identity" : "swift-syntax",
96 | "kind" : "remoteSourceControl",
97 | "location" : "https://github.com/apple/swift-syntax.git",
98 | "state" : {
99 | "revision" : "4c6cc0a3b9e8f14b3ae2307c5ccae4de6167ac2c",
100 | "version" : "600.0.0-prerelease-2024-06-12"
101 | }
102 | },
103 | {
104 | "identity" : "swift-testing",
105 | "kind" : "remoteSourceControl",
106 | "location" : "https://github.com/apple/swift-testing.git",
107 | "state" : {
108 | "revision" : "69d59cfc76e5daf498ca61f5af409f594768eef9",
109 | "version" : "0.10.0"
110 | }
111 | },
112 | {
113 | "identity" : "swiftyuserdefaults",
114 | "kind" : "remoteSourceControl",
115 | "location" : "https://github.com/sunshinejr/SwiftyUserDefaults",
116 | "state" : {
117 | "revision" : "f66bcd04088582c8fbb5cb8554d577e303bae396",
118 | "version" : "5.3.0"
119 | }
120 | }
121 | ],
122 | "version" : 3
123 | }
124 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample.xcodeproj/xcshareddata/xcschemes/SwiftUIAndReduxExample.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
43 |
49 |
50 |
51 |
52 |
53 |
63 |
65 |
71 |
72 |
73 |
74 |
80 |
82 |
88 |
89 |
90 |
91 |
93 |
94 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample.xcodeproj/xcshareddata/xcschemes/SwiftUIAndReduxExampleMockApi.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
60 |
62 |
68 |
69 |
70 |
71 |
73 |
74 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/App/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/01/06.
6 | //
7 |
8 | import UIKit
9 |
10 | final class AppDelegate: NSObject, UIApplicationDelegate {
11 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
12 |
13 | // MEMO: UIKit側におけるデザイン調整用の追加処理
14 | setupNavigationAppearnces()
15 | setupTabBarAppearances()
16 |
17 | return true
18 | }
19 |
20 | // MARK: - Private Function
21 |
22 | private func setupNavigationAppearnces() {
23 |
24 | // MEMO: NavigationBarのタイトル色を白色に合わせる対応
25 | var titleTextAttributes: [NSAttributedString.Key : Any] = [:]
26 | titleTextAttributes[NSAttributedString.Key.font] = UIFont(name: "HelveticaNeue-Bold", size: 15.0)!
27 | titleTextAttributes[NSAttributedString.Key.foregroundColor] = UIColor.white
28 | let newNavigationAppearance = UINavigationBarAppearance()
29 | newNavigationAppearance.configureWithTransparentBackground()
30 | newNavigationAppearance.backgroundColor = UIColor(code: "#b9d9c3")
31 | newNavigationAppearance.titleTextAttributes = titleTextAttributes
32 | UINavigationBar.appearance().standardAppearance = newNavigationAppearance
33 | UINavigationBar.appearance().scrollEdgeAppearance = newNavigationAppearance
34 | }
35 |
36 | private func setupTabBarAppearances() {
37 |
38 | // MEMO: UITabBarItemの選択時と非選択時の文字色の装飾設定
39 | let tabBarAppearance = UITabBarAppearance()
40 | let tabBarItemAppearance = UITabBarItemAppearance()
41 | tabBarItemAppearance.normal.titleTextAttributes = [
42 | NSAttributedString.Key.foregroundColor : UIColor.lightGray
43 | ]
44 | tabBarItemAppearance.selected.titleTextAttributes = [
45 | NSAttributedString.Key.foregroundColor : UIColor(code: "#b9d9c3")
46 | ]
47 | tabBarAppearance.stackedLayoutAppearance = tabBarItemAppearance
48 | UITabBar.appearance().standardAppearance = tabBarAppearance
49 | UITabBar.appearance().scrollEdgeAppearance = tabBarAppearance
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/App/SwiftUIAndReduxExampleApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUIAndReduxExampleApp.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2021/09/08.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct SwiftUIAndReduxExampleApp: App {
12 |
13 | // MEMO: AppDelegate
14 | @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
15 |
16 | // MARK: - Body
17 |
18 | var body: some Scene {
19 | // 👉 このアプリで利用するStoreを初期化する
20 | // ※ middlewaresの配列内にAPI通信/Realm/UserDefaultを操作するための関数を追加する
21 | // ※ TestCodeやPreview画面ではmiddlewaresの関数にはMockを適用する形にすればさらに良いかもしれない...
22 | #if MOCKAPI
23 | let store = Store(
24 | reducer: appReducer,
25 | state: AppState(),
26 | middlewares: [
27 | // MEMO: API処理をMockにして実行するMiddlewareを登録する(他はそのままの処理)
28 | // OnBoarding
29 | // ※ onBoardingを表示しない場合
30 | //onboardingMockHideMiddleware(),
31 | onboardingMockShowMiddleware(),
32 | onboardingMockCloseMiddleware(),
33 | // Home
34 | homeMockSuccessMiddleware(),
35 | // Archive
36 | archiveMockSuccessMiddleware(),
37 | addMockArchiveObjectMiddleware(),
38 | deleteMockArchiveObjectMiddleware(),
39 | // Favorite
40 | favoriteMockSuccessMiddleware(),
41 | // Profile
42 | profileMockSuccessMiddleware()
43 | ]
44 | )
45 | #else
46 | let store = Store(
47 | reducer: appReducer,
48 | state: AppState(),
49 | middlewares: [
50 | // MEMO: 正規の処理を実行するMiddlewareを登録する
51 | // OnBoarding
52 | onboardingMiddleware(),
53 | onboardingCloseMiddleware(),
54 | // Home
55 | homeMiddleware(),
56 | // Archive
57 | archiveMiddleware(),
58 | addArchiveObjectMiddleware(),
59 | deleteArchiveObjectMiddleware(),
60 | // Favorite
61 | favoriteMiddleware(),
62 | // Profile
63 | profileMiddleware(),
64 | ]
65 | )
66 | #endif
67 | // 👉 ContentViewには.environmentObjectを経由してstoreを適用する
68 | WindowGroup {
69 | let isUnitTest = ProcessInfo.processInfo.environment["XCTestBundlePath"] != nil
70 | if isUnitTest {
71 | Text("Executing SwiftUIAndReduxExampleTests ...")
72 | .font(.footnote)
73 | } else {
74 | ContentView()
75 | .environmentObject(store)
76 | }
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/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 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Archive/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Archive/archive_sample_image.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "archive_sample_image.jpg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Archive/archive_sample_image.imageset/archive_sample_image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Archive/archive_sample_image.imageset/archive_sample_image.jpg
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Onboarding/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Onboarding/onboarding1.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "onboarding1.jpg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Onboarding/onboarding1.imageset/onboarding1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Onboarding/onboarding1.imageset/onboarding1.jpg
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Onboarding/onboarding2.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "onboarding2.jpg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Onboarding/onboarding2.imageset/onboarding2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Onboarding/onboarding2.imageset/onboarding2.jpg
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Onboarding/onboarding3.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "onboarding3.jpg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Onboarding/onboarding3.imageset/onboarding3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Onboarding/onboarding3.imageset/onboarding3.jpg
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Profile/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Profile/profile_avatar_sample.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "profile_avatar_sample.jpg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Profile/profile_avatar_sample.imageset/profile_avatar_sample.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Profile/profile_avatar_sample.imageset/profile_avatar_sample.jpg
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Profile/profile_header_sample.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "profile_background_sample.jpg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Profile/profile_header_sample.imageset/profile_background_sample.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Assets.xcassets/Profile/profile_header_sample.imageset/profile_background_sample.jpg
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Extension/FirstAppearExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FirstAppearExtension.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/19.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | // MEMO: 最初の1度だけ処理を発火させるためのExtension
12 | // 参考: https://www.swiftjectivec.com/swiftui-run-code-only-once-versus-onappear-or-task/
13 |
14 | // MARK: - Extension
15 |
16 | public extension View {
17 |
18 | // MARK: - Function
19 |
20 | func onFirstAppear(_ onceAction: @escaping () -> Void) -> some View {
21 | // 👉 FirstAppear Modifierを設定する
22 | modifier(FirstAppear(onceAction: onceAction))
23 | }
24 | }
25 |
26 | // MARK: - ViewModifier
27 |
28 | private struct FirstAppear: ViewModifier {
29 |
30 | // MARK: - Property
31 |
32 | private let onceAction: () -> Void
33 |
34 | // 初回のみの実行かを判定するためのフラグ値
35 | @State private var hasAppeared = false
36 |
37 | // MARK: - Initializer
38 |
39 | init(onceAction: @escaping () -> Void) {
40 | self.onceAction = onceAction
41 | _hasAppeared = State(initialValue: false)
42 | }
43 |
44 | // MARK: - Body
45 |
46 | func body(content: Content) -> some View {
47 | content.onAppear {
48 | guard !hasAppeared else {
49 | return
50 | }
51 | // 👉 一度発火をしたらフラグ値を更新して以降は実行されない様にする
52 | hasAppeared = true
53 | // 👉 closureで引き渡された処理を一度だけ実行する
54 | onceAction()
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Extension/StringExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StringExtension.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/02.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | extension String {
12 |
13 | // MEMO: 設定されたUIFontの値から幅を取得する(※Tab型切り替えMenu画面等で活用する)
14 | func widthOfString(usingFont font: UIFont) -> CGFloat {
15 | let fontAttributes = [NSAttributedString.Key.font: font]
16 | let size = self.size(withAttributes: fontAttributes)
17 | return size.width
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Extension/SwiftUIColorExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUIColorExtension.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/01/06.
6 | //
7 |
8 | import SwiftUI
9 |
10 | // MEMO: 下記Stackoverflowの内容を参照
11 | // https://stackoverflow.com/questions/56874133/use-hex-color-in-swiftui
12 | extension Color {
13 | init(hex: Int, opacity: Double = 1.0) {
14 | let red = Double((hex & 0xff0000) >> 16) / 255.0
15 | let green = Double((hex & 0xff00) >> 8) / 255.0
16 | let blue = Double((hex & 0xff) >> 0) / 255.0
17 | self.init(.sRGB, red: red, green: green, blue: blue, opacity: opacity)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Extension/UIColorExtension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColorExtension.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2021/10/16.
6 | //
7 |
8 | import Foundation
9 | import UIKit
10 |
11 | // UIColorの拡張
12 | extension UIColor {
13 |
14 | // 16進数のカラーコードをiOSの設定に変換するメソッド
15 | // 参考:【Swift】Tips: あると便利だったextension達(UIColor編)
16 | // https://dev.classmethod.jp/smartphone/utilty-extension-uicolor/
17 | // iOS13での変更点: scanHexInt32がdeprecatedとなったのでscanHexInt64を使用する
18 | convenience init(code: String, alpha: CGFloat = 1.0) {
19 | var color: UInt64 = 0
20 | var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0
21 | if Scanner(string: code.replacingOccurrences(of: "#", with: "")).scanHexInt64(&color) {
22 | r = CGFloat((color & 0xFF0000) >> 16) / 255.0
23 | g = CGFloat((color & 0x00FF00) >> 8) / 255.0
24 | b = CGFloat( color & 0x0000FF ) / 255.0
25 | }
26 | self.init(red: r, green: g, blue: b, alpha: alpha)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | NSAppTransportSecurity
24 |
25 | NSAllowsArbitraryLoads
26 |
27 |
28 | UIApplicationSceneManifest
29 |
30 | UIApplicationSupportsMultipleScenes
31 |
32 |
33 | UIApplicationSupportsIndirectInputEvents
34 |
35 | UILaunchScreen
36 |
37 | UIRequiredDeviceCapabilities
38 |
39 | armv7
40 |
41 | UISupportedInterfaceOrientations
42 |
43 | UIInterfaceOrientationPortrait
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Infrastructure/RealmAccessManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RealmAccessManager.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/11/12.
6 | //
7 |
8 | import Foundation
9 | import RealmSwift
10 |
11 | // MARK: - Protocol
12 |
13 | protocol RealmAccessProtocol {
14 |
15 | // StockArchiveRealmEntityオブジェクトの一覧を取得する
16 | func getAllStockArchiveRealmEntities() -> [StockArchiveRealmEntity]
17 |
18 | // 新規にStockArchiveRealmEntityオブジェクトを追加する
19 | func saveStockArchiveRealmEntity(_ stockArchiveRealmEntity: StockArchiveRealmEntity)
20 |
21 | // 既存のStockArchiveRealmEntityオブジェクトを削除する
22 | func deleteStockArchiveRealmEntity(_ stockArchiveRealmEntity: StockArchiveRealmEntity)
23 | }
24 |
25 | final class RealmAccessManager {
26 |
27 | // MEMO: 下記の様なイメージでRealmを利用するにあたって基本的な操作となる部分を定義する
28 | // 補足: このサンプル実装では、アプリ内部にBundleさせているJsonから追加したデータをRealmObjectにMappingして検索画面のために利用する
29 |
30 | // MARK: - Singleton Instance
31 |
32 | static let shared = RealmAccessManager()
33 |
34 | // MARK: - Properies
35 |
36 | private let schemaConfig = Realm.Configuration(schemaVersion: 0)
37 |
38 | // MARK: - Function
39 |
40 | // 引数で与えられた型に該当するRealmオブジェクトを全件取得する
41 | func getAllObjects(_ realmObjectType: T.Type) -> Results? {
42 | let realm = try! Realm(configuration: schemaConfig)
43 | return realm.objects(T.self)
44 | }
45 |
46 | // 該当するRealmオブジェクトを追加する
47 | func save(_ realmObject: T) {
48 | let realm = try! Realm(configuration: schemaConfig)
49 | try! realm.write() {
50 | realm.add(realmObject)
51 | }
52 | }
53 |
54 | // 該当するRealmオブジェクトを削除する
55 | func delete(_ realmObject: T) {
56 | let realm = try! Realm(configuration: schemaConfig)
57 | try! realm.write() {
58 | realm.delete(realmObject)
59 | }
60 | }
61 | }
62 |
63 | // MARK: - RealmAccessProtocol
64 |
65 | extension RealmAccessManager: RealmAccessProtocol {
66 |
67 | // StockしたArchiveデータの一覧を取得する
68 | func getAllStockArchiveRealmEntities() -> [StockArchiveRealmEntity] {
69 | if let stockArchiveRealmEntities = getAllObjects(StockArchiveRealmEntity.self) {
70 | // MEMO: ResultsをArrayに変換をしたい場合には下記の様な形とする
71 | // 参考: https://stackoverflow.com/questions/31100011/realmswift-convert-results-to-swift-array
72 | return Array(stockArchiveRealmEntities)
73 | } else {
74 | return []
75 | }
76 | }
77 |
78 | // 該当するEntityをRealmへ追加する
79 | func saveStockArchiveRealmEntity(_ stockArchiveRealmEntity: StockArchiveRealmEntity) {
80 | save(stockArchiveRealmEntity)
81 | }
82 |
83 | // 該当するEntityをRealmから削除する
84 | func deleteStockArchiveRealmEntity(_ stockArchiveRealmEntity: StockArchiveRealmEntity) {
85 | delete(stockArchiveRealmEntity)
86 | }
87 | }
88 |
89 | // MEMO: RealmのMockとして利用するAccessManager
90 | // 👉 実際はただのSingletonInstanceでRealmのフリをするためのもの
91 |
92 | final class RealmMockAccessManager {
93 |
94 | // MARK: - Singleton Instance
95 |
96 | static let shared = RealmMockAccessManager()
97 |
98 | // MEMO: Mockで利用する仮のDBを模したDictionary
99 | var mockDataStore: [Int : StockArchiveRealmEntity] = [:]
100 | }
101 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Infrastructure/UserDefaultManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UserDefaultManager.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/11/12.
6 | //
7 |
8 | import Foundation
9 | import SwiftyUserDefaults
10 |
11 | // MEMO: ライブラリ「SwiftyUserDefaults」を利用する形
12 | // 補足: Quick/Nimbleを用いたテストコードで書きやすい点やPropertyWrapperにも標準で対応している
13 |
14 | extension DefaultsKeys {
15 |
16 | // MARK: - Property
17 |
18 | // MEMO: 下記の様なイメージでUserDefault値を定義する(こちらはは初回起動のフラグ値を持つ場合の例)
19 | // 補足1: 一時的なフラグ値や条件分岐の設定等の場合に用いる
20 | // 補足2: 設定する値についてはDictionaryやEnumも利用可能
21 | // 補足3: Xcode14以降では下記のワークアラウンドは不要
22 | // https://github.com/sunshinejr/SwiftyUserDefaults/issues/285#issuecomment-1066897689
23 |
24 | var onboardingStatus: DefaultsKey {
25 | .init("onboardingStatus", defaultValue: true)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/InternalData/Testing/campaign_banners.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "banner_contents_id": 1001,
5 | "banner_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/campaign_banner1.jpg",
6 | "title": "産地直送の「秋の味覚」特集(1)",
7 | "caption": "この季節だから食べたくて甘くて美味しいさつまいもの魅力紹介",
8 | "announcement_at": "2022-12-01T07:30:00.000+0000"
9 | },
10 | {
11 | "id": 2,
12 | "banner_contents_id": 1002,
13 | "banner_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/campaign_banner2.jpg",
14 | "title": "産地直送の「秋の味覚」特集(2)",
15 | "caption": "秋を代表するフルーツといえばコレ!甘くて素敵な柿特集",
16 | "announcement_at": "2022-12-01T07:30:00.000+0000"
17 | },
18 | {
19 | "id": 3,
20 | "banner_contents_id": 1003,
21 | "banner_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/campaign_banner3.jpg",
22 | "title": "産地直送の「秋の味覚」特集(3)",
23 | "caption": "秋は海の幸を美味しい季節。旬の魚たちを一挙ご紹介",
24 | "announcement_at": "2022-12-01T07:30:00.000+0000"
25 | },
26 | {
27 | "id": 4,
28 | "banner_contents_id": 1004,
29 | "banner_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/campaign_banner4.jpg",
30 | "title": "産地直送の「秋の味覚」特集(4)",
31 | "caption": "新鮮なお米で炊いたご飯はモノが違う。感動する新米を是非",
32 | "announcement_at": "2022-12-01T07:30:00.000+0000"
33 | },
34 | {
35 | "id": 5,
36 | "banner_contents_id": 1005,
37 | "banner_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/campaign_banner5.jpg",
38 | "title": "産地直送の「秋の味覚」特集(5)",
39 | "caption": "秋は新しいスイーツが誕生する季節。一味違ったスイーツ集",
40 | "announcement_at": "2022-12-01T07:30:00.000+0000"
41 | },
42 | {
43 | "id": 6,
44 | "banner_contents_id": 1006,
45 | "banner_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/campaign_banner6.jpg",
46 | "title": "産地直送の「秋の味覚」特集(6)",
47 | "caption": "冷え込む季節の強い味方。自宅でも嬉しいお取り寄せ鍋紹介",
48 | "announcement_at": "2022-12-01T07:30:00.000+0000"
49 | }
50 | ]
51 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/InternalData/Testing/featured_topics.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "featured_topics_id": 90001,
5 | "rating": 3.7,
6 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/featured_topic1.jpg",
7 | "title": "ボリューム満点の洋食セット",
8 | "caption": "この満足感はそう簡単には味わえないがうまい😆",
9 | "published_at": "2022-12-01T07:30:00.000+0000"
10 | },
11 | {
12 | "id": 2,
13 | "featured_topics_id": 90002,
14 | "rating": 3.4,
15 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/featured_topic2.jpg",
16 | "title": "ランチのお寿司セット",
17 | "caption": "こんなに豪華ラインナップなのにこのお値段👀",
18 | "published_at": "2022-12-01T07:30:00.000+0000"
19 | },
20 | {
21 | "id": 3,
22 | "featured_topics_id": 90003,
23 | "rating": 3.9,
24 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/featured_topic3.jpg",
25 | "title": "カキフライ&焼きはまぐり",
26 | "caption": "貝料理の王道が2つ揃って出てくる幸せ😄",
27 | "published_at": "2022-12-01T07:30:00.000+0000"
28 | },
29 | {
30 | "id": 4,
31 | "featured_topics_id": 90004,
32 | "rating": 3.7,
33 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/featured_topic4.jpg",
34 | "title": "洋食の王道ハンバーグ",
35 | "caption": "濃厚なデミグラスソースと肉汁のハーモニー👍",
36 | "published_at": "2022-12-01T07:30:00.000+0000"
37 | },
38 | {
39 | "id": 5,
40 | "featured_topics_id": 90005,
41 | "rating": 3.4,
42 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/featured_topic5.jpg",
43 | "title": "おしゃれな巻き寿司",
44 | "caption": "海苔ではなくお肉ときゅうりで巻いた変り種",
45 | "published_at": "2022-12-01T07:30:00.000+0000"
46 | },
47 | {
48 | "id": 6,
49 | "featured_topics_id": 90006,
50 | "rating": 3.9,
51 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/featured_topic6.jpg",
52 | "title": "野菜たっぷり焼きカレー",
53 | "caption": "ヘルシーな野菜達がカレーと相性抜群😊",
54 | "published_at": "2022-12-01T07:30:00.000+0000"
55 | },
56 | {
57 | "id": 7,
58 | "featured_topics_id": 90007,
59 | "rating": 3.2,
60 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/featured_topic7.jpg",
61 | "title": "うなぎの蒲焼き",
62 | "caption": "そのままでもご飯と一緒でも美味しい",
63 | "published_at": "2022-12-01T07:30:00.000+0000"
64 | },
65 | {
66 | "id": 8,
67 | "featured_topics_id": 90008,
68 | "rating": 4.3,
69 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/featured_topic8.jpg",
70 | "title": "黒毛和牛のすき焼き",
71 | "caption": "贅沢で脂の上品なお肉を使った逸品",
72 | "published_at": "2022-12-01T07:30:00.000+0000"
73 | }
74 | ]
75 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/InternalData/Testing/profile_announcement.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 5,
4 | "category": "公式情報",
5 | "title": "App運営事務局からのお知らせ",
6 | "published_at": "2023-01-15T07:30:00.000+0000",
7 | "description": "最新バージョンでは細かなアプリ内の機能改善対応と新たに加入をして下さった生産者様と店舗様の情報が閲覧できる様になりました!今後ともよろしくお願いします。"
8 | },
9 | {
10 | "id": 4,
11 | "category": "公式情報",
12 | "title": "App運営事務局より営業開始のご挨拶",
13 | "published_at": "2023-01-04T07:30:00.000+0000",
14 | "description": "本日よりアプリのお問い合わせ対応を順次行ってまいりますので、何卒よろしくお願い申し上げます。"
15 | },
16 | {
17 | "id": 3,
18 | "category": "公式情報",
19 | "title": "App運営事務局より新年のご挨拶",
20 | "published_at": "2023-01-01T07:30:00.000+0000",
21 | "description": "新年明けましておめでとうございます。本年もよろしくお願い申し上げます。(営業開始は2023.01.04からとなります。)"
22 | },
23 | {
24 | "id": 2,
25 | "category": "公式情報",
26 | "title": "年末年始の休業時間のお知らせ",
27 | "published_at": "2022-12-26T07:30:00.000+0000",
28 | "description": "いつもありがとうございます。誠に勝手ながら2022.12.29〜2023.01.03の期間につきましては休業とさせて頂きます。休業期間中はご不便をおかけしますがよろしくお願い申し上げます。"
29 | },
30 | {
31 | "id": 1,
32 | "category": "公式情報",
33 | "title": "クリスマスシーズンキャンペーンの結果報告",
34 | "published_at": "2022-12-25T07:30:00.000+0000",
35 | "description": "2022.12.01〜2022.12.25に開催されたクリスマスシーズンキャンペーンの結果を公開しております。今後の記事執筆やキャンペーン参加をご検討されているユーザー様はご一読頂けますと嬉しく思います。"
36 | }
37 | ]
38 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/InternalData/Testing/profile_comment.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 5,
4 | "emotion": "😊感謝!",
5 | "title": "ご来店頂きましてありがとうございます!",
6 | "published_at": "2023-01-07T07:30:00.000+0000",
7 | "comment": "この度はご来店頂きまして本当にありがとうございました。当店のお料理はご堪能頂けましたでしょうか?今後ともお客様に驚きと感動をご提供できる様に精進して参りますので、是非店舗の方もフォロー頂けますと嬉しく思います。"
8 | },
9 | {
10 | "id": 4,
11 | "emotion": "📝感謝!",
12 | "title": "店舗口コミ記事投稿ありがとうございます!",
13 | "published_at": "2023-01-06T07:30:00.000+0000",
14 | "comment": "この度は店舗口コミ記事を書いて下さりまして本当にありがとうございました!従業員一同も本当に喜んでおります。まだまだオープンしたてで規模はまだまだ小さいですが、今後ともよろしくお願い致します!"
15 | },
16 | {
17 | "id": 3,
18 | "emotion": "✊ご挨拶",
19 | "title": "本年もよろしくお願い致します!",
20 | "published_at": "2023-01-04T07:30:00.000+0000",
21 | "comment": "旧年中は本当にお世話になりました!皆様のご愛顧のお陰で無事にここまで来る事ができた事、本当に嬉しく思います。本年また変わらぬご愛顧でお引き立て賜ります様、宜しくお願い申し上げます。"
22 | },
23 | {
24 | "id": 2,
25 | "emotion": "✨ご挨拶",
26 | "title": "新年のご挨拶",
27 | "published_at": "2023-01-01T07:30:00.000+0000",
28 | "comment": "新年あけましておめでとうございます。本年も宜しくお願い申し上げます。通常営業は1月5日より開始しますので、お待ちしております。"
29 | },
30 | {
31 | "id": 1,
32 | "emotion": "📝お知らせ",
33 | "title": "年末年始の営業とTake Outについて",
34 | "published_at": "2022-12-25T07:30:00.000+0000",
35 | "comment": "誠に勝手ながら店舗営業につきましては、年末年始期間は2022.12.27〜2023.01.05までとなりますが、お料理のTake Outにつきましては、年末:2022.12.29まで・年始:2023.01.03から開始致しますのでお間違えのない様にお願い致します。"
36 | }
37 | ]
38 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/InternalData/Testing/profile_personal.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 100,
3 | "nickname": "謎多き料理人",
4 | "created_at": "2022-11-16T07:30:00.000+0000",
5 | "avatar_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/profile_avatar_sample.jpg",
6 | "background_image_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/profile_background_sample.jpg",
7 | "introduction": "普段は東京でイタリアンレストランのシェフをしていますが、その傍らで自宅でも美味しく食べられる本格イタリアンデザート等のプロデュース等も手掛けております。普段は仕事が忙しいのもあって外食が多くなりがちではあるので、ジャンル問わずに幅広く食べ歩くのが趣味です。ただ最近は運動不足もあってちょっと体重が増えかけているので、自分でもヘルシーな食事を心がけたり、お酒を控えめにしています。よろしくお願いします。",
8 | "histories": {
9 | "profile_view_count": 6083,
10 | "article_post_count": 37,
11 | "total_page_view_count": 103570,
12 | "total_available_points": 4000,
13 | "total_use_coupon_count": 24,
14 | "total_visit_shop_count": 58
15 | },
16 | "social_media": {
17 | "twitter_url": "https://twitter.com/",
18 | "facebook_url": "https://facebook.com/",
19 | "instagram_url": "https://instagram.com/"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/InternalData/Testing/profile_recent_favorite.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 5,
4 | "category":"新商品のご案内🍔",
5 | "title": "和牛を使ったハンバーガーを40%OFFで販売中です✨",
6 | "published_at": "2023-01-15T07:30:00.000+0000",
7 | "description": "厚さ7.5cmの食べ応え十分のハンバーガーに固まりで仕入れた和牛を丁寧に叩いて作ったハンバーグを豪快にサンドした一品です!溢れんばかりの肉汁と当店で焼き上げているバンズのハーモニーを存分にお楽しみ下さい😊"
8 | },
9 | {
10 | "id": 4,
11 | "category": "新商品のご案内🍰",
12 | "title": "ショートケーキの詰め合わせ4点セットを15%OFFで販売中です✨",
13 | "published_at": "2023-01-04T07:30:00.000+0000",
14 | "description": "丁寧に焼き上げたスポンジ部分と北海道産の生クリームを惜しみなく使ったパティシエこだわりの一品をお家でも!コーヒータイムの素敵なお供にいかがでしょうか😊"
15 | },
16 | {
17 | "id": 3,
18 | "category": "新商品のご案内🍞",
19 | "title": "焼きたて食パンを15%OFFで販売中です✨",
20 | "published_at": "2023-01-01T07:30:00.000+0000",
21 | "description": "北海道産の「はるゆたか」を使った高級食パンはそのまま食べても良し、サンドイッチにしても良しの、何にでもよく合うこと間違いなし!創業から守り続けた変わらぬ味わいをお楽しみ下さい😊"
22 | },
23 | {
24 | "id": 2,
25 | "category": "新商品のご案内🍜",
26 | "title": "自宅でも楽しめる生麺セットはじめました✨",
27 | "published_at": "2023-01-01T07:30:00.000+0000",
28 | "description": "離れた場所でもお店と変わらぬラーメンを食べたい!お陰様でそんなご要望を頂きましたので、自宅でも楽しむための生麺セットの販売をはじめましたので、店舗ホームページをご確認下さい😊"
29 | },
30 | {
31 | "id": 1,
32 | "category": "新商品のご案内🍣",
33 | "title": "にぎり寿司のランチテイクアウトはじめました✨",
34 | "published_at": "2023-01-01T07:30:00.000+0000",
35 | "description": "おまかせにぎり12貫セットをランチテイクアウトスタイルで1500円にて販売することにしました!ちょっと贅沢なお弁当としてもピッタリですので、是非とも一度お試し下さいませ😊"
36 | }
37 | ]
38 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/InternalData/Testing/recent_news.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/news_thumbnail1.jpg",
5 | "title": "美味しい玉ねぎの年末年始の対応について",
6 | "news_category": "生産者からのお知らせ",
7 | "published_at": "2022-12-01T07:30:00.000+0000"
8 | },
9 | {
10 | "id": 2,
11 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/news_thumbnail2.jpg",
12 | "title": "私共のぶどう園が作った渾身のデザートワイン販売",
13 | "news_category": "生産者からのお知らせ",
14 | "published_at": "2022-12-01T07:30:00.000+0000"
15 | },
16 | {
17 | "id": 3,
18 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/news_thumbnail3.jpg",
19 | "title": "お正月にもう一品!伊勢海老&鮑のお刺身セット",
20 | "news_category": "新商品のご紹介",
21 | "published_at": "2022-12-01T07:30:00.000+0000"
22 | },
23 | {
24 | "id": 4,
25 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/news_thumbnail4.jpg",
26 | "title": "甘さと酸っぱさのハーモニー「いよかんジュース」のご紹介",
27 | "news_category": "生産者からのお知らせ",
28 | "published_at": "2022-12-01T07:30:00.000+0000"
29 | },
30 | {
31 | "id": 5,
32 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/news_thumbnail5.jpg",
33 | "title": "美味しいさつまいもの年末年始の対応について",
34 | "news_category": "生産者からのお知らせ",
35 | "published_at": "2022-12-01T07:30:00.000+0000"
36 | },
37 | {
38 | "id": 6,
39 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/news_thumbnail6.jpg",
40 | "title": "余った梨を活用した簡単デザートレシピ紹介",
41 | "news_category": "家庭での楽しみ方Tips",
42 | "published_at": "2022-12-01T07:30:00.000+0000"
43 | },
44 | {
45 | "id": 7,
46 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/news_thumbnail7.jpg",
47 | "title": "おうち時間を彩るテイクアウトメニュー達",
48 | "news_category": "おうちでも楽しめるテイクアウト",
49 | "published_at": "2022-12-01T07:30:00.000+0000"
50 | },
51 | {
52 | "id": 8,
53 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/news_thumbnail8.jpg",
54 | "title": "家庭の強い味方!野菜がたっぷり献立セットのご紹介",
55 | "news_category": "新商品のご紹介",
56 | "published_at": "2022-12-01T07:30:00.000+0000"
57 | },
58 | {
59 | "id": 9,
60 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/news_thumbnail9.jpg",
61 | "title": "簡単な手順でも美味しいご馳走レシピのご紹介",
62 | "news_category": "家庭での楽しみ方Tips",
63 | "published_at": "2022-12-01T07:30:00.000+0000"
64 | },
65 | {
66 | "id": 10,
67 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/news_thumbnail10.jpg",
68 | "title": "偏った食生活におさらばできる具沢山のお味噌汁集",
69 | "news_category": "家庭での楽しみ方Tips",
70 | "published_at": "2022-12-01T07:30:00.000+0000"
71 | },
72 | {
73 | "id": 11,
74 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/news_thumbnail11.jpg",
75 | "title": "農家の愛が詰まった新米の年末年始の対応について",
76 | "news_category": "生産者からのお知らせ",
77 | "published_at": "2022-12-01T07:30:00.000+0000"
78 | },
79 | {
80 | "id": 12,
81 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/news_thumbnail12.jpg",
82 | "title": "美味しいみかんの年末年始の対応について",
83 | "news_category": "生産者からのお知らせ",
84 | "published_at": "2022-12-01T07:30:00.000+0000"
85 | }
86 | ]
87 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/InternalData/Testing/trend_articles.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/trend_article1.jpg",
5 | "title": "クリスマスの料理に関する思い出(1)",
6 | "introduction": "子供の頃はクリスマスを楽しみにしていた思い出を大人になった今でも覚えている方は沢山いらっしゃるかもしれません。また、家族と一緒に料理をする機会が多いご家庭の中ではこの機会が貴重な一家団欒の場となっていたことでしょう。今回はクリスマスが近いシーズンにピッタリな「心温まるクリスマスに因んだストーリー」をいくつかご紹介できればと思います🎄",
7 | "published_at": "2022-12-01T07:30:00.000+0000"
8 | },
9 | {
10 | "id": 2,
11 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/trend_article2.jpg",
12 | "title": "クリスマスの料理に関する思い出(2)",
13 | "introduction": "クリスマスには人それぞれに様々な人生の忘れられない1ページの瞬間はあるものです。プロポーズの瞬間、愛の告白、子供達へのプレゼント、クリスマスケーキを囲んでの家族の時間等、何かきっかけが生まれる瞬間をきっと頭の片隅に記憶として残っているものがきっとあるものです。そんな思い出の1ページを集めたものをここではピックアップしています🎂",
14 | "published_at": "2022-12-01T07:30:00.000+0000"
15 | },
16 | {
17 | "id": 3,
18 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/trend_article3.jpg",
19 | "title": "おせち料理の験担ぎ、ご存じですか?(1)",
20 | "introduction": "おせち料理は日本古来から親しまれ現在でもお正月にふるまわれる料理でもあります。最近では家庭でも作る機会は減っていることもあり、「なぜこの料理が入っているの?」という素朴な疑問を持つかもしれません。ところが実は1つ1つの料理の中にはしっかりとした「験担ぎ」が込められている事はご存じでしようか?今回はそんなおせち料理の世界に触れてみることにしましょう😏",
21 | "published_at": "2022-12-01T07:30:00.000+0000"
22 | },
23 | {
24 | "id": 4,
25 | "thumbnail_url": "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/trend_article4.jpg",
26 | "title": "おせち料理の験担ぎ、ご存じですか?(2)",
27 | "introduction": "こちらは前回の続きで「おせち料理の験担ぎ」に関するものの中でもかなりマニアック?と思われるものをピックアップしています。前回は皆様にとっても馴染みのあるものに関するラインナップでしたが今回はなかなか難しいですよ😁",
28 | "published_at": "2022-12-01T07:30:00.000+0000"
29 | }
30 | ]
31 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/DataTransfer/Response/Archive/ArchiveSceneResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArchiveSceneResponse.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/11.
6 | //
7 |
8 | import Foundation
9 |
10 | // MEMO: お気に入り一覧表示用のAPIレスポンス定義
11 | struct ArchiveSceneResponse: ArchiveResponse, Decodable, Equatable {
12 |
13 | let result: [ArchiveSceneEntity]
14 |
15 | // MARK: - Enum
16 |
17 | private enum Keys: String, CodingKey {
18 | case result
19 | }
20 |
21 | // MARK: - Initializer
22 |
23 | init(result: [ArchiveSceneEntity]) {
24 | self.result = result
25 | }
26 |
27 | // MARK: - Equatable
28 |
29 | // MEMO: Equatableプロトコルに適合させるための処理
30 |
31 | static func == (lhs: ArchiveSceneResponse, rhs: ArchiveSceneResponse) -> Bool {
32 | return lhs.result == rhs.result
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/DataTransfer/Response/ArchiveResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArchiveResponse.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/11.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol ArchiveResponse {}
11 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/DataTransfer/Response/Favorite/FavoriteSceneResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FavoriteSceneResponse.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/12/20.
6 | //
7 |
8 | import Foundation
9 |
10 | // MEMO: お気に入り一覧表示用のAPIレスポンス定義
11 | struct FavoriteSceneResponse: FavoriteResponse, Decodable, Equatable {
12 |
13 | let result: [FavoriteSceneEntity]
14 |
15 | // MARK: - Enum
16 |
17 | private enum Keys: String, CodingKey {
18 | case result
19 | }
20 |
21 | // MARK: - Initializer
22 |
23 | init(result: [FavoriteSceneEntity]) {
24 | self.result = result
25 | }
26 |
27 | // MARK: - Equatable
28 |
29 | // MEMO: Equatableプロトコルに適合させるための処理
30 |
31 | static func == (lhs: FavoriteSceneResponse, rhs: FavoriteSceneResponse) -> Bool {
32 | return lhs.result == rhs.result
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/DataTransfer/Response/FavoriteResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FavoriteResponse.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/12/20.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol FavoriteResponse {}
11 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/DataTransfer/Response/Home/CampaignBannersResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CampaignBannersResponse.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2021/10/17.
6 | //
7 |
8 | import Foundation
9 |
10 | // MEMO: キャンペーンバナー一覧表示用のAPIレスポンス定義
11 | struct CampaignBannersResponse: HomeResponse, Decodable, Equatable {
12 |
13 | let result: [CampaignBannerEntity]
14 |
15 | // MARK: - Enum
16 |
17 | private enum Keys: String, CodingKey {
18 | case result
19 | }
20 |
21 | // MARK: - Initializer
22 |
23 | init(result: [CampaignBannerEntity]) {
24 | self.result = result
25 | }
26 |
27 | // MARK: - Equatable
28 |
29 | // MEMO: Equatableプロトコルに適合させるための処理
30 |
31 | static func == (lhs: CampaignBannersResponse, rhs: CampaignBannersResponse) -> Bool {
32 | return lhs.result == rhs.result
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/DataTransfer/Response/Home/FeaturedTopicsResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FeaturedTopicsResponse.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/11/11.
6 | //
7 |
8 | import Foundation
9 |
10 | // MEMO: 特集コンテンツ一覧表示用のAPIレスポンス定義
11 | struct FeaturedTopicsResponse: HomeResponse, Decodable, Equatable {
12 |
13 | let result: [FeaturedTopicEntity]
14 |
15 | // MARK: - Enum
16 |
17 | private enum Keys: String, CodingKey {
18 | case result
19 | }
20 |
21 | // MARK: - Initializer
22 |
23 | init(result: [FeaturedTopicEntity]) {
24 | self.result = result
25 | }
26 |
27 | // MARK: - Equatable
28 |
29 | // MEMO: Equatableプロトコルに適合させるための処理
30 |
31 | static func == (lhs: FeaturedTopicsResponse, rhs: FeaturedTopicsResponse) -> Bool {
32 | return lhs.result == rhs.result
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/DataTransfer/Response/Home/PickupPhotoResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PickupPhotoResponse.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/11/11.
6 | //
7 |
8 | import Foundation
9 |
10 | // MEMO: ピックアップ写真一覧表示用のAPIレスポンス定義
11 | struct PickupPhotoResponse: HomeResponse, Decodable, Equatable {
12 |
13 | let result: [PickupPhotoEntity]
14 |
15 | // MARK: - Enum
16 |
17 | private enum Keys: String, CodingKey {
18 | case result
19 | }
20 |
21 | // MARK: - Initializer
22 |
23 | init(result: [PickupPhotoEntity]) {
24 | self.result = result
25 | }
26 |
27 | // MARK: - Equatable
28 |
29 | // MEMO: Equatableプロトコルに適合させるための処理
30 |
31 | static func == (lhs: PickupPhotoResponse, rhs: PickupPhotoResponse) -> Bool {
32 | return lhs.result == rhs.result
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/DataTransfer/Response/Home/RecentNewsResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecentNewsResponse.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/11/11.
6 | //
7 |
8 | import Foundation
9 |
10 | // MEMO: 最新ニュース一覧表示用のAPIレスポンス定義
11 | struct RecentNewsResponse: HomeResponse, Decodable, Equatable {
12 |
13 | let result: [RecentNewsEntity]
14 |
15 | // MARK: - Enum
16 |
17 | private enum Keys: String, CodingKey {
18 | case result
19 | }
20 |
21 | // MARK: - Initializer
22 |
23 | init(result: [RecentNewsEntity]) {
24 | self.result = result
25 | }
26 |
27 | // MARK: - Equatable
28 |
29 | // MEMO: Equatableプロトコルに適合させるための処理
30 |
31 | static func == (lhs: RecentNewsResponse, rhs: RecentNewsResponse) -> Bool {
32 | return lhs.result == rhs.result
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/DataTransfer/Response/Home/TrendArticleResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TrendArticleResponse.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/11/11.
6 | //
7 |
8 | import Foundation
9 |
10 | // MEMO: トレンド入り記事一覧表示用のAPIレスポンス定義
11 | struct TrendArticleResponse: HomeResponse, Decodable, Equatable {
12 |
13 | let result: [TrendArticleEntity]
14 |
15 | // MARK: - Enum
16 |
17 | private enum Keys: String, CodingKey {
18 | case result
19 | }
20 |
21 | // MARK: - Initializer
22 |
23 | init(result: [TrendArticleEntity]) {
24 | self.result = result
25 | }
26 |
27 | // MARK: - Equatable
28 |
29 | // MEMO: Equatableプロトコルに適合させるための処理
30 |
31 | static func == (lhs: TrendArticleResponse, rhs: TrendArticleResponse) -> Bool {
32 | return lhs.result == rhs.result
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/DataTransfer/Response/HomeResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeResponse.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/18.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol HomeResponse {}
11 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/DataTransfer/Response/Profile/ProfileAnnoucementResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileAnnoucementResponse.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/21.
6 | //
7 |
8 | import Foundation
9 |
10 | // MEMO: プロフィール画面内の運営からのお知らせ部分のAPIレスポンス定義
11 | struct ProfileAnnoucementResponse: ProfileResponse, Decodable, Equatable {
12 |
13 | let result: [ProfileAnnoucementEntity]
14 |
15 | // MARK: - Enum
16 |
17 | private enum Keys: String, CodingKey {
18 | case result
19 | }
20 |
21 | // MARK: - Initializer
22 |
23 | init(result: [ProfileAnnoucementEntity]) {
24 | self.result = result
25 | }
26 |
27 | // MARK: - Equatable
28 |
29 | // MEMO: Equatableプロトコルに適合させるための処理
30 |
31 | static func == (lhs: ProfileAnnoucementResponse, rhs: ProfileAnnoucementResponse) -> Bool {
32 | return lhs.result == rhs.result
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/DataTransfer/Response/Profile/ProfileCommentResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileCommentResponse.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/21.
6 | //
7 |
8 | import Foundation
9 |
10 | // MEMO: プロフィール画面内のコメント部分のAPIレスポンス定義
11 | struct ProfileCommentResponse: ProfileResponse, Decodable, Equatable {
12 |
13 | let result: [ProfileCommentEntity]
14 |
15 | // MARK: - Enum
16 |
17 | private enum Keys: String, CodingKey {
18 | case result
19 | }
20 |
21 | // MARK: - Initializer
22 |
23 | init(result: [ProfileCommentEntity]) {
24 | self.result = result
25 | }
26 |
27 | // MARK: - Equatable
28 |
29 | // MEMO: Equatableプロトコルに適合させるための処理
30 |
31 | static func == (lhs: ProfileCommentResponse, rhs: ProfileCommentResponse) -> Bool {
32 | return lhs.result == rhs.result
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/DataTransfer/Response/Profile/ProfilePersonalResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfilePersonalResponse.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/21.
6 | //
7 |
8 | import Foundation
9 |
10 | // MEMO: プロフィール表示情報用のAPIレスポンス定義
11 | struct ProfilePersonalResponse: ProfileResponse, Decodable, Equatable {
12 |
13 | let result: ProfilePersonalEntity
14 |
15 | // MARK: - Enum
16 |
17 | private enum Keys: String, CodingKey {
18 | case result
19 | }
20 |
21 | // MARK: - Initializer
22 |
23 | init(result: ProfilePersonalEntity) {
24 | self.result = result
25 | }
26 |
27 | // MARK: - Equatable
28 |
29 | // MEMO: Equatableプロトコルに適合させるための処理
30 |
31 | static func == (lhs: ProfilePersonalResponse, rhs: ProfilePersonalResponse) -> Bool {
32 | return lhs.result == rhs.result
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/DataTransfer/Response/Profile/ProfileRecentFavoriteResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileRecentFavoriteResponse.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/21.
6 | //
7 |
8 | import Foundation
9 |
10 | // MEMO: プロフィール画面内の最近のお気に入り部分のAPIレスポンス定義
11 | struct ProfileRecentFavoriteResponse: ProfileResponse, Decodable, Equatable {
12 |
13 | let result: [ProfileRecentFavoriteEntity]
14 |
15 | // MARK: - Enum
16 |
17 | private enum Keys: String, CodingKey {
18 | case result
19 | }
20 |
21 | // MARK: - Initializer
22 |
23 | init(result: [ProfileRecentFavoriteEntity]) {
24 | self.result = result
25 | }
26 |
27 | // MARK: - Equatable
28 |
29 | // MEMO: Equatableプロトコルに適合させるための処理
30 |
31 | static func == (lhs: ProfileRecentFavoriteResponse, rhs: ProfileRecentFavoriteResponse) -> Bool {
32 | return lhs.result == rhs.result
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/DataTransfer/Response/ProfileResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileResponse.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/21.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol ProfileResponse {}
11 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/Entity/Archive/ArchiveSceneEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArchiveSceneEntity.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/11/27.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ArchiveSceneEntity: Hashable, Decodable {
11 |
12 | let id: Int
13 | let photoUrl: String
14 | let category: String
15 | let dishName: String
16 | let shopName: String
17 | let introduction: String
18 |
19 | // MARK: - Enum
20 |
21 | private enum Keys: String, CodingKey {
22 | case id
23 | case photoUrl = "photo_url"
24 | case category
25 | case dishName = "dish_name"
26 | case shopName = "shop_name"
27 | case introduction
28 | }
29 |
30 | // MARK: - Initializer
31 |
32 | init(from decoder: Decoder) throws {
33 |
34 | // JSONの配列内の要素を取得する
35 | let container = try decoder.container(keyedBy: Keys.self)
36 |
37 | // JSONの配列内の要素にある値をDecodeして初期化する
38 | self.id = try container.decode(Int.self, forKey: .id)
39 | self.photoUrl = try container.decode(String.self, forKey: .photoUrl)
40 | self.category = try container.decode(String.self, forKey: .category)
41 | self.dishName = try container.decode(String.self, forKey: .dishName)
42 | self.shopName = try container.decode(String.self, forKey: .shopName)
43 | self.introduction = try container.decode(String.self, forKey: .introduction)
44 | }
45 |
46 | // MARK: - Hashable
47 |
48 | // MEMO: Hashableプロトコルに適合させるための処理
49 | func hash(into hasher: inout Hasher) {
50 | hasher.combine(id)
51 | }
52 |
53 | static func == (lhs: ArchiveSceneEntity, rhs: ArchiveSceneEntity) -> Bool {
54 | return lhs.id == rhs.id
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/Entity/Favorite/FavoriteSceneEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FavoriteSceneEntity.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/12/20.
6 | //
7 |
8 | import Foundation
9 |
10 | struct FavoriteSceneEntity: Hashable, Decodable {
11 |
12 | let id: Int
13 | let photoUrl: String
14 | let author: String
15 | let title: String
16 | let category: String
17 | let shopName: String
18 | let comment: String
19 | let publishedAt: String
20 |
21 | // MARK: - Enum
22 |
23 | private enum Keys: String, CodingKey {
24 | case id
25 | case photoUrl = "photo_url"
26 | case author
27 | case title
28 | case category
29 | case shopName = "shop_name"
30 | case comment
31 | case publishedAt = "published_at"
32 | }
33 |
34 | // MARK: - Initializer
35 |
36 | init(from decoder: Decoder) throws {
37 |
38 | // JSONの配列内の要素を取得する
39 | let container = try decoder.container(keyedBy: Keys.self)
40 |
41 | // JSONの配列内の要素にある値をDecodeして初期化する
42 | self.id = try container.decode(Int.self, forKey: .id)
43 | self.photoUrl = try container.decode(String.self, forKey: .photoUrl)
44 | self.author = try container.decode(String.self, forKey: .author)
45 | self.title = try container.decode(String.self, forKey: .title)
46 | self.category = try container.decode(String.self, forKey: .category)
47 | self.shopName = try container.decode(String.self, forKey: .shopName)
48 | self.comment = try container.decode(String.self, forKey: .comment)
49 | self.publishedAt = try container.decode(String.self, forKey: .publishedAt)
50 | }
51 |
52 | // MARK: - Hashable
53 |
54 | // MEMO: Hashableプロトコルに適合させるための処理
55 | func hash(into hasher: inout Hasher) {
56 | hasher.combine(id)
57 | }
58 |
59 | static func == (lhs: FavoriteSceneEntity, rhs: FavoriteSceneEntity) -> Bool {
60 | return lhs.id == rhs.id
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/Entity/Home/CampaignBannerEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CampaignBannerEntity.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2021/10/17.
6 | //
7 |
8 | import Foundation
9 |
10 | struct CampaignBannerEntity: Hashable, Decodable {
11 |
12 | let id: Int
13 | let bannerContentsId: Int
14 | let bannerUrl: String
15 | let title: String
16 | let caption: String
17 | let announcementAt: String
18 |
19 | // MARK: - Enum
20 |
21 | private enum Keys: String, CodingKey {
22 | case id
23 | case bannerContentsId = "banner_contents_id"
24 | case bannerUrl = "banner_url"
25 | case title
26 | case caption
27 | case announcementAt = "announcement_at"
28 | }
29 |
30 | // MARK: - Initializer
31 |
32 | init(from decoder: Decoder) throws {
33 |
34 | // JSONの配列内の要素を取得する
35 | let container = try decoder.container(keyedBy: Keys.self)
36 |
37 | // JSONの配列内の要素にある値をDecodeして初期化する
38 | self.id = try container.decode(Int.self, forKey: .id)
39 | self.bannerContentsId = try container.decode(Int.self, forKey: .bannerContentsId)
40 | self.bannerUrl = try container.decode(String.self, forKey: .bannerUrl)
41 | self.title = try container.decode(String.self, forKey: .title)
42 | self.caption = try container.decode(String.self, forKey: .caption)
43 | self.announcementAt = try container.decode(String.self, forKey: .announcementAt)
44 | }
45 |
46 | // MARK: - Hashable
47 |
48 | // MEMO: Hashableプロトコルに適合させるための処理
49 | func hash(into hasher: inout Hasher) {
50 | hasher.combine(id)
51 | }
52 |
53 | static func == (lhs: CampaignBannerEntity, rhs: CampaignBannerEntity) -> Bool {
54 | return lhs.id == rhs.id
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/Entity/Home/FeaturedTopicEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FeaturedTopicEntity.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/11/11.
6 | //
7 |
8 | import Foundation
9 |
10 | struct FeaturedTopicEntity: Hashable, Decodable {
11 |
12 | let id: Int
13 | let featuredTopicsId: Int
14 | let rating: Double
15 | let thumbnailUrl: String
16 | let title: String
17 | let caption: String
18 | let publishedAt: String
19 |
20 | // MARK: - Enum
21 |
22 | private enum Keys: String, CodingKey {
23 | case id
24 | case featuredTopicsId = "featured_topics_id"
25 | case rating
26 | case thumbnailUrl = "thumbnail_url"
27 | case title
28 | case caption
29 | case publishedAt = "published_at"
30 | }
31 |
32 | // MARK: - Initializer
33 |
34 | init(from decoder: Decoder) throws {
35 |
36 | // JSONの配列内の要素を取得する
37 | let container = try decoder.container(keyedBy: Keys.self)
38 |
39 | // JSONの配列内の要素にある値をDecodeして初期化する
40 | self.id = try container.decode(Int.self, forKey: .id)
41 | self.featuredTopicsId = try container.decode(Int.self, forKey: .featuredTopicsId)
42 | self.rating = try container.decode(Double.self, forKey: .rating)
43 | self.thumbnailUrl = try container.decode(String.self, forKey: .thumbnailUrl)
44 | self.title = try container.decode(String.self, forKey: .title)
45 | self.caption = try container.decode(String.self, forKey: .caption)
46 | self.publishedAt = try container.decode(String.self, forKey: .publishedAt)
47 | }
48 |
49 | // MARK: - Hashable
50 |
51 | // MEMO: Hashableプロトコルに適合させるための処理
52 | func hash(into hasher: inout Hasher) {
53 | hasher.combine(id)
54 | }
55 |
56 | static func == (lhs: FeaturedTopicEntity, rhs: FeaturedTopicEntity) -> Bool {
57 | return lhs.id == rhs.id
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/Entity/Home/PickupPhotoEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PickupPhotoEntity.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/11/11.
6 | //
7 |
8 | import Foundation
9 |
10 | struct PickupPhotoEntity: Hashable, Decodable {
11 |
12 | let id: Int
13 | let photoUrl: String
14 | let photoWidth: Int
15 | let photoHeight: Int
16 | let title: String
17 | let caption: String
18 | let publishedAt: String
19 |
20 | // MARK: - Enum
21 |
22 | private enum Keys: String, CodingKey {
23 | case id
24 | case photoUrl = "photo_url"
25 | case photoWidth = "photo_width"
26 | case photoHeight = "photo_height"
27 | case title
28 | case caption
29 | case publishedAt = "published_at"
30 | }
31 |
32 | // MARK: - Initializer
33 |
34 | init(from decoder: Decoder) throws {
35 |
36 | // JSONの配列内の要素を取得する
37 | let container = try decoder.container(keyedBy: Keys.self)
38 |
39 | // JSONの配列内の要素にある値をDecodeして初期化する
40 | self.id = try container.decode(Int.self, forKey: .id)
41 | self.photoUrl = try container.decode(String.self, forKey: .photoUrl)
42 | self.photoWidth = try container.decode(Int.self, forKey: .photoWidth)
43 | self.photoHeight = try container.decode(Int.self, forKey: .photoHeight)
44 | self.title = try container.decode(String.self, forKey: .title)
45 | self.caption = try container.decode(String.self, forKey: .caption)
46 | self.publishedAt = try container.decode(String.self, forKey: .publishedAt)
47 | }
48 |
49 | // MARK: - Hashable
50 |
51 | // MEMO: Hashableプロトコルに適合させるための処理
52 | func hash(into hasher: inout Hasher) {
53 | hasher.combine(id)
54 | }
55 |
56 | static func == (lhs: PickupPhotoEntity, rhs: PickupPhotoEntity) -> Bool {
57 | return lhs.id == rhs.id
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/Entity/Home/RecentNewsEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecentNewsEntity.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/11/11.
6 | //
7 |
8 | import Foundation
9 |
10 | struct RecentNewsEntity: Hashable, Decodable {
11 |
12 | let id: Int
13 | let thumbnailUrl: String
14 | let title: String
15 | let newsCategory: String
16 | let publishedAt: String
17 |
18 | // MARK: - Enum
19 |
20 | private enum Keys: String, CodingKey {
21 | case id
22 | case thumbnailUrl = "thumbnail_url"
23 | case title
24 | case newsCategory = "news_category"
25 | case publishedAt = "published_at"
26 | }
27 |
28 | // MARK: - Initializer
29 |
30 | init(from decoder: Decoder) throws {
31 |
32 | // JSONの配列内の要素を取得する
33 | let container = try decoder.container(keyedBy: Keys.self)
34 |
35 | // JSONの配列内の要素にある値をDecodeして初期化する
36 | self.id = try container.decode(Int.self, forKey: .id)
37 | self.thumbnailUrl = try container.decode(String.self, forKey: .thumbnailUrl)
38 | self.title = try container.decode(String.self, forKey: .title)
39 | self.newsCategory = try container.decode(String.self, forKey: .newsCategory)
40 | self.publishedAt = try container.decode(String.self, forKey: .publishedAt)
41 | }
42 |
43 | // MARK: - Hashable
44 |
45 | // MEMO: Hashableプロトコルに適合させるための処理
46 | func hash(into hasher: inout Hasher) {
47 | hasher.combine(id)
48 | }
49 |
50 | static func == (lhs: RecentNewsEntity, rhs: RecentNewsEntity) -> Bool {
51 | return lhs.id == rhs.id
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/Entity/Home/TrendArticleEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TrendArticleEntity.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/11/11.
6 | //
7 |
8 | import Foundation
9 |
10 | struct TrendArticleEntity: Hashable, Decodable {
11 |
12 | let id: Int
13 | let thumbnailUrl: String
14 | let title: String
15 | let introduction: String
16 | let publishedAt: String
17 |
18 | // MARK: - Enum
19 |
20 | private enum Keys: String, CodingKey {
21 | case id
22 | case thumbnailUrl = "thumbnail_url"
23 | case title
24 | case introduction
25 | case publishedAt = "published_at"
26 | }
27 |
28 | // MARK: - Initializer
29 |
30 | init(from decoder: Decoder) throws {
31 |
32 | // JSONの配列内の要素を取得する
33 | let container = try decoder.container(keyedBy: Keys.self)
34 |
35 | // JSONの配列内の要素にある値をDecodeして初期化する
36 | self.id = try container.decode(Int.self, forKey: .id)
37 | self.thumbnailUrl = try container.decode(String.self, forKey: .thumbnailUrl)
38 | self.title = try container.decode(String.self, forKey: .title)
39 | self.introduction = try container.decode(String.self, forKey: .introduction)
40 | self.publishedAt = try container.decode(String.self, forKey: .publishedAt)
41 | }
42 |
43 | // MARK: - Hashable
44 |
45 | // MEMO: Hashableプロトコルに適合させるための処理
46 | func hash(into hasher: inout Hasher) {
47 | hasher.combine(id)
48 | }
49 |
50 | static func == (lhs: TrendArticleEntity, rhs: TrendArticleEntity) -> Bool {
51 | return lhs.id == rhs.id
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/Entity/Profile/ProfileAnnoucementEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileAnnoucementEntity.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/21.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ProfileAnnoucementEntity: Hashable, Decodable {
11 |
12 | let id: Int
13 | let category: String
14 | let title: String
15 | let publishedAt: String
16 | let description: String
17 |
18 | // MARK: - Enum
19 |
20 | private enum Keys: String, CodingKey {
21 | case id
22 | case category
23 | case title
24 | case publishedAt = "published_at"
25 | case description
26 | }
27 |
28 | // MARK: - Initializer
29 |
30 | init(from decoder: Decoder) throws {
31 |
32 | // JSONの配列内の要素を取得する
33 | let container = try decoder.container(keyedBy: Keys.self)
34 |
35 | // JSONの配列内の要素にある値をDecodeして初期化する
36 | self.id = try container.decode(Int.self, forKey: .id)
37 | self.category = try container.decode(String.self, forKey: .category)
38 | self.title = try container.decode(String.self, forKey: .title)
39 | self.publishedAt = try container.decode(String.self, forKey: .publishedAt)
40 | self.description = try container.decode(String.self, forKey: .description)
41 | }
42 |
43 | // MARK: - Hashable
44 |
45 | // MEMO: Hashableプロトコルに適合させるための処理
46 | func hash(into hasher: inout Hasher) {
47 | hasher.combine(id)
48 | }
49 |
50 | static func == (lhs: ProfileAnnoucementEntity, rhs: ProfileAnnoucementEntity) -> Bool {
51 | return lhs.id == rhs.id
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/Entity/Profile/ProfileCommentEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileCommentEntity.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/21.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ProfileCommentEntity: Hashable, Decodable {
11 |
12 | let id: Int
13 | let emotion: String
14 | let title: String
15 | let publishedAt: String
16 | let comment: String
17 |
18 | // MARK: - Enum
19 |
20 | private enum Keys: String, CodingKey {
21 | case id
22 | case emotion
23 | case title
24 | case publishedAt = "published_at"
25 | case comment
26 | }
27 |
28 | // MARK: - Initializer
29 |
30 | init(from decoder: Decoder) throws {
31 |
32 | // JSONの配列内の要素を取得する
33 | let container = try decoder.container(keyedBy: Keys.self)
34 |
35 | // JSONの配列内の要素にある値をDecodeして初期化する
36 | self.id = try container.decode(Int.self, forKey: .id)
37 | self.emotion = try container.decode(String.self, forKey: .emotion)
38 | self.title = try container.decode(String.self, forKey: .title)
39 | self.publishedAt = try container.decode(String.self, forKey: .publishedAt)
40 | self.comment = try container.decode(String.self, forKey: .comment)
41 | }
42 |
43 | // MARK: - Hashable
44 |
45 | // MEMO: Hashableプロトコルに適合させるための処理
46 | func hash(into hasher: inout Hasher) {
47 | hasher.combine(id)
48 | }
49 |
50 | static func == (lhs: ProfileCommentEntity, rhs: ProfileCommentEntity) -> Bool {
51 | return lhs.id == rhs.id
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/Entity/Profile/ProfilePersonalEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfilePersonalEntity.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/21.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ProfilePersonalEntity: Hashable, Decodable {
11 |
12 | let id: Int
13 | let nickname: String
14 | let createdAt: String
15 | let avatarUrl: String
16 | let backgroundImageUrl: String
17 | let introduction: String
18 | let histories: ProfileHistoriesEntity
19 | let socialMedia: ProfileSocialMediaEntity
20 |
21 | // MARK: - Enum
22 |
23 | private enum Keys: String, CodingKey {
24 | case id
25 | case nickname
26 | case createdAt = "created_at"
27 | case avatarUrl = "avatar_url"
28 | case backgroundImageUrl = "background_image_url"
29 | case introduction
30 | case histories
31 | case socialMedia = "social_media"
32 | }
33 |
34 | // MARK: - Initializer
35 |
36 | init(from decoder: Decoder) throws {
37 |
38 | // JSONの配列内の要素を取得する
39 | let container = try decoder.container(keyedBy: Keys.self)
40 |
41 | // JSONの配列内の要素にある値をDecodeして初期化する
42 | self.id = try container.decode(Int.self, forKey: .id)
43 | self.nickname = try container.decode(String.self, forKey: .nickname)
44 | self.createdAt = try container.decode(String.self, forKey: .createdAt)
45 | self.avatarUrl = try container.decode(String.self, forKey: .avatarUrl)
46 | self.backgroundImageUrl = try container.decode(String.self, forKey: .backgroundImageUrl)
47 | self.introduction = try container.decode(String.self, forKey: .introduction)
48 | self.histories = try container.decode(ProfileHistoriesEntity.self, forKey: .histories)
49 | self.socialMedia = try container.decode(ProfileSocialMediaEntity.self, forKey: .socialMedia)
50 | }
51 |
52 | // MARK: - Hashable
53 |
54 | // MEMO: Hashableプロトコルに適合させるための処理
55 | func hash(into hasher: inout Hasher) {
56 | hasher.combine(id)
57 | }
58 |
59 | static func == (lhs: ProfilePersonalEntity, rhs: ProfilePersonalEntity) -> Bool {
60 | return lhs.id == rhs.id
61 | }
62 | }
63 |
64 | struct ProfileHistoriesEntity: Decodable {
65 |
66 | let profileViewCount: Int
67 | let articlePostCount: Int
68 | let totalPageViewCount: Int
69 | let totalAvailablePoints: Int
70 | let totalUseCouponCount: Int
71 | let totalVisitShopCount: Int
72 |
73 | // MARK: - Enum
74 |
75 | private enum Keys: String, CodingKey {
76 | case profileViewCount = "profile_view_count"
77 | case articlePostCount = "article_post_count"
78 | case totalPageViewCount = "total_page_view_count"
79 | case totalAvailablePoints = "total_available_points"
80 | case totalUseCouponCount = "total_use_coupon_count"
81 | case totalVisitShopCount = "total_visit_shop_count"
82 | }
83 |
84 | // MARK: - Initializer
85 |
86 | init(from decoder: Decoder) throws {
87 |
88 | // JSONの配列内の要素を取得する
89 | let container = try decoder.container(keyedBy: Keys.self)
90 |
91 | // JSONの配列内の要素にある値をDecodeして初期化する
92 | self.profileViewCount = try container.decode(Int.self, forKey: .profileViewCount)
93 | self.articlePostCount = try container.decode(Int.self, forKey: .articlePostCount)
94 | self.totalPageViewCount = try container.decode(Int.self, forKey: .totalPageViewCount)
95 | self.totalAvailablePoints = try container.decode(Int.self, forKey: .totalAvailablePoints)
96 | self.totalUseCouponCount = try container.decode(Int.self, forKey: .totalUseCouponCount)
97 | self.totalVisitShopCount = try container.decode(Int.self, forKey: .totalVisitShopCount)
98 | }
99 | }
100 |
101 | struct ProfileSocialMediaEntity: Decodable {
102 |
103 | let twitterUrl: String
104 | let facebookUrl: String
105 | let instagramUrl: String
106 |
107 | // MARK: - Enum
108 |
109 | private enum Keys: String, CodingKey {
110 | case twitterUrl = "twitter_url"
111 | case facebookUrl = "facebook_url"
112 | case instagramUrl = "instagram_url"
113 | }
114 |
115 | // MARK: - Initializer
116 |
117 | init(from decoder: Decoder) throws {
118 |
119 | // JSONの配列内の要素を取得する
120 | let container = try decoder.container(keyedBy: Keys.self)
121 |
122 | // JSONの配列内の要素にある値をDecodeして初期化する
123 | self.twitterUrl = try container.decode(String.self, forKey: .twitterUrl)
124 | self.facebookUrl = try container.decode(String.self, forKey: .facebookUrl)
125 | self.instagramUrl = try container.decode(String.self, forKey: .instagramUrl)
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/Entity/Profile/ProfileRecentFavoriteEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileRecentFavoriteEntity.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/21.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ProfileRecentFavoriteEntity: Hashable, Decodable {
11 |
12 | let id: Int
13 | let category: String
14 | let title: String
15 | let publishedAt: String
16 | let description: String
17 |
18 | // MARK: - Enum
19 |
20 | private enum Keys: String, CodingKey {
21 | case id
22 | case category
23 | case title
24 | case publishedAt = "published_at"
25 | case description
26 | }
27 |
28 | // MARK: - Initializer
29 |
30 | init(from decoder: Decoder) throws {
31 |
32 | // JSONの配列内の要素を取得する
33 | let container = try decoder.container(keyedBy: Keys.self)
34 |
35 | // JSONの配列内の要素にある値をDecodeして初期化する
36 | self.id = try container.decode(Int.self, forKey: .id)
37 | self.category = try container.decode(String.self, forKey: .category)
38 | self.title = try container.decode(String.self, forKey: .title)
39 | self.publishedAt = try container.decode(String.self, forKey: .publishedAt)
40 | self.description = try container.decode(String.self, forKey: .description)
41 | }
42 |
43 | // MARK: - Hashable
44 |
45 | // MEMO: Hashableプロトコルに適合させるための処理
46 | func hash(into hasher: inout Hasher) {
47 | hasher.combine(id)
48 | }
49 |
50 | static func == (lhs: ProfileRecentFavoriteEntity, rhs: ProfileRecentFavoriteEntity) -> Bool {
51 | return lhs.id == rhs.id
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Model/RealmObject/StockArchiveRealmEntity.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StockArchiveRealmEntity.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/16.
6 | //
7 |
8 | import Foundation
9 | import RealmSwift
10 |
11 | final class StockArchiveRealmEntity: Object {
12 |
13 | // MARK: - Property
14 |
15 | @objc dynamic var id: Int = 0
16 | @objc dynamic var photoUrl: String = ""
17 | @objc dynamic var category: String = ""
18 | @objc dynamic var dishName: String = ""
19 | @objc dynamic var shopName: String = ""
20 | @objc dynamic var introduction: String = ""
21 |
22 | // MEMO: PrimaryKey部分の設定対応
23 | override static func primaryKey() -> String? {
24 | return "id"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Repository/Archive/RequestArchiveRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RequestArchiveRepository.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/02/04.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - Protocol
11 |
12 | protocol RequestArchiveRepository {
13 | func getArchiveResponse(keyword: String, category: String) async throws -> ArchiveResponse
14 | }
15 |
16 | final class RequestArchiveRepositoryImpl: RequestArchiveRepository {
17 |
18 | // MARK: - Function
19 |
20 | func getArchiveResponse(keyword: String, category: String) async throws -> ArchiveResponse {
21 | return try await ApiClientManager.shared.getAchiveImages(keyword: keyword, category: category)
22 | }
23 | }
24 |
25 | // MARK: - MockSuccessRequestArchiveRepositoryImpl
26 |
27 | final class MockSuccessRequestArchiveRepositoryImpl: RequestArchiveRepository {
28 |
29 | // MARK: - Function
30 |
31 | func getArchiveResponse(keyword: String, category: String) async throws -> ArchiveResponse {
32 | // 👉 実際にAPIリクエストで発生する処理に近しいものをMockで再現する
33 | // 第2引数で与えられるcategoryと全く同じ値であるものだけを取り出す
34 | // 第1引数で与えられるkeywordが(dishName / shopName / introduction)いずれかに含まれるものだけを取り出す
35 | var filteredResult = getArchiveSceneResponse().result
36 | if !category.isEmpty {
37 | filteredResult = filteredResult.filter { $0.category == category }
38 | }
39 | if !keyword.isEmpty {
40 | filteredResult = filteredResult.filter { $0.dishName.contains(keyword) || $0.shopName.contains(keyword) || $0.introduction.contains(keyword) }
41 | }
42 | return ArchiveSceneResponse(result: filteredResult)
43 | }
44 |
45 | // MARK: - Private Function
46 |
47 | private func getArchiveSceneResponse() -> ArchiveSceneResponse {
48 | guard let path = Bundle.main.path(forResource: "achive_images", ofType: "json") else {
49 | fatalError()
50 | }
51 | guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
52 | fatalError()
53 | }
54 | guard let result = try? JSONDecoder().decode([ArchiveSceneEntity].self, from: data) else {
55 | fatalError()
56 | }
57 | return ArchiveSceneResponse(result: result)
58 | }
59 | }
60 |
61 | // MARK: - Factory
62 |
63 | struct RequestArchiveRepositoryFactory {
64 | static func create() -> RequestArchiveRepository {
65 | return RequestArchiveRepositoryImpl()
66 | }
67 | }
68 |
69 | struct MockSuccessRequestArchiveRepositoryFactory {
70 | static func create() -> RequestArchiveRepository {
71 | return MockSuccessRequestArchiveRepositoryImpl()
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Repository/Archive/StoredArchiveDataRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StoredArchiveDataRepository.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/02/04.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - Protocol
11 |
12 | protocol StoredArchiveDataRepository {
13 | func getAllObjectsFromRealm() -> [StockArchiveRealmEntity]
14 | func createToRealm(archiveCellViewObject: ArchiveCellViewObject)
15 | func deleteFromRealm(archiveCellViewObject: ArchiveCellViewObject)
16 | }
17 |
18 | final class StoredArchiveDataRepositoryImpl: StoredArchiveDataRepository {
19 |
20 | // MARK: - Function
21 |
22 | func getAllObjectsFromRealm() -> [StockArchiveRealmEntity] {
23 | if let stockArchiveRealmEntities = RealmAccessManager.shared.getAllObjects(StockArchiveRealmEntity.self) {
24 | return stockArchiveRealmEntities.map { $0 }
25 | } else {
26 | return []
27 | }
28 | }
29 |
30 | func createToRealm(archiveCellViewObject: ArchiveCellViewObject) {
31 | let stockArchiveRealmEntity = convertToRealmObject(archiveCellViewObject: archiveCellViewObject)
32 | RealmAccessManager.shared.saveStockArchiveRealmEntity(stockArchiveRealmEntity)
33 | }
34 |
35 | func deleteFromRealm(archiveCellViewObject: ArchiveCellViewObject) {
36 | if let stockArchiveRealmEntities = RealmAccessManager.shared.getAllObjects(StockArchiveRealmEntity.self),
37 | let stockArchiveRealmEntity = stockArchiveRealmEntities.map({ $0 }).filter({ $0.id == archiveCellViewObject.id }).first
38 | {
39 | RealmAccessManager.shared.deleteStockArchiveRealmEntity(stockArchiveRealmEntity)
40 | } else {
41 | fatalError("削除対象のデータは登録されていませんでした。")
42 | }
43 | }
44 |
45 | // MARK: - Private Function
46 |
47 | private func convertToRealmObject(archiveCellViewObject: ArchiveCellViewObject) -> StockArchiveRealmEntity {
48 | let realmObject = StockArchiveRealmEntity()
49 | realmObject.id = archiveCellViewObject.id
50 | realmObject.photoUrl = archiveCellViewObject.photoUrl?.absoluteString ?? ""
51 | realmObject.category = archiveCellViewObject.category
52 | realmObject.dishName = archiveCellViewObject.dishName
53 | realmObject.shopName = archiveCellViewObject.shopName
54 | realmObject.introduction = archiveCellViewObject.introduction
55 | return realmObject
56 | }
57 | }
58 |
59 | final class MockStoredArchiveDataRepositoryImpl: StoredArchiveDataRepository {
60 |
61 | // MARK: - Function
62 |
63 | func getAllObjectsFromRealm() -> [StockArchiveRealmEntity] {
64 | return RealmMockAccessManager.shared.mockDataStore.values.map({ $0 })
65 | }
66 |
67 | func createToRealm(archiveCellViewObject: ArchiveCellViewObject) {
68 | RealmMockAccessManager.shared.mockDataStore[archiveCellViewObject.id] = convertToRealmObject(archiveCellViewObject: archiveCellViewObject)
69 | }
70 |
71 | func deleteFromRealm(archiveCellViewObject: ArchiveCellViewObject) {
72 | RealmMockAccessManager.shared.mockDataStore.removeValue(forKey: archiveCellViewObject.id)
73 | }
74 |
75 | // MARK: - Private Function
76 |
77 | private func convertToRealmObject(archiveCellViewObject: ArchiveCellViewObject) -> StockArchiveRealmEntity {
78 | let realmObject = StockArchiveRealmEntity()
79 | realmObject.id = archiveCellViewObject.id
80 | realmObject.photoUrl = archiveCellViewObject.photoUrl?.absoluteString ?? ""
81 | realmObject.category = archiveCellViewObject.category
82 | realmObject.dishName = archiveCellViewObject.dishName
83 | realmObject.shopName = archiveCellViewObject.shopName
84 | realmObject.introduction = archiveCellViewObject.introduction
85 | return realmObject
86 | }
87 | }
88 |
89 | // MARK: - Factory
90 |
91 | struct StoredArchiveDataRepositoryFactory {
92 | static func create() -> StoredArchiveDataRepository {
93 | return StoredArchiveDataRepositoryImpl()
94 | }
95 | }
96 |
97 | struct MockStoredArchiveDataRepositoryFactory {
98 | static func create() -> StoredArchiveDataRepository {
99 | return MockStoredArchiveDataRepositoryImpl()
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Repository/Favorite/FavioriteRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FavioriteRepository.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/19.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - Protocol
11 |
12 | protocol FavioriteRepository {
13 | func getFavioriteResponse() async throws -> FavoriteResponse
14 | }
15 |
16 | final class FavioriteRepositoryImpl: FavioriteRepository {
17 |
18 | // MARK: - Function
19 |
20 | func getFavioriteResponse() async throws -> FavoriteResponse {
21 | return try await ApiClientManager.shared.getFavoriteScenes()
22 | }
23 | }
24 |
25 | // MARK: - MockSuccessFavioriteRepositoryImpl
26 |
27 | final class MockSuccessFavioriteRepositoryImpl: FavioriteRepository {
28 |
29 | // MARK: - Function
30 |
31 | func getFavioriteResponse() async throws -> FavoriteResponse {
32 | return getFavoriteSceneResponse()
33 | }
34 |
35 | // MARK: - Private Function
36 |
37 | private func getFavoriteSceneResponse() -> FavoriteSceneResponse {
38 | guard let path = Bundle.main.path(forResource: "favorite_scenes", ofType: "json") else {
39 | fatalError()
40 | }
41 | guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
42 | fatalError()
43 | }
44 | guard let result = try? JSONDecoder().decode([FavoriteSceneEntity].self, from: data) else {
45 | fatalError()
46 | }
47 | return FavoriteSceneResponse(result: result)
48 | }
49 | }
50 |
51 | // MARK: - Factory
52 |
53 | struct FavioriteRepositoryFactory {
54 | static func create() -> FavioriteRepository {
55 | return FavioriteRepositoryImpl()
56 | }
57 | }
58 |
59 | struct MockSuccessFavioriteRepositoryFactory {
60 | static func create() -> FavioriteRepository {
61 | return MockSuccessFavioriteRepositoryImpl()
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Repository/Onboarding/OnboardingRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OnboardingRepository.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/02/05.
6 | //
7 |
8 | import Foundation
9 | import SwiftyUserDefaults
10 |
11 | // MARK: - Protocol
12 |
13 | protocol OnboardingRepository {
14 | func shouldShowOnboarding() -> Bool
15 | func changeOnboardingStatusFalse()
16 | }
17 |
18 | final class OnboardingRepositoryImpl: OnboardingRepository {
19 |
20 | // MARK: - Function
21 |
22 | func shouldShowOnboarding() -> Bool {
23 | let result = Defaults[\.onboardingStatus]
24 | return result
25 | }
26 |
27 | func changeOnboardingStatusFalse() {
28 | Defaults[\.onboardingStatus] = false
29 | }
30 | }
31 |
32 | // MARK: - MockShowOnboardingRepositoryImpl
33 |
34 | final class MockShowOnboardingRepositoryImpl: OnboardingRepository {
35 |
36 | // MARK: - Function
37 |
38 | func shouldShowOnboarding() -> Bool {
39 | return true
40 | }
41 |
42 | func changeOnboardingStatusFalse() {
43 | // Do Nothing.
44 | }
45 | }
46 |
47 | // MARK: - MockHideOnboardingRepositoryImpl
48 |
49 | final class MockHideOnboardingRepositoryImpl: OnboardingRepository {
50 |
51 | // MARK: - Function
52 |
53 | func shouldShowOnboarding() -> Bool {
54 | return false
55 | }
56 |
57 | func changeOnboardingStatusFalse() {
58 | // Do Nothing.
59 | }
60 | }
61 |
62 | // MARK: - Factory
63 |
64 | struct OnboardingRepositoryFactory {
65 | static func create() -> OnboardingRepository {
66 | return OnboardingRepositoryImpl()
67 | }
68 | }
69 |
70 | struct MockShowOnboardingRepositoryFactory {
71 | static func create() -> OnboardingRepository {
72 | return MockShowOnboardingRepositoryImpl()
73 | }
74 | }
75 |
76 | struct MockHideOnboardingRepositoryFactory {
77 | static func create() -> OnboardingRepository {
78 | return MockHideOnboardingRepositoryImpl()
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Store/Actions/ArchiveActions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArchiveActions.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/02/02.
6 | //
7 |
8 | import Foundation
9 |
10 | struct RequestArchiveWithInputTextAction: Action {
11 | let inputText: String
12 | }
13 |
14 | struct RequestArchiveWithSelectedCategoryAction: Action {
15 | let selectedCategory: String
16 | }
17 |
18 | struct RequestArchiveWithNoConditionsAction: Action {}
19 |
20 | struct SuccessArchiveAction: Action {
21 | let archiveSceneEntities: [ArchiveSceneEntity]
22 | let storedIds: [Int]
23 | }
24 |
25 | struct FailureArchiveAction: Action {}
26 |
27 | // MEMO: 下記2つのActionはStateの変化をさせない(Realmへの追加or削除を実行するだけ)のAction
28 |
29 | struct AddArchiveObjectAction: Action {
30 | let archiveCellViewObject: ArchiveCellViewObject
31 | }
32 |
33 | struct DeleteArchiveObjectAction: Action {
34 | let archiveCellViewObject: ArchiveCellViewObject
35 | }
36 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Store/Actions/FavoriteActions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FavoriteActions.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/23.
6 | //
7 |
8 | import Foundation
9 |
10 | struct RequestFavoriteAction: Action {}
11 |
12 | struct SuccessFavoriteAction: Action {
13 | let favoriteSceneEntities: [FavoriteSceneEntity]
14 | }
15 |
16 | struct FailureFavoriteAction: Action {}
17 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Store/Actions/HomeActions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeActions.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/11/12.
6 | //
7 |
8 | import Foundation
9 |
10 | struct RequestHomeAction: Action {}
11 |
12 | struct SuccessHomeAction: Action {
13 | let campaignBannerEntities: [CampaignBannerEntity]
14 | let recentNewsEntities: [RecentNewsEntity]
15 | let featuredTopicEntities: [FeaturedTopicEntity]
16 | let trendArticleEntities: [TrendArticleEntity]
17 | let pickupPhotoEntities: [PickupPhotoEntity]
18 | }
19 |
20 | struct FailureHomeAction: Action {}
21 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Store/Actions/OnboardingActions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OnboardingActions.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/02/05.
6 | //
7 |
8 | import Foundation
9 |
10 | struct RequestOnboardingAction: Action {}
11 |
12 | struct ShowOnboardingAction: Action {}
13 |
14 | struct CloseOnboardingAction: Action {}
15 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Store/Actions/ProfileActions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileActions.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/24.
6 | //
7 |
8 | import Foundation
9 |
10 | struct RequestProfileAction: Action {}
11 |
12 | struct SuccessProfileAction: Action {
13 | let profilePersonalEntity: ProfilePersonalEntity
14 | let profileAnnoucementEntities: [ProfileAnnoucementEntity]
15 | var profileCommentEntities: [ProfileCommentEntity]
16 | var profileRecentFavoriteEntities: [ProfileRecentFavoriteEntity]
17 | }
18 |
19 | struct FailureProfileAction: Action {}
20 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Store/Middlewares/FavoriteMiddleware.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FavoriteMiddleware.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/23.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - Function (Production)
11 |
12 | // APIリクエスト結果に応じたActionを発行する
13 | // ※テストコードの場合は検証用のfavoriteMiddlewareのものに差し替える想定
14 | func favoriteMiddleware() -> Middleware {
15 | return { state, action, dispatch in
16 | switch action {
17 | case let action as RequestFavoriteAction:
18 | // 👉 RequestFavoriteActionを受け取ったらその後にAPIリクエスト処理を実行する
19 | requestFavoriteScenes(action: action, dispatch: dispatch)
20 | default:
21 | break
22 | }
23 | }
24 | }
25 |
26 | // MARK: - Private Function (Production)
27 |
28 | // 👉 APIリクエスト処理を実行するためのメソッド
29 | // ※テストコードの場合は想定するStubデータを返すものに差し替える想定
30 | private func requestFavoriteScenes(action: RequestFavoriteAction, dispatch: @escaping Dispatcher) {
31 | Task { @MainActor in
32 | do {
33 | let favoriteResponse = try await FavioriteRepositoryFactory.create().getFavioriteResponse()
34 | if let favoriteSceneResponse = favoriteResponse as? FavoriteSceneResponse {
35 | // お望みのレスポンスが取得できた場合は成功時のActionを発行する
36 | dispatch(SuccessFavoriteAction(favoriteSceneEntities: favoriteSceneResponse.result))
37 | } else {
38 | // お望みのレスポンスが取得できなかった場合はErrorをthrowして失敗時のActionを発行する
39 | throw APIError.error(message: "No FavoriteSceneResponse exists.")
40 | }
41 | dump(favoriteResponse)
42 | } catch APIError.error(let message) {
43 | // 通信エラーないしはお望みのレスポンスが取得できなかった場合は成功時のActionを発行する
44 | dispatch(FailureFavoriteAction())
45 | print(message)
46 | }
47 | }
48 | }
49 |
50 | // MARK: - Function (Mock for Success)
51 |
52 | // テストコードで利用するAPIリクエスト結果に応じたActionを発行する(Success時)
53 | func favoriteMockSuccessMiddleware() -> Middleware {
54 | return { state, action, dispatch in
55 | switch action {
56 | case let action as RequestFavoriteAction:
57 | // 👉 RequestFavoriteActionを受け取ったらその後にAPIリクエスト処理を実行する
58 | mockSuccessRequestFavoriteScenes(action: action, dispatch: dispatch)
59 | default:
60 | break
61 | }
62 | }
63 | }
64 |
65 | // MARK: - Function (Mock for Failure)
66 |
67 | // テストコードで利用するAPIリクエスト結果に応じたActionを発行する(Failure時)
68 | func favoriteMockFailureMiddleware() -> Middleware {
69 | return { state, action, dispatch in
70 | switch action {
71 | case let action as RequestFavoriteAction:
72 | // 👉 RequestFavoriteActionを受け取ったらその後にAPIリクエスト処理を実行する
73 | mockFailureRequestFavoriteScenes(action: action, dispatch: dispatch)
74 | default:
75 | break
76 | }
77 | }
78 | }
79 |
80 | // MARK: - Private Function (Dispatch Action Success/Failure)
81 |
82 | // 👉 成功時のAPIリクエストを想定した処理を実行するためのメソッド
83 | private func mockSuccessRequestFavoriteScenes(action: RequestFavoriteAction, dispatch: @escaping Dispatcher) {
84 | Task { @MainActor in
85 | let _ = try await Task.sleep(for: .seconds(0.64))
86 | let favoriteResponse = try await MockSuccessFavioriteRepositoryFactory.create().getFavioriteResponse()
87 | if let favoriteSceneResponse = favoriteResponse as? FavoriteSceneResponse {
88 | dispatch(SuccessFavoriteAction(favoriteSceneEntities: favoriteSceneResponse.result))
89 | } else {
90 | throw APIError.error(message: "No favoriteSceneResponse exists.")
91 | }
92 | }
93 | }
94 |
95 | // 👉 失敗時のAPIリクエストを想定した処理を実行するためのメソッド
96 | private func mockFailureRequestFavoriteScenes(action: RequestFavoriteAction, dispatch: @escaping Dispatcher) {
97 | Task { @MainActor in
98 | let _ = try await Task.sleep(for: .seconds(0.64))
99 | dispatch(FailureFavoriteAction())
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Store/Middlewares/OnboardingMiddleware.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OnboardingMiddleware.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/02/05.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - Function (Production)
11 |
12 | // オンボーディングの表示フラグ値に応じたActionを発行する
13 | // ※テストコードの場合は検証用のhomeMiddlewareのものに差し替える想定
14 | func onboardingMiddleware() -> Middleware {
15 | return { state, action, dispatch in
16 | switch action {
17 | case _ as RequestOnboardingAction:
18 | // 👉 RequestOnboardingActionを受け取ったらその後にオンボーディングの表示フラグ値に応じた処理を実行する
19 | handleOnboardingStatus(dispatch: dispatch)
20 | default:
21 | break
22 | }
23 | }
24 | }
25 |
26 | func onboardingCloseMiddleware() -> Middleware {
27 | return { state, action, dispatch in
28 | switch action {
29 | case _ as CloseOnboardingAction:
30 | // 👉 CloseOnboardingActionを受け取ったらその後にオンボーディングの表示フラグ値を更新する
31 | changeOnboardingStatus()
32 | default:
33 | break
34 | }
35 | }
36 | }
37 |
38 | // MARK: - Private Function (Production)
39 |
40 | // 👉 オンボーディングの表示フラグ値を取得し、条件に合致すれば該当するActionを発行するためのメソッド
41 | // ※テストコードの場合は想定するStubデータを返すものに差し替える想定
42 | private func handleOnboardingStatus(dispatch: @escaping Dispatcher) {
43 | let shouldShowOnboarding = OnboardingRepositoryFactory.create().shouldShowOnboarding()
44 | if shouldShowOnboarding {
45 | dispatch(ShowOnboardingAction())
46 | }
47 | }
48 |
49 | // 👉 オンボーディングの表示フラグ値を変更するためのメソッド
50 | // ※テストコードの場合は想定するStubデータを返すものに差し替える想定
51 | private func changeOnboardingStatus() {
52 | let _ = OnboardingRepositoryFactory.create().changeOnboardingStatusFalse()
53 | }
54 |
55 | // MARK: - Function (Mock for Show/Hide Onboarding)
56 |
57 | func onboardingMockShowMiddleware() -> Middleware {
58 | return { state, action, dispatch in
59 | switch action {
60 | case _ as RequestOnboardingAction:
61 | // 👉 RequestOnboardingActionを受け取ったらその後にオンボーディングの表示フラグ値に応じた処理を実行する
62 | mockShowOnboardingStatus(dispatch: dispatch)
63 | default:
64 | break
65 | }
66 | }
67 | }
68 |
69 | func onboardingMockCloseMiddleware() -> Middleware {
70 | return { state, action, dispatch in
71 | switch action {
72 | case _ as CloseOnboardingAction:
73 | // 👉 CloseOnboardingActionを受け取ったらその後にオンボーディングの表示フラグ値を更新する
74 | changeOnboardingMockStatus()
75 | default:
76 | break
77 | }
78 | }
79 | }
80 |
81 | func onboardingMockHideMiddleware() -> Middleware {
82 | return { state, action, dispatch in
83 | switch action {
84 | case _ as RequestOnboardingAction:
85 | // 👉 RequestOnboardingActionを受け取ったらその後にオンボーディングの表示フラグ値に応じた処理を実行する
86 | mockHideOnboardingStatus(dispatch: dispatch)
87 | default:
88 | break
89 | }
90 | }
91 | }
92 |
93 | // MARK: - Private Function (Mock for Show/Hide Onboarding)
94 |
95 | private func mockShowOnboardingStatus(dispatch: @escaping Dispatcher) {
96 | // この部分をMockに置き換えている(※実際はUserDefaultからの取得処理は実施しない)
97 | let shouldShowOnboarding = MockShowOnboardingRepositoryFactory.create().shouldShowOnboarding()
98 | if shouldShowOnboarding {
99 | dispatch(ShowOnboardingAction())
100 | }
101 | }
102 |
103 | private func mockHideOnboardingStatus(dispatch: @escaping Dispatcher) {
104 | // この部分をMockに置き換えている(※実際はUserDefaultからの取得処理は実施しない)
105 | let _ = MockHideOnboardingRepositoryFactory.create().shouldShowOnboarding()
106 | }
107 |
108 | private func changeOnboardingMockStatus() {
109 | // この部分をMockに置き換えている(※実際はUserDefaultへの登録は実施しない)
110 | let _ = MockShowOnboardingRepositoryFactory.create().changeOnboardingStatusFalse()
111 | }
112 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Store/Reducers/AppReducer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Reducers.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2021/10/17.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - Function
11 |
12 | // 👉 AppReducerはそれぞれの画面で利用するReducerを集約している部分
13 |
14 | func appReducer(_ state: AppState, _ action: Action) -> AppState {
15 | var state = state
16 | // MEMO: OnboardingReducerの適用
17 | state.onboardingState = onboardingReducer(state.onboardingState, action)
18 | // MEMO: HomeReducerの適用
19 | state.homeState = homeReducer(state.homeState, action)
20 | // MEMO: ArchiveReducerの適用
21 | state.archiveState = archiveReducer(state.archiveState, action)
22 | // MEMO: FavoriteReducerの適用
23 | state.favoriteState = favoriteReducer(state.favoriteState, action)
24 | // MEMO: ProfileReducerの適用
25 | state.profileState = profileReducer(state.profileState, action)
26 | return state
27 | }
28 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Store/Reducers/ArchiveReducer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArchiveReducer.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/02/02.
6 | //
7 |
8 | import Foundation
9 |
10 | func archiveReducer(_ state: ArchiveState, _ action: Action) -> ArchiveState {
11 | var state = state
12 | switch action {
13 | case let action as RequestArchiveWithInputTextAction:
14 | state.isLoading = true
15 | state.isError = false
16 | state.inputText = action.inputText
17 | case let action as RequestArchiveWithSelectedCategoryAction:
18 | state.isLoading = true
19 | state.isError = false
20 | state.selectedCategory = action.selectedCategory
21 | case _ as RequestArchiveWithNoConditionsAction:
22 | state.isLoading = true
23 | state.isError = false
24 | state.inputText = ""
25 | state.selectedCategory = ""
26 | case let action as SuccessArchiveAction:
27 | // MEMO: 画面要素表示用
28 | // 👉 currentFavoriteStateについてはRealmより取得する(MockではDictionaryを設けて代わりとする)
29 | let storedIds = action.storedIds
30 | state.archiveCellViewObjects = action.archiveSceneEntities.map {
31 | ArchiveCellViewObject(
32 | id: $0.id,
33 | photoUrl: URL(string: $0.photoUrl) ?? nil,
34 | category: $0.category,
35 | dishName: $0.dishName,
36 | shopName: $0.shopName,
37 | introduction: $0.introduction,
38 | isStored: storedIds.contains($0.id)
39 | )
40 | }
41 | state.isLoading = false
42 | state.isError = false
43 | case _ as FailureArchiveAction:
44 | state.isLoading = false
45 | state.isError = true
46 | default:
47 | break
48 | }
49 | return state
50 | }
51 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Store/Reducers/FavoriteReducer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FavoriteReducer.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/23.
6 | //
7 |
8 | import Foundation
9 |
10 | func favoriteReducer(_ state: FavoriteState, _ action: Action) -> FavoriteState {
11 | var state = state
12 | switch action {
13 | case _ as RequestFavoriteAction:
14 | state.isLoading = true
15 | state.isError = false
16 | case let action as SuccessFavoriteAction:
17 | // MEMO: 画面要素表示用
18 | state.favoritePhotosCardViewObjects = action.favoriteSceneEntities.map {
19 | FavoritePhotosCardViewObject(
20 | id: $0.id,
21 | photoUrl: URL(string: $0.photoUrl) ?? nil,
22 | author: $0.author,
23 | title: $0.title,
24 | category: $0.category,
25 | shopName: $0.shopName,
26 | comment: $0.comment,
27 | publishedAt: DateLabelFormatter.getDateStringFromAPI(apiDateString: $0.publishedAt)
28 | )
29 | }
30 | state.isLoading = false
31 | state.isError = false
32 | case _ as FailureFavoriteAction:
33 | state.isLoading = false
34 | state.isError = true
35 | default:
36 | break
37 | }
38 | return state
39 | }
40 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Store/Reducers/HomeReducer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeReducer.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/11/12.
6 | //
7 |
8 | import Foundation
9 |
10 | func homeReducer(_ state: HomeState, _ action: Action) -> HomeState {
11 | var state = state
12 | switch action {
13 | case _ as RequestHomeAction:
14 | state.isLoading = true
15 | state.isError = false
16 | case let action as SuccessHomeAction:
17 | // MEMO: 画面要素表示用
18 | state.campaignBannerCarouselViewObjects = action.campaignBannerEntities.map {
19 | CampaignBannerCarouselViewObject(
20 | id: $0.id,
21 | bannerContentsId: $0.bannerContentsId,
22 | bannerUrl: URL(string: $0.bannerUrl) ?? nil
23 | )
24 | }
25 | state.recentNewsCarouselViewObjects = action.recentNewsEntities.map {
26 | RecentNewsCarouselViewObject(
27 | id: $0.id,
28 | thumbnailUrl: URL(string: $0.thumbnailUrl) ?? nil,
29 | title: $0.title,
30 | newsCategory: $0.newsCategory,
31 | publishedAt: DateLabelFormatter.getDateStringFromAPI(apiDateString: $0.publishedAt)
32 | )
33 | }
34 | state.featuredTopicsCarouselViewObjects = action.featuredTopicEntities.map {
35 | FeaturedTopicsCarouselViewObject(
36 | id: $0.id,
37 | rating: $0.rating,
38 | thumbnailUrl: URL(string: $0.thumbnailUrl) ?? nil,
39 | title: $0.title,
40 | caption: $0.caption,
41 | publishedAt: DateLabelFormatter.getDateStringFromAPI(apiDateString: $0.publishedAt)
42 | )
43 | }
44 | state.trendArticlesGridViewObjects = action.trendArticleEntities.map {
45 | TrendArticlesGridViewObject(
46 | id: $0.id,
47 | thumbnailUrl: URL(string: $0.thumbnailUrl) ?? nil,
48 | title: $0.title,
49 | introduction:$0.introduction,
50 | publishedAt: DateLabelFormatter.getDateStringFromAPI(apiDateString: $0.publishedAt)
51 | )
52 | }
53 | state.pickupPhotosGridViewObjects = action.pickupPhotoEntities.map {
54 | PickupPhotosGridViewObject(
55 | id: $0.id,
56 | title: $0.title,
57 | caption: $0.caption,
58 | photoUrl: URL(string: $0.photoUrl) ?? nil,
59 | photoWidth: CGFloat($0.photoWidth),
60 | photoHeight: CGFloat($0.photoHeight)
61 | )
62 | }
63 | // MEMO: 画面表示ハンドリング用
64 | state.isLoading = false
65 | state.isError = false
66 | case _ as FailureHomeAction:
67 | state.isLoading = false
68 | state.isError = true
69 | default:
70 | break
71 | }
72 | return state
73 | }
74 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Store/Reducers/OnboardingReducer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OnboardingReducer.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/02/05.
6 | //
7 |
8 | import Foundation
9 |
10 | func onboardingReducer(_ state: OnboardingState, _ action: Action) -> OnboardingState {
11 | var state = state
12 | switch action {
13 | case _ as ShowOnboardingAction:
14 | state.showOnboarding = true
15 | case _ as CloseOnboardingAction:
16 | state.showOnboarding = false
17 | default:
18 | break
19 | }
20 | return state
21 | }
22 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Store/Reducers/ProfileReducer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileReducer.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/24.
6 | //
7 |
8 | import Foundation
9 |
10 | func profileReducer(_ state: ProfileState, _ action: Action) -> ProfileState {
11 | var state = state
12 | switch action {
13 | case _ as RequestProfileAction:
14 | state.isLoading = true
15 | state.isError = false
16 | case let action as SuccessProfileAction:
17 | // MEMO: 画面要素表示用
18 | let profileId = action.profilePersonalEntity.id
19 | state.backgroundImageUrl = URL(string: action.profilePersonalEntity.backgroundImageUrl) ?? nil
20 | state.profilePersonalViewObject = ProfilePersonalViewObject(
21 | id: profileId,
22 | nickname: action.profilePersonalEntity.nickname,
23 | createdAt: DateLabelFormatter.getDateStringFromAPI(apiDateString: action.profilePersonalEntity.createdAt),
24 | avatarUrl: URL(string: action.profilePersonalEntity.avatarUrl) ?? nil
25 | )
26 | state.profileSelfIntroductionViewObject = ProfileSelfIntroductionViewObject(
27 | id: profileId,
28 | introduction: action.profilePersonalEntity.introduction
29 | )
30 | state.profilePointsAndHistoryViewObject = ProfilePointsAndHistoryViewObject(
31 | id: profileId,
32 | profileViewCount: action.profilePersonalEntity.histories.profileViewCount,
33 | articlePostCount: action.profilePersonalEntity.histories.articlePostCount,
34 | totalPageViewCount: action.profilePersonalEntity.histories.totalPageViewCount,
35 | totalAvailablePoints: action.profilePersonalEntity.histories.totalAvailablePoints,
36 | totalUseCouponCount: action.profilePersonalEntity.histories.totalUseCouponCount,
37 | totalVisitShopCount: action.profilePersonalEntity.histories.totalVisitShopCount
38 | )
39 | state.profileSocialMediaViewObject = ProfileSocialMediaViewObject(
40 | id: profileId,
41 | twitterUrl: URL(string: action.profilePersonalEntity.socialMedia.twitterUrl) ?? nil,
42 | facebookUrl: URL(string: action.profilePersonalEntity.socialMedia.facebookUrl) ?? nil,
43 | instagramUrl: URL(string: action.profilePersonalEntity.socialMedia.instagramUrl) ?? nil
44 | )
45 | state.profileInformationViewObject = ProfileInformationViewObject(
46 | id: profileId,
47 | profileAnnoucementViewObjects: action.profileAnnoucementEntities.map {
48 | ProfileAnnoucementViewObject(
49 | id: $0.id,
50 | category: $0.category,
51 | title: $0.title,
52 | publishedAt: DateLabelFormatter.getDateStringFromAPI(apiDateString: $0.publishedAt),
53 | description: $0.description
54 | )
55 | },
56 | profileCommentViewObjects: action.profileCommentEntities.map {
57 | ProfileCommentViewObject(
58 | id: $0.id,
59 | emotion: $0.emotion,
60 | title: $0.title,
61 | publishedAt: DateLabelFormatter.getDateStringFromAPI(apiDateString: $0.publishedAt),
62 | comment: $0.comment
63 | )
64 | },
65 | profileRecentFavoriteViewObjects: action.profileRecentFavoriteEntities.map {
66 | ProfileRecentFavoriteViewObject(
67 | id: $0.id,
68 | category: $0.category,
69 | title: $0.title,
70 | publishedAt: DateLabelFormatter.getDateStringFromAPI(apiDateString: $0.publishedAt),
71 | description: $0.description
72 | )
73 | }
74 | )
75 | // MEMO: 画面表示ハンドリング用
76 | state.isLoading = false
77 | state.isError = false
78 | case _ as FailureProfileAction:
79 | state.isLoading = false
80 | state.isError = true
81 | default:
82 | break
83 | }
84 | return state
85 | }
86 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Store/States/AppState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppState.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2021/10/17.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - AppState
11 |
12 | // 👉 アプリ全体のState定義(画面ないしは機能ごとのState定義を集約する部分)
13 | struct AppState: ReduxState {
14 | // MEMO: Onboarding表示で利用するState
15 | var onboardingState: OnboardingState = OnboardingState()
16 | // MEMO: Home画面表示で利用するState
17 | var homeState: HomeState = HomeState()
18 | // MEMO: Archive画面表示で利用するState
19 | var archiveState: ArchiveState = ArchiveState()
20 | // MEMO: Favorite画面表示で利用するState
21 | var favoriteState: FavoriteState = FavoriteState()
22 | // MEMO: Profile画面表示で利用するState
23 | var profileState: ProfileState = ProfileState()
24 | }
25 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Store/States/ArchiveState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArchiveState.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/02/02.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ArchiveState: ReduxState, Equatable {
11 |
12 | // MARK: - Property
13 |
14 | // MEMO: 読み込み中状態
15 | var isLoading: Bool = false
16 | // MEMO: エラー状態
17 | var isError: Bool = false
18 |
19 | // MEMO: 検索用に必要なパラメーター値
20 | var inputText: String = ""
21 | var selectedCategory: String = ""
22 |
23 | // MEMO: Archive画面で利用する情報として必要なViewObject情報
24 | // ※ このコードではViewObjectとView表示要素のComponentが1:1対応となる想定で作っています。
25 | var archiveCellViewObjects: [ArchiveCellViewObject] = []
26 |
27 | // MARK: - Equatable
28 |
29 | static func == (lhs: ArchiveState, rhs: ArchiveState) -> Bool {
30 | return lhs.isLoading == rhs.isLoading
31 | && lhs.isError == rhs.isError
32 | && lhs.inputText == rhs.inputText
33 | && lhs.selectedCategory == rhs.selectedCategory
34 | && lhs.archiveCellViewObjects == rhs.archiveCellViewObjects
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Store/States/FavoriteState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FavoriteState.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/23.
6 | //
7 |
8 | import Foundation
9 |
10 | struct FavoriteState: ReduxState, Equatable {
11 |
12 | // MARK: - Property
13 |
14 | // MEMO: 読み込み中状態
15 | var isLoading: Bool = false
16 | // MEMO: エラー状態
17 | var isError: Bool = false
18 |
19 | // MEMO: Favorite画面で利用する情報として必要なViewObject情報
20 | var favoritePhotosCardViewObjects: [FavoritePhotosCardViewObject] = []
21 |
22 | // MARK: - Equatable
23 |
24 | static func == (lhs: FavoriteState, rhs: FavoriteState) -> Bool {
25 | return lhs.isLoading == rhs.isLoading
26 | && lhs.isError == rhs.isError
27 | && lhs.favoritePhotosCardViewObjects == rhs.favoritePhotosCardViewObjects
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Store/States/HomeState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeState.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/11/12.
6 | //
7 |
8 | import Foundation
9 |
10 | struct HomeState: ReduxState, Equatable {
11 |
12 | // MARK: - Property
13 |
14 | // MEMO: 読み込み中状態
15 | var isLoading: Bool = false
16 | // MEMO: エラー状態
17 | var isError: Bool = false
18 |
19 | // MEMO: Home画面で利用する情報として必要なViewObject情報
20 | // ※ このコードではViewObjectとView表示要素のComponentが1:1対応となる想定で作っています。
21 | var campaignBannerCarouselViewObjects: [CampaignBannerCarouselViewObject] = []
22 | var recentNewsCarouselViewObjects: [RecentNewsCarouselViewObject] = []
23 | var featuredTopicsCarouselViewObjects: [FeaturedTopicsCarouselViewObject] = []
24 | var trendArticlesGridViewObjects: [TrendArticlesGridViewObject] = []
25 | var pickupPhotosGridViewObjects: [PickupPhotosGridViewObject] = []
26 |
27 | // MARK: - Equatable
28 |
29 | static func == (lhs: HomeState, rhs: HomeState) -> Bool {
30 | return lhs.isLoading == rhs.isLoading
31 | && lhs.isError == rhs.isError
32 | && lhs.campaignBannerCarouselViewObjects == rhs.campaignBannerCarouselViewObjects
33 | && lhs.recentNewsCarouselViewObjects == rhs.recentNewsCarouselViewObjects
34 | && lhs.featuredTopicsCarouselViewObjects == rhs.featuredTopicsCarouselViewObjects
35 | && lhs.trendArticlesGridViewObjects == rhs.trendArticlesGridViewObjects
36 | && lhs.pickupPhotosGridViewObjects == rhs.pickupPhotosGridViewObjects
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Store/States/OnboardingState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OnboardingState.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/02/05.
6 | //
7 |
8 | import Foundation
9 |
10 | struct OnboardingState: ReduxState, Equatable {
11 |
12 | // MARK: - Property
13 |
14 | // MEMO: オンボーディング表示フラグ
15 | var showOnboarding: Bool = false
16 |
17 | // MARK: - Equatable
18 |
19 | static func == (lhs: OnboardingState, rhs: OnboardingState) -> Bool {
20 | return lhs.showOnboarding == rhs.showOnboarding
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Store/States/ProfileState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileState.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/23.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ProfileState: ReduxState, Equatable {
11 |
12 | // MARK: - Property
13 |
14 | // MEMO: 読み込み中状態
15 | var isLoading: Bool = false
16 | // MEMO: エラー状態
17 | var isError: Bool = false
18 |
19 | // MEMO: Profile画面で利用する情報として必要なViewObject等の情報
20 | var backgroundImageUrl: URL?
21 | var profilePersonalViewObject: ProfilePersonalViewObject?
22 | var profileSelfIntroductionViewObject: ProfileSelfIntroductionViewObject?
23 | var profilePointsAndHistoryViewObject: ProfilePointsAndHistoryViewObject?
24 | var profileSocialMediaViewObject: ProfileSocialMediaViewObject?
25 | var profileInformationViewObject: ProfileInformationViewObject?
26 |
27 | // MARK: - Equatable
28 |
29 | static func == (lhs: ProfileState, rhs: ProfileState) -> Bool {
30 | return lhs.isLoading == rhs.isLoading
31 | && lhs.isError == rhs.isError
32 | && lhs.backgroundImageUrl == rhs.backgroundImageUrl
33 | && lhs.profilePersonalViewObject == rhs.profilePersonalViewObject
34 | && lhs.profileSelfIntroductionViewObject == rhs.profileSelfIntroductionViewObject
35 | && lhs.profileSocialMediaViewObject == rhs.profileSocialMediaViewObject
36 | && lhs.profilePointsAndHistoryViewObject == rhs.profilePointsAndHistoryViewObject
37 | && lhs.profileInformationViewObject == rhs.profileInformationViewObject
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/Store/Store.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Store.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2021/10/16.
6 | //
7 |
8 | import Foundation
9 |
10 | // MEMO: Store部分はasync/awaitで書くなら、MainActorで良いんじゃないかという仮説
11 | // https://developer.apple.com/forums/thread/690957
12 |
13 | // FYI: 他にも全体的にCombineを利用した書き方も可能 (※他にも事例は探してみると面白そう)
14 | // https://wojciechkulik.pl/ios/redux-architecture-and-mind-blowing-features
15 | // https://kazaimazai.com/redux-in-ios/
16 | // https://www.raywenderlich.com/22096649-getting-a-redux-vibe-into-swiftui
17 |
18 | // MARK: - Typealias
19 |
20 | // 👉 Dispatcher・Reducer・Middlewareのtypealiasを定義する
21 | // ※おそらくエッセンスとしてはReact等の感じに近くなるイメージとなる
22 | typealias Dispatcher = (Action) -> Void
23 | typealias Reducer = (_ state: State, _ action: Action) -> State
24 | typealias Middleware = (StoreState, Action, @escaping Dispatcher) -> Void
25 |
26 | // MARK: - Protocol
27 |
28 | protocol ReduxState {}
29 |
30 | protocol Action {}
31 |
32 | // MARK: - Store
33 |
34 | final class Store: ObservableObject {
35 |
36 | // MARK: - Property
37 |
38 | @Published private(set) var state: StoreState
39 | private var reducer: Reducer
40 | private var middlewares: [Middleware]
41 |
42 | // MARK: - Initialzer
43 |
44 | init(
45 | reducer: @escaping Reducer,
46 | state: StoreState,
47 | middlewares: [Middleware] = []
48 | ) {
49 | self.reducer = reducer
50 | self.state = state
51 | self.middlewares = middlewares
52 | }
53 |
54 | // MARK: - Function
55 |
56 | func dispatch(action: Action) {
57 |
58 | // MEMO: Actionを発行するDispatcherの定義
59 | // 👉 新しいstateに差し替える処理については、メインスレッドで操作したいのでMainActor内で実行する
60 | Task { @MainActor in
61 | self.state = reducer(
62 | self.state,
63 | action
64 | )
65 | }
66 |
67 | // MEMO: 利用する全てのMiddlewareを適用
68 | // 補足: MiddlewareにAPI通信処理等を全て寄せずに実装したい場合には別途ActionCreatorの様なStructを用意する方法もある
69 | // https://qiita.com/fumiyasac@github/items/f25465a955afdcb795a2
70 | middlewares.forEach { middleware in
71 | middleware(state, action, dispatch)
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/UsefulFunction/DateLabelFormatter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DateLabelFormatter.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/12/09.
6 | //
7 |
8 | import Foundation
9 |
10 | final class DateLabelFormatter {
11 |
12 | // MARK: - Properties
13 |
14 | private static var convertDateFormatter: DateFormatter = {
15 | let dateFormatter = DateFormatter()
16 | dateFormatter.locale = Locale(identifier: "en_US_POSIX")
17 | dateFormatter.timeZone = TimeZone.current
18 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
19 | return dateFormatter
20 | }()
21 |
22 | private static var stringDateFormatter: DateFormatter = {
23 | let dateFormatter = DateFormatter()
24 | dateFormatter.locale = Locale(identifier: "en_US_POSIX")
25 | dateFormatter.timeZone = TimeZone.current
26 | return dateFormatter
27 | }()
28 |
29 | // MARK: - Static Functions
30 |
31 | // APIで取得された日付フォーマットを任意の表記に変換する
32 | static func getDateStringFromAPI(apiDateString: String, printFormatter: String = "yyyy.MM.dd") -> String {
33 | let apiDate = convertDateFormatter.date(from: apiDateString)!
34 | stringDateFormatter.dateFormat = printFormatter
35 | return stringDateFormatter.string(from: apiDate)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/View/Components/Archive/ArchiveContentsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArchiveContentsView.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/12/29.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ArchiveContentsView: View {
11 |
12 | // MARK: - Typealias
13 |
14 | typealias TapIsStoredButtonAction = (ArchiveCellViewObject, Bool) -> Void
15 |
16 | // MARK: - Property
17 |
18 | // 画面表示内容を格納するための変数
19 | private let archiveCellViewObjects: [ArchiveCellViewObject]
20 |
21 | // 親のViewから受け取った検索キーワードを格納するための変数
22 | private let targetKeyword: String
23 |
24 | // 親のViewから受け取ったカテゴリー名を格納するための変数
25 | private let targetCategory: String
26 |
27 | // Storeボタン(ハート型ボタン要素)タップ時にArchiveCellViewに引き渡すClosure変数
28 | private let tapIsStoredButtonAction: ArchiveContentsView.TapIsStoredButtonAction
29 |
30 | // MARK: - Initializer
31 |
32 | init(
33 | archiveCellViewObjects: [ArchiveCellViewObject],
34 | targetKeyword: String = "",
35 | targetCategory: String = "",
36 | tapIsStoredButtonAction: @escaping ArchiveContentsView.TapIsStoredButtonAction
37 | ) {
38 | // 画面表示内容を格納する配列の初期化
39 | self.archiveCellViewObjects = archiveCellViewObjects
40 | // ArchiveCellViewにカテゴリー及び検索キーワードをハイライトする文字列の初期化
41 | self.targetKeyword = targetKeyword
42 | self.targetCategory = targetCategory
43 | // Storeボタン(ハート型ボタン要素)タップ時のClosureの初期化
44 | self.tapIsStoredButtonAction = tapIsStoredButtonAction
45 | }
46 |
47 | // MARK: - Body
48 |
49 | var body: some View {
50 | ScrollView {
51 | ForEach(archiveCellViewObjects) { viewObject in
52 | ArchiveCellView(
53 | viewObject: viewObject,
54 | targetKeyword: targetKeyword,
55 | targetCategory: targetCategory,
56 | tapIsStoredButtonAction: { isStored in
57 | // 👉 Favoriteボタン(ハート型ボタン要素)タップ時に実行されるClosure
58 | tapIsStoredButtonAction(viewObject, isStored)
59 | }
60 | )
61 | }
62 | }
63 | }
64 | }
65 |
66 | // MARK: - Preview
67 |
68 | #Preview("ArchiveContentsView Preview") {
69 | // MEMO: Preview表示用にレスポンスを想定したJsonを読み込んで画面に表示させる
70 | let achiveSceneResponse = getArchiveSceneResponse()
71 | let archiveCellViewObjects = achiveSceneResponse.result
72 | .map {
73 | ArchiveCellViewObject(
74 | id: $0.id,
75 | photoUrl: URL(string: $0.photoUrl) ?? nil,
76 | category: $0.category,
77 | dishName: $0.dishName,
78 | shopName: $0.shopName,
79 | introduction: $0.introduction
80 | )
81 | }
82 |
83 | // Preview: ArchiveContentsView
84 | return ArchiveContentsView(
85 | archiveCellViewObjects: archiveCellViewObjects,
86 | targetKeyword: "",
87 | targetCategory: "",
88 | tapIsStoredButtonAction: { _,_ in }
89 | )
90 |
91 | // MARK: - Function
92 |
93 | func getArchiveSceneResponse() -> ArchiveSceneResponse {
94 | guard let path = Bundle.main.path(forResource: "achive_images", ofType: "json") else {
95 | fatalError()
96 | }
97 | guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
98 | fatalError()
99 | }
100 | guard let result = try? JSONDecoder().decode([ArchiveSceneEntity].self, from: data) else {
101 | fatalError()
102 | }
103 | return ArchiveSceneResponse(result: result)
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/View/Components/Archive/Section/ArchiveCurrentCountView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArchiveCurrentCountView.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/09.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ArchiveCurrentCountView: View {
11 |
12 | // MARK: - Typealias
13 |
14 | typealias TapAllClearAction = () -> Void
15 |
16 | // MARK: - Property
17 |
18 | private var currentCountTitleFont: Font {
19 | return Font.custom("AvenirNext-Regular", size: 12)
20 | }
21 |
22 | private var currentCountTitleColor: Color {
23 | return Color.primary
24 | }
25 |
26 | private var allClearButtonTitleFont: Font {
27 | return Font.custom("AvenirNext-Regular", size: 12)
28 | }
29 |
30 | private var allClearButtonTitleColor: Color {
31 | return Color.primary
32 | }
33 |
34 | private let currentCount: Int
35 | private let tapAllClearAction: ArchiveCurrentCountView.TapAllClearAction
36 |
37 | // MARK: - Initializer
38 |
39 | init(
40 | currentCount: Int,
41 | tapAllClearAction: @escaping ArchiveCurrentCountView.TapAllClearAction
42 | ) {
43 | self.currentCount = currentCount
44 | self.tapAllClearAction = tapAllClearAction
45 | }
46 |
47 | // MARK: - Body
48 |
49 | var body: some View {
50 | VStack(spacing: 0.0) {
51 | HStack {
52 | Text("現在表示しているデータ: 全\(currentCount)件")
53 | .font(currentCountTitleFont)
54 | .foregroundColor(currentCountTitleColor)
55 | .padding([.top], 2.0)
56 | .padding([.bottom], 6.0)
57 | Spacer()
58 | Button(action: tapAllClearAction, label: {
59 | Text("▶︎条件をクリア")
60 | .font(allClearButtonTitleFont)
61 | .foregroundColor(allClearButtonTitleColor)
62 | .underline()
63 | })
64 | }
65 | .padding([.leading, .trailing], 12.0)
66 | }
67 | }
68 | }
69 |
70 | // MARK: - Preview
71 |
72 | #Preview {
73 | ArchiveCurrentCountView(currentCount: 36, tapAllClearAction: {})
74 | }
75 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/View/Components/Archive/Section/ArchiveEmptyView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArchiveEmptyView.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/02/02.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ArchiveEmptyView: View {
11 |
12 | // MARK: - Property
13 |
14 | private var archiveEmptyTitleFont: Font {
15 | return Font.custom("AvenirNext-Bold", size: 18)
16 | }
17 |
18 | private var archiveEmptyTitleColor: Color {
19 | return Color.primary
20 | }
21 |
22 | private var archiveEmptyDescriptionFont: Font {
23 | return Font.custom("AvenirNext-Regular", size: 12)
24 | }
25 |
26 | private var archiveEmptyDescriptionColor: Color {
27 | return Color.secondary
28 | }
29 |
30 | // MARK: - body
31 |
32 | var body: some View {
33 | VStack(spacing: 0.0) {
34 | // 1. Spacer
35 | Spacer()
36 | // 2. コンテンツ表示部分
37 | VStack {
38 | // (1) エラータイトル表示
39 | Text("エラー: 該当データがありません")
40 | .font(archiveEmptyTitleFont)
41 | .foregroundColor(archiveEmptyTitleColor)
42 | .padding([.bottom], 16.0)
43 | // (2) エラー文言表示
44 | HStack {
45 | Text("指定したカテゴリーや検索キーワードに合致するデータがありませんでした。カテゴリーの選択肢を変更したり、検索キーワードを変えて再度お試し下さい。")
46 | .font(archiveEmptyDescriptionFont)
47 | .foregroundColor(archiveEmptyDescriptionColor)
48 | .multilineTextAlignment(.leading)
49 | }
50 | }
51 | // 3. Spacer
52 | Spacer()
53 | }
54 | .padding([.leading, .trailing], 12.0)
55 | }
56 | }
57 |
58 | // MARK: - Preview
59 |
60 | #Preview("ArchiveEmptyView Preview") {
61 | ArchiveEmptyView()
62 | }
63 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/View/Components/Common/ConnectionErrorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ConnectionErrorView.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/24.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ConnectionErrorView: View {
11 |
12 | // MARK: - Typealias
13 |
14 | typealias TapButtonAction = () -> Void
15 |
16 | // MARK: - Property
17 |
18 | private var connectionErrorTitleFont: Font {
19 | return Font.custom("AvenirNext-Bold", size: 18)
20 | }
21 |
22 | private var connectionErrorTitleColor: Color {
23 | return Color.primary
24 | }
25 |
26 | private var connectionErrorDescriptionFont: Font {
27 | return Font.custom("AvenirNext-Regular", size: 12)
28 | }
29 |
30 | private var connectionErrorDescriptionColor: Color {
31 | return Color.secondary
32 | }
33 |
34 | private var connectionErrorButtonFont: Font {
35 | return Font.custom("AvenirNext-Bold", size: 16)
36 | }
37 |
38 | private var connectionErrorButtonColor: Color {
39 | return Color(uiColor: UIColor(code: "#b9d9c3"))
40 | }
41 |
42 | private var tapButtonAction: ProfileSpecialContentsView.TapButtonAction
43 |
44 | // MARK: - Initializer
45 |
46 | init(tapButtonAction: @escaping ConnectionErrorView.TapButtonAction) {
47 | self.tapButtonAction = tapButtonAction
48 | }
49 |
50 | // MARK: - Body
51 |
52 | var body: some View {
53 | VStack(spacing: 0.0) {
54 | // 1. Spacer
55 | Spacer()
56 | // 2. コンテンツ表示部分
57 | VStack {
58 | // (1) エラータイトル表示
59 | Text("エラー: 画面表示に失敗しました")
60 | .font(connectionErrorTitleFont)
61 | .foregroundColor(connectionErrorTitleColor)
62 | .padding([.bottom], 16.0)
63 | // (2) エラー文言表示
64 | HStack {
65 | Text("ネットワークの接続エラー等の要因でデータを取得することができなかった可能性があります。通信の良い環境等で再度のリクエスト実行をお試し下さい。またそれでも解決しない場合には、運営へのお問い合わせをお手数ですが宜しくお願い致します。")
66 | .font(connectionErrorDescriptionFont)
67 | .foregroundColor(connectionErrorDescriptionColor)
68 | .multilineTextAlignment(.leading)
69 | }
70 | // (3) リクエスト再実行ボタン表示
71 | HStack {
72 | Spacer()
73 | Button(action: tapButtonAction, label: {
74 | // MEMO: 縁取りをした角丸ボタンのための装飾
75 | Text("再度リクエストを実行する")
76 | .font(connectionErrorButtonFont)
77 | .foregroundColor(connectionErrorButtonColor)
78 | .background(.white)
79 | .frame(width: 240.0, height: 48.0)
80 | .cornerRadius(24.0)
81 | .overlay(
82 | RoundedRectangle(cornerRadius: 24.0)
83 | .stroke(connectionErrorButtonColor, lineWidth: 1.0)
84 | )
85 | })
86 | Spacer()
87 | }
88 | .padding([.top, .bottom], 24.0)
89 | }
90 | // 3. Spacer
91 | Spacer()
92 | }
93 | .padding([.leading, .trailing], 12.0)
94 | }
95 | }
96 |
97 | // MARK: - Preview
98 |
99 | #Preview("ConnectionErrorView Preview") {
100 | ConnectionErrorView(tapButtonAction: {})
101 | }
102 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/View/Components/Common/ExecutingConnectionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ExecutingConnectionView.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/25.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ExecutingConnectionView: View {
11 |
12 | // MARK: - Property
13 |
14 | private var executingConnectionTitleFont: Font {
15 | return Font.custom("AvenirNext-Regular", size: 12)
16 | }
17 |
18 | private var executingConnectionTitleColor: Color {
19 | return Color.secondary
20 | }
21 |
22 | private var executingConnectionBoxColor: Color {
23 | return Color.gray
24 | }
25 |
26 | // MARK: - Body
27 |
28 | var body: some View {
29 | VStack(spacing: 0.0) {
30 | // 1. Spacer
31 | Spacer()
32 | // 2. コンテンツ表示部分
33 | VStack {
34 | // (1) 読み込み中文言表示
35 | Text("読み込み中です...")
36 | .font(executingConnectionTitleFont)
37 | .foregroundColor(executingConnectionTitleColor)
38 | .padding([.bottom], 4.0)
39 | // (2) Indicator表示
40 | LoadingIndicatorViewRepresentable(isLoading: .constant(true))
41 | }
42 | .frame(width: 122.0, height: 88.0)
43 | // MEMO: VStack自体に囲み線をつける対応
44 | // https://ios-docs.dev/stack-border/
45 | .overlay(
46 | RoundedRectangle(cornerRadius: 8.0)
47 | .stroke(executingConnectionBoxColor, lineWidth: 1.0)
48 | )
49 | .clipShape(RoundedRectangle(cornerRadius: 8.0))
50 | // 3. Spacer
51 | Spacer()
52 | }
53 | }
54 | }
55 |
56 | // MARK: - Preview
57 |
58 | #Preview("ExecutingConnectionView Preview") {
59 | ExecutingConnectionView()
60 | }
61 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/View/Components/Favorite/FavoriteCommonSectionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FavoriteCommonSectionView.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/12/14.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct FavoriteCommonSectionView: View {
11 |
12 | // MARK: - Property
13 |
14 | private let screen = UIScreen.main.bounds
15 |
16 | private var headerWidth: CGFloat {
17 | return screen.width
18 | }
19 |
20 | private var headerHeight: CGFloat {
21 | return 86.0
22 | }
23 |
24 | private var sectionTitleFont: Font {
25 | return Font.custom("AvenirNext-Bold", size: 18)
26 | }
27 |
28 | private var sectionSubtitleFont: Font {
29 | return Font.custom("AvenirNext-Regular", size: 12)
30 | }
31 |
32 | private var sectionTitleColor: Color {
33 | return Color.primary
34 | }
35 |
36 | private var sectionSubtitleColor: Color {
37 | return Color.secondary
38 | }
39 |
40 | // MARK: - Initializer
41 |
42 | init() {}
43 |
44 | // MARK: - Body
45 |
46 | var body: some View {
47 | HStack {
48 | VStack(alignment: .leading) {
49 | Text("編集部が選ぶお気に入りのグルメ")
50 | .font(sectionTitleFont)
51 | .foregroundColor(sectionTitleColor)
52 | .lineLimit(1)
53 | Text("Favorite Gourmet Selected by The Editorial Department.")
54 | .font(sectionSubtitleFont)
55 | .foregroundColor(sectionSubtitleColor)
56 | .lineLimit(1)
57 | Text("👉スワイプすると続きを見る事ができます。")
58 | .font(sectionSubtitleFont)
59 | .foregroundColor(sectionSubtitleColor)
60 | .lineLimit(1)
61 | .padding([.top], -8.0)
62 | }
63 | Spacer()
64 | }
65 | .padding(12.0)
66 | .frame(width: headerWidth)
67 | .frame(height: headerHeight)
68 | }
69 | }
70 |
71 | // MARK: - Preview
72 |
73 | #Preview("FavoriteCommonSectionView Preview") {
74 | FavoriteCommonSectionView()
75 | }
76 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/View/Components/Favorite/FavoriteContentsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FavoriteContentsView.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/29.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct FavoriteContentsView: View {
11 |
12 | // MARK: - Property
13 |
14 | private let favoritePhotosCardViewObjects: [FavoritePhotosCardViewObject]
15 |
16 | // MARK: - Initializer
17 |
18 | init(favoritePhotosCardViewObjects: [FavoritePhotosCardViewObject]) {
19 | self.favoritePhotosCardViewObjects = favoritePhotosCardViewObjects
20 | }
21 |
22 | // MARK: - Body
23 |
24 | var body: some View {
25 | VStack(spacing: 0) {
26 | FavoriteCommonSectionView()
27 | FavoriteSwipePagingView(favoritePhotosCardViewObjects: favoritePhotosCardViewObjects)
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/View/Components/Home/HomeCommonSectionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeCommonSectionView.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/12/07.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct HomeCommonSectionView: View {
11 |
12 | // MARK: - Property
13 |
14 | private let screen = UIScreen.main.bounds
15 |
16 | private var headerWidth: CGFloat {
17 | return screen.width
18 | }
19 |
20 | private var sectionTitleFont: Font {
21 | return Font.custom("AvenirNext-Bold", size: 18)
22 | }
23 |
24 | private var sectionSubtitleFont: Font {
25 | return Font.custom("AvenirNext-Regular", size: 12)
26 | }
27 |
28 | private var sectionTitleColor: Color {
29 | return Color.primary
30 | }
31 |
32 | private var sectionSubtitleColor: Color {
33 | return Color.secondary
34 | }
35 |
36 | @State private var titleTextSet: (title: String, subTitle: String)
37 |
38 | // MARK: - Initializer
39 |
40 | init(title: String, subTitle: String) {
41 | _titleTextSet = State(initialValue: (title: title, subTitle: subTitle))
42 | }
43 |
44 | // MARK: - Body
45 |
46 | var body: some View {
47 | HStack {
48 | VStack(alignment: .leading) {
49 | Text(titleTextSet.title)
50 | .font(sectionTitleFont)
51 | .foregroundColor(sectionTitleColor)
52 | .lineLimit(1)
53 | Text(titleTextSet.subTitle)
54 | .font(sectionSubtitleFont)
55 | .foregroundColor(sectionSubtitleColor)
56 | .lineLimit(1)
57 | }
58 | Spacer()
59 | }
60 | .padding(12.0)
61 | .frame(width: headerWidth)
62 | }
63 | }
64 |
65 | // MARK: - Preview
66 |
67 | #Preview("季節の特集コンテンツ一覧") {
68 | HomeCommonSectionView(title: "季節の特集コンテンツ一覧", subTitle: "Introduce seasonal shopping and features.")
69 | }
70 |
71 | #Preview("最新のおしらせ") {
72 | HomeCommonSectionView(title: "最新のおしらせ", subTitle: "Let's Check Here for App-only Notifications.")
73 | }
74 |
75 | #Preview("特集掲載店舗") {
76 | HomeCommonSectionView(title: "特集掲載店舗", subTitle: "Please Teach Us Your Favorite Gourmet.")
77 | }
78 |
79 | #Preview("トレンド記事紹介") {
80 | HomeCommonSectionView(title: "トレンド記事紹介", subTitle: "Memorial Articles about Special Season.")
81 | }
82 |
83 | #Preview("ピックアップ写真集") {
84 | HomeCommonSectionView(title: "ピックアップ写真集", subTitle: "Let's Enjoy Pickup Gourmet Photo Archives.")
85 | }
86 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/View/Components/Home/HomeContentsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeContentsView.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/29.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct HomeContentsView: View {
11 |
12 | // MARK: - Property
13 |
14 | private let campaignBannerCarouselViewObjects: [CampaignBannerCarouselViewObject]
15 | private let recentNewsCarouselViewObjects: [RecentNewsCarouselViewObject]
16 | private let featuredTopicsCarouselViewObjects: [FeaturedTopicsCarouselViewObject]
17 | private let trendArticlesGridViewObjects: [TrendArticlesGridViewObject]
18 | private let pickupPhotosGridViewObjects: [PickupPhotosGridViewObject]
19 |
20 | // MARK: - Initializer
21 |
22 | init(
23 | campaignBannerCarouselViewObjects: [CampaignBannerCarouselViewObject],
24 | recentNewsCarouselViewObjects: [RecentNewsCarouselViewObject],
25 | featuredTopicsCarouselViewObjects: [FeaturedTopicsCarouselViewObject],
26 | trendArticlesGridViewObjects: [TrendArticlesGridViewObject],
27 | pickupPhotosGridViewObjects: [PickupPhotosGridViewObject]
28 | ) {
29 | self.campaignBannerCarouselViewObjects = campaignBannerCarouselViewObjects
30 | self.recentNewsCarouselViewObjects = recentNewsCarouselViewObjects
31 | self.featuredTopicsCarouselViewObjects = featuredTopicsCarouselViewObjects
32 | self.trendArticlesGridViewObjects = trendArticlesGridViewObjects
33 | self.pickupPhotosGridViewObjects = pickupPhotosGridViewObjects
34 | }
35 |
36 | // MARK: - Body
37 |
38 | var body: some View {
39 | // 各Sectionに該当するView要素に表示に必要なViewObjectを反映する
40 | ScrollView {
41 | // (1) 季節の特集コンテンツ一覧
42 | HomeCommonSectionView(
43 | title: "季節の特集コンテンツ一覧",
44 | subTitle: "Introduce seasonal shopping and features."
45 | )
46 | CampaignBannerCarouselView(
47 | campaignBannerCarouselViewObjects: campaignBannerCarouselViewObjects
48 | )
49 | // (2) 最新のおしらせ
50 | HomeCommonSectionView(
51 | title: "最新のおしらせ",
52 | subTitle: "Let's Check Here for App-only Notifications."
53 | )
54 | RecentNewsCarouselView(
55 | recentNewsCarouselViewObjects: recentNewsCarouselViewObjects
56 | )
57 | // (3) 特集掲載店舗
58 | HomeCommonSectionView(
59 | title: "特集掲載店舗",
60 | subTitle: "Please Teach Us Your Favorite Gourmet."
61 | )
62 | FeaturedTopicsCarouselView(
63 | featuredTopicsCarouselViewObjects: featuredTopicsCarouselViewObjects
64 | )
65 | // (4) トレンド記事紹介
66 | HomeCommonSectionView(
67 | title: "トレンド記事紹介",
68 | subTitle: "Memorial Articles about Special Season."
69 | )
70 | TrendArticlesGridView(
71 | trendArticlesGridViewObjects: trendArticlesGridViewObjects
72 | )
73 | // (5) ピックアップ写真集
74 | HomeCommonSectionView(
75 | title: "ピックアップ写真集",
76 | subTitle: "Let's Enjoy Pickup Gourmet Photo Archives."
77 | )
78 | PickupPhotosGridView(
79 | pickupPhotosGridViewObjects: pickupPhotosGridViewObjects
80 | )
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/View/Components/Onboarding/OnboardingContentsView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OnboardingContentsView.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/02/05.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct OnboardingContentsView: View {
11 |
12 | // MARK: - Typealias
13 |
14 | typealias CloseOnboardingAction = () -> Void
15 |
16 | // MARK: - Property
17 |
18 | private let screen = UIScreen.main.bounds
19 |
20 | private var screenWidth: CGFloat {
21 | return screen.width - 64.0
22 | }
23 |
24 | private var baseBackgroundColor: Color {
25 | return Color.white
26 | }
27 |
28 | private var baseBorderColor: Color {
29 | return Color(uiColor: .lightGray)
30 | }
31 |
32 | private var quitOnboardingButtonFont: Font {
33 | return Font.custom("AvenirNext-Bold", size: 16)
34 | }
35 |
36 | private var quitOnboardingButtonColor: Color {
37 | return Color(uiColor: UIColor(code: "#b9d9c3"))
38 | }
39 |
40 | private var closeOnboardingAction: OnboardingContentsView.CloseOnboardingAction
41 |
42 | // MARK: - Initializer
43 |
44 | init(closeOnboardingAction: @escaping OnboardingContentsView.CloseOnboardingAction) {
45 | self.closeOnboardingAction = closeOnboardingAction
46 | }
47 |
48 | // MARK: - Body
49 |
50 | var body: some View {
51 | VStack(spacing: 0.0) {
52 | TabView {
53 | OnboardingItemView(
54 | imageName: "onboarding1",
55 | title: "Welcome to App.",
56 | summary: "アプリへようこそ!"
57 | )
58 | .tag(0)
59 | OnboardingItemView(
60 | imageName: "onboarding2",
61 | title: "Find my favorite.",
62 | summary: "お気に入りに出会おう!"
63 | )
64 | .tag(1)
65 | OnboardingItemView(
66 | imageName: "onboarding3",
67 | title: "Come on! Let's go!",
68 | summary: "さあ!使ってみよう!"
69 | )
70 | .tag(2)
71 | }
72 | .tabViewStyle(PageTabViewStyle())
73 | .indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
74 | HStack {
75 | Spacer()
76 | Button(action: closeOnboardingAction, label: {
77 | // MEMO: 縁取りをした角丸ボタンのための装飾
78 | Text("オンボーディングを終了")
79 | .font(quitOnboardingButtonFont)
80 | .foregroundColor(quitOnboardingButtonColor)
81 | .background(.white)
82 | .frame(width: 240.0, height: 48.0)
83 | .cornerRadius(24.0)
84 | .overlay(
85 | RoundedRectangle(cornerRadius: 24.0)
86 | .stroke(quitOnboardingButtonColor, lineWidth: 1.0)
87 | )
88 | })
89 | Spacer()
90 | }
91 | .padding(.top, 24.0)
92 | .padding(.bottom, 24.0)
93 | }
94 | .background(baseBackgroundColor)
95 | .frame(width: screenWidth, height: 480.0)
96 | .padding(.vertical, 64.0)
97 | .padding(.horizontal, 44.0)
98 | }
99 | }
100 |
101 | // MARK: - Preview
102 |
103 | #Preview("OnboardingContentsView Preview") {
104 | OnboardingContentsView(closeOnboardingAction: {})
105 | }
106 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/View/Components/Onboarding/Section/OnboardingItemView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OnboardingItemView.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/02/05.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct OnboardingItemView: View {
11 |
12 | // MARK: - Property
13 |
14 | private let screen = UIScreen.main.bounds
15 |
16 | private var screenWidth: CGFloat {
17 | return screen.width - 64.0
18 | }
19 |
20 | private var itemTitleFont: Font {
21 | return Font.custom("AvenirNext-Bold", size: 24)
22 | }
23 |
24 | private var itemTitleColor: Color {
25 | return Color.white
26 | }
27 |
28 | private var itemSummaryFont: Font {
29 | return Font.custom("AvenirNext-Bold", size: 16)
30 | }
31 |
32 | private var itemSummaryColor: Color {
33 | return Color.white
34 | }
35 |
36 | private var itemThumbnailMaskColor: Color {
37 | return Color.black.opacity(0.16)
38 | }
39 |
40 | private var imageName: String
41 | private var title: String
42 | private var summary: String
43 |
44 | // MARK: - Initializer
45 |
46 | init(
47 | imageName: String,
48 | title: String,
49 | summary: String
50 | ) {
51 | self.imageName = imageName
52 | self.title = title
53 | self.summary = summary
54 | }
55 |
56 | // MARK: - Body
57 |
58 | var body: some View {
59 | // 👉 ZStack内部の要素についてはサムネイル表示のサイズと合わせています。
60 | ZStack {
61 | // (1) サムネイル画像表示
62 | Image(imageName)
63 | .resizable()
64 | .scaledToFill()
65 | // MEMO: .frameの後ろに.clippedを入れないとサムネイル画像が切り取られないので注意
66 | .frame(width: screenWidth)
67 | .clipped()
68 | // (2) 半透明マスク表示部分
69 | Rectangle()
70 | .foregroundColor(itemThumbnailMaskColor)
71 | .frame(width: screenWidth)
72 | // (3) タイトル&サマリーテキスト表示部分
73 | VStack(spacing: 0.0) {
74 | HStack {
75 | Text(title)
76 | .font(itemTitleFont)
77 | .foregroundColor(itemTitleColor)
78 | .padding(.top, 24.0)
79 | .padding(.horizontal, 16.0)
80 | Spacer()
81 | }
82 | HStack {
83 | Text(summary)
84 | .font(itemSummaryFont)
85 | .foregroundColor(itemSummaryColor)
86 | .padding(.top, 8.0)
87 | .padding(.horizontal, 16.0)
88 | .padding(.bottom, 8.0)
89 | Spacer()
90 | }
91 | Spacer()
92 | }
93 | .frame(width: screenWidth)
94 | }
95 | .frame(width: screenWidth)
96 | }
97 | }
98 |
99 | // MARK: - Preview
100 |
101 | #Preview("OnboardingItemView Preview") {
102 | OnboardingItemView(
103 | imageName: "onboarding1",
104 | title: "Welcome to App.",
105 | summary: "アプリへようこそ!"
106 | )
107 | }
108 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/View/Components/Profile/ProfileCommonSectionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileCommonSectionView.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/12/30.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ProfileCommonSectionView: View {
11 |
12 | // MARK: - Property
13 |
14 | private let screen = UIScreen.main.bounds
15 |
16 | private var headerWidth: CGFloat {
17 | return screen.width
18 | }
19 |
20 | private var headerHeight: CGFloat {
21 | return 68.0
22 | }
23 |
24 | private var sectionTitleFont: Font {
25 | return Font.custom("AvenirNext-Bold", size: 18)
26 | }
27 |
28 | private var sectionSubtitleFont: Font {
29 | return Font.custom("AvenirNext-Regular", size: 12)
30 | }
31 |
32 | private var sectionTitleColor: Color {
33 | return Color.primary
34 | }
35 |
36 | private var sectionSubtitleColor: Color {
37 | return Color.secondary
38 | }
39 |
40 | @State private var titleTextSet: (title: String, subTitle: String)
41 |
42 | // MARK: - Initializer
43 |
44 | init(title: String, subTitle: String) {
45 | _titleTextSet = State(initialValue: (title: title, subTitle: subTitle))
46 | }
47 |
48 | // MARK: - Body
49 |
50 | var body: some View {
51 | HStack {
52 | VStack(alignment: .leading) {
53 | Text(titleTextSet.title)
54 | .font(sectionTitleFont)
55 | .foregroundColor(sectionTitleColor)
56 | .lineLimit(1)
57 | Text(titleTextSet.subTitle)
58 | .font(sectionSubtitleFont)
59 | .foregroundColor(sectionSubtitleColor)
60 | .lineLimit(1)
61 | }
62 | Spacer()
63 | }
64 | .padding(12.0)
65 | .frame(width: headerWidth, height: headerHeight)
66 | }
67 | }
68 |
69 | // MARK: - Preview
70 |
71 | #Preview("ProfileCommonSectionView Preview") {
72 | ProfileCommonSectionView(
73 | title: "自己紹介文",
74 | subTitle: "Self Inftoduction"
75 | )
76 | }
77 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/View/Components/Profile/Section/ProfilePersonal/ProfilePersonalView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfilePersonalView.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/12/30.
6 | //
7 |
8 | import SwiftUI
9 | import Kingfisher
10 |
11 | struct ProfilePersonalView: View {
12 |
13 | // MARK: - Property
14 |
15 | private var personNameFont: Font {
16 | return Font.custom("AvenirNext-Bold", size: 14)
17 | }
18 |
19 | private var personNameColor: Color {
20 | return Color.primary
21 | }
22 |
23 | private var personRegistrationFont: Font {
24 | return Font.custom("AvenirNext-Regular", size: 12)
25 | }
26 |
27 | private var personRegistrationColor: Color {
28 | return Color.gray
29 | }
30 |
31 | private var personIdFont: Font {
32 | return Font.custom("AvenirNext-Regular", size: 12)
33 | }
34 |
35 | private var personIdColor: Color {
36 | return Color.gray
37 | }
38 |
39 | private var viewHeight: CGFloat {
40 | return 86.0
41 | }
42 |
43 | private let profilePersonalViewObject: ProfilePersonalViewObject
44 |
45 | // MARK: - Initializer
46 |
47 | init(profilePersonalViewObject: ProfilePersonalViewObject) {
48 | self.profilePersonalViewObject = profilePersonalViewObject
49 | }
50 |
51 | // MARK: - Body
52 |
53 | var body: some View {
54 | VStack(alignment: .leading, spacing: 0.0) {
55 | HStack {
56 | // 1. プロフィール用アバター表示
57 | KFImage(profilePersonalViewObject.avatarUrl)
58 | .resizable()
59 | .scaledToFill()
60 | .clipShape(Circle())
61 | .frame(width: 58.0, height: 58.0)
62 | .shadow(radius: 4.0)
63 | // 2. プロフィール用基本情報表示
64 | VStack(alignment: .leading) {
65 | // 2-(1). ユーザー名表示
66 | Text(profilePersonalViewObject.nickname)
67 | .font(personNameFont)
68 | .foregroundColor(personNameColor)
69 | // 2-(2). ユーザー登録日表示
70 | Text(profilePersonalViewObject.createdAt)
71 | .font(personRegistrationFont)
72 | .foregroundColor(personRegistrationColor)
73 | .padding([.top], -8.0)
74 | // 2-(3). ユーザー最終ログイン日時表示
75 | Text("ユーザーID: \(profilePersonalViewObject.id)")
76 | .font(personIdFont)
77 | .foregroundColor(personIdColor)
78 | .padding([.top], -8.0)
79 | }
80 | .padding([.leading], 8.0)
81 | Spacer()
82 | }
83 | }
84 | .padding([.leading, .trailing], 12.0)
85 | .frame(height: viewHeight)
86 | }
87 | }
88 |
89 | // MARK: - Preview
90 |
91 | #Preview("ProfilePersonalView Preview") {
92 | // MEMO: 部品1つあたりを表示するためのViewObject
93 | let profilePersonalViewObject = ProfilePersonalViewObject(
94 | id: 100,
95 | nickname: "謎多き料理人",
96 | createdAt: DateLabelFormatter.getDateStringFromAPI(apiDateString: "2022-11-16T07:30:00.000+0000"),
97 | avatarUrl: URL(string: "https://ones-mind-topics.s3.ap-northeast-1.amazonaws.com/profile_avatar_sample.jpg")
98 | )
99 | // Preview: ProfilePersonalView
100 | return ProfilePersonalView(profilePersonalViewObject: profilePersonalViewObject)
101 | }
102 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/View/Components/Profile/Section/ProfilePointAndHistory/ProfilePointsAndHistoryView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfilePointsAndHistoryView.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/12/31.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ProfilePointsAndHistoryView: View {
11 |
12 | // MARK: - Property
13 |
14 | private var pointAndHistoryTitleFont: Font {
15 | return Font.custom("AvenirNext-Bold", size: 12)
16 | }
17 |
18 | private var pointAndHistoryValueFont: Font {
19 | return Font.custom("AvenirNext-Regular", size: 12)
20 | }
21 |
22 | private var pointAndHistoryTitleColor: Color {
23 | return Color.secondary
24 | }
25 |
26 | private var pointAndHistoryValueColor: Color {
27 | return Color.gray
28 | }
29 |
30 | private var pointAndHistoryBorderColor: Color {
31 | return Color(uiColor: .lightGray)
32 | }
33 |
34 | private let profilePointsAndHistoryViewObject: ProfilePointsAndHistoryViewObject
35 |
36 | // MEMO: LazyVGridに表示する内容を格納するための変数
37 | @State private var pointAndHistoryPairs: [PointAndHistoryPair] = []
38 |
39 | // MARK: - Typealias
40 |
41 | typealias PointAndHistoryPair = (title: String, score: Int)
42 |
43 | // MARK: - Initializer
44 |
45 | init(profilePointsAndHistoryViewObject: ProfilePointsAndHistoryViewObject) {
46 | self.profilePointsAndHistoryViewObject = profilePointsAndHistoryViewObject
47 |
48 | // イニシャライザ内で「_(変数名)」値を代入することでState値の初期化を実行する
49 | _pointAndHistoryPairs = State(
50 | initialValue: [
51 | PointAndHistoryPair(title: "😁 Profile訪問数:", score: profilePointsAndHistoryViewObject.profileViewCount),
52 | PointAndHistoryPair(title: "📝 記事投稿数:", score: profilePointsAndHistoryViewObject.articlePostCount),
53 | PointAndHistoryPair(title: "✨ 総合PV数:", score: profilePointsAndHistoryViewObject.totalPageViewCount),
54 | PointAndHistoryPair(title: "💰 獲得ポイント:", score: profilePointsAndHistoryViewObject.totalAvailablePoints),
55 | PointAndHistoryPair(title: "🎫 クーポン利用回数:", score: profilePointsAndHistoryViewObject.totalUseCouponCount),
56 | PointAndHistoryPair(title: "🍔 お店に行った回数:", score: profilePointsAndHistoryViewObject.totalVisitShopCount)
57 | ]
58 | )
59 | }
60 |
61 | // MARK: - Body
62 |
63 | var body: some View {
64 | VStack(spacing: 0.0) {
65 | // 上側Divider
66 | Divider()
67 | .background(.gray)
68 | // 変数pointAndHistoryPairsより取得した値を合わせて表示する
69 | ForEach(0.. Void
15 |
16 | // MARK: - Property
17 |
18 | private var specialContentsDescriptionFont: Font {
19 | return Font.custom("AvenirNext-Regular", size: 12)
20 | }
21 |
22 | private var specialContentsDescriptionColor: Color {
23 | return Color.secondary
24 | }
25 |
26 | private var specialContentsButtonFont: Font {
27 | return Font.custom("AvenirNext-Bold", size: 16)
28 | }
29 |
30 | private var specialContentsButtonColor: Color {
31 | return Color(uiColor: UIColor(code: "#b9d9c3"))
32 | }
33 |
34 | private var tapButtonAction: ProfileSpecialContentsView.TapButtonAction
35 |
36 | // MARK: - Initializer
37 |
38 | init(tapButtonAction: @escaping ProfileSpecialContentsView.TapButtonAction) {
39 | self.tapButtonAction = tapButtonAction
40 | }
41 |
42 | // MARK: - Body
43 |
44 | var body: some View {
45 | VStack(alignment: .leading, spacing: 0.0) {
46 | HStack {
47 | Text("これまで書いた店舗のご紹介記事や行ったお店、さらには投稿した写真等を元にしたデータ分析結果から当アプリがレコメンドする情報をお届けしております。気になったお店のクーポンやメニューを見つけて、素敵なお店やお食事と出会う機会を是非増やしてみてください!")
48 | .font(specialContentsDescriptionFont)
49 | .foregroundColor(specialContentsDescriptionColor)
50 | }
51 | HStack {
52 | Spacer()
53 | Button(action: tapButtonAction, label: {
54 | // MEMO: 縁取りをした角丸ボタンのための装飾
55 | Text("特集コンテンツを確認する")
56 | .font(specialContentsButtonFont)
57 | .foregroundColor(specialContentsButtonColor)
58 | .background(.white)
59 | .frame(width: 240.0, height: 48.0)
60 | .cornerRadius(24.0)
61 | .overlay(
62 | RoundedRectangle(cornerRadius: 24.0)
63 | .stroke(specialContentsButtonColor, lineWidth: 1.0)
64 | )
65 | })
66 | Spacer()
67 | }
68 | .padding([.top, .bottom], 24.0)
69 | }
70 | .padding([.leading, .trailing], 12.0)
71 | }
72 | }
73 |
74 | // MARK: - Preview
75 |
76 | #Preview("ProfileSpecialContentsView Preview") {
77 | ProfileSpecialContentsView(tapButtonAction: {})
78 | }
79 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/View/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2021/09/08.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ContentView: View {
11 |
12 | // MARK: - EnvironmentObject
13 |
14 | // 👉 画面全体用のView要素についても同様に.environmentObjectを利用してstoreを適用する
15 | @EnvironmentObject var store: Store
16 |
17 | private struct Props {
18 | // Immutableに扱うProperty 👉 画面状態管理用
19 | let showOnboarding: Bool
20 | // Action発行用のClosure
21 | let requestOnboarding: () -> Void
22 | let closeOnboarding: () -> Void
23 | }
24 |
25 | private func mapStateToProps(state: OnboardingState) -> Props {
26 | Props(
27 | showOnboarding: state.showOnboarding,
28 | requestOnboarding: {
29 | store.dispatch(action: RequestOnboardingAction())
30 | },
31 | closeOnboarding: {
32 | store.dispatch(action: CloseOnboardingAction())
33 | }
34 | )
35 | }
36 |
37 | // MARK: - Body
38 |
39 | var body: some View {
40 | // 該当画面で利用するState(ここではOnboardingState)をこの画面用のPropsにマッピングする
41 | let props = mapStateToProps(state: store.state.onboardingState)
42 |
43 | // 表示に必要な値をPropsから取得する
44 | let onboardingState = mapToshowOnboarding(props: props)
45 |
46 | // 画面用のPropsに応じた画面要素表示処理を実行する
47 | ZStack {
48 | // (1) TabView表示要素の配置
49 | TabView {
50 | HomeScreenView()
51 | .environmentObject(store)
52 | .tabItem {
53 | VStack {
54 | Image(systemName: "house.fill")
55 | Text("Home")
56 | }
57 | }
58 | .tag(0)
59 | ArchiveScreenView()
60 | .environmentObject(store)
61 | .tabItem {
62 | VStack {
63 | Image(systemName: "archivebox.fill")
64 | Text("Archive")
65 | }
66 | }.tag(1)
67 | FavoriteScreenView()
68 | .environmentObject(store)
69 | .tabItem {
70 | VStack {
71 | Image(systemName: "bookmark.square.fill")
72 | Text("Favorite")
73 | }
74 | }.tag(2)
75 | ProfileScreenView()
76 | .environmentObject(store)
77 | .tabItem {
78 | VStack {
79 | Image(systemName: "person.crop.circle.fill")
80 | Text("Profile")
81 | }
82 | }.tag(3)
83 | }
84 | .accentColor(Color(uiColor: UIColor(code: "#b9d9c3")))
85 | // (2) 初回起動ダイアログ表示要素の配置
86 | if onboardingState {
87 | withAnimation(.linear(duration: 0.3)) {
88 | Group {
89 | Color.black.opacity(0.64)
90 | OnboardingContentsView(closeOnboardingAction: props.closeOnboarding)
91 | }
92 | .edgesIgnoringSafeArea(.all)
93 | }
94 | }
95 | }
96 | .onFirstAppear(props.requestOnboarding)
97 | }
98 |
99 | // MARK: - Private Function
100 |
101 | private func mapToshowOnboarding(props: Props) -> Bool {
102 | return props.showOnboarding
103 | }
104 | }
105 |
106 | // MARK: - Preview
107 |
108 | #Preview("ContentView Preview") {
109 | let store = Store(
110 | reducer: appReducer,
111 | state: AppState(),
112 | middlewares: [
113 | // 👉 Preview表示確認用にMockを適用しています
114 | // OnBoarding
115 | // ※ onBoardingを表示しない場合
116 | //onboardingMockHideMiddleware(),
117 | onboardingMockShowMiddleware(),
118 | onboardingMockCloseMiddleware(),
119 | // Home
120 | homeMockSuccessMiddleware(),
121 | // Archive
122 | archiveMockSuccessMiddleware(),
123 | addMockArchiveObjectMiddleware(),
124 | deleteMockArchiveObjectMiddleware(),
125 | // Favorite
126 | favoriteMockSuccessMiddleware(),
127 | // Profile
128 | profileMockSuccessMiddleware()
129 | ]
130 | )
131 | return ContentView()
132 | .environmentObject(store)
133 | }
134 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/View/Representable/LoadingIndicatorViewRepresentable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoadingIndicatorViewRepresentable.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/10.
6 | //
7 |
8 | import SwiftUI
9 | import UIKit
10 |
11 | struct LoadingIndicatorViewRepresentable: UIViewRepresentable {
12 |
13 | // MARK: - Property
14 |
15 | // 👉 親のView要素から受け取ったRatingの値をこの構造体の中で利用していく。
16 | @Binding var isLoading: Bool
17 |
18 | // MARK: - Function
19 |
20 | func makeUIView(context: Context) -> UIActivityIndicatorView {
21 | UIActivityIndicatorView()
22 | }
23 |
24 | func updateUIView(_ uiView: UIActivityIndicatorView, context: Context) {
25 |
26 | // このインジケータ表示に関する初期設定
27 | uiView.style = .medium
28 | uiView.hidesWhenStopped = true
29 |
30 | // @Bindingで設定された読み込み中か否かの状態を反映する
31 | if isLoading {
32 | uiView.startAnimating()
33 | } else {
34 | uiView.stopAnimating()
35 | }
36 |
37 | // 内在サイズに則って自動でCosmosViewをリサイズする
38 | // 参考: 内在サイズについての説明
39 | // https://developer.mozilla.org/ja/docs/Glossary/Intrinsic_Size
40 | uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
41 | uiView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/View/Representable/RatingViewRepresentable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RatingViewRepresentable.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2022/01/09.
6 | //
7 |
8 | import SwiftUI
9 | import Cosmos
10 |
11 | // MEMO: UIKit製ライブラリ「CosmosView」をSwiftUIで利用する
12 | // https://github.com/evgenyneu/Cosmos/wiki/Using-Cosmos-with-SwiftUI
13 |
14 | struct RatingViewRepresentable: UIViewRepresentable {
15 |
16 | // MARK: - Property
17 |
18 | // 👉 親のView要素から受け取ったRatingの値をこの構造体の中で利用していく。
19 | @Binding var rating: Double
20 |
21 | // MARK: - Function
22 |
23 | func makeUIView(context: Context) -> CosmosView {
24 | return CosmosView()
25 | }
26 |
27 | func updateUIView(_ uiView: CosmosView, context: Context) {
28 |
29 | // @Bindingで設定されたRatingの数値を反映する
30 | uiView.rating = rating
31 |
32 | // 内在サイズに則って自動でCosmosViewをリサイズする
33 | // 参考: 内在サイズについての説明
34 | // https://developer.mozilla.org/ja/docs/Glossary/Intrinsic_Size
35 | uiView.setContentHuggingPriority(.defaultHigh, for: .vertical)
36 | uiView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
37 |
38 | // ライブラリ「Cosmos」で調整可能な値を独自に調整する際に利用する
39 | setupCosmosViewSettings(uiView)
40 | }
41 |
42 | private func setupCosmosViewSettings(_ uiView: CosmosView) {
43 |
44 | // MEMO: ライブラリ「Cosmos」の基本設定部分
45 | // 👉 色やサイズをはじめ表示モード等についても細かく設定が可能です。
46 | uiView.settings.fillMode = .precise
47 | uiView.settings.starSize = 26
48 | uiView.settings.emptyBorderWidth = 1.0
49 | uiView.settings.filledBorderWidth = 1.0
50 | uiView.settings.emptyBorderColor = .systemYellow
51 | uiView.settings.filledColor = .systemYellow
52 | uiView.settings.filledBorderColor = .systemYellow
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/View/Screens/FavoriteScreenView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FavoriteScreenView.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2021/10/16.
6 | //
7 |
8 | import SwiftUI
9 | import CollectionViewPagingLayout
10 |
11 | struct FavoriteScreenView: View {
12 |
13 | // MARK: - Redux
14 |
15 | @EnvironmentObject var store: Store
16 |
17 | private struct Props {
18 | // Immutableに扱うProperty 👉 画面状態管理用
19 | let isLoading: Bool
20 | let isError: Bool
21 | // Immutableに扱うProperty 👉 画面表示要素用
22 | let favoritePhotosCardViewObjects: [FavoritePhotosCardViewObject]
23 | // Action発行用のClosure
24 | let requestFavorite: () -> Void
25 | let retryFavorite: () -> Void
26 | }
27 |
28 | private func mapStateToProps(state: FavoriteState) -> Props {
29 | Props(
30 | isLoading: state.isLoading,
31 | isError: state.isError,
32 | favoritePhotosCardViewObjects: state.favoritePhotosCardViewObjects,
33 | requestFavorite: {
34 | store.dispatch(action: RequestFavoriteAction())
35 | },
36 | retryFavorite: {
37 | store.dispatch(action: RequestFavoriteAction())
38 | }
39 | )
40 | }
41 |
42 | // MARK: - Body
43 |
44 | var body: some View {
45 | // 該当画面で利用するState(ここではHomeState)をこの画面用のPropsにマッピングする
46 | let props = mapStateToProps(state: store.state.favoriteState)
47 |
48 | // 表示に必要な値をPropsから取得する
49 | let isLoading = mapToIsLoading(props: props)
50 | let isError = mapToIsError(props: props)
51 |
52 | // 画面用のPropsに応じた画面要素表示処理を実行する
53 | NavigationStack {
54 | Group {
55 | if isLoading {
56 | // ローディング画面を表示
57 | ExecutingConnectionView()
58 | } else if isError {
59 | // エラー画面を表示
60 | ConnectionErrorView(tapButtonAction: props.retryFavorite)
61 | } else {
62 | // Favorite画面を表示
63 | showFavoriteContentsView(props: props)
64 | }
65 | }
66 | .navigationTitle("Favorite")
67 | .navigationBarTitleDisplayMode(.inline)
68 | // 画面が表示された際に一度だけAPIリクエストを実行する形にしています。
69 | .onFirstAppear(props.requestFavorite)
70 | }
71 | }
72 |
73 | // MARK: - Private Function
74 |
75 | @ViewBuilder
76 | private func showFavoriteContentsView(props: Props) -> some View {
77 | // Propsから表示用のViewObjectを取り出す
78 | let favoritePhotosCardViewObjects = mapToFavoritePhotosCardViewObjects(props: props)
79 | FavoriteContentsView(favoritePhotosCardViewObjects: favoritePhotosCardViewObjects)
80 | }
81 |
82 | private func mapToFavoritePhotosCardViewObjects(props: Props) -> [FavoritePhotosCardViewObject] {
83 | return props.favoritePhotosCardViewObjects
84 | }
85 |
86 | private func mapToIsError(props: Props) -> Bool {
87 | return props.isError
88 | }
89 |
90 | private func mapToIsLoading(props: Props) -> Bool {
91 | return props.isLoading
92 | }
93 | }
94 |
95 | // MARK: - Preview
96 |
97 | struct FavoriteScreenView_Previews: PreviewProvider {
98 | static var previews: some View {
99 | // Success時の画面表示
100 | let favoriteSuccessStore = Store(
101 | reducer: appReducer,
102 | state: AppState(),
103 | middlewares: [
104 | favoriteMockSuccessMiddleware()
105 | ]
106 | )
107 | FavoriteScreenView()
108 | .environmentObject(favoriteSuccessStore)
109 | .previewDisplayName("Favorite Secreen Success Preview")
110 | // Failure時の画面表示
111 | let favoriteFailureStore = Store(
112 | reducer: appReducer,
113 | state: AppState(),
114 | middlewares: [
115 | favoriteMockFailureMiddleware()
116 | ]
117 | )
118 | FavoriteScreenView()
119 | .environmentObject(favoriteFailureStore)
120 | .previewDisplayName("Favorite Secreen Failure Preview")
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/ViewObject/Archive/ArchiveCellViewObject.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArchiveCellViewObject.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/30.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - ViewObject
11 |
12 | struct ArchiveCellViewObject: Identifiable, Equatable {
13 |
14 | // MARK: - Property
15 |
16 | let id: Int
17 | let photoUrl: URL?
18 | let category: String
19 | let dishName: String
20 | let shopName: String
21 | let introduction: String
22 | // MEMO: 表示処理時点でのハートマークの状態を示す
23 | var isStored: Bool = false
24 |
25 | // MARK: - Equatable
26 |
27 | static func == (lhs: ArchiveCellViewObject, rhs: ArchiveCellViewObject) -> Bool {
28 | return lhs.id == rhs.id
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/ViewObject/Favorite/FavoritePhotosCardViewObject.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FavoritePhotosCardViewObject.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/28.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - ViewObject
11 |
12 | struct FavoritePhotosCardViewObject: Identifiable, Equatable {
13 |
14 | // MARK: - Property
15 |
16 | let id: Int
17 | let photoUrl: URL?
18 | let author: String
19 | let title: String
20 | let category: String
21 | let shopName: String
22 | let comment: String
23 | let publishedAt: String
24 |
25 | // MARK: - Equatable
26 |
27 | static func == (lhs: FavoritePhotosCardViewObject, rhs: FavoritePhotosCardViewObject) -> Bool {
28 | return lhs.id == rhs.id
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/ViewObject/Home/CampaignBannerCarouselViewObject.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CampaignBannerCarouselViewObject.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/28.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - ViewObject
11 |
12 | struct CampaignBannerCarouselViewObject: Identifiable, Equatable {
13 |
14 | // MARK: - Property
15 |
16 | let id: Int
17 | let bannerContentsId: Int
18 | let bannerUrl: URL?
19 |
20 | // MARK: - Equatable
21 |
22 | static func == (lhs: CampaignBannerCarouselViewObject, rhs: CampaignBannerCarouselViewObject) -> Bool {
23 | return lhs.id == rhs.id
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/ViewObject/Home/FeaturedTopicsCarouselViewObject.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FeaturedTopicsCarouselViewObject.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/28.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - ViewObject
11 |
12 | struct FeaturedTopicsCarouselViewObject: Identifiable, Equatable {
13 |
14 | // MARK: - Property
15 |
16 | let id: Int
17 | let rating: Double
18 | let thumbnailUrl: URL?
19 | let title: String
20 | let caption: String
21 | let publishedAt: String
22 |
23 | // MARK: - Equatable
24 |
25 | static func == (lhs: FeaturedTopicsCarouselViewObject, rhs: FeaturedTopicsCarouselViewObject) -> Bool {
26 | return lhs.id == rhs.id
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/ViewObject/Home/PickupPhotosGridViewObject.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PickupPhotosGridViewObject.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/28.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - ViewObject
11 |
12 | struct PickupPhotosGridViewObject: Identifiable, Equatable {
13 |
14 | // MARK: - Property
15 |
16 | let id: Int
17 | let title: String
18 | let caption: String
19 | let photoUrl: URL?
20 | let photoWidth: CGFloat
21 | let photoHeight: CGFloat
22 |
23 | // MARK: - Equatable
24 |
25 | static func == (lhs: PickupPhotosGridViewObject, rhs: PickupPhotosGridViewObject) -> Bool {
26 | return lhs.id == rhs.id
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/ViewObject/Home/RecentNewsCarouselViewObject.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecentNewsCarouselViewObject.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/28.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - ViewObject
11 |
12 | struct RecentNewsCarouselViewObject: Identifiable, Equatable {
13 |
14 | // MARK: - Property
15 |
16 | let id: Int
17 | let thumbnailUrl: URL?
18 | let title: String
19 | let newsCategory: String
20 | let publishedAt: String
21 |
22 | // MARK: - Equatable
23 |
24 | static func == (lhs: RecentNewsCarouselViewObject, rhs: RecentNewsCarouselViewObject) -> Bool {
25 | return lhs.id == rhs.id
26 | }
27 | }
28 |
29 | struct GroupedRecentNewsCarouselViewObject: Identifiable {
30 |
31 | // MARK: - Property
32 |
33 | let id: UUID
34 | let recentNewsCarouselViewObjects: [RecentNewsCarouselViewObject]
35 | }
36 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/ViewObject/Home/TrendArticlesGridViewObject.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TrendArticlesGridViewObject.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/28.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - ViewObject
11 |
12 | struct TrendArticlesGridViewObject: Identifiable, Equatable {
13 |
14 | // MARK: - Property
15 |
16 | let id: Int
17 | let thumbnailUrl: URL?
18 | let title: String
19 | let introduction: String
20 | let publishedAt: String
21 |
22 | // MARK: - Equatable
23 |
24 | static func == (lhs: TrendArticlesGridViewObject, rhs: TrendArticlesGridViewObject) -> Bool {
25 | return lhs.id == rhs.id
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/ViewObject/Profile/ProfileInformationViewObject.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileInformationViewObject.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/28.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ProfileInformationViewObject: Identifiable, Equatable {
11 |
12 | // MARK: - Property
13 |
14 | let id: Int
15 | let profileAnnoucementViewObjects: [ProfileAnnoucementViewObject]
16 | let profileCommentViewObjects: [ProfileCommentViewObject]
17 | let profileRecentFavoriteViewObjects: [ProfileRecentFavoriteViewObject]
18 |
19 | // MARK: - Equatable
20 |
21 | static func == (lhs: ProfileInformationViewObject, rhs: ProfileInformationViewObject) -> Bool {
22 | return lhs.id == rhs.id
23 | }
24 | }
25 |
26 | struct ProfileAnnoucementViewObject: Identifiable {
27 |
28 | // MARK: - Property
29 |
30 | let id: Int
31 | let category: String
32 | let title: String
33 | let publishedAt: String
34 | let description: String
35 | }
36 |
37 | struct ProfileCommentViewObject: Identifiable {
38 |
39 | // MARK: - Property
40 |
41 | let id: Int
42 | let emotion: String
43 | let title: String
44 | let publishedAt: String
45 | let comment: String
46 | }
47 |
48 | struct ProfileRecentFavoriteViewObject: Identifiable {
49 |
50 | // MARK: - Property
51 |
52 | let id: Int
53 | let category: String
54 | let title: String
55 | let publishedAt: String
56 | let description: String
57 | }
58 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/ViewObject/Profile/ProfilePersonalViewObject.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfilePersonalViewObject.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/28.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ProfilePersonalViewObject: Identifiable, Equatable {
11 |
12 | // MARK: - Property
13 |
14 | let id: Int
15 | let nickname: String
16 | let createdAt: String
17 | let avatarUrl: URL?
18 |
19 | // MARK: - Equatable
20 |
21 | static func == (lhs: ProfilePersonalViewObject, rhs: ProfilePersonalViewObject) -> Bool {
22 | return lhs.id == rhs.id
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/ViewObject/Profile/ProfilePointsAndHistoryViewObject.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfilePointsAndHistoryViewObject.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/28.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ProfilePointsAndHistoryViewObject: Identifiable, Equatable {
11 |
12 | // MARK: - Property
13 |
14 | let id: Int
15 | let profileViewCount: Int
16 | let articlePostCount: Int
17 | let totalPageViewCount: Int
18 | let totalAvailablePoints: Int
19 | let totalUseCouponCount: Int
20 | let totalVisitShopCount: Int
21 |
22 | // MARK: - Equatable
23 |
24 | static func == (lhs: ProfilePointsAndHistoryViewObject, rhs: ProfilePointsAndHistoryViewObject) -> Bool {
25 | return lhs.id == rhs.id
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/ViewObject/Profile/ProfileSelfIntroductionViewObject.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileSelfIntroductionViewObject.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/28.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ProfileSelfIntroductionViewObject: Identifiable, Equatable {
11 |
12 | // MARK: - Property
13 |
14 | let id: Int
15 | let introduction: String
16 |
17 | // MARK: - Equatable
18 |
19 | static func == (lhs: ProfileSelfIntroductionViewObject, rhs: ProfileSelfIntroductionViewObject) -> Bool {
20 | return lhs.id == rhs.id
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExample/ViewObject/Profile/ProfileSocialMediaViewObject.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ProfileSocialMediaViewObject.swift
3 | // SwiftUIAndReduxExample
4 | //
5 | // Created by 酒井文也 on 2023/01/28.
6 | //
7 |
8 | import Foundation
9 |
10 | struct ProfileSocialMediaViewObject: Identifiable, Equatable {
11 |
12 | // MARK: - Property
13 |
14 | let id: Int
15 | let twitterUrl: URL?
16 | let facebookUrl: URL?
17 | let instagramUrl: URL?
18 |
19 | // MARK: - Equatable
20 |
21 | static func == (lhs: ProfileSocialMediaViewObject, rhs: ProfileSocialMediaViewObject) -> Bool {
22 | return lhs.id == rhs.id
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExampleMockApi-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | NSAppTransportSecurity
24 |
25 | NSAllowsArbitraryLoads
26 |
27 |
28 | UIApplicationSceneManifest
29 |
30 | UIApplicationSupportsMultipleScenes
31 |
32 |
33 | UIApplicationSupportsIndirectInputEvents
34 |
35 | UILaunchScreen
36 |
37 | UIRequiredDeviceCapabilities
38 |
39 | armv7
40 |
41 | UISupportedInterfaceOrientations
42 |
43 | UIInterfaceOrientationPortrait
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExampleTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExampleTests/OnboardingStateTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OnboardingStateTest.swift
3 | // SwiftUIAndReduxExampleTests
4 | //
5 | // Created by 酒井文也 on 2023/02/06.
6 | //
7 |
8 | @testable import SwiftUIAndReduxExample
9 |
10 | import XCTest
11 | import Combine
12 | import CombineExpectations
13 | import Nimble
14 | import Quick
15 |
16 | // MEMO: CombineExpectationsを利用してUnitTestを作成する
17 | // https://github.com/groue/CombineExpectations#usage
18 |
19 | final class OnboardingStateTest: QuickSpec {
20 |
21 | // MARK: - Override
22 |
23 | override class func spec() {
24 |
25 | // MEMO: Quick+NimbleをベースにしたUnitTestを実行する
26 | describe("#オンボーディング表示対象時のテストケース") {
27 | // 👉 storeをインスタンス化する際に、想定するMiddlewareのMockを適用する
28 | let store = Store(
29 | reducer: appReducer,
30 | state: AppState(),
31 | middlewares: [
32 | onboardingMockShowMiddleware(),
33 | onboardingMockCloseMiddleware()
34 | ]
35 | )
36 | // CombineExpectationを利用してAppStateの変化を記録するようにしたい
37 | // 👉 このサンプルではAppStateで`@Published`を利用しているので、AppStateを記録対象とする
38 | var onboardingStateRecorder: Recorder!
39 | context("オンボーディング有無を判定するActionを発行した際に表示対象であった場合") {
40 | // 👉 UnitTest実行前後で実行する処理
41 | beforeEach {
42 | onboardingStateRecorder = store.$state.record()
43 | }
44 | afterEach {
45 | onboardingStateRecorder = nil
46 | }
47 | // 👉 オンボーディング可否を取得するActionを発行する
48 | // この後にOnboardingStateの変化を見る
49 | store.dispatch(action: RequestOnboardingAction())
50 | // 対象のState値が変化することを確認する
51 | // ※ onboardingStateはImmutable / Recorderで対象秒間における値変化を全て保持している
52 | it("showOnboardingがtrueであること") {
53 | // timeout部分で0.16秒後の変化を見る
54 | let onboardingStateRecorderResult = try! self.current.wait(for: onboardingStateRecorder.availableElements, timeout: 0.16)
55 | // 0.16秒間の変化を見て、最後の値が変化していることを確認する
56 | let targetResult = onboardingStateRecorderResult.last!
57 | let showOnboarding = targetResult.onboardingState.showOnboarding
58 | expect(showOnboarding).to(equal(true))
59 | }
60 | }
61 | }
62 | describe("#オンボーディング表示対象からオンボーディング画面を閉じる時のテストケース") {
63 | let store = Store(
64 | reducer: appReducer,
65 | state: AppState(),
66 | middlewares: [
67 | onboardingMockShowMiddleware(),
68 | onboardingMockCloseMiddleware()
69 | ]
70 | )
71 | var onboardingStateRecorder: Recorder!
72 | context("オンボーディング有無を判定するActionを発行した際に表示対象であったが、その後にオンボーディング画面を閉じた場合") {
73 | beforeEach {
74 | onboardingStateRecorder = store.$state.record()
75 | }
76 | afterEach {
77 | onboardingStateRecorder = nil
78 | }
79 | // 👉 オンボーディング可否を取得するActionを発行し、その後にオンボーディングを閉じるActionを発行する
80 | store.dispatch(action: RequestOnboardingAction())
81 | store.dispatch(action: CloseOnboardingAction())
82 | it("showOnboardingがfalseであること") {
83 | let onboardingStateRecorderResult = try! self.current.wait(for: onboardingStateRecorder.availableElements, timeout: 0.16)
84 | let targetResult = onboardingStateRecorderResult.last!
85 | let showOnboarding = targetResult.onboardingState.showOnboarding
86 | expect(showOnboarding).to(equal(false))
87 | }
88 | }
89 | }
90 | }
91 | }
92 |
93 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExampleUITests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/SwiftUIAndReduxExample/SwiftUIAndReduxExampleUITests/SwiftUIAndReduxExampleUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUIAndReduxExampleUITests.swift
3 | // SwiftUIAndReduxExampleUITests
4 | //
5 | // Created by 酒井文也 on 2021/09/08.
6 | //
7 |
8 | //import XCTest
9 |
10 | // MEMO: UITestはこのプロジェクトでは利用しない
11 | // class SwiftUIAndReduxExampleUITests: XCTestCase {}
12 |
--------------------------------------------------------------------------------
/images/3-1-fundamental_of_redux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/images/3-1-fundamental_of_redux.png
--------------------------------------------------------------------------------
/images/3-2-example_of_middleware.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/images/3-2-example_of_middleware.png
--------------------------------------------------------------------------------
/images/4-1-1-3d_carousel_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/images/4-1-1-3d_carousel_example.png
--------------------------------------------------------------------------------
/images/4-1-2-drag_carousel_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/images/4-1-2-drag_carousel_example.png
--------------------------------------------------------------------------------
/images/4-1-3-simple_horizontal_carousel_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/images/4-1-3-simple_horizontal_carousel_example.png
--------------------------------------------------------------------------------
/images/4-1-4-simple_2column_grid_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/images/4-1-4-simple_2column_grid_example.png
--------------------------------------------------------------------------------
/images/4-1-5-waterfall_grid_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/images/4-1-5-waterfall_grid_example.png
--------------------------------------------------------------------------------
/images/4-1-6-swipe_paging_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/images/4-1-6-swipe_paging_example.png
--------------------------------------------------------------------------------
/images/4-2-profile_ui_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/images/4-2-profile_ui_example.png
--------------------------------------------------------------------------------
/images/4-3-archive_ui_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/images/4-3-archive_ui_example.png
--------------------------------------------------------------------------------
/images/build-target-setting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/images/build-target-setting.png
--------------------------------------------------------------------------------
/images/design_memo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/images/design_memo.png
--------------------------------------------------------------------------------
/images/sample_screen1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/images/sample_screen1.png
--------------------------------------------------------------------------------
/images/sample_screen2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/images/sample_screen2.png
--------------------------------------------------------------------------------
/images/sample_screen3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/images/sample_screen3.png
--------------------------------------------------------------------------------
/images/sample_screen4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fumiyasac/SwiftUIAndReduxExample/d30a1c482f115cac5261fade0992f4b9ea492774/images/sample_screen4.png
--------------------------------------------------------------------------------
/mock_server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mock_server",
3 | "version": "1.0.0",
4 | "main": "server.ts",
5 | "license": "MIT",
6 | "dependencies": {
7 | "json-server": "^0.17.0",
8 | "typescript": "^4.7.4"
9 | },
10 | "scripts": {
11 | "start": "npx ts-node server.ts"
12 | },
13 | "devDependencies": {
14 | "@types/json-server": "^0.14.4"
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/mock_server/server.ts:
--------------------------------------------------------------------------------
1 | // ⭐️参考: json-serverの実装に関する参考資料
2 | // https://blog.eleven-labs.com/en/json-server
3 | // ⭐️関連1: TypeScriptで始めるNode.js入門
4 | // https://ics.media/entry/4682/
5 | // ⭐️関連2: JSON ServerをCLIコマンドを使わずTypescript&node.jsからサーバーを立てるやり方
6 | // https://deep.tacoskingdom.com/blog/151
7 |
8 | // Mock用のJSONレスポンスサーバーの初期化設定
9 | import jsonServer from 'json-server';
10 | const server = jsonServer.create();
11 |
12 | // Database構築用のJSONファイル
13 | const router = jsonServer.router('db/db.json');
14 |
15 | // 各種設定用
16 | const middlewares = jsonServer.defaults();
17 |
18 | // ミドルウェアを設定する (※コンソール出力するロガーやキャッシュの設定等)
19 | server.use(middlewares);
20 |
21 | // ルーティングを設定する
22 | server.use(router);
23 |
24 | // サーバをポート3000で起動する
25 | server.listen(3000, () => {
26 | console.log('SwiftUIAndReduxExample Mock Server is running...');
27 | });
28 |
--------------------------------------------------------------------------------