├── .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 | --------------------------------------------------------------------------------