diff --git a/LionHeart-iOS/LionHeart-iOS.xcodeproj/project.pbxproj b/LionHeart-iOS/LionHeart-iOS.xcodeproj/project.pbxproj index 0e68afb0..3614550f 100644 --- a/LionHeart-iOS/LionHeart-iOS.xcodeproj/project.pbxproj +++ b/LionHeart-iOS/LionHeart-iOS.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 4A2050152A5DCD1900C7AF3C /* UICollectionView+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A2050142A5DCD1900C7AF3C /* UICollectionView+.swift */; }; 4A3D72872A5D405C00A36189 /* BookmarkListCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A3D72862A5D405C00A36189 /* BookmarkListCollectionViewCell.swift */; }; + 4A3F0C312B05E8E8004BD076 /* CompleteOnboardingViewModelImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A3F0C302B05E8E8004BD076 /* CompleteOnboardingViewModelImpl.swift */; }; 4A52DD9B2ADBBF2C00858230 /* SpalshFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A52DD9A2ADBBF2C00858230 /* SpalshFactory.swift */; }; 4A52DD9D2ADBBF4C00858230 /* TodayFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A52DD9C2ADBBF4C00858230 /* TodayFactory.swift */; }; 4A52DD9F2ADBC0E500858230 /* AuthFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A52DD9E2ADBC0E500858230 /* AuthFactory.swift */; }; @@ -119,6 +120,10 @@ B5C6A2BE2A5DE6590021BE5E /* GeneralTitleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C6A2BD2A5DE6590021BE5E /* GeneralTitleTableViewCell.swift */; }; B5C6A2C22A5DEA1B0021BE5E /* CopyRightTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C6A2C12A5DEA1B0021BE5E /* CopyRightTableViewCell.swift */; }; B5C6A2C82A5EF4EB0021BE5E /* ArticleDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C6A2C72A5EF4EB0021BE5E /* ArticleDetail.swift */; }; + B5E78E002B03302200EA67A2 /* GetFetalNicknameViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E78DFF2B03302200EA67A2 /* GetFetalNicknameViewModel.swift */; }; + B5E78E022B03304F00EA67A2 /* GetFetalNicknameViewModelImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E78E012B03304F00EA67A2 /* GetFetalNicknameViewModelImpl.swift */; }; + B5E78E132B05D33B00EA67A2 /* SplashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E78E122B05D33B00EA67A2 /* SplashViewModel.swift */; }; + B5E78E152B05D38B00EA67A2 /* SplashViewModelImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E78E142B05D38B00EA67A2 /* SplashViewModelImpl.swift */; }; B5F323E92A6A8F0000047869 /* CurriculumWeekBackgroundDummy.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5F323E82A6A8F0000047869 /* CurriculumWeekBackgroundDummy.swift */; }; C003CC1B2AD9176B00AFFAAC /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C003CC1A2AD9176B00AFFAAC /* Coordinator.swift */; }; C003CC1D2AD917CF00AFFAAC /* AppCoordinatorImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C003CC1C2AD917CF00AFFAAC /* AppCoordinatorImpl.swift */; }; @@ -146,7 +151,7 @@ C003CC4B2ADA4F6300AFFAAC /* BookmarkNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C003CC4A2ADA4F6300AFFAAC /* BookmarkNavigation.swift */; }; C003CC4D2ADA4F7600AFFAAC /* TodayNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C003CC4C2ADA4F7600AFFAAC /* TodayNavigation.swift */; }; C003CC4F2ADA4F8D00AFFAAC /* OnboardingNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C003CC4E2ADA4F8D00AFFAAC /* OnboardingNavigation.swift */; }; - C003CC512ADA4FA600AFFAAC /* CompleteOnbardingNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C003CC502ADA4FA600AFFAAC /* CompleteOnbardingNavigation.swift */; }; + C003CC512ADA4FA600AFFAAC /* CompleteOnboardingNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C003CC502ADA4FA600AFFAAC /* CompleteOnboardingNavigation.swift */; }; C003CC532ADA4FC100AFFAAC /* LoginNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C003CC522ADA4FC100AFFAAC /* LoginNavigation.swift */; }; C003CC552ADA4FD500AFFAAC /* SplashNavigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = C003CC542ADA4FD500AFFAAC /* SplashNavigation.swift */; }; C003CC572ADA4FEB00AFFAAC /* UserState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C003CC562ADA4FEB00AFFAAC /* UserState.swift */; }; @@ -173,6 +178,7 @@ C009E9FA2ADBC4DE00112F18 /* BookmarkViewControllerable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C009E9F92ADBC4DE00112F18 /* BookmarkViewControllerable.swift */; }; C009E9FC2ADBC4FD00112F18 /* ChallengeViewControllerable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C009E9FB2ADBC4FD00112F18 /* ChallengeViewControllerable.swift */; }; C009E9FE2ADBC51400112F18 /* ArticleCategoryViewControllerable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C009E9FD2ADBC51400112F18 /* ArticleCategoryViewControllerable.swift */; }; + C0138C282B05FAEE002BA766 /* CompleteOnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0138C272B05FAEE002BA766 /* CompleteOnboardingViewModel.swift */; }; C034EDD62ADE21BD00AD6FF3 /* SplashCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C034EDD52ADE21BD00AD6FF3 /* SplashCoordinator.swift */; }; C034EDD82ADE21D200AD6FF3 /* SplashAdaptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C034EDD72ADE21D200AD6FF3 /* SplashAdaptor.swift */; }; C034EDDA2ADE2A0E00AD6FF3 /* AuthCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C034EDD92ADE2A0E00AD6FF3 /* AuthCoordinator.swift */; }; @@ -189,8 +195,8 @@ C065F0092ADBD2640094912C /* TodayViewControllerable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C065F0082ADBD2640094912C /* TodayViewControllerable.swift */; }; C065F00B2ADBD2800094912C /* LoginViewControllerable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C065F00A2ADBD2800094912C /* LoginViewControllerable.swift */; }; C065F00D2ADBD29A0094912C /* OnboardingViewControllerable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C065F00C2ADBD29A0094912C /* OnboardingViewControllerable.swift */; }; - C065F00F2ADBD2AD0094912C /* CompleteOnbardingViewControllerable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C065F00E2ADBD2AD0094912C /* CompleteOnbardingViewControllerable.swift */; }; - C065F0112ADBD2C40094912C /* SplashViewControllerable.swift in Sources */ = {isa = PBXBuildFile; fileRef = C065F0102ADBD2C40094912C /* SplashViewControllerable.swift */; }; + C06D23D02B0481F00048AFDB /* OnboardingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06D23CF2B0481F00048AFDB /* OnboardingViewModel.swift */; }; + C06D23D22B04821B0048AFDB /* OnboardingViewModelImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06D23D12B04821B0048AFDB /* OnboardingViewModelImpl.swift */; }; C06E381B2A65346700B00600 /* UserDefaultToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06E381A2A65346700B00600 /* UserDefaultToken.swift */; }; C06E381D2A65348A00B00600 /* LoginRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06E381C2A65348A00B00600 /* LoginRequest.swift */; }; C06E38212A65351600B00600 /* SignUpRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = C06E38202A65351600B00600 /* SignUpRequest.swift */; }; @@ -223,6 +229,8 @@ C09A33222A62D46300B40770 /* LHToast.swift in Sources */ = {isa = PBXBuildFile; fileRef = C09A33202A62D46300B40770 /* LHToast.swift */; }; C09A33232A62D46300B40770 /* LHToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C09A33212A62D46300B40770 /* LHToastView.swift */; }; C09A33242A630A6400B40770 /* BookmarkViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DF036C2A5A9C9A0037F740 /* BookmarkViewController.swift */; }; + C09A564D2B030C070012A7FD /* GetPregnancyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C09A564C2B030C070012A7FD /* GetPregnancyViewModel.swift */; }; + C09A564F2B030CB20012A7FD /* GetPregnancyViewModelImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = C09A564E2B030CB20012A7FD /* GetPregnancyViewModelImpl.swift */; }; C0B15E132AC010CD0058D56B /* LHKingfisherService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B15E122AC010CD0058D56B /* LHKingfisherService.swift */; }; C0B15E152AC011840058D56B /* LHLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B15E142AC011840058D56B /* LHLabel.swift */; }; C0B15E172AC015360058D56B /* LHImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B15E162AC015360058D56B /* LHImageView.swift */; }; @@ -268,7 +276,7 @@ C0DF039F2A5CABC10037F740 /* GetPregnancyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DF039E2A5CABC10037F740 /* GetPregnancyViewController.swift */; }; C0DF03A12A5CAC220037F740 /* GetFetalNicknameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DF03A02A5CAC220037F740 /* GetFetalNicknameViewController.swift */; }; C0DF03A42A5CACF00037F740 /* OnboardingPageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DF03A32A5CACF00037F740 /* OnboardingPageType.swift */; }; - C0DF03A62A5CB8610037F740 /* CompleteOnbardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DF03A52A5CB8610037F740 /* CompleteOnbardingViewController.swift */; }; + C0DF03A62A5CB8610037F740 /* CompleteOnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DF03A52A5CB8610037F740 /* CompleteOnboardingViewController.swift */; }; C0DF03A92A5CF0460037F740 /* UserOnboardingModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DF03A82A5CF0460037F740 /* UserOnboardingModel.swift */; }; C0DF03AB2A5D73C80037F740 /* NHOnboardingTextfield.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DF03AA2A5D73C80037F740 /* NHOnboardingTextfield.swift */; }; C0F029C72A5EFB9D00E0D185 /* LHProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0F029C62A5EFB9D00E0D185 /* LHProgressView.swift */; }; @@ -330,6 +338,7 @@ /* Begin PBXFileReference section */ 4A2050142A5DCD1900C7AF3C /* UICollectionView+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UICollectionView+.swift"; sourceTree = ""; }; 4A3D72862A5D405C00A36189 /* BookmarkListCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkListCollectionViewCell.swift; sourceTree = ""; }; + 4A3F0C302B05E8E8004BD076 /* CompleteOnboardingViewModelImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompleteOnboardingViewModelImpl.swift; sourceTree = ""; }; 4A52DD9A2ADBBF2C00858230 /* SpalshFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpalshFactory.swift; sourceTree = ""; }; 4A52DD9C2ADBBF4C00858230 /* TodayFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayFactory.swift; sourceTree = ""; }; 4A52DD9E2ADBC0E500858230 /* AuthFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthFactory.swift; sourceTree = ""; }; @@ -438,6 +447,10 @@ B5C6A2BD2A5DE6590021BE5E /* GeneralTitleTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralTitleTableViewCell.swift; sourceTree = ""; }; B5C6A2C12A5DEA1B0021BE5E /* CopyRightTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyRightTableViewCell.swift; sourceTree = ""; }; B5C6A2C72A5EF4EB0021BE5E /* ArticleDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleDetail.swift; sourceTree = ""; }; + B5E78DFF2B03302200EA67A2 /* GetFetalNicknameViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetFetalNicknameViewModel.swift; sourceTree = ""; }; + B5E78E012B03304F00EA67A2 /* GetFetalNicknameViewModelImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetFetalNicknameViewModelImpl.swift; sourceTree = ""; }; + B5E78E122B05D33B00EA67A2 /* SplashViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewModel.swift; sourceTree = ""; }; + B5E78E142B05D38B00EA67A2 /* SplashViewModelImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewModelImpl.swift; sourceTree = ""; }; B5F323E82A6A8F0000047869 /* CurriculumWeekBackgroundDummy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurriculumWeekBackgroundDummy.swift; sourceTree = ""; }; C003CC1A2AD9176B00AFFAAC /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; C003CC1C2AD917CF00AFFAAC /* AppCoordinatorImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinatorImpl.swift; sourceTree = ""; }; @@ -465,7 +478,7 @@ C003CC4A2ADA4F6300AFFAAC /* BookmarkNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkNavigation.swift; sourceTree = ""; }; C003CC4C2ADA4F7600AFFAAC /* TodayNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayNavigation.swift; sourceTree = ""; }; C003CC4E2ADA4F8D00AFFAAC /* OnboardingNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingNavigation.swift; sourceTree = ""; }; - C003CC502ADA4FA600AFFAAC /* CompleteOnbardingNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompleteOnbardingNavigation.swift; sourceTree = ""; }; + C003CC502ADA4FA600AFFAAC /* CompleteOnboardingNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompleteOnboardingNavigation.swift; sourceTree = ""; }; C003CC522ADA4FC100AFFAAC /* LoginNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginNavigation.swift; sourceTree = ""; }; C003CC542ADA4FD500AFFAAC /* SplashNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashNavigation.swift; sourceTree = ""; }; C003CC562ADA4FEB00AFFAAC /* UserState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserState.swift; sourceTree = ""; }; @@ -492,6 +505,7 @@ C009E9F92ADBC4DE00112F18 /* BookmarkViewControllerable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkViewControllerable.swift; sourceTree = ""; }; C009E9FB2ADBC4FD00112F18 /* ChallengeViewControllerable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChallengeViewControllerable.swift; sourceTree = ""; }; C009E9FD2ADBC51400112F18 /* ArticleCategoryViewControllerable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleCategoryViewControllerable.swift; sourceTree = ""; }; + C0138C272B05FAEE002BA766 /* CompleteOnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompleteOnboardingViewModel.swift; sourceTree = ""; }; C034EDD52ADE21BD00AD6FF3 /* SplashCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashCoordinator.swift; sourceTree = ""; }; C034EDD72ADE21D200AD6FF3 /* SplashAdaptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashAdaptor.swift; sourceTree = ""; }; C034EDD92ADE2A0E00AD6FF3 /* AuthCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthCoordinator.swift; sourceTree = ""; }; @@ -507,8 +521,8 @@ C065F0082ADBD2640094912C /* TodayViewControllerable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodayViewControllerable.swift; sourceTree = ""; }; C065F00A2ADBD2800094912C /* LoginViewControllerable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewControllerable.swift; sourceTree = ""; }; C065F00C2ADBD29A0094912C /* OnboardingViewControllerable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewControllerable.swift; sourceTree = ""; }; - C065F00E2ADBD2AD0094912C /* CompleteOnbardingViewControllerable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompleteOnbardingViewControllerable.swift; sourceTree = ""; }; - C065F0102ADBD2C40094912C /* SplashViewControllerable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewControllerable.swift; sourceTree = ""; }; + C06D23CF2B0481F00048AFDB /* OnboardingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModel.swift; sourceTree = ""; }; + C06D23D12B04821B0048AFDB /* OnboardingViewModelImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewModelImpl.swift; sourceTree = ""; }; C06E381A2A65346700B00600 /* UserDefaultToken.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultToken.swift; sourceTree = ""; }; C06E381C2A65348A00B00600 /* LoginRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginRequest.swift; sourceTree = ""; }; C06E38202A65351600B00600 /* SignUpRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignUpRequest.swift; sourceTree = ""; }; @@ -540,6 +554,8 @@ C09217722A61895A00231C66 /* UIWindow+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIWindow+.swift"; sourceTree = ""; }; C09A33202A62D46300B40770 /* LHToast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LHToast.swift; sourceTree = ""; }; C09A33212A62D46300B40770 /* LHToastView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LHToastView.swift; sourceTree = ""; }; + C09A564C2B030C070012A7FD /* GetPregnancyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetPregnancyViewModel.swift; sourceTree = ""; }; + C09A564E2B030CB20012A7FD /* GetPregnancyViewModelImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetPregnancyViewModelImpl.swift; sourceTree = ""; }; C0B15E122AC010CD0058D56B /* LHKingfisherService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LHKingfisherService.swift; sourceTree = ""; }; C0B15E142AC011840058D56B /* LHLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LHLabel.swift; sourceTree = ""; }; C0B15E162AC015360058D56B /* LHImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LHImageView.swift; sourceTree = ""; }; @@ -586,7 +602,7 @@ C0DF039E2A5CABC10037F740 /* GetPregnancyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetPregnancyViewController.swift; sourceTree = ""; }; C0DF03A02A5CAC220037F740 /* GetFetalNicknameViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetFetalNicknameViewController.swift; sourceTree = ""; }; C0DF03A32A5CACF00037F740 /* OnboardingPageType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingPageType.swift; sourceTree = ""; }; - C0DF03A52A5CB8610037F740 /* CompleteOnbardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompleteOnbardingViewController.swift; sourceTree = ""; }; + C0DF03A52A5CB8610037F740 /* CompleteOnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompleteOnboardingViewController.swift; sourceTree = ""; }; C0DF03A82A5CF0460037F740 /* UserOnboardingModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserOnboardingModel.swift; sourceTree = ""; }; C0DF03AA2A5D73C80037F740 /* NHOnboardingTextfield.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NHOnboardingTextfield.swift; sourceTree = ""; }; C0F029C62A5EFB9D00E0D185 /* LHProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LHProgressView.swift; sourceTree = ""; }; @@ -995,6 +1011,8 @@ isa = PBXGroup; children = ( C0DF037E2A5A9D090037F740 /* SplashViewController.swift */, + B5E78E122B05D33B00EA67A2 /* SplashViewModel.swift */, + B5E78E142B05D38B00EA67A2 /* SplashViewModelImpl.swift */, ); path = Splash; sourceTree = ""; @@ -1012,6 +1030,7 @@ B59892052A56B34200CE1FEB /* Onboarding */ = { isa = PBXGroup; children = ( + C09A564B2B030BD20012A7FD /* ViewModels */, C09217652A605DEE00231C66 /* OnboardingTextFieldResultType */, C0F029C52A5EFB5B00E0D185 /* OnboardingComponent */, C0DF03A72A5CF0280037F740 /* OnboardingModel */, @@ -1214,7 +1233,7 @@ C0DF037A2A5A9CF30037F740 /* OnboardingViewController.swift */, C0DF039E2A5CABC10037F740 /* GetPregnancyViewController.swift */, C0DF03A02A5CAC220037F740 /* GetFetalNicknameViewController.swift */, - C0DF03A52A5CB8610037F740 /* CompleteOnbardingViewController.swift */, + C0DF03A52A5CB8610037F740 /* CompleteOnboardingViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -1313,8 +1332,6 @@ C065F0082ADBD2640094912C /* TodayViewControllerable.swift */, C065F00A2ADBD2800094912C /* LoginViewControllerable.swift */, C065F00C2ADBD29A0094912C /* OnboardingViewControllerable.swift */, - C065F00E2ADBD2AD0094912C /* CompleteOnbardingViewControllerable.swift */, - C065F0102ADBD2C40094912C /* SplashViewControllerable.swift */, C034EDDD2ADE2F3500AD6FF3 /* ArticleListByCategoryViewControllerable.swift */, ); path = ControllerableInterface; @@ -1384,7 +1401,7 @@ C003CC4A2ADA4F6300AFFAAC /* BookmarkNavigation.swift */, C003CC4C2ADA4F7600AFFAAC /* TodayNavigation.swift */, C003CC4E2ADA4F8D00AFFAAC /* OnboardingNavigation.swift */, - C003CC502ADA4FA600AFFAAC /* CompleteOnbardingNavigation.swift */, + C003CC502ADA4FA600AFFAAC /* CompleteOnboardingNavigation.swift */, C003CC522ADA4FC100AFFAAC /* LoginNavigation.swift */, C003CC542ADA4FD500AFFAAC /* SplashNavigation.swift */, ); @@ -1628,6 +1645,21 @@ path = LHToast; sourceTree = ""; }; + C09A564B2B030BD20012A7FD /* ViewModels */ = { + isa = PBXGroup; + children = ( + C09A564C2B030C070012A7FD /* GetPregnancyViewModel.swift */, + C09A564E2B030CB20012A7FD /* GetPregnancyViewModelImpl.swift */, + B5E78DFF2B03302200EA67A2 /* GetFetalNicknameViewModel.swift */, + B5E78E012B03304F00EA67A2 /* GetFetalNicknameViewModelImpl.swift */, + C06D23CF2B0481F00048AFDB /* OnboardingViewModel.swift */, + C06D23D12B04821B0048AFDB /* OnboardingViewModelImpl.swift */, + 4A3F0C302B05E8E8004BD076 /* CompleteOnboardingViewModelImpl.swift */, + C0138C272B05FAEE002BA766 /* CompleteOnboardingViewModel.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; C0DF03292A5A91450037F740 /* Protocols */ = { isa = PBXGroup; children = ( @@ -1952,7 +1984,7 @@ C003CC412ADA4EE000AFFAAC /* CurriculumListByWeekNavigation.swift in Sources */, C009E9FE2ADBC51400112F18 /* ArticleCategoryViewControllerable.swift in Sources */, C003CC1B2AD9176B00AFFAAC /* Coordinator.swift in Sources */, - C0DF03A62A5CB8610037F740 /* CompleteOnbardingViewController.swift in Sources */, + C0DF03A62A5CB8610037F740 /* CompleteOnboardingViewController.swift in Sources */, C003CC372AD923D100AFFAAC /* ExpireNavigation.swift in Sources */, C0DF034B2A5A9B6A0037F740 /* CurriculumViewController.swift in Sources */, C009E9F42ADBC24A00112F18 /* ChallengeFactoryImpl.swift in Sources */, @@ -1964,6 +1996,7 @@ D342807A2A67F12200DA1499 /* ChallengeDataResponse.swift in Sources */, B57BEB652A6134B800D1727C /* setRootViewController.swift in Sources */, C003CC232AD9183600AFFAAC /* AuthCoordinatorImpl.swift in Sources */, + C0138C282B05FAEE002BA766 /* CompleteOnboardingViewModel.swift in Sources */, C0856B892ABFDA240026D9F8 /* Badge.swift in Sources */, B5C6A2C22A5DEA1B0021BE5E /* CopyRightTableViewCell.swift in Sources */, C0856B7B2ABFC9640026D9F8 /* SplashManagerImpl.swift in Sources */, @@ -1972,6 +2005,7 @@ C0F029E82A5FB9EF00E0D185 /* RoundContainerView.swift in Sources */, B59BFD3F2ADBBF2B005D2D81 /* CurriculumFactory.swift in Sources */, B532E8652A5529F100F0DB19 /* BaseResponse.swift in Sources */, + B5E78E132B05D33B00EA67A2 /* SplashViewModel.swift in Sources */, F4DB30B82A612D2800413EB9 /* CurriculumArticleByWeekTableViewCell.swift in Sources */, C0DF03452A5A9A910037F740 /* CurriculumTableViewCell.swift in Sources */, C07CB81E2A62C5B1000198CC /* TodayArticleView.swift in Sources */, @@ -1979,8 +2013,8 @@ C0B15E1B2AC0169D0058D56B /* LHCollectionView.swift in Sources */, F4490C072A5CEEA300A6D9D7 /* CurriculumDummyData.swift in Sources */, F4DB30AE2A611C7C00413EB9 /* CurriculumListByWeekViewController.swift in Sources */, + C06D23D02B0481F00048AFDB /* OnboardingViewModel.swift in Sources */, B59892EC2A5B94E100CE1FEB /* UIApplication+.swift in Sources */, - C065F0112ADBD2C40094912C /* SplashViewControllerable.swift in Sources */, C0856B652ABFB8640026D9F8 /* TodayManagerImpl.swift in Sources */, 4AE19A1A2A65886100C1DB7E /* BookmarkReponse.swift in Sources */, B5F323E92A6A8F0000047869 /* CurriculumWeekBackgroundDummy.swift in Sources */, @@ -2007,9 +2041,11 @@ C07CB81A2A62C54D000198CC /* TodayModel.swift in Sources */, C06E38212A65351600B00600 /* SignUpRequest.swift in Sources */, C003CC432ADA4EFF00AFFAAC /* MyPageNavigation.swift in Sources */, - C003CC512ADA4FA600AFFAAC /* CompleteOnbardingNavigation.swift in Sources */, + C003CC512ADA4FA600AFFAAC /* CompleteOnboardingNavigation.swift in Sources */, C09A33232A62D46300B40770 /* LHToastView.swift in Sources */, B53F4EF82ADE2FB0001C5752 /* ChallengeCoordinatorImpl.swift in Sources */, + C06D23D22B04821B0048AFDB /* OnboardingViewModelImpl.swift in Sources */, + B5E78E152B05D38B00EA67A2 /* SplashViewModelImpl.swift in Sources */, B532E8632A5529B000F0DB19 /* HTTPHeaderField.swift in Sources */, D34280772A66B90C00DA1499 /* UILabelPadding.swift in Sources */, B598930F2A5BED0F00CE1FEB /* Font.swift in Sources */, @@ -2021,6 +2057,7 @@ C003CC592ADA4FFF00AFFAAC /* TokenState.swift in Sources */, C0F029E22A5FAE2700E0D185 /* LHOnboardingErrorLabel.swift in Sources */, C034EDDC2ADE2A2C00AD6FF3 /* AuthAdaptor.swift in Sources */, + C09A564D2B030C070012A7FD /* GetPregnancyViewModel.swift in Sources */, B57BEB702A6275F500D1727C /* ViewControllerServiceable.swift in Sources */, C0DF032F2A5A92170037F740 /* NameSpace.swift in Sources */, B53F4EFC2ADE3341001C5752 /* BookmarkCoordinatorImpl.swift in Sources */, @@ -2068,6 +2105,7 @@ C003CC1D2AD917CF00AFFAAC /* AppCoordinatorImpl.swift in Sources */, B532E8322A5525C600F0DB19 /* AppDelegate.swift in Sources */, C0856B872ABFD24E0026D9F8 /* CurriculumListManagerImpl.swift in Sources */, + C09A564F2B030CB20012A7FD /* GetPregnancyViewModelImpl.swift in Sources */, C003CC2B2AD9187B00AFFAAC /* ChallengeCoordinator.swift in Sources */, C0F029DE2A5FAC6100E0D185 /* LHOnboardingTitleLabel.swift in Sources */, C0856B7D2ABFCA330026D9F8 /* LoginMangerImpl.swift in Sources */, @@ -2135,7 +2173,7 @@ C0856B732ABFC3070026D9F8 /* BookmarkMangerImpl.swift in Sources */, B57BEB632A612DA100D1727C /* UserDefaultsManager.swift in Sources */, C0DF037B2A5A9CF30037F740 /* OnboardingViewController.swift in Sources */, - C065F00F2ADBD2AD0094912C /* CompleteOnbardingViewControllerable.swift in Sources */, + 4A3F0C312B05E8E8004BD076 /* CompleteOnboardingViewModelImpl.swift in Sources */, C09217682A605DEE00231C66 /* OnboardingFetalNicknameTextFieldResultType.swift in Sources */, B59BFD452ADBC08D005D2D81 /* MyPageControllerable.swift in Sources */, C0F62FCA2A67CDCE0003ADFA /* BookmarkDetailCollectionViewCell.swift in Sources */, @@ -2162,6 +2200,7 @@ C0B15E132AC010CD0058D56B /* LHKingfisherService.swift in Sources */, C009E9F62ADBC41D00112F18 /* BookmarkFactory.swift in Sources */, 4A918E7B2AE11839003CB8F9 /* ArticleAdaptor.swift in Sources */, + B5E78E002B03302200EA67A2 /* GetFetalNicknameViewModel.swift in Sources */, 4A8980CE2A617F7100746C58 /* CollectionHeaderViewRegisterDequeueProtocol.swift in Sources */, 4A860AD62A6265B2002BA428 /* BookmarkModel.swift in Sources */, B5C6A2BE2A5DE6590021BE5E /* GeneralTitleTableViewCell.swift in Sources */, @@ -2177,6 +2216,7 @@ B52C6BB42AF8AE1D008E3B99 /* Combine+.swift in Sources */, B53F4EE72ADBC29F001C5752 /* CurriculumFactoryImpl.swift in Sources */, C0DF03672A5A9C680037F740 /* ArticleDetailViewController.swift in Sources */, + B5E78E022B03304F00EA67A2 /* GetFetalNicknameViewModelImpl.swift in Sources */, C003CC392ADA4D9600AFFAAC /* PopNavigation.swift in Sources */, C003CC4D2ADA4F7600AFFAAC /* TodayNavigation.swift in Sources */, 4AE19A1E2A6597E700C1DB7E /* BookmarkRequest.swift in Sources */, diff --git a/LionHeart-iOS/LionHeart-iOS/Global/Extensions/Combine+.swift b/LionHeart-iOS/LionHeart-iOS/Global/Extensions/Combine+.swift index 178f63ac..730da5ad 100644 --- a/LionHeart-iOS/LionHeart-iOS/Global/Extensions/Combine+.swift +++ b/LionHeart-iOS/LionHeart-iOS/Global/Extensions/Combine+.swift @@ -78,52 +78,3 @@ extension UIButton { .eraseToAnyPublisher() } } - - -extension Publisher { - - func tryAwaitMap(_ transform: @escaping (Self.Output) async throws -> T) -> Publishers.FlatMap, Self> { - flatMap { value in - Future { promise in - Task { - do { - let result = try await transform(value) - promise(.success(result)) - } - catch { - promise(.failure(error as! NetworkError)) - } - } - } - } - } - - func task(maxPublishers: Subscribers.Demand = .unlimited, _ transform: @escaping (Output) async -> T) - -> Publishers.FlatMap>, Self> { - flatMap(maxPublishers: maxPublishers) { value in - Deferred { - Future { promise in - Task { - let output = await transform(value) - promise(.success(output)) - } - } - } - } - } - - func errorTask(maxPublishers: Subscribers.Demand = .unlimited, _ transform: @escaping (Output) async throws -> T) - -> Publishers.FlatMap>, Self> { - flatMap(maxPublishers: maxPublishers) { value in - Deferred { - Future { promise in - Task { - let output = try await transform(value) - promise(.success(output)) - } - } - } - } - } -} - diff --git a/LionHeart-iOS/LionHeart-iOS/Global/UIComponents/LHNavigationBarView.swift b/LionHeart-iOS/LionHeart-iOS/Global/UIComponents/LHNavigationBarView.swift index 47762a3b..ae2fbd41 100644 --- a/LionHeart-iOS/LionHeart-iOS/Global/UIComponents/LHNavigationBarView.swift +++ b/LionHeart-iOS/LionHeart-iOS/Global/UIComponents/LHNavigationBarView.swift @@ -11,14 +11,14 @@ final class LHNavigationBarView: UIView { private let titleLabel = LHLabel(type: .head4, color: .white) - private let leftBarItem: UIButton = { + let leftBarItem: UIButton = { let button = UIButton() button.tintColor = .designSystem(.white) button.marginImageWithText(margin: 14) return button }() - private let rightFirstBarItem: UIButton = { + let rightFirstBarItem: UIButton = { let button = UIButton() button.setImage(ImageLiterals.NavigationBar.bookMark, for: .normal) button.tintColor = .designSystem(.white) @@ -26,7 +26,7 @@ final class LHNavigationBarView: UIView { return button }() - private let rightSecondBarItem: UIButton = { + let rightSecondBarItem: UIButton = { let button = UIButton() button.setImage(ImageLiterals.NavigationBar.profile, for: .normal) button.tintColor = .designSystem(.white) diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Adaptor/AuthAdaptor.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Adaptor/AuthAdaptor.swift index 3183ee28..382447aa 100644 --- a/LionHeart-iOS/LionHeart-iOS/Scenes/Adaptor/AuthAdaptor.swift +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Adaptor/AuthAdaptor.swift @@ -7,7 +7,7 @@ import Foundation -typealias EntireAuthNaviation = LoginNavigation & OnboardingNavigation & CompleteOnbardingNavigation +typealias EntireAuthNaviation = LoginNavigation & OnboardingNavigation & CompleteOnboardingNavigation final class AuthAdaptor: EntireAuthNaviation { @@ -21,7 +21,7 @@ final class AuthAdaptor: EntireAuthNaviation { } func onboardingCompleted(data: UserOnboardingModel) { - self.coordinator.showOnboardingCompleteViewController(data: data) + self.coordinator.showCompleteOnboardingViewController(data: data) } func startButtonTapped() { diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/ControllerableInterface/CompleteOnbardingViewControllerable.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/ControllerableInterface/CompleteOnbardingViewControllerable.swift deleted file mode 100644 index fe191a12..00000000 --- a/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/ControllerableInterface/CompleteOnbardingViewControllerable.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// CompleteOnbardingViewControllerable.swift -// LionHeart-iOS -// -// Created by uiskim on 2023/10/15. -// - -import UIKit - -protocol CompleteOnbardingViewControllerable where Self: UIViewController { - var navigator: CompleteOnbardingNavigation { get set } - var userData: UserOnboardingModel? { get set } -} diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/ControllerableInterface/LoginViewControllerable.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/ControllerableInterface/LoginViewControllerable.swift index d3780fe2..e327fc67 100644 --- a/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/ControllerableInterface/LoginViewControllerable.swift +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/ControllerableInterface/LoginViewControllerable.swift @@ -13,7 +13,3 @@ protocol ViewModel where Self: AnyObject { func transform(input: Input) -> Output } - -protocol LoginViewModelPresentable { - var navigator: LoginNavigation { get set } // Coordinator -} diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/ControllerableInterface/OnboardingViewControllerable.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/ControllerableInterface/OnboardingViewControllerable.swift index 27f3c121..54480da2 100644 --- a/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/ControllerableInterface/OnboardingViewControllerable.swift +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/ControllerableInterface/OnboardingViewControllerable.swift @@ -7,7 +7,7 @@ import UIKit -protocol OnboardingViewControllerable where Self: UIViewController { - var navigator: OnboardingNavigation { get set } - func setKakaoAccessToken(_ token: String?) -} +//protocol OnboardingViewControllerable where Self: UIViewController { +// var navigator: OnboardingNavigation { get set } +// func setKakaoAccessToken(_ token: String?) +//} diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/ControllerableInterface/SplashViewControllerable.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/ControllerableInterface/SplashViewControllerable.swift deleted file mode 100644 index 05fb2b8b..00000000 --- a/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/ControllerableInterface/SplashViewControllerable.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// SplashViewControllerable.swift -// LionHeart-iOS -// -// Created by uiskim on 2023/10/15. -// - -import UIKit - -protocol SplashViewControllerable where Self: UIViewController { - var navigator: SplashNavigation { get set } -} diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/CoordinatorImpl/AuthCoordinatorImpl.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/CoordinatorImpl/AuthCoordinatorImpl.swift index 60b56798..89c96818 100644 --- a/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/CoordinatorImpl/AuthCoordinatorImpl.swift +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/CoordinatorImpl/AuthCoordinatorImpl.swift @@ -34,9 +34,8 @@ final class AuthCoordinatorImpl: AuthCoordinator { splashCoorinator?.showTabbarViewContoller() } - func showOnboardingCompleteViewController(data: UserOnboardingModel) { - let completeViewController = factory.makeCompleteOnbardingViewController(coordinator: self) - completeViewController.userData = data + func showCompleteOnboardingViewController(data: UserOnboardingModel) { + let completeViewController = factory.makeCompleteOnboardingViewController(coordinator: self, data: data) self.navigationController.pushViewController(completeViewController, animated: true) } @@ -46,12 +45,10 @@ final class AuthCoordinatorImpl: AuthCoordinator { case .verified: splashCoorinator?.showTabbarViewContoller() case .nonVerified: - let onboardingViewController = factory.makeOnboardingViewController(coordinator: self) - onboardingViewController.setKakaoAccessToken(kakaoToken) + let onboardingViewController = factory.makeOnboardingViewController(token: kakaoToken, coordinator: self) DispatchQueue.main.async { self.navigationController.pushViewController(onboardingViewController, animated: true) } - } } diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/CoordinatorInterface/AuthCoordinator.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/CoordinatorInterface/AuthCoordinator.swift index 7f791f12..8fb6e275 100644 --- a/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/CoordinatorInterface/AuthCoordinator.swift +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Coordinator/CoordinatorInterface/AuthCoordinator.swift @@ -10,7 +10,7 @@ import Foundation protocol AuthCoordinator: Coordinator { func showLoginViewController() func showTabbarController() - func showOnboardingCompleteViewController(data: UserOnboardingModel) + func showCompleteOnboardingViewController(data: UserOnboardingModel) func showTabbarOrOnboardingViewController(userState: UserState, kakaoToken: String?) func pop() func exitApplication() diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Factory/FactoryImpl/AuthFactoryImpl.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Factory/FactoryImpl/AuthFactoryImpl.swift index a7c964ac..4a7b8e63 100644 --- a/LionHeart-iOS/LionHeart-iOS/Scenes/Factory/FactoryImpl/AuthFactoryImpl.swift +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Factory/FactoryImpl/AuthFactoryImpl.swift @@ -8,10 +8,23 @@ import UIKit struct AuthFactoryImpl: AuthFactory { + func makeCompleteOnboardingViewModel(coordinator: AuthCoordinator, data: UserOnboardingModel) -> any CompleteOnboardingViewModel & CompleteOnboardingViewModelPresentable { + let adaptor = self.makeAuthAdaptor(coordinator: coordinator) + let viewModel = CompleteOnboardingViewModelImpl(navigator: adaptor) + viewModel.setUserData(data) + return viewModel + } + func makeOnboardingViewModel(coordinator: AuthCoordinator) -> any OnboardingViewModel & OnboardingViewModelPresentable { + let adaptor = self.makeAuthAdaptor(coordinator: coordinator) + let apiService = APIService() + let serviceImpl = AuthServiceImpl(apiService: apiService) + let managerImpl = OnboardingManagerImpl(authService: serviceImpl) + return OnboardingViewModelImpl(navigator: adaptor, manager: managerImpl) + } + func makeLoginViewModel(coordinator: AuthCoordinator) -> any LoginViewModel & LoginViewModelPresentable { let adaptor = self.makeAuthAdaptor(coordinator: coordinator) - let apiService = APIService() let serviceImpl = AuthServiceImpl(apiService: apiService) let managerImpl = LoginMangerImpl(authService: serviceImpl) @@ -22,21 +35,20 @@ struct AuthFactoryImpl: AuthFactory { return AuthAdaptor(coordinator: coordinator) } - func makeLoginViewController(coordinator: AuthCoordinator) -> LoginViewController { + func makeLoginViewController(coordinator: AuthCoordinator) -> LoginViewControllerable { let viewModel = self.makeLoginViewModel(coordinator: coordinator) return LoginViewController(viewModel: viewModel) } - func makeCompleteOnbardingViewController(coordinator: AuthCoordinator) -> CompleteOnbardingViewControllerable { - let adaptor = self.makeAuthAdaptor(coordinator: coordinator) - let completeViewController = CompleteOnbardingViewController(navigator: adaptor) - return completeViewController + func makeCompleteOnboardingViewController(coordinator: AuthCoordinator, data: UserOnboardingModel) -> CompleteOnboardingViewControllerable { + let viewModel = self.makeCompleteOnboardingViewModel(coordinator: coordinator, data: data) + return CompleteOnboardingViewController(viewModel: viewModel) } - - func makeOnboardingViewController(coordinator: AuthCoordinator) -> OnboardingViewControllerable { - let onboardingViewController = OnboardingViewController(manager: OnboardingManagerImpl(authService: AuthServiceImpl(apiService: APIService())), navigator: self.makeAuthAdaptor(coordinator: coordinator)) - return onboardingViewController + + func makeOnboardingViewController(token: String?, coordinator: AuthCoordinator) -> OnboardingViewControllerable { + let viewModel = self.makeOnboardingViewModel(coordinator: coordinator) + viewModel.setKakaoAccessToken(token) + return OnboardingViewController(viewModel: viewModel) } - } diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Factory/FactoryImpl/SplashFactoryImpl.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Factory/FactoryImpl/SplashFactoryImpl.swift index 95aff7d3..7d31f6c2 100644 --- a/LionHeart-iOS/LionHeart-iOS/Scenes/Factory/FactoryImpl/SplashFactoryImpl.swift +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Factory/FactoryImpl/SplashFactoryImpl.swift @@ -8,11 +8,20 @@ import UIKit struct SplashFactoryImpl: SplashFactory { + func makeSplashViewModel(coordinator: SplashCoordinator) -> any SplashViewModel & SplashViewModelPresentable { + let adaptor = self.makeSplashAdaptor(coordinator: coordinator) + let apiService = APIService() + let serviceImpl = AuthServiceImpl(apiService: apiService) + let managerImpl = SplashManagerImpl(authService: serviceImpl) + return SplashViewModelImpl(navigator: adaptor, manager: managerImpl) + } + func makeSplashAdaptor(coordinator: SplashCoordinator) -> EntireSplashNavigation { return SplashAdaptor(coordinator: coordinator) } func makeSplashViewController(coordinator: SplashCoordinator) -> SplashViewControllerable { - return SplashViewController(manager: SplashManagerImpl(authService: AuthServiceImpl(apiService: APIService())), adaptor: self.makeSplashAdaptor(coordinator: coordinator)) + let viewModel = self.makeSplashViewModel(coordinator: coordinator) + return SplashViewController(viewModel: viewModel) } } diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Factory/FactoryInterface/AuthFactory.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Factory/FactoryInterface/AuthFactory.swift index e661d985..0c902654 100644 --- a/LionHeart-iOS/LionHeart-iOS/Scenes/Factory/FactoryInterface/AuthFactory.swift +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Factory/FactoryInterface/AuthFactory.swift @@ -8,14 +8,12 @@ import Foundation protocol AuthFactory { - func makeLoginViewModel(coordinator: AuthCoordinator) -> any LoginViewModel & LoginViewModelPresentable + func makeOnboardingViewModel(coordinator: AuthCoordinator) -> any OnboardingViewModel & OnboardingViewModelPresentable + func makeCompleteOnboardingViewModel(coordinator: AuthCoordinator, data: UserOnboardingModel) -> any CompleteOnboardingViewModel & CompleteOnboardingViewModelPresentable func makeAuthAdaptor(coordinator: AuthCoordinator) -> EntireAuthNaviation - func makeLoginViewController(coordinator: AuthCoordinator) -> LoginViewController - - func makeCompleteOnbardingViewController(coordinator: AuthCoordinator) -> CompleteOnbardingViewControllerable - func makeOnboardingViewController(coordinator: AuthCoordinator) -> OnboardingViewControllerable + func makeLoginViewController(coordinator: AuthCoordinator) -> LoginViewControllerable + func makeCompleteOnboardingViewController(coordinator: AuthCoordinator, data: UserOnboardingModel) -> CompleteOnboardingViewControllerable + func makeOnboardingViewController(token: String?, coordinator: AuthCoordinator) -> OnboardingViewControllerable } - - diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Factory/FactoryInterface/SpalshFactory.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Factory/FactoryInterface/SpalshFactory.swift index 7f67370d..6c556c0e 100644 --- a/LionHeart-iOS/LionHeart-iOS/Scenes/Factory/FactoryInterface/SpalshFactory.swift +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Factory/FactoryInterface/SpalshFactory.swift @@ -8,6 +8,7 @@ import UIKit protocol SplashFactory { + func makeSplashViewModel(coordinator: SplashCoordinator) -> any SplashViewModel & SplashViewModelPresentable func makeSplashAdaptor(coordinator: SplashCoordinator) -> EntireSplashNavigation func makeSplashViewController(coordinator: SplashCoordinator) -> SplashViewControllerable } diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Login/LoginViewController.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Login/LoginViewController.swift index 9e713a24..4527c30d 100644 --- a/LionHeart-iOS/LionHeart-iOS/Scenes/Login/LoginViewController.swift +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Login/LoginViewController.swift @@ -10,8 +10,9 @@ import UIKit import SnapKit import Combine +protocol LoginViewControllerable where Self: UIViewController {} -final class LoginViewController: UIViewController { +final class LoginViewController: UIViewController, LoginViewControllerable { // MARK: - Properties diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Login/LoginViewModel.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Login/LoginViewModel.swift index a4983d3d..a8f78f52 100644 --- a/LionHeart-iOS/LionHeart-iOS/Scenes/Login/LoginViewModel.swift +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Login/LoginViewModel.swift @@ -8,6 +8,8 @@ import Foundation import Combine +protocol LoginViewModelPresentable {} + protocol LoginViewModel: ViewModel where Input == LoginViewModelInput, Output == LoginViewModelOutput {} struct LoginViewModelInput { diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/NavigationInterface/CompleteOnbardingNavigation.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/NavigationInterface/CompleteOnboardingNavigation.swift similarity index 54% rename from LionHeart-iOS/LionHeart-iOS/Scenes/NavigationInterface/CompleteOnbardingNavigation.swift rename to LionHeart-iOS/LionHeart-iOS/Scenes/NavigationInterface/CompleteOnboardingNavigation.swift index 9b1d6250..559c5e9c 100644 --- a/LionHeart-iOS/LionHeart-iOS/Scenes/NavigationInterface/CompleteOnbardingNavigation.swift +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/NavigationInterface/CompleteOnboardingNavigation.swift @@ -1,5 +1,5 @@ // -// CompleteOnbardingNavigation.swift +// CompleteOnbordingNavigation.swift // LionHeart-iOS // // Created by uiskim on 2023/10/14. @@ -7,6 +7,6 @@ import Foundation -protocol CompleteOnbardingNavigation: ExpireNavigation { +protocol CompleteOnboardingNavigation: ExpireNavigation { func startButtonTapped() } diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/OnboardingPageType/OnboardingPageType.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/OnboardingPageType/OnboardingPageType.swift index 44e2d998..fab8824c 100644 --- a/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/OnboardingPageType/OnboardingPageType.swift +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/OnboardingPageType/OnboardingPageType.swift @@ -17,24 +17,3 @@ enum OnbardingFlowType: Int { case toFetalNickname case toCompleteOnboarding } - -extension OnboardingPageType { - - var progressValue: Float { - switch self { - case .getPregnancy: - return .half - case .getFetalNickname: - return .full - } - } - - var forward: OnbardingFlowType { - switch self { - case .getPregnancy: - return .toFetalNickname - case .getFetalNickname: - return .toCompleteOnboarding - } - } -} diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewControllers/CompleteOnbardingViewController.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewControllers/CompleteOnboardingViewController.swift similarity index 59% rename from LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewControllers/CompleteOnbardingViewController.swift rename to LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewControllers/CompleteOnboardingViewController.swift index 64d3dec0..2e0ff173 100644 --- a/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewControllers/CompleteOnbardingViewController.swift +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewControllers/CompleteOnboardingViewController.swift @@ -1,5 +1,5 @@ // -// CompleteOnbardingViewController.swift +// CompleteOnboardingViewController.swift // LionHeart-iOS // // Created by uiskim on 2023/07/11. @@ -7,23 +7,22 @@ // import UIKit +import Combine import SnapKit -final class CompleteOnbardingViewController: UIViewController, CompleteOnbardingViewControllerable { +protocol CompleteOnboardingViewControllerable where Self: UIViewController { } + +final class CompleteOnboardingViewController: UIViewController, CompleteOnboardingViewControllerable { private enum SizeInspector { static let sideOffset: CGFloat = 58 } - var userData: UserOnboardingModel? { - didSet { - guard let fetalNickName = userData?.fetalNickname else { return } - self.titleLabel.text = "\(fetalNickName)님\n반가워요!" - } - } - - var navigator: CompleteOnbardingNavigation + private var cancelBag = Set() + private let viewModel: any CompleteOnboardingViewModel + private let startButtonTapped = PassthroughSubject() + private let viewWillAppearSubject = PassthroughSubject() private let titleLabel = LHOnboardingTitleLabel(nil, align: .center) private let descriptionLabel = LHOnboardingDescriptionLabel("아티클 맞춤 환경이 준비되었어요.") @@ -37,8 +36,8 @@ final class CompleteOnbardingViewController: UIViewController, CompleteOnbarding return imageView }() - init(navigator: CompleteOnbardingNavigation) { - self.navigator = navigator + init(viewModel: some CompleteOnboardingViewModel) { + self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } @@ -52,11 +51,37 @@ final class CompleteOnbardingViewController: UIViewController, CompleteOnbarding setUI() setHierarchy() setLayout() - setButtonAction() + bind() + bindInput() + } + + override func viewWillAppear(_ animated: Bool) { + viewWillAppearSubject.send(()) + } + + private func bind() { + let input = CompleteOnboardingViewModelInput(startButtonTapped: startButtonTapped, + viewWillAppear: viewWillAppearSubject) + let output = viewModel.transform(input: input) + + output.fetalNickname + .sink { [weak self] fetalNickname in + guard let fetalNickname = fetalNickname else { return } + self?.titleLabel.text = "\(fetalNickname)님\n반가워요!" + } + .store(in: &cancelBag) + } + + private func bindInput() { + startButton.tapPublisher + .sink { [weak self] in + self?.startButtonTapped.send(()) + } + .store(in: &cancelBag) } } -private extension CompleteOnbardingViewController { +private extension CompleteOnboardingViewController { func setUI() { view.backgroundColor = .designSystem(.background) } @@ -88,10 +113,4 @@ private extension CompleteOnbardingViewController { make.height.equalTo(50) } } - - func setButtonAction() { - startButton.addButtonAction { sender in - self.navigator.startButtonTapped() - } - } } diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewControllers/GetFetalNicknameViewController.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewControllers/GetFetalNicknameViewController.swift index 879a6435..4739db71 100644 --- a/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewControllers/GetFetalNicknameViewController.swift +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewControllers/GetFetalNicknameViewController.swift @@ -7,35 +7,70 @@ // import UIKit +import Combine import SnapKit -protocol FetalNicknameCheckDelegate: AnyObject { - func checkFetalNickname(resultType: OnboardingFetalNicknameTextFieldResultType) - func sendFetalNickname(nickName: String) -} final class GetFetalNicknameViewController: UIViewController { - weak var delegate: FetalNicknameCheckDelegate? + let fetalIsValid = PassthroughSubject<(fetalNickname: String, isValid:Bool), Never>() + + private let viewModel: any GetFetalNicknameViewModel + + private let fetalTextfieldDidChanged = PassthroughSubject() + + private var cancelBag = Set() + private let titleLabel = LHOnboardingTitleLabel("태명을 정하셨나요?", align: .left) private let descriptionLabel = LHOnboardingDescriptionLabel("아직이라면, 닉네임을 적어주세요.") private let fetalNickNameErrorLabel = LHOnboardingErrorLabel() private let fetalNickNameTextfield = NHOnboardingTextfield(textFieldType: .fetalNickname) private let textFieldUnderLine = LHUnderLine(lineColor: .lionRed) - + init(viewModel: some GetFetalNicknameViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + public override func viewDidLoad() { super.viewDidLoad() setUI() setHierarchy() setLayout() setTextField() + bindInput() + bind() } override func viewWillAppear(_ animated: Bool) { fetalNickNameTextfield.becomeFirstResponder() } + + private func bindInput() { + fetalNickNameTextfield.textPublisher + .sink { [weak self] in + self?.fetalTextfieldDidChanged.send($0) + } + .store(in: &cancelBag) + } + + private func bind() { + let input = GetFetalNicknameViewModelInput(fetalTextfieldDidChanged: fetalTextfieldDidChanged) + let output = viewModel.transform(input: input) + + output.fetalTextfieldValidationMessage + .sink { [weak self] in + self?.fetalNickNameErrorLabel.text = $0.validationMessage + self?.fetalIsValid.send(($0.fetalNickName, $0.isHidden)) + } + .store(in: &cancelBag) + + } } private extension GetFetalNicknameViewController { @@ -77,43 +112,9 @@ private extension GetFetalNicknameViewController { } func setTextField() { - fetalNickNameTextfield.delegate = self if let clearButton = fetalNickNameTextfield.value(forKeyPath: "_clearButton") as? UIButton { clearButton.setImage(.assetImage(.textFieldClear), for: .normal) } self.fetalNickNameTextfield.clearButtonMode = UITextField.ViewMode.whileEditing } } - -extension GetFetalNicknameViewController: UITextFieldDelegate { - func textFieldDidChangeSelection(_ textField: UITextField) { - guard let text = textField.text else { return } - if text.count == 0 { - textFieldSettingWhenEmpty() - } else if text.count <= 10 { - textFieldSettingWhenValid() - } else { - textFieldSettingWhenOver() - } - delegate?.sendFetalNickname(nickName: text) - } -} - -private extension GetFetalNicknameViewController { - func textFieldSettingWhenEmpty() { - delegate?.checkFetalNickname(resultType: .fetalNicknameTextFieldEmpty) - fetalNickNameErrorLabel.text = OnboardingFetalNicknameTextFieldResultType.fetalNicknameTextFieldEmpty.errorMessage - fetalNickNameErrorLabel.isHidden = false - } - - func textFieldSettingWhenValid() { - delegate?.checkFetalNickname(resultType: .fetalNicknameTextFieldValid) - fetalNickNameErrorLabel.isHidden = true - } - - func textFieldSettingWhenOver() { - delegate?.checkFetalNickname(resultType: .fetalNicknameTextFieldOver) - fetalNickNameErrorLabel.text = OnboardingFetalNicknameTextFieldResultType.fetalNicknameTextFieldOver.errorMessage - fetalNickNameErrorLabel.isHidden = false - } -} diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewControllers/GetPregnancyViewController.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewControllers/GetPregnancyViewController.swift index f0c406f5..43302c2a 100644 --- a/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewControllers/GetPregnancyViewController.swift +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewControllers/GetPregnancyViewController.swift @@ -7,17 +7,18 @@ // import UIKit +import Combine import SnapKit -protocol PregnancyCheckDelegate: AnyObject { - func checkPregnancy(resultType: OnboardingPregnancyTextFieldResultType) - func sendPregnancyContent(pregnancy: Int) -} final class GetPregnancyViewController: UIViewController { - weak var delegate: PregnancyCheckDelegate? + var pregnancyIsValid = PassthroughSubject<(pregnancy: Int, isValid: Bool), Never>() + + private var cancelBag = Set() + private var pregancyTextfieldDidChanged = PassthroughSubject() + private var viewModel: any GetPregnancyViewModel private let titleLabel = LHOnboardingTitleLabel("현재 임신 주수를\n알려주세요", align: .left) private let descriptionLabel = LHOnboardingDescriptionLabel("시기별 맞춤 아티클을 전해드려요") private let pregnancyTextfield = NHOnboardingTextfield(textFieldType: .pregancy) @@ -33,6 +34,17 @@ final class GetPregnancyViewController: UIViewController { setLayout() setDelegate() setTextField() + bindInput() + bind() + } + + init(viewModel: some GetPregnancyViewModel) { + self.viewModel = viewModel + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } override func viewWillAppear(_ animated: Bool) { @@ -46,7 +58,6 @@ private extension GetPregnancyViewController { } func setHierarchy() { - userInputContainerView.addSubviews(pregnancyTextfield, fixedWeekLabel) roundRectView.addSubviews(userInputContainerView) view.addSubviews(titleLabel, descriptionLabel, roundRectView, pregnancyErrorLabel) @@ -91,7 +102,6 @@ private extension GetPregnancyViewController { make.top.equalTo(roundRectView.snp.bottom).offset(8) make.leading.equalTo(roundRectView.snp.leading) } - } func setDelegate() { @@ -103,32 +113,25 @@ private extension GetPregnancyViewController { pregnancyTextfield.keyboardType = .numberPad } + func bindInput() { + pregnancyTextfield.textPublisher + .sink { [weak self] in self?.pregancyTextfieldDidChanged.send($0) } + .store(in: &cancelBag) + } + + func bind() { + let input = GetPregnancyViewModelInput(pregancyTextfieldDidChanged: pregancyTextfieldDidChanged) + let output = viewModel.transform(input: input) + output.pregancyTextfieldValidationMessage + .sink { [weak self] in + self?.pregnancyErrorLabel.text = $0.validationMessage + self?.pregnancyIsValid.send(($0.pregnancy, $0.isHidden)) + } + .store(in: &cancelBag) + } } extension GetPregnancyViewController: UITextFieldDelegate { - func textFieldDidChangeSelection(_ textField: UITextField) { - guard let text = textField.text else { return } - - /// textField의 text의 갯수에 따른 로직 - textField.placeholder = text.count == 0 ? "1~40" : "" - if text.count == 0 { - textFieldSettingWhenEmpty() - } - - /// textField의 text의 숫자에 따른 로직 - guard let textNumber = Int(text) else { return } - if textNumber == 0 { - textFieldSettingWhenInputNumberZero() - } else if 4 <= textNumber && textNumber <= 40 { - textFieldSettingWhenInputNumberValid() - } else { - textFieldSettingWhenInpubNumberOver() - } - - /// 현재 textField에 text를 PageViewController로 보내주는 delegate - delegate?.sendPregnancyContent(pregnancy: textNumber) - } - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if let char = string.cString(using: String.Encoding.utf8) { let isBackSpace = strcmp(char, "\\b") @@ -137,27 +140,4 @@ extension GetPregnancyViewController: UITextFieldDelegate { guard let text = textField.text else { return false } return text.count >= 2 ? false : true } - -} - -extension GetPregnancyViewController { - func textFieldSettingWhenEmpty() { - delegate?.checkPregnancy(resultType: .pregnancyTextFieldEmpty) - pregnancyErrorLabel.text = OnboardingPregnancyTextFieldResultType.pregnancyTextFieldEmpty.errorMessage - } - - func textFieldSettingWhenInputNumberZero() { - delegate?.checkPregnancy(resultType: .pregnancyTextFieldEmpty) - pregnancyErrorLabel.text = OnboardingPregnancyTextFieldResultType.pregnancyTextFieldOver.errorMessage - } - - func textFieldSettingWhenInputNumberValid() { - delegate?.checkPregnancy(resultType: .pregnancyTextFieldValid) - pregnancyErrorLabel.text = "" - } - - func textFieldSettingWhenInpubNumberOver() { - delegate?.checkPregnancy(resultType: .pregnancyTextFieldOver) - pregnancyErrorLabel.text = OnboardingPregnancyTextFieldResultType.pregnancyTextFieldOver.errorMessage - } } diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewControllers/OnboardingViewController.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewControllers/OnboardingViewController.swift index 899cfae4..b8f18636 100644 --- a/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewControllers/OnboardingViewController.swift +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewControllers/OnboardingViewController.swift @@ -7,46 +7,30 @@ // import UIKit +import Combine import SnapKit -final class OnboardingViewController: UIViewController, OnboardingViewControllerable { +protocol OnboardingViewControllerable where Self: UIViewController {} + +final class OnboardingViewController: UIViewController, OnboardingViewControllerable { typealias OnboardingViews = [UIViewController] - - var navigator: OnboardingNavigation - private let manager: OnboardingManager - - private var fetalNickName: String? - private var pregnancy: Int? - private var kakaoAccessToken: String? + + private let pregenacy = CurrentValueSubject<(pregnancy: Int, isValid: Bool), Never>((pregnancy: 0, isValid: true)) + private let fetalNickname = CurrentValueSubject<(fetalNickname: String, isValid: Bool), Never>((fetalNickname: "", isValid: true)) + private let backButtonTapped = PassthroughSubject() + private let nextButtonTapped = PassthroughSubject() + private var cancelBag = Set() private let nextButton = LHOnboardingButton() private let onboardingProgressView = LHProgressView() private let onboardingViewController = LHOnboardingPageViewController() private var pageDataSource: OnboardingViews = [] private lazy var onboardingNavigationbar = LHNavigationBarView(type: .onboarding, viewController: self) - - private var currentPage: OnboardingPageType = .getPregnancy - private var onboardingFlow: OnbardingFlowType = .toGetPregnacny { - didSet { - switch onboardingFlow { - case .toGetPregnacny, .toFetalNickname: - presentOnboardingView(oldValue: onboardingFlow) - case .toCompleteOnboarding: - presentCompleteOnboardingView() - } - } - } - - private var onboardingCompletePercentage: Float = 0 { - didSet { - fillProgressView(from: onboardingCompletePercentage) - } - } + private var viewModel: any OnboardingViewModel - init(manager: OnboardingManager, navigator: OnboardingNavigation) { - self.manager = manager - self.navigator = navigator + init(viewModel: some OnboardingViewModel) { + self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } @@ -63,11 +47,48 @@ final class OnboardingViewController: UIViewController, OnboardingViewController setProgressView() setHierarchy() setLayout() - setAddTarget() + bindInput() + bind() } - func setKakaoAccessToken(_ token: String?) { - self.kakaoAccessToken = token + func bindInput() { + nextButton.tapPublisher + .sink { [weak self] in self?.nextButtonTapped.send(()) } + .store(in: &cancelBag) + + onboardingNavigationbar.leftBarItem.tapPublisher + .sink { [weak self] in self?.backButtonTapped.send(()) } + .store(in: &cancelBag) + } + + func bind() { + let input = OnboardingViewModelInput(pregenacy: pregenacy, fetalNickname: fetalNickname, backButtonTapped: backButtonTapped, nextButtonTapped: nextButtonTapped) + let output = viewModel.transform(input: input) + + output.fetalButtonState + .sink { [ weak self ] in self?.nextButton.isHidden = $0 } + .store(in: &cancelBag) + + output.pregenacyButtonState + .sink { [ weak self ] in self?.nextButton.isHidden = $0 } + .store(in: &cancelBag) + + output.onboardingFlow + .sink { [weak self] in + if $0 == .toCompleteOnboarding { + self?.nextButton.isUserInteractionEnabled = false + } + self?.nextButton.isHidden = true + self?.presentOnboardingView() + self?.fillProgressView(from: 1) + } + .store(in: &cancelBag) + + output.signUpSubject + .sink { errorMessage in + print(errorMessage) + } + .store(in: &cancelBag) } } @@ -105,7 +126,6 @@ private extension OnboardingViewController { make.leading.trailing.equalToSuperview() } - nextButton.snp.makeConstraints { make in make.bottom.equalTo(view.keyboardLayoutGuide.snp.top) make.leading.trailing.equalToSuperview() @@ -117,31 +137,17 @@ private extension OnboardingViewController { make.leading.trailing.equalToSuperview() } } - - func setAddTarget() { - nextButton.addButtonAction { _ in - guard let fetalNickName = self.fetalNickName else { - self.nextOnboardingProcessWithNonActiveButtonState() - return - } - self.nextOnboaringProcess(nickName: fetalNickName, minCount: 1, maxCount: 10) - } - - onboardingNavigationbar.backButtonAction { - if self.currentPage == .getPregnancy { - self.navigator.backButtonTapped() - } else { - self.backOnboardingProcess() - } - } - } - + func setChildViewController() { - let pregnancyViewController = GetPregnancyViewController() - pregnancyViewController.delegate = self + let pregnancyViewController = GetPregnancyViewController(viewModel: GetPregnancyViewModelImpl()) + pregnancyViewController.pregnancyIsValid + .sink { [weak self] in self?.pregenacy.send($0) } + .store(in: &cancelBag) pageDataSource.append(pregnancyViewController) - let fetalNicknameViewController = GetFetalNicknameViewController() - fetalNicknameViewController.delegate = self + let fetalNicknameViewController = GetFetalNicknameViewController(viewModel: GetFetalNicknameViewModelImpl()) + fetalNicknameViewController.fetalIsValid + .sink { [weak self] in self?.fetalNickname.send($0) } + .store(in: &cancelBag) pageDataSource.append(fetalNicknameViewController) } @@ -157,103 +163,7 @@ private extension OnboardingViewController { } } - func presentLoginView() { - self.navigationController?.popViewController(animated: true) - self.navigator.backButtonTapped() - } - - func presentOnboardingView(oldValue: OnbardingFlowType) { - onboardingViewController.setViewControllers([pageDataSource[onboardingFlow.rawValue]], - direction: oldValue.rawValue > onboardingFlow.rawValue ? .reverse : .forward, - animated: false) { _ in - guard let currentPage = OnboardingPageType(rawValue: self.onboardingFlow.rawValue) else { return } - self.currentPage = currentPage - } - } - - func presentCompleteOnboardingView() { - self.view.endEditing(true) - self.nextButton.isUserInteractionEnabled = false - let passingData = UserOnboardingModel(kakaoAccessToken: self.kakaoAccessToken, pregnacny: self.pregnancy, fetalNickname: self.fetalNickName) - Task { - showLoading() - do { - try await manager.signUp(type: .kakao, onboardingModel: passingData) - hideLoading() - self.navigator.onboardingCompleted(data: passingData) - } catch { - guard let error = error as? NetworkError else { return } - handleError(error) - } - - } - } -} - -private extension OnboardingViewController { - func nextOnboaringProcess(nickName: String, minCount: Int, maxCount: Int) { - self.nextButton.isHidden = nickName.count >= minCount && nickName.count <= maxCount ? false : true - self.onboardingFlow = self.currentPage.forward - self.onboardingCompletePercentage = self.currentPage.progressValue - } - - func nextOnboardingProcessWithNonActiveButtonState() { - self.nextButton.isHidden = true - self.onboardingFlow = self.currentPage.forward - self.onboardingCompletePercentage = self.currentPage.progressValue - } - - func backOnboardingProcess() { - self.view.endEditing(true) - self.nextButton.isHidden = false - self.onboardingFlow = .toGetPregnacny - self.onboardingCompletePercentage = self.currentPage.progressValue - } -} - -extension OnboardingViewController: FetalNicknameCheckDelegate { - func sendFetalNickname(nickName: String) { - self.fetalNickName = nickName - } - - func checkFetalNickname(resultType: OnboardingFetalNicknameTextFieldResultType) { - nextButton.isHidden = resultType.isHidden + func presentOnboardingView(newValue: OnbardingFlowType = .toFetalNickname) { + onboardingViewController.setViewControllers([pageDataSource[newValue.rawValue]], direction: .forward, animated: false) } } - -extension OnboardingViewController: PregnancyCheckDelegate { - func sendPregnancyContent(pregnancy: Int) { - self.pregnancy = pregnancy - } - - func checkPregnancy(resultType: OnboardingPregnancyTextFieldResultType) { - nextButton.isHidden = resultType.isHidden - } -} - -//extension OnboardingViewController: ViewControllerServiceable { -// func handleError(_ error: NetworkError) { -// switch error { -// case .urlEncodingError: -// print("✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅") -//// LHToast.show(message: "인코딩에러") -// case .jsonDecodingError: -// print("✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅") -//// LHToast.show(message: "디코딩에러") -// case .badCasting: -// print("✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅") -//// LHToast.show(message: "배드캐스트") -// case .fetchImageError: -// print("✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅") -//// LHToast.show(message: "이미지패치에러") -// case .unAuthorizedError: -// navigator.checkTokenIsExpired() -// case .clientError(_, let message): -// print("✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅") -//// LHToast.show(message: message) -// case .serverError: -// print("✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅") -//// LHToast.show(message: error.description) -// } -// } -//} diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/CompleteOnboardingViewModel.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/CompleteOnboardingViewModel.swift new file mode 100644 index 00000000..0903be6d --- /dev/null +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/CompleteOnboardingViewModel.swift @@ -0,0 +1,24 @@ +// +// CompleteOnboardingViewModel.swift +// LionHeart-iOS +// +// Created by uiskim on 2023/11/16. +// + +import Foundation +import Combine + +protocol CompleteOnboardingViewModelPresentable { + func setUserData(_ userData: UserOnboardingModel) +} + +protocol CompleteOnboardingViewModel: ViewModel where Input == CompleteOnboardingViewModelInput, Output == CompleteOnboardingViewModelOutput {} + +struct CompleteOnboardingViewModelInput { + let startButtonTapped: PassthroughSubject + let viewWillAppear: PassthroughSubject +} + +struct CompleteOnboardingViewModelOutput { + let fetalNickname: AnyPublisher +} diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/CompleteOnboardingViewModelImpl.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/CompleteOnboardingViewModelImpl.swift new file mode 100644 index 00000000..1c999387 --- /dev/null +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/CompleteOnboardingViewModelImpl.swift @@ -0,0 +1,51 @@ +// +// CompleteOnboardingViewModelImpl.swift +// LionHeart-iOS +// +// Created by 황찬미 on 2023/11/16. +// + +import Foundation +import Combine + +final class CompleteOnboardingViewModelImpl: CompleteOnboardingViewModel { + + private var userData: UserOnboardingModel? + private var navigator: CompleteOnboardingNavigation + private var navigatorSubject = PassthroughSubject() + private var cancelBag = Set() + + init(navigator: CompleteOnboardingNavigation) { + self.navigator = navigator + } + + func transform(input: CompleteOnboardingViewModelInput) -> CompleteOnboardingViewModelOutput { + + navigatorSubject + .receive(on: RunLoop.main) + .sink { _ in + self.navigator.startButtonTapped() + } + .store(in: &cancelBag) + + let fetalNickname = input.viewWillAppear + .map { _ in + return self.userData?.fetalNickname + } + .eraseToAnyPublisher() + + input.startButtonTapped + .sink { _ in + self.navigatorSubject.send(()) + } + .store(in: &cancelBag) + + return CompleteOnboardingViewModelOutput(fetalNickname: fetalNickname) + } +} + +extension CompleteOnboardingViewModelImpl: CompleteOnboardingViewModelPresentable { + func setUserData(_ userData: UserOnboardingModel) { + self.userData = userData + } +} diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/GetFetalNicknameViewModel.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/GetFetalNicknameViewModel.swift new file mode 100644 index 00000000..7b07bd42 --- /dev/null +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/GetFetalNicknameViewModel.swift @@ -0,0 +1,19 @@ +// +// GetFetalNicknameViewModel.swift +// LionHeart-iOS +// +// Created by 김민재 on 11/14/23. +// + +import Foundation +import Combine + +protocol GetFetalNicknameViewModel: ViewModel where Input == GetFetalNicknameViewModelInput, Output == GetFetalNicknameViewModelOutput {} + +struct GetFetalNicknameViewModelInput { + let fetalTextfieldDidChanged: PassthroughSubject +} + +struct GetFetalNicknameViewModelOutput { + let fetalTextfieldValidationMessage: AnyPublisher<(fetalNickName: String, validationMessage: String, isHidden: Bool), Never> +} diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/GetFetalNicknameViewModelImpl.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/GetFetalNicknameViewModelImpl.swift new file mode 100644 index 00000000..56ef1a0c --- /dev/null +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/GetFetalNicknameViewModelImpl.swift @@ -0,0 +1,32 @@ +// +// GetFetalNicknameViewModelImpl.swift +// LionHeart-iOS +// +// Created by 김민재 on 11/14/23. +// + +import Foundation +import Combine + +final class GetFetalNicknameViewModelImpl: GetFetalNicknameViewModel { + func transform(input: GetFetalNicknameViewModelInput) -> GetFetalNicknameViewModelOutput { + let pregancyTextfieldValidationMessage = input.fetalTextfieldDidChanged + .map { text in + var isHidden = false + var validationMessage = "" + if text.count == 0 { + validationMessage = OnboardingFetalNicknameTextFieldResultType.fetalNicknameTextFieldEmpty.errorMessage + isHidden = true + } else if text.count <= 10 { + isHidden = false + } else { + validationMessage = OnboardingFetalNicknameTextFieldResultType.fetalNicknameTextFieldOver.errorMessage + isHidden = true + } + return (fetalNickName: text, validationMessage: validationMessage, isHidden: isHidden) + } + .eraseToAnyPublisher() + + return GetFetalNicknameViewModelOutput(fetalTextfieldValidationMessage: pregancyTextfieldValidationMessage) + } +} diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/GetPregnancyViewModel.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/GetPregnancyViewModel.swift new file mode 100644 index 00000000..4456720e --- /dev/null +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/GetPregnancyViewModel.swift @@ -0,0 +1,20 @@ +// +// GetPregnancyViewModel.swift +// LionHeart-iOS +// +// Created by uiskim on 2023/11/14. +// + +import Foundation +import Combine + +protocol GetPregnancyViewModel: ViewModel where Input == GetPregnancyViewModelInput, Output == GetPregnancyViewModelOutput {} + +struct GetPregnancyViewModelInput { + let pregancyTextfieldDidChanged: PassthroughSubject +} + +struct GetPregnancyViewModelOutput { + let pregancyTextfieldValidationMessage: AnyPublisher<(pregnancy: Int, validationMessage: String, isHidden: Bool), Never> +} + diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/GetPregnancyViewModelImpl.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/GetPregnancyViewModelImpl.swift new file mode 100644 index 00000000..f622133e --- /dev/null +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/GetPregnancyViewModelImpl.swift @@ -0,0 +1,43 @@ +// +// GetPregnancyViewModelImpl.swift +// LionHeart-iOS +// +// Created by uiskim on 2023/11/14. +// + +import Foundation +import Combine + +final class GetPregnancyViewModelImpl: GetPregnancyViewModel { + + + func transform(input: GetPregnancyViewModelInput) -> GetPregnancyViewModelOutput { + let pregancyTextfieldValidationMessage = input.pregancyTextfieldDidChanged + .compactMap { self.checkValidation(from: $0) } + .eraseToAnyPublisher() + return GetPregnancyViewModelOutput(pregancyTextfieldValidationMessage: pregancyTextfieldValidationMessage) + } + + + private func checkValidation(from text: String) -> (pregnancy: Int, validationMessage: String, isHidden: Bool)? { + var isHidden = false + var validationMessage = "" + if text.count == 0 { + validationMessage = OnboardingPregnancyTextFieldResultType.pregnancyTextFieldEmpty.errorMessage + isHidden = OnboardingPregnancyTextFieldResultType.pregnancyTextFieldEmpty.isHidden + } + guard let textNumber = Int(text) else { return nil } + if textNumber == 0 { + validationMessage = OnboardingPregnancyTextFieldResultType.pregnancyTextFieldOver.errorMessage + isHidden = OnboardingPregnancyTextFieldResultType.pregnancyTextFieldOver.isHidden + } else if 4 <= textNumber && textNumber <= 40 { + validationMessage = "" + isHidden = OnboardingPregnancyTextFieldResultType.pregnancyTextFieldValid.isHidden + } else { + validationMessage = OnboardingPregnancyTextFieldResultType.pregnancyTextFieldOver.errorMessage + isHidden = OnboardingPregnancyTextFieldResultType.pregnancyTextFieldOver.isHidden + } + + return (pregnancy: textNumber, validationMessage: validationMessage, isHidden: isHidden) + } +} diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/OnboardingViewModel.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/OnboardingViewModel.swift new file mode 100644 index 00000000..570faf18 --- /dev/null +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/OnboardingViewModel.swift @@ -0,0 +1,29 @@ +// +// OnboardingViewModel.swift +// LionHeart-iOS +// +// Created by uiskim on 2023/11/15. +// + +import Foundation +import Combine + +protocol OnboardingViewModelPresentable { + func setKakaoAccessToken(_ token: String?) +} + +protocol OnboardingViewModel: ViewModel where Input == OnboardingViewModelInput, Output == OnboardingViewModelOutput {} + +struct OnboardingViewModelInput { + let pregenacy: CurrentValueSubject<(pregnancy: Int, isValid: Bool), Never> + let fetalNickname: CurrentValueSubject<(fetalNickname: String, isValid: Bool), Never> + let backButtonTapped: PassthroughSubject + let nextButtonTapped: PassthroughSubject +} + +struct OnboardingViewModelOutput { + let pregenacyButtonState: AnyPublisher + let fetalButtonState: AnyPublisher + let onboardingFlow: AnyPublisher + let signUpSubject: AnyPublisher +} diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/OnboardingViewModelImpl.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/OnboardingViewModelImpl.swift new file mode 100644 index 00000000..fe7fe6c7 --- /dev/null +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Onboarding/ViewModels/OnboardingViewModelImpl.swift @@ -0,0 +1,98 @@ +// +// OnboardingViewModelImpl.swift +// LionHeart-iOS +// +// Created by uiskim on 2023/11/15. +// + +import Foundation +import Combine + +final class OnboardingViewModelImpl: OnboardingViewModel { + + private enum FlowType { + case onboardingCompleted(UserOnboardingModel) + case backButtonTapped + } + + private var navigator: OnboardingNavigation + private let manager: OnboardingManager + private var kakaoAccessToken: String? + private let signUpSubject = PassthroughSubject() + private var navigatorSubject = PassthroughSubject() + private var cancelBag = Set() + private var currentPage: OnboardingPageType = .getPregnancy + private var onboardingFlow: OnbardingFlowType = .toGetPregnacny + + init(navigator: OnboardingNavigation, manager: OnboardingManager) { + self.navigator = navigator + self.manager = manager + } + + func transform(input: OnboardingViewModelInput) -> OnboardingViewModelOutput { + + navigatorSubject + .receive(on: RunLoop.main) + .sink { type in + switch type { + case .backButtonTapped: + self.navigator.backButtonTapped() + case .onboardingCompleted(let data): + self.navigator.onboardingCompleted(data: data) + } + } + .store(in: &cancelBag) + + let fetalButtonState = input.fetalNickname + .map { $0.isValid } + .eraseToAnyPublisher() + + let pregenacyButtonState = input.pregenacy + .map { $0.isValid } + .eraseToAnyPublisher() + + let onboardingFlow = input.nextButtonTapped + .map { _ in + if self.onboardingFlow == .toFetalNickname { + self.signUpSubject.send(()) + } + self.onboardingFlow = OnbardingFlowType.toFetalNickname + return self.onboardingFlow + } + .eraseToAnyPublisher() + + let signUpSubject = signUpSubject + .flatMap { _ -> AnyPublisher in + return Future { promise in + Task { + do { + let passingData = UserOnboardingModel(kakaoAccessToken: self.kakaoAccessToken, pregnacny: input.pregenacy.value.pregnancy, fetalNickname: input.fetalNickname.value.fetalNickname) + try await self.manager.signUp(type: .kakao, onboardingModel: passingData) + self.navigatorSubject.send(.onboardingCompleted(passingData)) + } catch { + promise(.failure(error as! NetworkError)) + } + } + } + .catch { error in + Just(error.description) + } + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() + + input.backButtonTapped + .sink { [weak self] in + self?.navigatorSubject.send(.backButtonTapped) + } + .store(in: &cancelBag) + + return OnboardingViewModelOutput(pregenacyButtonState: pregenacyButtonState, fetalButtonState: fetalButtonState, onboardingFlow: onboardingFlow, signUpSubject: signUpSubject) + } +} + +extension OnboardingViewModelImpl: OnboardingViewModelPresentable { + func setKakaoAccessToken(_ token: String?) { + self.kakaoAccessToken = token + } +} diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Splash/SplashViewController.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Splash/SplashViewController.swift index bb5f476a..7de16c37 100644 --- a/LionHeart-iOS/LionHeart-iOS/Scenes/Splash/SplashViewController.swift +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Splash/SplashViewController.swift @@ -7,19 +7,24 @@ // import UIKit +import Combine import SnapKit +protocol SplashViewControllerable where Self: UIViewController {} + final class SplashViewController: UIViewController, SplashViewControllerable { + + private let lottiePlayFinished = PassthroughSubject() - var navigator: SplashNavigation - private let manager: SplashManager + private let viewModel: any SplashViewModel + + private var cancelBag = Set() private let lottieImageView = LHLottie(name: "motion_logo_final") - init(manager: SplashManager, adaptor: SplashNavigation) { - self.manager = manager - self.navigator = adaptor + init(viewModel: some SplashViewModel) { + self.viewModel = viewModel super.init(nibName: nil, bundle: nil) } @@ -32,23 +37,23 @@ final class SplashViewController: UIViewController, SplashViewControllerable { setUI() setHierarchy() setLayout() + bind() } override func viewWillAppear(_ animated: Bool) { - lottieImageView.play { _ in - guard let accessToken = UserDefaultsManager.tokenKey?.accessToken, let refreshToken = UserDefaultsManager.tokenKey?.refreshToken else { - self.navigator.checkToken(state: .expired) - return - } - Task { - try? await self.reissueToken(refreshToken: refreshToken, accessToken: accessToken) - } + lottieImageView.play { [weak self] _ in + self?.lottiePlayFinished.send(()) } - } - private func checkIsRefreshTokenExist() -> String? { - return UserDefaultsManager.tokenKey?.refreshToken + private func bind() { + let input = SplashViewModelInput(lottiePlayFinished: lottiePlayFinished) + let output = viewModel.transform(input: input) + output.splashNetworkErrorMessage + .sink { errorMessage in + print(errorMessage) + } + .store(in: &cancelBag) } } @@ -70,41 +75,3 @@ private extension SplashViewController { } } } - -private extension SplashViewController { - - func reissueToken(refreshToken: String, accessToken: String) async throws { - do { - let dtoToken = try await manager.reissueToken(token: Token(accessToken: accessToken, refreshToken: refreshToken)) - UserDefaultsManager.tokenKey?.accessToken = dtoToken?.accessToken - UserDefaultsManager.tokenKey?.refreshToken = dtoToken?.refreshToken - self.navigator.checkToken(state: .valid) - } catch { - guard let errorModel = error as? NetworkError else { return } - await handleError(errorModel) - } - } - - func logout(token: UserDefaultToken) async { - do { - try await manager.logout(token: token) - } catch { - print(error) - } - } - - func handleError(_ error: NetworkError) async { - switch error { - case .clientError(let code, _): - if code == NetworkErrorCode.unauthorizedErrorCode { - guard let token = UserDefaultsManager.tokenKey else { return } - await logout(token: token) - self.navigator.checkToken(state: .expired) - } else if code == NetworkErrorCode.unfoundUserErrorCode { - self.navigator.checkToken(state: .expired) - } - default: - print(error) - } - } -} diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Splash/SplashViewModel.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Splash/SplashViewModel.swift new file mode 100644 index 00000000..dae84b22 --- /dev/null +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Splash/SplashViewModel.swift @@ -0,0 +1,21 @@ +// +// SplashViewModel.swift +// LionHeart-iOS +// +// Created by 김민재 on 11/16/23. +// + +import Foundation +import Combine + +protocol SplashViewModelPresentable {} + +protocol SplashViewModel: ViewModel where Input == SplashViewModelInput, Output == SplashViewModelOutput {} + +struct SplashViewModelInput { + let lottiePlayFinished: PassthroughSubject +} + +struct SplashViewModelOutput { + let splashNetworkErrorMessage: AnyPublisher +} diff --git a/LionHeart-iOS/LionHeart-iOS/Scenes/Splash/SplashViewModelImpl.swift b/LionHeart-iOS/LionHeart-iOS/Scenes/Splash/SplashViewModelImpl.swift new file mode 100644 index 00000000..bb93f9cc --- /dev/null +++ b/LionHeart-iOS/LionHeart-iOS/Scenes/Splash/SplashViewModelImpl.swift @@ -0,0 +1,128 @@ +// +// SplashViewModelImpl.swift +// LionHeart-iOS +// +// Created by 김민재 on 11/16/23. +// + +import Foundation +import Combine + + +final class SplashViewModelImpl: SplashViewModel, SplashViewModelPresentable { + + private let navigator: SplashNavigation + private let manager: SplashManager + + private let tokenNonExistSubject = PassthroughSubject() + private let logoutSubject = PassthroughSubject() + private let navigationSubject = PassthroughSubject() + + private var cancelBag = Set() + + init(navigator: SplashNavigation, manager: SplashManager) { + self.navigator = navigator + self.manager = manager + } + + func transform(input: SplashViewModelInput) -> SplashViewModelOutput { + + navigationSubject + .receive(on: RunLoop.main) + .sink { state in + switch state { + case .expired: + self.navigator.checkToken(state: .expired) + case .valid: + self.navigator.checkToken(state: .valid) + } + } + .store(in: &cancelBag) + + let splashErrorMessage = input.lottiePlayFinished + .map { [weak self] _ -> (String?, String?) in + guard let accessToken = UserDefaultsManager.tokenKey?.accessToken, + let refreshToken = UserDefaultsManager.tokenKey?.refreshToken else { + self?.navigationSubject.send(.expired) + return (nil, nil) + } + return (accessToken, refreshToken) + } + .filter { (accessToken, refreshToken) in + return accessToken != nil && refreshToken != nil + } + .flatMap { (accessToken, refreshToken) -> AnyPublisher in + + return Future { _ in + Task { + do { + guard let accessToken = UserDefaultsManager.tokenKey?.accessToken, + let refreshToken = UserDefaultsManager.tokenKey?.refreshToken else { return } + try await self.reissueToken(refreshToken: refreshToken, accessToken: accessToken) + } catch { + guard let errorModel = error as? NetworkError else { return } + await self.handleError(errorModel) + } + } + } + .catch { error in + return Just(error.description) + } + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() + + let logoutSubject = logoutSubject + .flatMap { _ -> AnyPublisher in + return Future { promise in + Task { + do { + guard let token = UserDefaultsManager.tokenKey else { return } + try await self.logout(token: token) + self.navigationSubject.send(.expired) + } catch { + promise(.failure(error as! NetworkError)) + } + } + }.catch { error in + return Just(error.description) + } + .eraseToAnyPublisher() + } + .eraseToAnyPublisher() + + + let mergeSubject = splashErrorMessage + .merge(with: logoutSubject) + .eraseToAnyPublisher() + + return SplashViewModelOutput(splashNetworkErrorMessage: mergeSubject) + } + +} + +private extension SplashViewModelImpl { + + func reissueToken(refreshToken: String, accessToken: String) async throws { + let dtoToken = try await manager.reissueToken(token: Token(accessToken: accessToken, refreshToken: refreshToken)) + UserDefaultsManager.tokenKey?.accessToken = dtoToken?.accessToken + UserDefaultsManager.tokenKey?.refreshToken = dtoToken?.refreshToken + self.navigationSubject.send(.valid) + } + + func handleError(_ error: NetworkError) async { + switch error { + case .clientError(let code, _): + if code == NetworkErrorCode.unauthorizedErrorCode { + logoutSubject.send(()) + } else if code == NetworkErrorCode.unfoundUserErrorCode { + self.navigationSubject.send(.expired) + } + default: break + } + } + + func logout(token: UserDefaultToken) async throws { + try await manager.logout(token: token) + } +}