diff --git a/iOS-NOTTODO/iOS-NOTTODO.xcodeproj/project.pbxproj b/iOS-NOTTODO/iOS-NOTTODO.xcodeproj/project.pbxproj index f87d023e..e30e510b 100644 --- a/iOS-NOTTODO/iOS-NOTTODO.xcodeproj/project.pbxproj +++ b/iOS-NOTTODO/iOS-NOTTODO.xcodeproj/project.pbxproj @@ -39,7 +39,7 @@ 098BFD5929B7999E008E80F9 /* MyProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 098BFD5829B7999E008E80F9 /* MyProfileCollectionViewCell.swift */; }; 098BFD5B29B79B6A008E80F9 /* MyInfoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 098BFD5A29B79B6A008E80F9 /* MyInfoModel.swift */; }; 098BFD5D29B79CE3008E80F9 /* InfoCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 098BFD5C29B79CE3008E80F9 /* InfoCollectionViewCell.swift */; }; - 098BFD5F29B7AECF008E80F9 /* MyInfoHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 098BFD5E29B7AECF008E80F9 /* MyInfoHeaderCollectionReusableView.swift */; }; + 098BFD5F29B7AECF008E80F9 /* MyInfoHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 098BFD5E29B7AECF008E80F9 /* MyInfoHeaderView.swift */; }; 099FC98129B3094F005B37E6 /* WeekMonthFSCalendar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099FC98029B3094F005B37E6 /* WeekMonthFSCalendar.swift */; }; 099FC98329B30A2E005B37E6 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099FC98229B30A2E005B37E6 /* Utils.swift */; }; 099FC98929B3233D005B37E6 /* CalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 099FC98829B3233D005B37E6 /* CalendarView.swift */; }; @@ -47,6 +47,7 @@ 09A146652A1964B500DDC308 /* AddAnotherDayResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09A146642A19649A00DDC308 /* AddAnotherDayResponseDTO.swift */; }; 09C8602D2AB14B4800C4F4B1 /* FSCalendar in Frameworks */ = {isa = PBXBuildFile; productRef = 09C8602C2AB14B4800C4F4B1 /* FSCalendar */; }; 09CF56042B09F23800526C8C /* HomeDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09CF56032B09F23800526C8C /* HomeDataSource.swift */; }; + 09CF56022B09E98A00526C8C /* DetailAchieveHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09CF56012B09E98A00526C8C /* DetailAchieveHeaderView.swift */; }; 09DCCD1F2A18ED76003DCF8A /* DailyMissionResponseDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09DCCD1E2A18ED76003DCF8A /* DailyMissionResponseDTO.swift */; }; 09DCCD212A18EF43003DCF8A /* HomeSevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09DCCD202A18EF43003DCF8A /* HomeSevice.swift */; }; 09DCCD232A18EFB0003DCF8A /* HomeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09DCCD222A18EFB0003DCF8A /* HomeAPI.swift */; }; @@ -62,6 +63,10 @@ 09F6719029CB6AB400708725 /* OnboardingFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F6718F29CB6AB400708725 /* OnboardingFooterView.swift */; }; 09F6719529CBFCD200708725 /* GradientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F6719429CBFCD200708725 /* GradientView.swift */; }; 09F6719729CC81B500708725 /* DetailAchievementCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09F6719629CC81B500708725 /* DetailAchievementCollectionViewCell.swift */; }; + 155E45532B5BB8AC008628E7 /* API_KEY.plist in Resources */ = {isa = PBXBuildFile; fileRef = 155E45522B5BB8AC008628E7 /* API_KEY.plist */; }; + 155E45662B5FF089008628E7 /* FirebaseRemoteConfig in Frameworks */ = {isa = PBXBuildFile; productRef = 155E45652B5FF089008628E7 /* FirebaseRemoteConfig */; }; + 155E45692B5FF2EE008628E7 /* FirebaseUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 155E45682B5FF2EE008628E7 /* FirebaseUtil.swift */; }; + 155E456D2B62B1A1008628E7 /* UpdateCheckViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 155E456C2B62B1A1008628E7 /* UpdateCheckViewController.swift */; }; 3B027A78299C31B500BEB65C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B027A77299C31B500BEB65C /* AppDelegate.swift */; }; 3B027A7A299C31B500BEB65C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B027A79299C31B500BEB65C /* SceneDelegate.swift */; }; 3B027A7C299C31B500BEB65C /* AuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B027A7B299C31B500BEB65C /* AuthViewController.swift */; }; @@ -101,7 +106,6 @@ 3B482FA5299EAB8800BCF424 /* TabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B482FA4299EAB8800BCF424 /* TabBarController.swift */; }; 3B482FA7299EB8FD00BCF424 /* UIViewController+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B482FA6299EB8FD00BCF424 /* UIViewController+.swift */; }; 3B482FA9299EB95400BCF424 /* UIScreen+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B482FA8299EB95400BCF424 /* UIScreen+.swift */; }; - 3B49204D2B58ECC100F53B19 /* API_KEY.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B49204C2B58ECC100F53B19 /* API_KEY.plist */; }; 3B4E12F22A27B621001D1EC1 /* NottodoModalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4E12F12A27B621001D1EC1 /* NottodoModalViewController.swift */; }; 3B4E12F62A27C0BE001D1EC1 /* QuitModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4E12F52A27C0BE001D1EC1 /* QuitModalView.swift */; }; 3B4E12F82A27C12F001D1EC1 /* WithdrawModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4E12F72A27C12F001D1EC1 /* WithdrawModalView.swift */; }; @@ -202,13 +206,14 @@ 098BFD5829B7999E008E80F9 /* MyProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyProfileCollectionViewCell.swift; sourceTree = ""; }; 098BFD5A29B79B6A008E80F9 /* MyInfoModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyInfoModel.swift; sourceTree = ""; }; 098BFD5C29B79CE3008E80F9 /* InfoCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoCollectionViewCell.swift; sourceTree = ""; }; - 098BFD5E29B7AECF008E80F9 /* MyInfoHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyInfoHeaderCollectionReusableView.swift; sourceTree = ""; }; + 098BFD5E29B7AECF008E80F9 /* MyInfoHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyInfoHeaderView.swift; sourceTree = ""; }; 099FC98029B3094F005B37E6 /* WeekMonthFSCalendar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekMonthFSCalendar.swift; sourceTree = ""; }; 099FC98229B30A2E005B37E6 /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; 099FC98829B3233D005B37E6 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = ""; }; 09A1465E2A192C4900DDC308 /* WeekMissionResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeekMissionResponseDTO.swift; sourceTree = ""; }; 09A146642A19649A00DDC308 /* AddAnotherDayResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAnotherDayResponseDTO.swift; sourceTree = ""; }; 09CF56032B09F23800526C8C /* HomeDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeDataSource.swift; sourceTree = ""; }; + 09CF56012B09E98A00526C8C /* DetailAchieveHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailAchieveHeaderView.swift; sourceTree = ""; }; 09DCCD1E2A18ED76003DCF8A /* DailyMissionResponseDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyMissionResponseDTO.swift; sourceTree = ""; }; 09DCCD202A18EF43003DCF8A /* HomeSevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeSevice.swift; sourceTree = ""; }; 09DCCD222A18EFB0003DCF8A /* HomeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeAPI.swift; sourceTree = ""; }; @@ -224,6 +229,9 @@ 09F6718F29CB6AB400708725 /* OnboardingFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingFooterView.swift; sourceTree = ""; }; 09F6719429CBFCD200708725 /* GradientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientView.swift; sourceTree = ""; }; 09F6719629CC81B500708725 /* DetailAchievementCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailAchievementCollectionViewCell.swift; sourceTree = ""; }; + 155E45522B5BB8AC008628E7 /* API_KEY.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = API_KEY.plist; sourceTree = ""; }; + 155E45682B5FF2EE008628E7 /* FirebaseUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirebaseUtil.swift; sourceTree = ""; }; + 155E456C2B62B1A1008628E7 /* UpdateCheckViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCheckViewController.swift; sourceTree = ""; }; 3B027A74299C31B500BEB65C /* iOS-NOTTODO.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "iOS-NOTTODO.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 3B027A77299C31B500BEB65C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 3B027A79299C31B500BEB65C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -261,7 +269,6 @@ 3B482FA4299EAB8800BCF424 /* TabBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarController.swift; sourceTree = ""; }; 3B482FA6299EB8FD00BCF424 /* UIViewController+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+.swift"; sourceTree = ""; }; 3B482FA8299EB95400BCF424 /* UIScreen+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIScreen+.swift"; sourceTree = ""; }; - 3B49204C2B58ECC100F53B19 /* API_KEY.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = API_KEY.plist; sourceTree = ""; }; 3B4E12F12A27B621001D1EC1 /* NottodoModalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NottodoModalViewController.swift; sourceTree = ""; }; 3B4E12F52A27C0BE001D1EC1 /* QuitModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuitModalView.swift; sourceTree = ""; }; 3B4E12F72A27C12F001D1EC1 /* WithdrawModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WithdrawModalView.swift; sourceTree = ""; }; @@ -335,6 +342,7 @@ 6C44127129A35A1000313C3F /* KakaoSDK in Frameworks */, 3B146DA4299D0A8600B17B62 /* Then in Frameworks */, 3B146DA7299D0AA300B17B62 /* Moya in Frameworks */, + 155E45662B5FF089008628E7 /* FirebaseRemoteConfig in Frameworks */, 3B2B59442AEB814B00B4619A /* FirebaseMessaging in Frameworks */, 6C44127729A35A1000313C3F /* KakaoSDKNavi in Frameworks */, 6C44128129A35A1000313C3F /* KakaoSDKUser in Frameworks */, @@ -434,7 +442,7 @@ 098BFD6029B80137008E80F9 /* Cell */ = { isa = PBXGroup; children = ( - 098BFD5E29B7AECF008E80F9 /* MyInfoHeaderCollectionReusableView.swift */, + 098BFD5E29B7AECF008E80F9 /* MyInfoHeaderView.swift */, 098BFD5829B7999E008E80F9 /* MyProfileCollectionViewCell.swift */, 098BFD5C29B79CE3008E80F9 /* InfoCollectionViewCell.swift */, ); @@ -590,6 +598,38 @@ 3B027AA1299C355800BEB65C /* AchievementViewController.swift */, 09582B5029C0BC3600EF3207 /* DetailAchievementViewController.swift */, 0930D37229B4FCAE0000C4AE /* StatisticsView.swift */, + 09CF56012B09E98A00526C8C /* DetailAchieveHeaderView.swift */, + ); + path = ViewControllers; + sourceTree = ""; + }; + 155E45642B5FF089008628E7 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 155E45672B5FF2DE008628E7 /* External */ = { + isa = PBXGroup; + children = ( + 155E45682B5FF2EE008628E7 /* FirebaseUtil.swift */, + ); + path = External; + sourceTree = ""; + }; + 155E456A2B62B17D008628E7 /* UpdateCheck */ = { + isa = PBXGroup; + children = ( + 155E456B2B62B18F008628E7 /* ViewControllers */, + ); + path = UpdateCheck; + sourceTree = ""; + }; + 155E456B2B62B18F008628E7 /* ViewControllers */ = { + isa = PBXGroup; + children = ( + 155E456C2B62B1A1008628E7 /* UpdateCheckViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -601,6 +641,7 @@ 3BDE6157299EDD02001CCEA9 /* .swiftlint.yml */, 3B027A76299C31B500BEB65C /* iOS-NOTTODO */, 3B027A75299C31B500BEB65C /* Products */, + 155E45642B5FF089008628E7 /* Frameworks */, ); sourceTree = ""; }; @@ -681,6 +722,7 @@ 3B027A90299C33CA00BEB65C /* Network */ = { isa = PBXGroup; children = ( + 155E45672B5FF2DE008628E7 /* External */, 093DB0312A1468F100ECA5F6 /* Service */, 093DB02F2A1468CE00ECA5F6 /* DataModel */, 093DB02B2A14687300ECA5F6 /* API */, @@ -692,6 +734,7 @@ 3B027A97299C343D00BEB65C /* Presentation */ = { isa = PBXGroup; children = ( + 155E456A2B62B17D008628E7 /* UpdateCheck */, 3B03D0D32B0F157700302872 /* NotificatoinDialog */, 09F6717E29CAD68100708725 /* Onboarding */, 3B027A9B299C348800BEB65C /* Achievement */, @@ -769,7 +812,7 @@ 3B027AA6299C359900BEB65C /* Resource */ = { isa = PBXGroup; children = ( - 3B49204C2B58ECC100F53B19 /* API_KEY.plist */, + 155E45522B5BB8AC008628E7 /* API_KEY.plist */, 3B3EF2F72AF35C90001F79BC /* GoogleService-Info.plist */, 3B027A85299C31B600BEB65C /* Info.plist */, 3B027AAA299C35D000BEB65C /* Assets */, @@ -1170,6 +1213,7 @@ 0943A9F42A531D0000614761 /* Amplitude */, 09C8602C2AB14B4800C4F4B1 /* FSCalendar */, 3B2B59432AEB814B00B4619A /* FirebaseMessaging */, + 155E45652B5FF089008628E7 /* FirebaseRemoteConfig */, ); productName = "iOS-NOTTODO"; productReference = 3B027A74299C31B500BEB65C /* iOS-NOTTODO.app */; @@ -1231,7 +1275,7 @@ 3B710A5C2A62D4AB00E95620 /* Settings.bundle in Resources */, 3B027A84299C31B600BEB65C /* LaunchScreen.storyboard in Resources */, 6CC54C1A2A28C3AE00AAD76D /* value.json in Resources */, - 3B49204D2B58ECC100F53B19 /* API_KEY.plist in Resources */, + 155E45532B5BB8AC008628E7 /* API_KEY.plist in Resources */, 3B3EF2F82AF35C90001F79BC /* GoogleService-Info.plist in Resources */, 6C049A312A595C670085E40B /* logo.mp4 in Resources */, 3B027A81299C31B600BEB65C /* Assets.xcassets in Resources */, @@ -1323,15 +1367,17 @@ 3B892ABB2A2FBD4C00A316BC /* RecommendSituationResponseDTO.swift in Sources */, 3B14A13B29A694C000F92897 /* UITextView+.swift in Sources */, 093DB0372A146BF900ECA5F6 /* MyInfoURL.swift in Sources */, - 098BFD5F29B7AECF008E80F9 /* MyInfoHeaderCollectionReusableView.swift in Sources */, + 098BFD5F29B7AECF008E80F9 /* MyInfoHeaderView.swift in Sources */, 3B482FA7299EB8FD00BCF424 /* UIViewController+.swift in Sources */, 0960C0D42A38BC6500A3D8DB /* KeychainUtil.swift in Sources */, 3B03D0D82B0F5EF300302872 /* CGSize+.swift in Sources */, 6CA208302A1925EE001C4247 /* RecommendActionResponseDTO.swift in Sources */, 09F6718629CB26E400708725 /* OnboardingHeaderView.swift in Sources */, 3B11740D2A4B574B0033DDF3 /* CALayer+.swift in Sources */, + 155E456D2B62B1A1008628E7 /* UpdateCheckViewController.swift in Sources */, 3B14A13D29A6FBD300F92897 /* UIView+.swift in Sources */, 09F6719529CBFCD200708725 /* GradientView.swift in Sources */, + 09CF56022B09E98A00526C8C /* DetailAchieveHeaderView.swift in Sources */, 3B4E12F82A27C12F001D1EC1 /* WithdrawModalView.swift in Sources */, 6CA208252A18FEEA001C4247 /* RecommendService.swift in Sources */, 3B482FA5299EAB8800BCF424 /* TabBarController.swift in Sources */, @@ -1393,6 +1439,7 @@ 3B0CBA222A45FC170004F2DB /* UpdateMissionResponseDTO.swift in Sources */, 3B892ABE2A2FBDDE00A316BC /* AddMissionAPI.swift in Sources */, 092C09B72A48596500E9B06B /* DeleteModalView.swift in Sources */, + 155E45692B5FF2EE008628E7 /* FirebaseUtil.swift in Sources */, 6CF4706329A690CD008D145C /* NetworkResult.swift in Sources */, 3B9532F42A284CC1006510F8 /* ModalProtocol.swift in Sources */, 3B5F8F8329BF90290063A7F8 /* SituationCollectionViewCell.swift in Sources */, @@ -1540,7 +1587,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = CQJ9UKUU35; GENERATE_INFOPLIST_FILE = YES; @@ -1578,7 +1625,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 0; + CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = CQJ9UKUU35; GENERATE_INFOPLIST_FILE = YES; @@ -1716,6 +1763,11 @@ package = 09C8602B2AB14B4700C4F4B1 /* XCRemoteSwiftPackageReference "FSCalendar" */; productName = FSCalendar; }; + 155E45652B5FF089008628E7 /* FirebaseRemoteConfig */ = { + isa = XCSwiftPackageProductDependency; + package = 3B2B59422AEB814B00B4619A /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseRemoteConfig; + }; 3B146DA0299D0A7A00B17B62 /* SnapKit */ = { isa = XCSwiftPackageProductDependency; package = 3B146D9F299D0A7A00B17B62 /* XCRemoteSwiftPackageReference "SnapKit" */; @@ -1794,4 +1846,4 @@ /* End XCSwiftPackageProductDependency section */ }; rootObject = 3B027A6C299C31B500BEB65C /* Project object */; -} +} \ No newline at end of file diff --git a/iOS-NOTTODO/iOS-NOTTODO/Application/AppDelegate.swift b/iOS-NOTTODO/iOS-NOTTODO/Application/AppDelegate.swift index 77f8729b..57bed8ea 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Application/AppDelegate.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Application/AppDelegate.swift @@ -16,6 +16,7 @@ import Amplitude import Firebase import FirebaseMessaging +import FirebaseRemoteConfig @main class AppDelegate: UIResponder, UIApplicationDelegate { @@ -27,9 +28,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { Amplitude.instance().initializeApiKey(Bundle.main.amplitudeAPIKey) KakaoSDK.initSDK(appKey: Bundle.main.kakaoAPIKey) FirebaseApp.configure() - - checkForUpdate() - + // 메시지 대리자 설정 Messaging.messaging().delegate = self @@ -42,20 +41,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { return true } - func skipAuthView() { - // 홈 화면으로 바로 이동 - DispatchQueue.main.async { - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, - let window = windowScene.windows.first { - let tabBarController = TabBarController() - let navigationController = UINavigationController(rootViewController: tabBarController) - navigationController.isNavigationBarHidden = true - window.rootViewController = navigationController - window.makeKeyAndVisible() - } - } - } - func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Messaging.messaging().apnsToken = deviceToken @@ -99,96 +84,3 @@ extension AppDelegate: UNUserNotificationCenterDelegate { completionHandler() } } - -extension AppDelegate { - func checkForUpdate() { - // 앱스토어 버전 - guard let appstoreVersion = getAppstoreVersion() else { return } - - // 현재 설치된 앱의 버전 - guard let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { return } - - if compareVersion(userVersion: appVersion, appstoreVersion: appstoreVersion) { - DispatchQueue.main.async { - self.showUpdateAlert() - } - } else { - if KeychainUtil.getAccessToken() != "" { - self.skipAuthView() - print("토큰 유효") - } - } - } - - /// 버전 비교하는 메서드 - func compareVersion(userVersion: String, appstoreVersion: String) -> Bool { - let userMajor = userVersion.split(separator: ".").map {Int($0)!}[0] - let appstoreMajor = appstoreVersion.split(separator: ".").map {Int($0)!}[0] - - if userMajor < appstoreMajor { - return true - } - - let userMinor = userVersion.split(separator: ".").map {Int($0)!}[1] - let appstoreMinor = appstoreVersion.split(separator: ".").map {Int($0)!}[1] - - if userMinor < appstoreMinor { - return true - } - - let userPatch = userVersion.split(separator: ".").map {Int($0)!}[2] - let appstorePatch = appstoreVersion.split(separator: ".").map {Int($0)!}[2] - - if userPatch < appstorePatch { - return true - } - - return false - } - - /// 앱스토어에 배포된 버전 가져오는 메서드 - func getAppstoreVersion() -> String? { - let appleID = Bundle.main.appleId - guard let url = URL(string: "https://itunes.apple.com/lookup?id=\(appleID)"), - let data = try? Data(contentsOf: url), - let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any], - let results = json["results"] as? [[String: Any]], - let appStoreVersion = results[0]["version"] as? String else { - return nil - } - return appStoreVersion - } - - /// 선택 업데이트 경고창 - func showUpdateAlert() { - let alertController = UIAlertController( - title: I18N.update, - message: I18N.updateAlert, - preferredStyle: .alert - ) - - let updateAction = UIAlertAction(title: I18N.update, style: .default) { _ in - // App Store로 이동 - if let appStoreURL = URL(string: "https://itunes.apple.com/app/\(Bundle.main.appleId)") { - UIApplication.shared.open(appStoreURL, options: [:], completionHandler: {_ in - if KeychainUtil.getAccessToken() != "" { - self.skipAuthView() - print("토큰 유효") - } - }) - } - } - - let cancelAction = UIAlertAction(title: I18N.later, style: .default) - - alertController.addAction(updateAction) - alertController.addAction(cancelAction) - - if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene { - if let keyWindow = windowScene.windows.first, - let rootViewController = keyWindow.rootViewController { - rootViewController.present(alertController, animated: true, completion: nil) - } - } - } -} diff --git a/iOS-NOTTODO/iOS-NOTTODO/Application/SceneDelegate.swift b/iOS-NOTTODO/iOS-NOTTODO/Application/SceneDelegate.swift index 5751950f..eed04f6c 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Application/SceneDelegate.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Application/SceneDelegate.swift @@ -18,7 +18,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let window = UIWindow(windowScene: windowScene) window.overrideUserInterfaceStyle = UIUserInterfaceStyle.light - let rootViewController = ValueOnboardingViewController() + let rootViewController = UpdateCheckViewController() let navigationController = UINavigationController(rootViewController: rootViewController) navigationController.isNavigationBarHidden = true window.rootViewController = navigationController @@ -52,7 +52,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } extension SceneDelegate { - func changeRootViewControllerTo(_ viewController: UIViewController) { guard let window = window else { return } diff --git a/iOS-NOTTODO/iOS-NOTTODO/Global/Enum/AnalyticsEvent.swift b/iOS-NOTTODO/iOS-NOTTODO/Global/Enum/AnalyticsEvent.swift index e9befe01..f25d6069 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Global/Enum/AnalyticsEvent.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Global/Enum/AnalyticsEvent.swift @@ -29,6 +29,7 @@ enum AnalyticsEvent { case clickOnboardingNext5 case clickPushAllow(section: Bool) case clickPushReject(section: Bool) + case clickOnboardingNext6 var name: String { switch self { @@ -39,6 +40,7 @@ enum AnalyticsEvent { case .clickOnboardingNext5: return "click_onboarding_next_5" case .clickPushAllow: return "click_push_allow" case .clickPushReject: return "click_push_reject" + case .clickOnboardingNext6: return "click_onboarding_next_6" } } @@ -51,6 +53,7 @@ enum AnalyticsEvent { case .clickOnboardingNext5: return nil case .clickPushAllow(section: let section): return ["section": section] case .clickPushReject(section: let section): return ["section": section] + case .clickOnboardingNext6: return nil } } } @@ -60,20 +63,25 @@ enum AnalyticsEvent { case viewSignIn case clickSignIn(provider: String) case completeSignIn(provider: String) + case clickAdModalCta + case clickAdModalClose(again: String) var name: String { switch self { case .viewSignIn: return "view_signin" case .clickSignIn: return "click_signin" case .completeSignIn: return "complete_signin" + case .clickAdModalCta: return "click_ad_modal_cta" + case .clickAdModalClose: return "click_ad_modal_close" } } var parameters: [String: Any]? { switch self { - case .viewSignIn: return nil + case .viewSignIn, .clickAdModalCta: return nil case .clickSignIn(provider: let provider ): return ["provider": provider] case .completeSignIn(provider: let provider): return ["provider": provider] + case .clickAdModalClose(again: let again): return ["again": again] } } } diff --git a/iOS-NOTTODO/iOS-NOTTODO/Global/Enum/DefaultKeys.swift b/iOS-NOTTODO/iOS-NOTTODO/Global/Enum/DefaultKeys.swift index a6194d3f..48159cdd 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Global/Enum/DefaultKeys.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Global/Enum/DefaultKeys.swift @@ -27,6 +27,6 @@ struct DefaultKeys { static let socialToken = "socialToken" static let accessToken = "accessToken" static let fcmToken = "fcmToken" - static let isSelected = "isSelected" static let isNotificationAccepted = "isNotificationAccepted" + static let isDeprecatedBtnClicked = "isDeprecatedBtnClicked" } diff --git a/iOS-NOTTODO/iOS-NOTTODO/Global/Enum/KeychainUtil.swift b/iOS-NOTTODO/iOS-NOTTODO/Global/Enum/KeychainUtil.swift index a267e046..f82f4efb 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Global/Enum/KeychainUtil.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Global/Enum/KeychainUtil.swift @@ -48,10 +48,10 @@ public final class KeychainUtil { static func getBool(_ key: String) -> Bool { UserDefaults.standard.bool(forKey: key) } - static func isSelected() -> Bool { - UserDefaults.standard.bool(forKey: DefaultKeys.isSelected) + static func isDeprecatedBtnClicked() -> Bool { + UserDefaults.standard.bool(forKey: DefaultKeys.isDeprecatedBtnClicked) } - + static func removeUserInfo() { if getBool(DefaultKeys.isAppleLogin) { UserDefaults.standard.removeObject(forKey: DefaultKeys.appleName) diff --git a/iOS-NOTTODO/iOS-NOTTODO/Global/Enum/MyInfoURL.swift b/iOS-NOTTODO/iOS-NOTTODO/Global/Enum/MyInfoURL.swift index 5a7d700d..b8b554f4 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Global/Enum/MyInfoURL.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Global/Enum/MyInfoURL.swift @@ -10,25 +10,23 @@ import Foundation import SafariServices enum MyInfoURL { - case guid, quesition, notice, contact, service, personalInfo, googleForm, opensource + case guid, faq, notice, question, service, personalInfo, googleForm var url: String { switch self { case .guid: return "https://teamnottodo.notion.site/f35a7f2d6d5c4b33b4d0949f6077e6cd" - case .quesition: + case .faq: return "https://teamnottodo.notion.site/a6ef7036bde24e289e576ace099f39dc" case .notice: return "https://teamnottodo.notion.site/a5dbb310ec1d43baae02b7e9bf0b3411" - case .contact: + case .question: return "http://pf.kakao.com/_fUIQxj/chat" case .service: return "https://teamnottodo.notion.site/81594da775614d23900cdb2475eadb73?pvs=4" case .personalInfo: return "https://teamnottodo.notion.site/5af34df7da3649fc941312c5f533c1eb" case .googleForm: - return "https://forms.gle/rWFJrpVv1RqHwTg28" - case .opensource: - return "https://teamnottodo.notion.site/a391274a627643f6a4d1f2412d4cf170?pvs=4" + return "https://forms.gle/gwBJ4hL4bCTjXRTP6" } } diff --git a/iOS-NOTTODO/iOS-NOTTODO/Global/Literals/Strings.swift b/iOS-NOTTODO/iOS-NOTTODO/Global/Literals/Strings.swift index e086c32a..f4c002ef 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Global/Literals/Strings.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Global/Literals/Strings.swift @@ -167,6 +167,9 @@ struct I18N { 최신 업데이트가 있습니다. 업데이트하시겠습니까? """ + static func forceUpdateAlert(newVersion: String) -> String { + return "낫투두의 새로운 버전이 있습니다. \(newVersion)버전 으로 업데이트 해주세요." + } static let later = "나중에" static let notiDialogTitle = """ 알림을 허용하면 diff --git a/iOS-NOTTODO/iOS-NOTTODO/Network/External/FirebaseUtil.swift b/iOS-NOTTODO/iOS-NOTTODO/Network/External/FirebaseUtil.swift new file mode 100644 index 00000000..244f6abb --- /dev/null +++ b/iOS-NOTTODO/iOS-NOTTODO/Network/External/FirebaseUtil.swift @@ -0,0 +1,47 @@ +// +// FirebaseUtil.swift +// iOS-NOTTODO +// +// Created by 김혜수 on 1/23/24. +// + +import Foundation + +import FirebaseRemoteConfig + +final class FirebaseUtil { + + static let shared = FirebaseUtil() + + private let config = RemoteConfig.remoteConfig() + + enum RemoteConfigType { + case minimumVersion + } + + init() { + self.setRemoteConfigSetting() + } + + func fetchRemoteConfig(type: RemoteConfigType) async -> String? { + return await withCheckedContinuation { continuation in + config.fetch { [weak self] status, _ in + if status == .success { + self?.config.activate() + guard let version = self?.config["minimum_version"].stringValue, !version.isEmpty else { + continuation.resume(returning: nil) + return + } + continuation.resume(returning: version) + } + } + } + } + + private func setRemoteConfigSetting() { + let setting = RemoteConfigSettings() + setting.minimumFetchInterval = 0 + setting.fetchTimeout = 10 + config.configSettings = setting + } +} diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/Cell/DetailAchievementCollectionViewCell.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/Cell/DetailAchievementCollectionViewCell.swift index 694e8b95..c665491c 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/Cell/DetailAchievementCollectionViewCell.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/Cell/DetailAchievementCollectionViewCell.swift @@ -7,6 +7,9 @@ import UIKit +import SnapKit +import Then + final class DetailAchievementCollectionViewCell: UICollectionViewCell { // MARK: - Properties @@ -16,8 +19,6 @@ final class DetailAchievementCollectionViewCell: UICollectionViewCell { // MARK: - UI Components let tagLabel = PaddingLabel(padding: UIEdgeInsets(top: 4, left: 12, bottom: 4, right: 12)) - private let horizontalStackView = UIStackView() - private let emptyView = UIView() let titleLabel = UILabel() private let checkImage = UIImageView() @@ -25,6 +26,7 @@ final class DetailAchievementCollectionViewCell: UICollectionViewCell { override init(frame: CGRect) { super.init(frame: frame) + setUI() setLayout() } @@ -37,55 +39,57 @@ final class DetailAchievementCollectionViewCell: UICollectionViewCell { // MARK: - Methods extension DetailAchievementCollectionViewCell { + private func setUI() { + contentView.backgroundColor = .clear tagLabel.do { $0.layer.backgroundColor = UIColor.bg?.cgColor $0.font = .Pretendard(.medium, size: 14) $0.textColor = .gray1 - $0.layer.cornerRadius = 10 + $0.layer.cornerRadius = 25/2 } - - horizontalStackView.do { - $0.addArrangedSubviews(titleLabel, emptyView, checkImage) - $0.axis = .horizontal - } - + titleLabel.do { $0.font = .Pretendard(.semiBold, size: 16) $0.textColor = .gray2 - $0.numberOfLines = 0 + $0.numberOfLines = 1 $0.textAlignment = .left } + checkImage.do { $0.image = .icChecked } } + private func setLayout() { - addSubviews(tagLabel, horizontalStackView) - + contentView.addSubviews(tagLabel, titleLabel, checkImage) + tagLabel.snp.makeConstraints { $0.top.equalToSuperview().offset(22) - $0.leading.equalToSuperview().offset(29) + $0.leading.equalToSuperview().inset(28) } - - horizontalStackView.snp.makeConstraints { + + titleLabel.snp.makeConstraints { $0.top.equalTo(tagLabel.snp.bottom).offset(7) $0.leading.equalToSuperview().inset(28) - $0.trailing.equalToSuperview() - $0.bottom.equalToSuperview().inset(24) + $0.trailing.equalToSuperview().inset(50) } checkImage.snp.makeConstraints { - $0.trailing.equalToSuperview() + $0.centerY.equalTo(titleLabel.snp.centerY) $0.size.equalTo(21) + $0.trailing.equalToSuperview().inset(28) + $0.bottom.equalToSuperview().inset(24) } } func configure(model: DailyMissionResponseDTO) { - tagLabel.text = model.title - titleLabel.text = model.situationName + tagLabel.text = model.situationName + titleLabel.text = model.title + titleLabel.lineBreakMode = .byTruncatingTail + switch model.completionStatus { case .CHECKED: checkImage.isHidden = false case .UNCHECKED: checkImage.isHidden = true diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/DetailAchieveHeaderView.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/DetailAchieveHeaderView.swift new file mode 100644 index 00000000..a638dd51 --- /dev/null +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/DetailAchieveHeaderView.swift @@ -0,0 +1,64 @@ +// +// DetailAchieveHeaderView.swift +// iOS-NOTTODO +// +// Created by JEONGEUN KIM on 11/19/23. +// + +import UIKit + +import SnapKit +import Then + +final class DetailAchieveHeaderView: UICollectionReusableView { + + // MARK: - Properties + + static let identifier = "DetailAchieveHeaderView" + + // MARK: - UI Components + + private let dateLabel = UILabel() + + // MARK: - Life Cycle + + override init(frame: CGRect) { + super.init(frame: .zero) + + setUI() + setLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +// MARK: - Method + +extension DetailAchieveHeaderView { + + private func setUI() { + + dateLabel.do { + $0.font = .Pretendard(.semiBold, size: 18) + $0.textColor = .gray2 + $0.textAlignment = .center + } + } + + private func setLayout() { + addSubview(dateLabel) + + dateLabel.snp.makeConstraints { + $0.top.equalToSuperview() + $0.centerX.equalToSuperview() + $0.bottom.equalToSuperview().inset(12) + } + } + + func configure(text: String) { + dateLabel.text = text + } +} diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/DetailAchievementViewController.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/DetailAchievementViewController.swift index 56cb9f3d..697ffb67 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/DetailAchievementViewController.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/DetailAchievementViewController.swift @@ -14,38 +14,44 @@ final class DetailAchievementViewController: UIViewController { // MARK: - Properties - var missionList: [DailyMissionResponseDTO] = [] - private var mission: String? - private var goal: String? - var selectedDate: Date? + typealias CellRegistration = UICollectionView.CellRegistration + typealias HeaderRegistration = UICollectionView.SupplementaryRegistration + typealias Item = DailyMissionResponseDTO + typealias DataSource = UICollectionViewDiffableDataSource + typealias SnapShot = NSDiffableDataSourceSnapshot + enum Section: Int, Hashable { case main } - typealias Item = AnyHashable - var dataSource: UICollectionViewDiffableDataSource! = nil + + var selectedDate: Date? + + private var dataSource: DataSource? + private lazy var safeArea = self.view.safeAreaLayoutGuide // MARK: - UI Components - private let backGroundView = UIView() - private let dateLabel = UILabel() - private lazy var collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout()) + private var collectionView = UICollectionView(frame: .zero, collectionViewLayout: .init()) // MARK: - Life Cycle override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + if let selectedDate = selectedDate { - requestDetailAPI(date: Utils.dateFormatterString(format: "YYYY-MM-dd", date: selectedDate)) + requestDetailAPI(date: Utils.dateFormatterString(format: "YYYY-MM-dd", + date: selectedDate)) } } + override func viewDidLoad() { super.viewDidLoad() - register() + setUI() setLayout() - setupDataSource() - reloadData() + setDataSource() + setSnapShot() } override func touchesBegan(_ touches: Set, with event: UIEvent?) { @@ -53,7 +59,7 @@ final class DetailAchievementViewController: UIViewController { let touch = touches.first! let location = touch.location(in: self.view) - if !backGroundView.frame.contains(location) { + if !collectionView.frame.contains(location) { AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Achieve.closeDailyMissionModal) self.dismiss(animated: true) } @@ -63,66 +69,59 @@ final class DetailAchievementViewController: UIViewController { // MARK: - Methods extension DetailAchievementViewController { - private func register() { - collectionView.register(DetailAchievementCollectionViewCell.self, forCellWithReuseIdentifier: DetailAchievementCollectionViewCell.identifier) - } private func setUI() { view.backgroundColor = .black.withAlphaComponent(0.6) - backGroundView.do { + collectionView.do { + $0.collectionViewLayout = layout() $0.layer.cornerRadius = 15 $0.backgroundColor = .white - $0.isUserInteractionEnabled = false - } - dateLabel.do { - if let selectedDate = selectedDate { - $0.text = Utils.dateFormatterString(format: "YYYY년 MM월 dd일", date: selectedDate) - } - $0.font = .Pretendard(.semiBold, size: 18) - $0.textColor = .gray2 - $0.textAlignment = .center - } - collectionView.do { - $0.backgroundColor = .clear $0.bounces = false $0.autoresizingMask = [.flexibleWidth, .flexibleHeight] } } - + private func setLayout() { - view.addSubview(backGroundView) - backGroundView.addSubviews(dateLabel, collectionView) + view.addSubview(collectionView) - backGroundView.snp.makeConstraints { + collectionView.snp.makeConstraints { $0.center.equalTo(safeArea) $0.directionalHorizontalEdges.equalTo(safeArea).inset(15) $0.height.equalTo(getDeviceWidth()*1.1) } - dateLabel.snp.makeConstraints { - $0.top.equalToSuperview().offset(26) - $0.centerX.equalToSuperview() - } - collectionView.snp.makeConstraints { - $0.top.equalTo(dateLabel.snp.bottom) - $0.leading.equalToSuperview() - $0.trailing.equalToSuperview().inset(15) - $0.bottom.equalToSuperview() - } } - private func setupDataSource() { - dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, item in - guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DetailAchievementCollectionViewCell.identifier, for: indexPath) as? DetailAchievementCollectionViewCell else { return UICollectionViewCell() } - cell.configure(model: item as! DailyMissionResponseDTO) - return cell + private func setDataSource() { + + let cellRegistration = CellRegistration {cell, _, item in + cell.configure(model: item) + } + + let headerRegistration = HeaderRegistration(elementKind: UICollectionView.elementKindSectionHeader) { headerView, _, _ in + if let date = self.selectedDate { + headerView.configure(text: Utils.dateFormatterString(format: "YYYY년 MM월 dd일", + date: date)) + } + } + + dataSource = DataSource(collectionView: collectionView, cellProvider: { collectionView, indexPath, item in + return collectionView.dequeueConfiguredReusableCell(using: cellRegistration, + for: indexPath, + item: item) }) + + dataSource?.supplementaryViewProvider = { collectionView, _, indexPath in + return collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, + for: indexPath) + } } - private func reloadData() { - var snapShot = NSDiffableDataSourceSnapshot() + private func setSnapShot() { + + var snapShot = SnapShot() defer { - dataSource.apply(snapShot, animatingDifferences: false) + dataSource?.apply(snapShot, animatingDifferences: false) } snapShot.appendSections([.main]) @@ -130,30 +129,45 @@ extension DetailAchievementViewController { } private func updateData(item: [DailyMissionResponseDTO]) { - var snapshot = dataSource.snapshot() + + guard var snapshot = dataSource?.snapshot() else { return } + snapshot.appendItems(item, toSection: .main) - dataSource.apply(snapshot) + dataSource?.apply(snapshot) + AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Achieve.appearDailyMissionModal(total: item.count)) } private func layout() -> UICollectionViewCompositionalLayout { - var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) + + var config = UICollectionLayoutListConfiguration(appearance: .plain) + config.headerMode = .supplementary config.backgroundColor = .clear config.separatorConfiguration.color = .gray5! - let listLayout = UICollectionViewCompositionalLayout.list(using: config) - return listLayout + config.separatorConfiguration.topSeparatorVisibility = .hidden + config.separatorConfiguration.bottomSeparatorInsets = .init(top: 0, + leading: 20, + bottom: 0, + trailing: 20) + config.itemSeparatorHandler = { indexPath, config in + var config = config + guard let itemCount = self.dataSource?.snapshot().itemIdentifiers(inSection: .main).count else { return config } + let isLastItem = indexPath.item == itemCount - 1 + config.bottomSeparatorVisibility = isLastItem ? .hidden : .visible + return config + } + + return UICollectionViewCompositionalLayout.list(using: config) } } extension DetailAchievementViewController { - private func requestDetailAPI(date: String) { HomeAPI.shared.getDailyMission(date: date) { [weak self] response in guard let self else { return } guard let response = response else { return } guard let data = response.data else { return } - let missionList = data - self.updateData(item: missionList) + self.updateData(item: data) } } } diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/StatisticsView.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/StatisticsView.swift index f1b42f42..4b56838f 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/StatisticsView.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Achievement/ViewControllers/StatisticsView.swift @@ -21,6 +21,7 @@ final class StatisticsView: UIView { override init(frame: CGRect) { super.init(frame: .zero) + setUI() setLayout() } @@ -33,10 +34,13 @@ final class StatisticsView: UIView { // MARK: - Methods extension StatisticsView { + private func setUI() { + totalImage.do { $0.image = .icSNS } + titleLabel.do { $0.text = I18N.total $0.font = .Pretendard(.regular, size: 14) @@ -45,6 +49,7 @@ extension StatisticsView { $0.textAlignment = .center } } + private func setLayout() { addSubviews(totalImage, titleLabel) diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Auth/AuthViewController.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Auth/AuthViewController.swift index 0ffeb4cb..cb753804 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Auth/AuthViewController.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Auth/AuthViewController.swift @@ -254,12 +254,12 @@ extension AuthViewController { } func checkNotificationSettings() { - UNUserNotificationCenter.current().getNotificationSettings { settings in + UNUserNotificationCenter.current().getNotificationSettings { [weak self] settings in switch settings.authorizationStatus { case .notDetermined: - self.showNotiDialogView() + self?.showNotiDialogView() default: - break + self?.presentToHomeViewController() } } } diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Common/Layout/CompositionalLayout.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Common/Layout/CompositionalLayout.swift index 761869f6..afadf6d3 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Common/Layout/CompositionalLayout.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Common/Layout/CompositionalLayout.swift @@ -26,17 +26,22 @@ final class CompositionalLayout { return section } - class func setUpSection(layoutEnvironment: NSCollectionLayoutEnvironment, mode: UICollectionLayoutListConfiguration.HeaderMode, _ top: CGFloat, _ bottom: CGFloat) -> NSCollectionLayoutSection { - var config = UICollectionLayoutListConfiguration(appearance: .insetGrouped) - config.headerMode = mode - config.showsSeparators = true - config.separatorConfiguration.color = UIColor.gray2! - config.backgroundColor = .clear - config.headerTopPadding = 22 + static func setUpSection(layoutEnvironment: NSCollectionLayoutEnvironment, + mode: UICollectionLayoutListConfiguration.HeaderMode = .none, + topContentInset: CGFloat = 0, + bottomContentInset: CGFloat = 0) + -> NSCollectionLayoutSection { + var listConfig = UICollectionLayoutListConfiguration(appearance: .insetGrouped) + listConfig.headerMode = mode + listConfig.showsSeparators = true + listConfig.separatorConfiguration.color = UIColor.gray2! + listConfig.backgroundColor = .clear + listConfig.headerTopPadding = 22 - let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: layoutEnvironment) - section.contentInsets = NSDirectionalEdgeInsets(top: top, leading: 0, bottom: bottom, trailing: 0) - if config.headerMode == .supplementary { + let section = NSCollectionLayoutSection.list(using: listConfig, layoutEnvironment: layoutEnvironment) + section.contentInsets = NSDirectionalEdgeInsets(top: topContentInset, leading: 0, bottom: bottomContentInset, trailing: 0) + + if listConfig.headerMode == .supplementary { let headerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .absolute(22)) let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top) section.boundarySupplementaryItems = [header] diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Common/Modal/CommonNotificationViewController.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Common/Modal/CommonNotificationViewController.swift index 6f76e237..211c4963 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Common/Modal/CommonNotificationViewController.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Common/Modal/CommonNotificationViewController.swift @@ -13,6 +13,10 @@ import SafariServices final class CommonNotificationViewController: UIViewController { + // MARK: - Property + + var tapCloseButton: (() -> Void)? + // MARK: - UI Components private let backgroundView = UIView() @@ -24,7 +28,7 @@ final class CommonNotificationViewController: UIViewController { private lazy var formButton = UIButton() private lazy var closeButton = UIButton() private lazy var deprecatedButton = UIButton() - + // MARK: - View Life Cycle override func viewDidLoad() { @@ -161,15 +165,18 @@ extension CommonNotificationViewController { @objc func didFormButtonTap() { - + guard let url = URL(string: MyInfoURL.googleForm.url) else { return } let safariView: SFSafariViewController = SFSafariViewController(url: url) safariView.delegate = self self.present(safariView, animated: true, completion: nil) + + AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Login.clickAdModalCta) } @objc func didCancelButtonTap() { + self.tapCloseButton?() dismissViewController() } @@ -177,7 +184,7 @@ extension CommonNotificationViewController { func didDeprecatedButtonTap() { deprecatedButton.isSelected.toggle() KeychainUtil.setBool(deprecatedButton.isSelected, - forKey: DefaultKeys.isSelected) + forKey: DefaultKeys.isDeprecatedBtnClicked) } } diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/ViewControllers/HomeViewController.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/ViewControllers/HomeViewController.swift index bd19779e..13df4837 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/ViewControllers/HomeViewController.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/Home/ViewControllers/HomeViewController.swift @@ -23,16 +23,14 @@ final class HomeViewController: UIViewController { private var current: Date? private lazy var safeArea = self.view.safeAreaLayoutGuide - private var isSelected: Bool { - return KeychainUtil.isSelected() - } + private var didCloseButtonTap: Bool = false + private var didDeprecatedButtonTap: Bool { return KeychainUtil.isDeprecatedBtnClicked() } // MARK: - UI Components private var missionCollectionView = UICollectionView(frame: .zero, collectionViewLayout: .init()) private lazy var missionDataSource = HomeDataSource(collectionView: missionCollectionView, missionList: missionList) - private lazy var alertViewContrilelr = CommonNotificationViewController() private let weekCalendar = CalendarView(calendarScope: .week, scrollDirection: .horizontal) private let addButton = UIButton() @@ -40,8 +38,8 @@ final class HomeViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - - showPopup(isSelected: isSelected) + + showPopup(isSelected: didCloseButtonTap) AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Home.viewHome) dailyLoadData() @@ -53,7 +51,6 @@ final class HomeViewController: UIViewController { setUI() setLayout() - } } @@ -88,7 +85,7 @@ extension HomeViewController { } private func setLayout() { - + view.addSubviews(weekCalendar, missionCollectionView, addButton) weekCalendar.calendar.select(today) @@ -220,7 +217,7 @@ extension HomeViewController: FSCalendarDelegate, FSCalendarDataSource, FSCalend AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Home.clickWeeklyDate(date: Utils.dateFormatterString(format: nil, date: date))) } - + func calendar(_ calendar: FSCalendar, titleFor date: Date) -> String? { Utils.dateFormatterString(format: "EEEEEE", date: date) } @@ -238,12 +235,12 @@ extension HomeViewController: FSCalendarDelegate, FSCalendarDataSource, FSCalend } func calendar(_ calendar: FSCalendar, cellFor date: Date, at position: FSCalendarMonthPosition) -> FSCalendarCell { - + let cell = calendar.dequeueReusableCell(withIdentifier: MissionCalendarCell.identifier, for: date, at: position) as! MissionCalendarCell guard let percentage = getPercentage(for: date) else { return cell } cell.configure(percent: percentage) - + return cell } } @@ -288,9 +285,9 @@ extension HomeViewController { } private func requestDeleteMission(index: Int, id: Int) { - HomeAPI.shared.deleteMission(id: id) { [weak self] _ in + HomeAPI.shared.deleteMission(id: id) { [weak self] _ in guard let self else { return } - + self.dailyLoadData() self.weeklyLoadData() self.missionDataSource.updateSnapShot(missionList: self.missionList) @@ -356,11 +353,17 @@ extension HomeViewController { extension HomeViewController { private func showPopup(isSelected: Bool) { - if !isSelected { + + if !(isSelected || didDeprecatedButtonTap) { let nextView = CommonNotificationViewController() nextView.modalPresentationStyle = .overFullScreen nextView.modalTransitionStyle = .crossDissolve self.present(nextView, animated: true) + + nextView.tapCloseButton = { + self.didCloseButtonTap = true + AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.Login.clickAdModalClose(again: self.didDeprecatedButtonTap ? "yes": "no" )) + } } } } diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Cell/InfoCollectionViewCell.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Cell/InfoCollectionViewCell.swift index 84e7c230..b667cb6d 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Cell/InfoCollectionViewCell.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Cell/InfoCollectionViewCell.swift @@ -7,7 +7,10 @@ import UIKit -class InfoCollectionViewCell: UICollectionViewCell { +import SnapKit +import Then + +final class InfoCollectionViewCell: UICollectionViewCell { // MARK: - Properties @@ -20,8 +23,11 @@ class InfoCollectionViewCell: UICollectionViewCell { private let titleLabel = UILabel() private let arrowImage = UIImageView() + // MARK: - Life Cycle + override init(frame: CGRect) { super.init(frame: .zero) + setUI() setLayout() } @@ -34,6 +40,7 @@ class InfoCollectionViewCell: UICollectionViewCell { // MARK: - Methods extension InfoCollectionViewCell { + private func setUI() { backgroundColor = .gray1 @@ -47,6 +54,7 @@ extension InfoCollectionViewCell { $0.axis = .horizontal $0.spacing = 6 } + arrowImage.do { $0.isHidden = true $0.image = .icRightArrow @@ -73,7 +81,8 @@ extension InfoCollectionViewCell { } } - func configureWithIcon(model: InfoModelTwo) { + func configureWithIcon(with model: InfoModel) { + iconImage.image = model.image titleLabel.text = model.title @@ -82,17 +91,10 @@ extension InfoCollectionViewCell { } } - func configureWithArrow(model: InfoModelThree) { - horizontalStackView.removeArrangedSubview(iconImage) - iconImage.removeFromSuperview() - titleLabel.text = model.title - arrowImage.isHidden = false - } - - func configure(model: InfoModelFour) { + func configure(with model: InfoModel, isHidden: Bool) { horizontalStackView.removeArrangedSubview(iconImage) iconImage.removeFromSuperview() titleLabel.text = model.title - arrowImage.isHidden = true + arrowImage.isHidden = isHidden } } diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Cell/MyInfoHeaderCollectionReusableView.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Cell/MyInfoHeaderView.swift similarity index 83% rename from iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Cell/MyInfoHeaderCollectionReusableView.swift rename to iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Cell/MyInfoHeaderView.swift index e29784e4..aecc8d6d 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Cell/MyInfoHeaderCollectionReusableView.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Cell/MyInfoHeaderView.swift @@ -10,18 +10,21 @@ import UIKit import Then import SnapKit -class MyInfoHeaderReusableView: UICollectionReusableView { +final class MyInfoHeaderView: UICollectionReusableView { // MARK: - Properties - static let identifier = "MyInfoHeaderReusableView" + static let identifier = "MyInfoHeaderView" // MARK: - UI Components private let myInfoLabel = UILabel() + // MARK: - Life Cycle + override init(frame: CGRect) { super.init(frame: .zero) + setUI() setLayout() } @@ -33,7 +36,8 @@ class MyInfoHeaderReusableView: UICollectionReusableView { // MARK: - Methods -extension MyInfoHeaderReusableView { +extension MyInfoHeaderView { + private func setUI() { myInfoLabel.do { $0.text = I18N.myInfo @@ -41,6 +45,7 @@ extension MyInfoHeaderReusableView { $0.textColor = .white } } + private func setLayout() { addSubview(myInfoLabel) diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Cell/MyProfileCollectionViewCell.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Cell/MyProfileCollectionViewCell.swift index 95ded053..2bdefe65 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Cell/MyProfileCollectionViewCell.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Cell/MyProfileCollectionViewCell.swift @@ -7,25 +7,31 @@ import UIKit -class MyProfileCollectionViewCell: UICollectionViewCell { +import SnapKit +import Then + +final class MyProfileCollectionViewCell: UICollectionViewCell { // MARK: - Properties - + static let identifier = "MyProfileCollectionViewCell" - + // MARK: - UI Components private let logoImage = UIImageView() private let verticalStackView = UIStackView() private let userLabel = UILabel() private let emailLabel = UILabel() - + + // MARK: - Life Cycle + override init(frame: CGRect) { super.init(frame: .zero) + setUI() setLayout() } - + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -34,6 +40,7 @@ class MyProfileCollectionViewCell: UICollectionViewCell { // MARK: - Methods extension MyProfileCollectionViewCell { + private func setUI() { backgroundColor = .gray1 @@ -43,12 +50,14 @@ extension MyProfileCollectionViewCell { $0.textColor = .white $0.font = .Pretendard(.regular, size: 15) } + emailLabel.do { $0.textAlignment = .left $0.numberOfLines = 1 $0.textColor = .gray4 $0.font = .Pretendard(.regular, size: 12) } + verticalStackView.do { $0.addArrangedSubviews(userLabel, emailLabel) $0.axis = .vertical @@ -73,7 +82,7 @@ extension MyProfileCollectionViewCell { } } - func configure(model: InfoModelOne) { + func configure(model: InfoModel) { logoImage.image = model.image userLabel.text = model.user emailLabel.text = model.email diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Model/MyInfoModel.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Model/MyInfoModel.swift index 396aabba..7396ce45 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Model/MyInfoModel.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Model/MyInfoModel.swift @@ -7,36 +7,23 @@ import UIKit -struct InfoModelOne: Hashable { - var image: UIImage - var user: String - var email: String -} - -struct InfoModelTwo: Hashable { - var image: UIImage - var title: String -} -extension InfoModelTwo { - static let items: [InfoModelTwo] = [InfoModelTwo(image: .icGuide, title: "낫투두 가이드"), - InfoModelTwo(image: .icQuestion1, title: "자주 묻는 질문") +struct InfoModel: Hashable { + + var image: UIImage? + var user: String? + var email: String? + var title: String? + + static var profile: [InfoModel] = [InfoModel(image: .imgUser, + user: UserDefaults.standard.bool(forKey: DefaultKeys.isAppleLogin) ? KeychainUtil.getAppleUsername() : KeychainUtil.getKakaoNickname(), + email: UserDefaults.standard.bool(forKey: DefaultKeys.isAppleLogin) ? KeychainUtil.getAppleEmail() : KeychainUtil.getKakaoEmail())] + + static let support: [InfoModel] = [InfoModel(image: .icGuide, title: "낫투두 가이드"), + InfoModel(image: .icQuestion1, title: "자주 묻는 질문") ] -} - -struct InfoModelThree: Hashable { - var title: String -} -extension InfoModelThree { - static let items: [InfoModelThree] = [InfoModelThree(title: "공지사항"), - InfoModelThree(title: "문의하기"), - InfoModelThree(title: "약관 및 정책") + static let info: [InfoModel] = [InfoModel(title: "공지사항"), + InfoModel(title: "문의하기"), + InfoModel(title: "약관 및 정책") ] -} - -struct InfoModelFour: Hashable { - var title: String -} -extension InfoModelFour { - - static let item: [InfoModelFour] = [InfoModelFour(title: "버전 정보 "+(Utils.version ?? "1.0.0"))] + static func version() -> [InfoModel] { return [InfoModel(title: "버전 정보 "+(Utils.version ?? "1.0.0"))] } } diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Viewcontrollers/MyInfoViewController.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Viewcontrollers/MyInfoViewController.swift index 542882fa..fff1a586 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Viewcontrollers/MyInfoViewController.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/MyInfo/Viewcontrollers/MyInfoViewController.swift @@ -13,49 +13,46 @@ import SnapKit final class MyInfoViewController: UIViewController { // MARK: - Properties + + typealias CellRegistration = UICollectionView.CellRegistration + typealias HeaderRegistration = UICollectionView.SupplementaryRegistration + typealias DataSource = UICollectionViewDiffableDataSource + typealias SnapShot = NSDiffableDataSourceSnapshot - private var infoOne: [InfoModelOne] = [InfoModelOne(image: .imgUser, user: KeychainUtil.getBool(DefaultKeys.isAppleLogin) ? KeychainUtil.getAppleUsername() : KeychainUtil.getKakaoNickname(), email: KeychainUtil.getBool(DefaultKeys.isAppleLogin) ? KeychainUtil.getAppleEmail() : KeychainUtil.getKakaoEmail())] - private let infoTwo: [InfoModelTwo] = InfoModelTwo.items - private let infoThree: [InfoModelThree] = InfoModelThree.items - private let infoFour: [InfoModelFour] = InfoModelFour.item - - enum Sections: Int, Hashable { - case one, two, three, four + enum Sections: Int, CaseIterable { + case profile, support, info, version } - typealias DataSource = UICollectionViewDiffableDataSource - var dataSource: DataSource? + + private var dataSource: DataSource? + private lazy var safeArea = self.view.safeAreaLayoutGuide // MARK: - UI Components - private lazy var myInfoCollectionView = UICollectionView(frame: .zero, collectionViewLayout: layout()) + private let myInfoCollectionView = UICollectionView(frame: .zero, collectionViewLayout: .init()) // MARK: - Life Cycle - + override func viewDidLoad() { super.viewDidLoad() + AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.MyInfo.viewMyInfo) setUI() - register() setLayout() setupDataSource() - reloadData() + setSnapShot() } } // MARK: - Methods extension MyInfoViewController { - private func register() { - myInfoCollectionView.register(MyProfileCollectionViewCell.self, forCellWithReuseIdentifier: MyProfileCollectionViewCell.identifier) - myInfoCollectionView.register(InfoCollectionViewCell.self, forCellWithReuseIdentifier: InfoCollectionViewCell.identifier) - myInfoCollectionView.register(MyInfoHeaderReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: MyInfoHeaderReusableView.identifier) - } private func setUI() { view.backgroundColor = .ntdBlack myInfoCollectionView.do { + $0.collectionViewLayout = layout() $0.backgroundColor = .clear $0.bounces = false $0.autoresizingMask = [.flexibleWidth, .flexibleHeight] @@ -73,100 +70,136 @@ extension MyInfoViewController { } } - // MARK: - Data - private func setupDataSource() { + + let profileCellRegistration = CellRegistration {cell, _, item in + cell.configure(model: item) + } + + let infoCellRegistration = CellRegistration {cell, indexPath, item in + + guard let section = Sections(rawValue: indexPath.section) else { return } + + switch section { + case .support: + cell.configureWithIcon(with: item) + case .info: + cell.configure(with: item, isHidden: false) + default: + cell.configure(with: item, isHidden: true) + } + } + + let headerRegistration = HeaderRegistration(elementKind: UICollectionView.elementKindSectionHeader) { _, _, _ in } + dataSource = DataSource(collectionView: myInfoCollectionView, cellProvider: { collectionView, indexPath, item in - let section = self.dataSource?.snapshot().sectionIdentifiers[indexPath.section] + + guard let section = Sections(rawValue: indexPath.section) else { return UICollectionViewCell() } + switch section { - case .one: - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyProfileCollectionViewCell.identifier, for: indexPath) as! MyProfileCollectionViewCell - cell.configure(model: item as! InfoModelOne ) - return cell - case .two: - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: InfoCollectionViewCell.identifier, for: indexPath) as! InfoCollectionViewCell - cell.configureWithIcon(model: item as! InfoModelTwo ) - return cell - case .three: - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: InfoCollectionViewCell.identifier, for: indexPath) as! InfoCollectionViewCell - cell.configureWithArrow(model: item as! InfoModelThree) - return cell - case .four, .none: - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: InfoCollectionViewCell.identifier, for: indexPath) as! InfoCollectionViewCell - cell.configure(model: item as! InfoModelFour) - return cell + case .profile: + return collectionView.dequeueConfiguredReusableCell(using: profileCellRegistration, + for: indexPath, + item: item) + case .support, .info, .version: + return collectionView.dequeueConfiguredReusableCell(using: infoCellRegistration, + for: indexPath, + item: item) } }) + + dataSource?.supplementaryViewProvider = { collectionView, _, indexPath in + return collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, + for: indexPath) + } } - private func reloadData() { - var snapShot = NSDiffableDataSourceSnapshot() + private func setSnapShot() { + + var snapShot = SnapShot() + defer { dataSource?.apply(snapShot, animatingDifferences: false) } - snapShot.appendSections([.one, .two, .three, .four]) - snapShot.appendItems(infoOne, toSection: .one) - snapShot.appendItems(infoTwo, toSection: .two) - snapShot.appendItems(infoThree, toSection: .three) - snapShot.appendItems(infoFour, toSection: .four) - dataSource?.supplementaryViewProvider = { (collectionView, _, indexPath) in - guard let header = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: MyInfoHeaderReusableView.identifier, for: indexPath) as? MyInfoHeaderReusableView else { return UICollectionReusableView() } - return header - } + snapShot.appendSections(Sections.allCases) + snapShot.appendItems(InfoModel.profile, toSection: .profile) + snapShot.appendItems(InfoModel.support, toSection: .support) + snapShot.appendItems(InfoModel.info, toSection: .info) + snapShot.appendItems(InfoModel.version(), toSection: .version) + } - // MARK: - Layout - private func layout() -> UICollectionViewLayout { + let layout = UICollectionViewCompositionalLayout { sectionIndex, env in - let section = self.dataSource?.snapshot().sectionIdentifiers[sectionIndex] + + guard let section = Sections(rawValue: sectionIndex) else { return nil } switch section { - case .one: - return CompositionalLayout.setUpSection(layoutEnvironment: env, mode: .supplementary, 24, 0) - case .two, .three: - return CompositionalLayout.setUpSection(layoutEnvironment: env, mode: .none, 18, 0) - case .four, .none: - return CompositionalLayout.setUpSection(layoutEnvironment: env, mode: .none, 18, 60) + case .profile: + return CompositionalLayout.setUpSection(layoutEnvironment: env, + mode: .supplementary, + topContentInset: 24) + case .support, .info: + return CompositionalLayout.setUpSection(layoutEnvironment: env, + topContentInset: 18) + case .version: + return CompositionalLayout.setUpSection(layoutEnvironment: env, + topContentInset: 18, + bottomContentInset: 60) + } + } return layout } } + +// MARK: - CollectionViewDelegate + extension MyInfoViewController: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { switch indexPath.section { case 0: - AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.MyInfo.clickMyInfo) - let nextViewController = MyInfoAccountViewController() - nextViewController.hidesBottomBarWhenPushed = false - navigationController?.pushViewController(nextViewController, animated: true) + profileSectionSelection() case 1: - switch indexPath.item { - case 0: - AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.MyInfo.clickGuide) - Utils.myInfoUrl(vc: self, url: MyInfoURL.guid.url) - default: - AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.MyInfo.clickFaq) - Utils.myInfoUrl(vc: self, url: MyInfoURL.quesition.url) - } + infoSectionSelection(for: indexPath, + events: [.clickGuide, .clickFaq], + urls: [.guid, .faq]) case 2: - switch indexPath.item { - case 0: - AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.MyInfo.clickNotice) - Utils.myInfoUrl(vc: self, url: MyInfoURL.notice.url) - case 1: - AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.MyInfo.clickQuestion) - Utils.myInfoUrl(vc: self, url: MyInfoURL.contact.url) - case 2: - AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.MyInfo.clickTerms) - Utils.myInfoUrl(vc: self, url: MyInfoURL.service.url) - default: - AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.MyInfo.clickOpenSource) - Utils.myInfoUrl(vc: self, url: MyInfoURL.opensource.url) - } + infoSectionSelection(for: indexPath, + events: [.clickNotice, .clickQuestion, .clickTerms], + urls: [.notice, .question, .service]) default: return } } + + private func profileSectionSelection() { + sendAnalyticsEvent(.clickMyInfo) { + + let nextViewController = MyInfoAccountViewController() + nextViewController.hidesBottomBarWhenPushed = false + navigationController?.pushViewController(nextViewController, animated: true) + } + } + + private func infoSectionSelection(for indexPath: IndexPath, + events: [AnalyticsEvent.MyInfo], + urls: [MyInfoURL]) { + guard let item = urls.indices.contains(indexPath.item) ? urls[indexPath.item] : nil, + let event = events.indices.contains(indexPath.item) ? events[indexPath.item] : nil else { + return + } + + sendAnalyticsEvent(event) { + Utils.myInfoUrl(vc: self, url: item.url) + } + } + + private func sendAnalyticsEvent(_ event: AnalyticsEvent.MyInfo, action: () -> Void) { + AmplitudeAnalyticsService.shared.send(event: event) + action() + } } diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/NotificatoinDialog/ViewControllers/NotificationDialogViewController.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/NotificatoinDialog/ViewControllers/NotificationDialogViewController.swift index 8ab5de09..f5af2feb 100644 --- a/iOS-NOTTODO/iOS-NOTTODO/Presentation/NotificatoinDialog/ViewControllers/NotificationDialogViewController.swift +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/NotificatoinDialog/ViewControllers/NotificationDialogViewController.swift @@ -183,5 +183,6 @@ extension NotificationDialogViewController { @objc private func buttonTapped() { buttonHandler?() + AmplitudeAnalyticsService.shared.send(event: AnalyticsEvent.OnboardingClick.clickOnboardingNext6) } } diff --git a/iOS-NOTTODO/iOS-NOTTODO/Presentation/UpdateCheck/ViewControllers/UpdateCheckViewController.swift b/iOS-NOTTODO/iOS-NOTTODO/Presentation/UpdateCheck/ViewControllers/UpdateCheckViewController.swift new file mode 100644 index 00000000..904585ef --- /dev/null +++ b/iOS-NOTTODO/iOS-NOTTODO/Presentation/UpdateCheck/ViewControllers/UpdateCheckViewController.swift @@ -0,0 +1,180 @@ +// +// UpdateCheckViewController.swift +// iOS-NOTTODO +// +// Created by 김혜수 on 1/26/24. +// + +import UIKit + +final class UpdateCheckViewController: UIViewController { + + enum UpdateType { + case optional + case force(newVersion: String) + case none + } + + enum UpdateCheckError: Error { + case versionFetchError + } + + enum Constant { + static let appstoreURL: String = "itms-apps://itunes.apple.com/app/\(Bundle.main.appleId)" + } + + override func viewDidLoad() { + super.viewDidLoad() + + self.setUI() + self.checkUpdate() + } +} + +extension UpdateCheckViewController { + private func setUI() { + self.view.backgroundColor = .ntdBlack + } + + private func checkUpdate() { + Task { + do { + let updateType = try await checkUpdateType() + switch updateType { + case .force(let newVersion): + self.showForceUpdateAlert(newVersion: newVersion) + case .optional: + self.showUpdateAlert() + case .none: + self.changeMainViewController() + } + } catch { + self.changeMainViewController() + } + } + } + + private func changeMainViewController() { + if KeychainUtil.getAccessToken().isEmpty { + SceneDelegate.shared?.changeRootViewControllerTo(ValueOnboardingViewController()) + } else { + SceneDelegate.shared?.changeRootViewControllerTo(TabBarController()) + } + } + + private func checkUpdateType() async throws -> UpdateType { + // 앱스토어 버전 + guard let appstoreVersion = try await getAppstoreVersion() else { + throw UpdateCheckError.versionFetchError + } + + // 현재 설치된 앱의 버전 + guard let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String else { + throw UpdateCheckError.versionFetchError + } + + // 최소 지원버전 + guard let minimumVersion = await FirebaseUtil.shared.fetchRemoteConfig(type: .minimumVersion) else { + throw UpdateCheckError.versionFetchError + } + + if shouldUpdate(userVersion: appVersion, appstoreVersion: minimumVersion) { + return .force(newVersion: appstoreVersion) + } + + if shouldUpdate(userVersion: appVersion, appstoreVersion: appstoreVersion) { + return .optional + } + + return .none + } + + /// 버전 비교하는 메서드 + private func shouldUpdate(userVersion: String, appstoreVersion: String) -> Bool { + let userMajor = userVersion.split(separator: ".").map {Int($0)!}[0] + let appstoreMajor = appstoreVersion.split(separator: ".").map {Int($0)!}[0] + + if userMajor < appstoreMajor { + return true + } + + let userMinor = userVersion.split(separator: ".").map {Int($0)!}[1] + let appstoreMinor = appstoreVersion.split(separator: ".").map {Int($0)!}[1] + + if userMinor < appstoreMinor { + return true + } + + let userPatch = userVersion.split(separator: ".").map {Int($0)!}[2] + let appstorePatch = appstoreVersion.split(separator: ".").map {Int($0)!}[2] + + if userPatch < appstorePatch { + return true + } + + return false + } + + /// 앱스토어에 배포된 버전 가져오는 메서드 + private func getAppstoreVersion() async throws -> String? { + let appleID = Bundle.main.appleId + guard let url = URL(string: "https://itunes.apple.com/lookup?id=\(appleID)") else { return nil } + let (data, _) = try await URLSession.shared.data(from: url) + let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] + guard let results = json?["results"] as? [[String: Any]], + let appStoreVersion = results[0]["version"] as? String else { + return nil + } + return appStoreVersion + } + + /// 선택 업데이트 경고창 + private func showUpdateAlert() { + let alertController = UIAlertController( + title: I18N.update, + message: I18N.updateAlert, + preferredStyle: .alert + ) + + let updateAction = UIAlertAction(title: I18N.update, style: .default) { _ in + // App Store로 이동 + if let appStoreURL = URL(string: Constant.appstoreURL) { + UIApplication.shared.open(appStoreURL, options: [:], completionHandler: { [weak self] _ in + self?.changeMainViewController() + }) + } + } + + let cancelAction = UIAlertAction(title: I18N.later, style: .default) { _ in + // App Store로 이동 + if let appStoreURL = URL(string: Constant.appstoreURL) { + UIApplication.shared.open(appStoreURL, options: [:], completionHandler: { [weak self] _ in + self?.changeMainViewController() + }) + } + } + + alertController.addAction(updateAction) + alertController.addAction(cancelAction) + + self.present(alertController, animated: true) + } + + /// 강제 업데이트 경고창 + private func showForceUpdateAlert(newVersion: String) { + let alertController = UIAlertController( + title: I18N.update, + message: I18N.forceUpdateAlert(newVersion: newVersion), + preferredStyle: .alert + ) + + let updateAction = UIAlertAction(title: I18N.update, style: .default) { _ in + // App Store로 이동 + if let appStoreURL = URL(string: Constant.appstoreURL) { + UIApplication.shared.open(appStoreURL, options: [:], completionHandler: nil) + } + } + alertController.addAction(updateAction) + self.present(alertController, animated: true) + } +}