diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f3923e7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,112 @@ +# Created by https://www.toptal.com/developers/gitignore/api/swift,cocoapods,xcode +# Edit at https://www.toptal.com/developers/gitignore?templates=swift,cocoapods,xcode + +#plist +privacyInfo.plist + +### CocoaPods ### +## CocoaPods GitIgnore Template + +# CocoaPods - Only use to conserve bandwidth / Save time on Pushing +# - Also handy if you have a large number of dependant pods +# - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE +Pods/ + +### Swift ### +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) +*.xcscmblueprint +*.xccheckout + +## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) +build/ +DerivedData/ +*.moved-aside +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# Package.resolved +# *.xcodeproj +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +# .swiftpm + +.build/ + +# CocoaPods +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# Pods/ +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# Accio dependency management +Dependencies/ +.accio/ + +# fastlane +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# Code Injection +# After new code Injection tools there's a generated folder /iOSInjectionProject +# https://github.com/johnno1962/injectionforxcode + +iOSInjectionProject/ + +### Xcode ### + +## Xcode 8 and earlier + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno +**/xcshareddata/WorkspaceSettings.xcsettings + +# End of https://www.toptal.com/developers/gitignore/api/swift,cocoapods,xcode \ No newline at end of file diff --git a/assignment/assignment.xcodeproj/project.pbxproj b/assignment/assignment.xcodeproj/project.pbxproj index 156fe92..4882ca6 100644 --- a/assignment/assignment.xcodeproj/project.pbxproj +++ b/assignment/assignment.xcodeproj/project.pbxproj @@ -7,23 +7,27 @@ objects = { /* Begin PBXBuildFile section */ - 782EED6C2BDF8FC2000A3B96 /* RootCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 782EED6B2BDF8FC2000A3B96 /* RootCollectionViewController.swift */; }; + 7808224B2C04698F006B10C8 /* LoginVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7808224A2C04698F006B10C8 /* LoginVM.swift */; }; + 7808224D2C046BB9006B10C8 /* Observable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7808224C2C046BB9006B10C8 /* Observable.swift */; }; + 780822512C0475DA006B10C8 /* NicknameVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 780822502C0475DA006B10C8 /* NicknameVM.swift */; }; + 780822532C04769D006B10C8 /* WelcomeVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 780822522C04769D006B10C8 /* WelcomeVM.swift */; }; + 782EED6C2BDF8FC2000A3B96 /* RootCVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 782EED6B2BDF8FC2000A3B96 /* RootCVC.swift */; }; 782EED6E2BE00B82000A3B96 /* LiveCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 782EED6D2BE00B82000A3B96 /* LiveCell.swift */; }; 78412EB92BD692C4002AA9E8 /* mainModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78412EB82BD692C4002AA9E8 /* mainModel.swift */; }; 78412EBB2BD699C8002AA9E8 /* ContentCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78412EBA2BD699C8002AA9E8 /* ContentCell.swift */; }; 78412EBF2BD69D6F002AA9E8 /* TitleHeaderViewCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78412EBE2BD69D6F002AA9E8 /* TitleHeaderViewCollectionViewCell.swift */; }; 78412EC12BD69D88002AA9E8 /* DoosanFooterViewCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78412EC02BD69D88002AA9E8 /* DoosanFooterViewCollectionViewCell.swift */; }; 78412EC32BD9E4EE002AA9E8 /* CustomFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78412EC22BD9E4EE002AA9E8 /* CustomFooterView.swift */; }; - 78A5108A2BD652FE005AAE0F /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A510892BD652FE005AAE0F /* MainViewController.swift */; }; - 78B643B42BDA418F00C2EBEF /* LookinServer in Frameworks */ = {isa = PBXBuildFile; productRef = 78B643B32BDA418F00C2EBEF /* LookinServer */; }; + 784DDEE22C0467D70061B432 /* LoginModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 784DDEE12C0467D70061B432 /* LoginModel.swift */; }; + 785AE1CA2C3467DC00677CA0 /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 785AE1C92C3467DC00677CA0 /* View.swift */; }; + 785AE1CD2C346BA900677CA0 /* BindingTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 785AE1CC2C346BA900677CA0 /* BindingTextField.swift */; }; + 78A5108A2BD652FE005AAE0F /* MainVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A510892BD652FE005AAE0F /* MainVC.swift */; }; 78B643D62BDE30CC00C2EBEF /* UnderlineSegment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78B643D52BDE30CC00C2EBEF /* UnderlineSegment.swift */; }; 78BAE7972BC43AC30013CACE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BAE7962BC43AC30013CACE /* AppDelegate.swift */; }; 78BAE7992BC43AC30013CACE /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BAE7982BC43AC30013CACE /* SceneDelegate.swift */; }; - 78BAE79B2BC43AC30013CACE /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BAE79A2BC43AC30013CACE /* LoginViewController.swift */; }; + 78BAE79B2BC43AC30013CACE /* LoginVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BAE79A2BC43AC30013CACE /* LoginVC.swift */; }; 78BAE7A02BC43AC40013CACE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 78BAE79F2BC43AC40013CACE /* Assets.xcassets */; }; 78BAE7A32BC43AC40013CACE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 78BAE7A12BC43AC40013CACE /* LaunchScreen.storyboard */; }; - 78BAE8062BC4E1EA0013CACE /* Then in Frameworks */ = {isa = PBXBuildFile; productRef = 78BAE8052BC4E1EA0013CACE /* Then */; }; - 78BAE8092BC4E1F00013CACE /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 78BAE8082BC4E1F00013CACE /* SnapKit */; }; 78BAE8152BC4E6BB0013CACE /* Pretendard-ExtraBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 78BAE80C2BC4E6BA0013CACE /* Pretendard-ExtraBold.ttf */; }; 78BAE8162BC4E6BB0013CACE /* Pretendard-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 78BAE80D2BC4E6BA0013CACE /* Pretendard-SemiBold.ttf */; }; 78BAE8172BC4E6BB0013CACE /* Pretendard-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 78BAE80E2BC4E6BB0013CACE /* Pretendard-Light.ttf */; }; @@ -33,27 +37,44 @@ 78BAE81B2BC4E6BB0013CACE /* Pretendard-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 78BAE8122BC4E6BB0013CACE /* Pretendard-Medium.ttf */; }; 78BAE81C2BC4E6BB0013CACE /* Pretendard-ExtraLight.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 78BAE8132BC4E6BB0013CACE /* Pretendard-ExtraLight.ttf */; }; 78BAE81D2BC4E6BB0013CACE /* Pretendard-Black.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 78BAE8142BC4E6BB0013CACE /* Pretendard-Black.ttf */; }; - 78BAE81F2BC91D5D0013CACE /* NicknameViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BAE81E2BC91D5D0013CACE /* NicknameViewController.swift */; }; - 78BAE8212BC91D6A0013CACE /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BAE8202BC91D6A0013CACE /* WelcomeViewController.swift */; }; - 78F3280A2BE899C2001EAC45 /* Moya in Frameworks */ = {isa = PBXBuildFile; productRef = 78F328092BE899C2001EAC45 /* Moya */; }; + 78BAE81F2BC91D5D0013CACE /* NicknameVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BAE81E2BC91D5D0013CACE /* NicknameVC.swift */; }; + 78BAE8212BC91D6A0013CACE /* WelcomeVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78BAE8202BC91D6A0013CACE /* WelcomeVC.swift */; }; + 78EAA4992C0A20E0004D1094 /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 78EAA4982C0A20E0004D1094 /* RxCocoa */; }; + 78EAA49C2C0A20F0004D1094 /* Then in Frameworks */ = {isa = PBXBuildFile; productRef = 78EAA49B2C0A20F0004D1094 /* Then */; }; + 78EAA49F2C0A211E004D1094 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = 78EAA49E2C0A211E004D1094 /* SnapKit */; }; + 78EAA4A22C0A213D004D1094 /* Moya in Frameworks */ = {isa = PBXBuildFile; productRef = 78EAA4A12C0A213D004D1094 /* Moya */; }; + 78EAA4A42C0A213D004D1094 /* RxMoya in Frameworks */ = {isa = PBXBuildFile; productRef = 78EAA4A32C0A213D004D1094 /* RxMoya */; }; 78F3280C2BE8B328001EAC45 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78F3280B2BE8B328001EAC45 /* Config.swift */; }; 78F3280E2BE8B600001EAC45 /* MovieAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78F3280D2BE8B600001EAC45 /* MovieAPI.swift */; }; + 78F8391F2C0DDF06002B7AD7 /* RootVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78F8391E2C0DDF06002B7AD7 /* RootVM.swift */; }; + 78F839462C0F25C0002B7AD7 /* privacyInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 78F839452C0F25C0002B7AD7 /* privacyInfo.plist */; }; + 78F839482C0F292C002B7AD7 /* extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78F839472C0F292C002B7AD7 /* extension.swift */; }; + 78F839512C0F573C002B7AD7 /* BoxofficeDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78F839502C0F573C002B7AD7 /* BoxofficeDTO.swift */; }; + 78F839532C0F5891002B7AD7 /* MovieManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78F839522C0F5891002B7AD7 /* MovieManager.swift */; }; + 78F839552C0F593E002B7AD7 /* NetworkError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78F839542C0F593E002B7AD7 /* NetworkError.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ - 782EED6B2BDF8FC2000A3B96 /* RootCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootCollectionViewController.swift; sourceTree = ""; }; + 7808224A2C04698F006B10C8 /* LoginVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginVM.swift; sourceTree = ""; }; + 7808224C2C046BB9006B10C8 /* Observable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observable.swift; sourceTree = ""; }; + 780822502C0475DA006B10C8 /* NicknameVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NicknameVM.swift; sourceTree = ""; }; + 780822522C04769D006B10C8 /* WelcomeVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeVM.swift; sourceTree = ""; }; + 782EED6B2BDF8FC2000A3B96 /* RootCVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootCVC.swift; sourceTree = ""; }; 782EED6D2BE00B82000A3B96 /* LiveCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveCell.swift; sourceTree = ""; }; 78412EB82BD692C4002AA9E8 /* mainModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = mainModel.swift; sourceTree = ""; }; 78412EBA2BD699C8002AA9E8 /* ContentCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentCell.swift; sourceTree = ""; }; 78412EBE2BD69D6F002AA9E8 /* TitleHeaderViewCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleHeaderViewCollectionViewCell.swift; sourceTree = ""; }; 78412EC02BD69D88002AA9E8 /* DoosanFooterViewCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoosanFooterViewCollectionViewCell.swift; sourceTree = ""; }; 78412EC22BD9E4EE002AA9E8 /* CustomFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomFooterView.swift; sourceTree = ""; }; - 78A510892BD652FE005AAE0F /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; + 784DDEE12C0467D70061B432 /* LoginModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginModel.swift; sourceTree = ""; }; + 785AE1C92C3467DC00677CA0 /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; + 785AE1CC2C346BA900677CA0 /* BindingTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BindingTextField.swift; sourceTree = ""; }; + 78A510892BD652FE005AAE0F /* MainVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainVC.swift; sourceTree = ""; }; 78B643D52BDE30CC00C2EBEF /* UnderlineSegment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnderlineSegment.swift; sourceTree = ""; }; 78BAE7932BC43AC30013CACE /* assignment.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = assignment.app; sourceTree = BUILT_PRODUCTS_DIR; }; 78BAE7962BC43AC30013CACE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 78BAE7982BC43AC30013CACE /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; - 78BAE79A2BC43AC30013CACE /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; + 78BAE79A2BC43AC30013CACE /* LoginVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginVC.swift; sourceTree = ""; }; 78BAE79F2BC43AC40013CACE /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 78BAE7A22BC43AC40013CACE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 78BAE7A42BC43AC40013CACE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -66,10 +87,16 @@ 78BAE8122BC4E6BB0013CACE /* Pretendard-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-Medium.ttf"; sourceTree = ""; }; 78BAE8132BC4E6BB0013CACE /* Pretendard-ExtraLight.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-ExtraLight.ttf"; sourceTree = ""; }; 78BAE8142BC4E6BB0013CACE /* Pretendard-Black.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Pretendard-Black.ttf"; sourceTree = ""; }; - 78BAE81E2BC91D5D0013CACE /* NicknameViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NicknameViewController.swift; sourceTree = ""; }; - 78BAE8202BC91D6A0013CACE /* WelcomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = ""; }; + 78BAE81E2BC91D5D0013CACE /* NicknameVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NicknameVC.swift; sourceTree = ""; }; + 78BAE8202BC91D6A0013CACE /* WelcomeVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeVC.swift; sourceTree = ""; }; 78F3280B2BE8B328001EAC45 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; 78F3280D2BE8B600001EAC45 /* MovieAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieAPI.swift; sourceTree = ""; }; + 78F8391E2C0DDF06002B7AD7 /* RootVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootVM.swift; sourceTree = ""; }; + 78F839452C0F25C0002B7AD7 /* privacyInfo.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = privacyInfo.plist; sourceTree = ""; }; + 78F839472C0F292C002B7AD7 /* extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = extension.swift; sourceTree = ""; }; + 78F839502C0F573C002B7AD7 /* BoxofficeDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxofficeDTO.swift; sourceTree = ""; }; + 78F839522C0F5891002B7AD7 /* MovieManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MovieManager.swift; sourceTree = ""; }; + 78F839542C0F593E002B7AD7 /* NetworkError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkError.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -77,22 +104,50 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 78BAE8062BC4E1EA0013CACE /* Then in Frameworks */, - 78B643B42BDA418F00C2EBEF /* LookinServer in Frameworks */, - 78F3280A2BE899C2001EAC45 /* Moya in Frameworks */, - 78BAE8092BC4E1F00013CACE /* SnapKit in Frameworks */, + 78EAA4A42C0A213D004D1094 /* RxMoya in Frameworks */, + 78EAA49F2C0A211E004D1094 /* SnapKit in Frameworks */, + 78EAA4992C0A20E0004D1094 /* RxCocoa in Frameworks */, + 78EAA4A22C0A213D004D1094 /* Moya in Frameworks */, + 78EAA49C2C0A20F0004D1094 /* Then in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 780822492C046965006B10C8 /* Login */ = { + isa = PBXGroup; + children = ( + 78BAE79A2BC43AC30013CACE /* LoginVC.swift */, + 784DDEE12C0467D70061B432 /* LoginModel.swift */, + 7808224A2C04698F006B10C8 /* LoginVM.swift */, + 785AE1C92C3467DC00677CA0 /* View.swift */, + ); + path = Login; + sourceTree = ""; + }; + 7808224E2C0474C3006B10C8 /* Nickname */ = { + isa = PBXGroup; + children = ( + 78BAE81E2BC91D5D0013CACE /* NicknameVC.swift */, + 780822502C0475DA006B10C8 /* NicknameVM.swift */, + ); + path = Nickname; + sourceTree = ""; + }; + 7808224F2C0474CE006B10C8 /* Welcome */ = { + isa = PBXGroup; + children = ( + 78BAE8202BC91D6A0013CACE /* WelcomeVC.swift */, + 780822522C04769D006B10C8 /* WelcomeVM.swift */, + ); + path = Welcome; + sourceTree = ""; + }; 78412EB72BD692B6002AA9E8 /* Model */ = { isa = PBXGroup; children = ( 78412EB82BD692C4002AA9E8 /* mainModel.swift */, - 78F3280B2BE8B328001EAC45 /* Config.swift */, - 78F3280D2BE8B600001EAC45 /* MovieAPI.swift */, ); path = Model; sourceTree = ""; @@ -110,12 +165,22 @@ path = Cell; sourceTree = ""; }; + 785AE1CB2C346B9500677CA0 /* Util */ = { + isa = PBXGroup; + children = ( + 7808224C2C046BB9006B10C8 /* Observable.swift */, + 785AE1CC2C346BA900677CA0 /* BindingTextField.swift */, + ); + path = Util; + sourceTree = ""; + }; 78A510872BD64D45005AAE0F /* onboarding */ = { isa = PBXGroup; children = ( - 78BAE79A2BC43AC30013CACE /* LoginViewController.swift */, - 78BAE81E2BC91D5D0013CACE /* NicknameViewController.swift */, - 78BAE8202BC91D6A0013CACE /* WelcomeViewController.swift */, + 785AE1CB2C346B9500677CA0 /* Util */, + 7808224F2C0474CE006B10C8 /* Welcome */, + 7808224E2C0474C3006B10C8 /* Nickname */, + 780822492C046965006B10C8 /* Login */, ); path = onboarding; sourceTree = ""; @@ -123,10 +188,12 @@ 78A510882BD652F0005AAE0F /* Main */ = { isa = PBXGroup; children = ( + 78F8391D2C0DDE59002B7AD7 /* View */, + 782EED6B2BDF8FC2000A3B96 /* RootCVC.swift */, + 78A510892BD652FE005AAE0F /* MainVC.swift */, + 78F8391C2C0DDE4E002B7AD7 /* ViewModel */, 78412EB72BD692B6002AA9E8 /* Model */, - 78412EC42BD9EE0E002AA9E8 /* Cell */, - 78A510892BD652FE005AAE0F /* MainViewController.swift */, - 782EED6B2BDF8FC2000A3B96 /* RootCollectionViewController.swift */, + 78F839472C0F292C002B7AD7 /* extension.swift */, ); path = Main; sourceTree = ""; @@ -137,6 +204,7 @@ 78BAE7952BC43AC30013CACE /* assignment */, 78BAE7942BC43AC30013CACE /* Products */, ); + indentWidth = 4; sourceTree = ""; }; 78BAE7942BC43AC30013CACE /* Products */ = { @@ -158,6 +226,7 @@ 78BAE79F2BC43AC40013CACE /* Assets.xcassets */, 78BAE7A12BC43AC40013CACE /* LaunchScreen.storyboard */, 78BAE7A42BC43AC40013CACE /* Info.plist */, + 78F839452C0F25C0002B7AD7 /* privacyInfo.plist */, ); path = assignment; sourceTree = ""; @@ -178,6 +247,75 @@ path = font; sourceTree = ""; }; + 78F8391C2C0DDE4E002B7AD7 /* ViewModel */ = { + isa = PBXGroup; + children = ( + 78F8391E2C0DDF06002B7AD7 /* RootVM.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 78F8391D2C0DDE59002B7AD7 /* View */ = { + isa = PBXGroup; + children = ( + 78F839492C0F4F7A002B7AD7 /* Network */, + 78412EC42BD9EE0E002AA9E8 /* Cell */, + ); + path = View; + sourceTree = ""; + }; + 78F839492C0F4F7A002B7AD7 /* Network */ = { + isa = PBXGroup; + children = ( + 78F8394E2C0F558C002B7AD7 /* Response */, + 78F8394D2C0F557E002B7AD7 /* Request */, + 78F8394C2C0F5577002B7AD7 /* Manager */, + 78F8394B2C0F556E002B7AD7 /* Foundation */, + 78F8394A2C0F5566002B7AD7 /* API */, + 78F3280D2BE8B600001EAC45 /* MovieAPI.swift */, + 78F3280B2BE8B328001EAC45 /* Config.swift */, + ); + path = Network; + sourceTree = ""; + }; + 78F8394A2C0F5566002B7AD7 /* API */ = { + isa = PBXGroup; + children = ( + ); + path = API; + sourceTree = ""; + }; + 78F8394B2C0F556E002B7AD7 /* Foundation */ = { + isa = PBXGroup; + children = ( + 78F839542C0F593E002B7AD7 /* NetworkError.swift */, + ); + path = Foundation; + sourceTree = ""; + }; + 78F8394C2C0F5577002B7AD7 /* Manager */ = { + isa = PBXGroup; + children = ( + 78F839522C0F5891002B7AD7 /* MovieManager.swift */, + ); + path = Manager; + sourceTree = ""; + }; + 78F8394D2C0F557E002B7AD7 /* Request */ = { + isa = PBXGroup; + children = ( + ); + path = Request; + sourceTree = ""; + }; + 78F8394E2C0F558C002B7AD7 /* Response */ = { + isa = PBXGroup; + children = ( + 78F839502C0F573C002B7AD7 /* BoxofficeDTO.swift */, + ); + path = Response; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -195,10 +333,11 @@ ); name = assignment; packageProductDependencies = ( - 78BAE8052BC4E1EA0013CACE /* Then */, - 78BAE8082BC4E1F00013CACE /* SnapKit */, - 78B643B32BDA418F00C2EBEF /* LookinServer */, - 78F328092BE899C2001EAC45 /* Moya */, + 78EAA4982C0A20E0004D1094 /* RxCocoa */, + 78EAA49B2C0A20F0004D1094 /* Then */, + 78EAA49E2C0A211E004D1094 /* SnapKit */, + 78EAA4A12C0A213D004D1094 /* Moya */, + 78EAA4A32C0A213D004D1094 /* RxMoya */, ); productName = assignment; productReference = 78BAE7932BC43AC30013CACE /* assignment.app */; @@ -229,10 +368,10 @@ ); mainGroup = 78BAE78A2BC43AC30013CACE; packageReferences = ( - 78BAE8042BC4E1EA0013CACE /* XCRemoteSwiftPackageReference "Then" */, - 78BAE8072BC4E1F00013CACE /* XCRemoteSwiftPackageReference "SnapKit" */, - 78B643B22BDA418F00C2EBEF /* XCRemoteSwiftPackageReference "LookinServer" */, - 78F328082BE899C2001EAC45 /* XCRemoteSwiftPackageReference "Moya" */, + 78EAA4972C0A20E0004D1094 /* XCRemoteSwiftPackageReference "RxSwift" */, + 78EAA49A2C0A20F0004D1094 /* XCRemoteSwiftPackageReference "Then" */, + 78EAA49D2C0A211E004D1094 /* XCRemoteSwiftPackageReference "SnapKit" */, + 78EAA4A02C0A213D004D1094 /* XCRemoteSwiftPackageReference "Moya" */, ); productRefGroup = 78BAE7942BC43AC30013CACE /* Products */; projectDirPath = ""; @@ -257,6 +396,7 @@ 78BAE8172BC4E6BB0013CACE /* Pretendard-Light.ttf in Resources */, 78BAE7A02BC43AC40013CACE /* Assets.xcassets in Resources */, 78BAE81C2BC4E6BB0013CACE /* Pretendard-ExtraLight.ttf in Resources */, + 78F839462C0F25C0002B7AD7 /* privacyInfo.plist in Resources */, 78BAE8192BC4E6BB0013CACE /* Pretendard-Bold.ttf in Resources */, 78BAE81B2BC4E6BB0013CACE /* Pretendard-Medium.ttf in Resources */, ); @@ -269,22 +409,34 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 78BAE81F2BC91D5D0013CACE /* NicknameViewController.swift in Sources */, - 78BAE8212BC91D6A0013CACE /* WelcomeViewController.swift in Sources */, + 780822512C0475DA006B10C8 /* NicknameVM.swift in Sources */, + 780822532C04769D006B10C8 /* WelcomeVM.swift in Sources */, + 7808224D2C046BB9006B10C8 /* Observable.swift in Sources */, + 78BAE81F2BC91D5D0013CACE /* NicknameVC.swift in Sources */, + 78BAE8212BC91D6A0013CACE /* WelcomeVC.swift in Sources */, + 785AE1CD2C346BA900677CA0 /* BindingTextField.swift in Sources */, 78B643D62BDE30CC00C2EBEF /* UnderlineSegment.swift in Sources */, 78F3280E2BE8B600001EAC45 /* MovieAPI.swift in Sources */, - 78BAE79B2BC43AC30013CACE /* LoginViewController.swift in Sources */, + 78F8391F2C0DDF06002B7AD7 /* RootVM.swift in Sources */, + 78BAE79B2BC43AC30013CACE /* LoginVC.swift in Sources */, 78412EBF2BD69D6F002AA9E8 /* TitleHeaderViewCollectionViewCell.swift in Sources */, - 78A5108A2BD652FE005AAE0F /* MainViewController.swift in Sources */, + 78A5108A2BD652FE005AAE0F /* MainVC.swift in Sources */, 78412EC32BD9E4EE002AA9E8 /* CustomFooterView.swift in Sources */, + 784DDEE22C0467D70061B432 /* LoginModel.swift in Sources */, 78F3280C2BE8B328001EAC45 /* Config.swift in Sources */, - 782EED6C2BDF8FC2000A3B96 /* RootCollectionViewController.swift in Sources */, + 785AE1CA2C3467DC00677CA0 /* View.swift in Sources */, + 782EED6C2BDF8FC2000A3B96 /* RootCVC.swift in Sources */, 78412EB92BD692C4002AA9E8 /* mainModel.swift in Sources */, 78BAE7972BC43AC30013CACE /* AppDelegate.swift in Sources */, + 78F839512C0F573C002B7AD7 /* BoxofficeDTO.swift in Sources */, 78412EC12BD69D88002AA9E8 /* DoosanFooterViewCollectionViewCell.swift in Sources */, + 7808224B2C04698F006B10C8 /* LoginVM.swift in Sources */, 782EED6E2BE00B82000A3B96 /* LiveCell.swift in Sources */, 78BAE7992BC43AC30013CACE /* SceneDelegate.swift in Sources */, + 78F839532C0F5891002B7AD7 /* MovieManager.swift in Sources */, + 78F839552C0F593E002B7AD7 /* NetworkError.swift in Sources */, 78412EBB2BD699C8002AA9E8 /* ContentCell.swift in Sources */, + 78F839482C0F292C002B7AD7 /* extension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -443,6 +595,7 @@ ); MARKETING_VERSION = 1.0; NEW_SETTING = ""; + OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = yizihoon.assignment; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -505,15 +658,15 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 78B643B22BDA418F00C2EBEF /* XCRemoteSwiftPackageReference "LookinServer" */ = { + 78EAA4972C0A20E0004D1094 /* XCRemoteSwiftPackageReference "RxSwift" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/QMUI/LookinServer/"; + repositoryURL = "https://github.com/ReactiveX/RxSwift.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 1.2.8; + minimumVersion = 6.7.1; }; }; - 78BAE8042BC4E1EA0013CACE /* XCRemoteSwiftPackageReference "Then" */ = { + 78EAA49A2C0A20F0004D1094 /* XCRemoteSwiftPackageReference "Then" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/devxoul/Then.git"; requirement = { @@ -521,7 +674,7 @@ minimumVersion = 3.0.0; }; }; - 78BAE8072BC4E1F00013CACE /* XCRemoteSwiftPackageReference "SnapKit" */ = { + 78EAA49D2C0A211E004D1094 /* XCRemoteSwiftPackageReference "SnapKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SnapKit/SnapKit.git"; requirement = { @@ -529,7 +682,7 @@ minimumVersion = 5.7.1; }; }; - 78F328082BE899C2001EAC45 /* XCRemoteSwiftPackageReference "Moya" */ = { + 78EAA4A02C0A213D004D1094 /* XCRemoteSwiftPackageReference "Moya" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Moya/Moya.git"; requirement = { @@ -540,26 +693,31 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 78B643B32BDA418F00C2EBEF /* LookinServer */ = { + 78EAA4982C0A20E0004D1094 /* RxCocoa */ = { isa = XCSwiftPackageProductDependency; - package = 78B643B22BDA418F00C2EBEF /* XCRemoteSwiftPackageReference "LookinServer" */; - productName = LookinServer; + package = 78EAA4972C0A20E0004D1094 /* XCRemoteSwiftPackageReference "RxSwift" */; + productName = RxCocoa; }; - 78BAE8052BC4E1EA0013CACE /* Then */ = { + 78EAA49B2C0A20F0004D1094 /* Then */ = { isa = XCSwiftPackageProductDependency; - package = 78BAE8042BC4E1EA0013CACE /* XCRemoteSwiftPackageReference "Then" */; + package = 78EAA49A2C0A20F0004D1094 /* XCRemoteSwiftPackageReference "Then" */; productName = Then; }; - 78BAE8082BC4E1F00013CACE /* SnapKit */ = { + 78EAA49E2C0A211E004D1094 /* SnapKit */ = { isa = XCSwiftPackageProductDependency; - package = 78BAE8072BC4E1F00013CACE /* XCRemoteSwiftPackageReference "SnapKit" */; + package = 78EAA49D2C0A211E004D1094 /* XCRemoteSwiftPackageReference "SnapKit" */; productName = SnapKit; }; - 78F328092BE899C2001EAC45 /* Moya */ = { + 78EAA4A12C0A213D004D1094 /* Moya */ = { isa = XCSwiftPackageProductDependency; - package = 78F328082BE899C2001EAC45 /* XCRemoteSwiftPackageReference "Moya" */; + package = 78EAA4A02C0A213D004D1094 /* XCRemoteSwiftPackageReference "Moya" */; productName = Moya; }; + 78EAA4A32C0A213D004D1094 /* RxMoya */ = { + isa = XCSwiftPackageProductDependency; + package = 78EAA4A02C0A213D004D1094 /* XCRemoteSwiftPackageReference "Moya" */; + productName = RxMoya; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 78BAE78B2BC43AC30013CACE /* Project object */; diff --git a/assignment/assignment.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/assignment/assignment.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 22c6a35..741efe0 100644 --- a/assignment/assignment.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/assignment/assignment.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -9,15 +9,6 @@ "version" : "5.9.1" } }, - { - "identity" : "lookinserver", - "kind" : "remoteSourceControl", - "location" : "https://github.com/QMUI/LookinServer/", - "state" : { - "revision" : "e553d1b689d147817dc54ad5c28fcff71e860101", - "version" : "1.2.8" - } - }, { "identity" : "moya", "kind" : "remoteSourceControl", diff --git a/assignment/assignment.xcodeproj/project.xcworkspace/xcuserdata/hoon.xcuserdatad/UserInterfaceState.xcuserstate b/assignment/assignment.xcodeproj/project.xcworkspace/xcuserdata/hoon.xcuserdatad/UserInterfaceState.xcuserstate index de09796..f5f83ec 100644 Binary files a/assignment/assignment.xcodeproj/project.xcworkspace/xcuserdata/hoon.xcuserdatad/UserInterfaceState.xcuserstate and b/assignment/assignment.xcodeproj/project.xcworkspace/xcuserdata/hoon.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/assignment/assignment.xcodeproj/xcuserdata/hoon.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/assignment/assignment.xcodeproj/xcuserdata/hoon.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index 54f8df7..a094a0e 100644 --- a/assignment/assignment.xcodeproj/xcuserdata/hoon.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/assignment/assignment.xcodeproj/xcuserdata/hoon.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -64,6 +64,21 @@ endingLineNumber = "45" offsetFromSymbolStart = "204"> + + @@ -112,6 +127,21 @@ endingLineNumber = "68" offsetFromSymbolStart = "76"> + + diff --git a/assignment/assignment.xcodeproj/xcuserdata/hoon.xcuserdatad/xcschemes/xcschememanagement.plist b/assignment/assignment.xcodeproj/xcuserdata/hoon.xcuserdatad/xcschemes/xcschememanagement.plist index e839dfd..1c11378 100644 --- a/assignment/assignment.xcodeproj/xcuserdata/hoon.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/assignment/assignment.xcodeproj/xcuserdata/hoon.xcuserdatad/xcschemes/xcschememanagement.plist @@ -9,126 +9,336 @@ isShown orderHint - 8 + 5 + + ReactiveSwift (Playground) 10.xcscheme + + isShown + + orderHint + 38 + + ReactiveSwift (Playground) 11.xcscheme + + isShown + + orderHint + 39 ReactiveSwift (Playground) 2.xcscheme isShown orderHint - 9 + 6 + + ReactiveSwift (Playground) 3.xcscheme + + isShown + + orderHint + 16 + + ReactiveSwift (Playground) 4.xcscheme + + isShown + + orderHint + 17 + + ReactiveSwift (Playground) 5.xcscheme + + isShown + + orderHint + 18 + + ReactiveSwift (Playground) 6.xcscheme + + isShown + + orderHint + 31 + + ReactiveSwift (Playground) 7.xcscheme + + isShown + + orderHint + 32 + + ReactiveSwift (Playground) 8.xcscheme + + isShown + + orderHint + 33 + + ReactiveSwift (Playground) 9.xcscheme + + isShown + + orderHint + 37 ReactiveSwift (Playground).xcscheme isShown orderHint - 7 + 3 ReactiveSwift-UIExamples (Playground) 1.xcscheme isShown orderHint - 5 + 8 + + ReactiveSwift-UIExamples (Playground) 10.xcscheme + + isShown + + orderHint + 41 + + ReactiveSwift-UIExamples (Playground) 11.xcscheme + + isShown + + orderHint + 42 ReactiveSwift-UIExamples (Playground) 2.xcscheme isShown orderHint - 6 + 9 + + ReactiveSwift-UIExamples (Playground) 3.xcscheme + + isShown + + orderHint + 10 + + ReactiveSwift-UIExamples (Playground) 4.xcscheme + + isShown + + orderHint + 14 + + ReactiveSwift-UIExamples (Playground) 5.xcscheme + + isShown + + orderHint + 15 + + ReactiveSwift-UIExamples (Playground) 6.xcscheme + + isShown + + orderHint + 34 + + ReactiveSwift-UIExamples (Playground) 7.xcscheme + + isShown + + orderHint + 35 + + ReactiveSwift-UIExamples (Playground) 8.xcscheme + + isShown + + orderHint + 36 + + ReactiveSwift-UIExamples (Playground) 9.xcscheme + + isShown + + orderHint + 40 ReactiveSwift-UIExamples (Playground).xcscheme isShown orderHint - 4 + 7 Rx (Playground) 1.xcscheme isShown orderHint - 11 + 12 + + Rx (Playground) 10.xcscheme + + isShown + + orderHint + 44 + + Rx (Playground) 11.xcscheme + + isShown + + orderHint + 45 Rx (Playground) 2.xcscheme isShown orderHint - 12 + 13 + + Rx (Playground) 3.xcscheme + + isShown + + orderHint + 22 + + Rx (Playground) 4.xcscheme + + isShown + + orderHint + 23 + + Rx (Playground) 5.xcscheme + + isShown + + orderHint + 24 + + Rx (Playground) 6.xcscheme + + isShown + + orderHint + 25 + + Rx (Playground) 7.xcscheme + + isShown + + orderHint + 26 + + Rx (Playground) 8.xcscheme + + isShown + + orderHint + 27 + + Rx (Playground) 9.xcscheme + + isShown + + orderHint + 43 Rx (Playground).xcscheme isShown orderHint - 10 + 11 SnapKitPlayground (Playground) 1.xcscheme isShown orderHint - 0 + 2 + + SnapKitPlayground (Playground) 10.xcscheme + + isShown + + orderHint + 47 + + SnapKitPlayground (Playground) 11.xcscheme + + isShown + + orderHint + 48 SnapKitPlayground (Playground) 2.xcscheme isShown orderHint - 3 + 4 SnapKitPlayground (Playground) 3.xcscheme isShown orderHint - 2 + 19 SnapKitPlayground (Playground) 4.xcscheme isShown orderHint - 5 + 20 SnapKitPlayground (Playground) 5.xcscheme isShown orderHint - 6 + 21 SnapKitPlayground (Playground) 6.xcscheme isShown orderHint - 7 + 28 SnapKitPlayground (Playground) 7.xcscheme isShown orderHint - 8 + 29 SnapKitPlayground (Playground) 8.xcscheme isShown orderHint - 9 + 30 + + SnapKitPlayground (Playground) 9.xcscheme + + isShown + + orderHint + 46 SnapKitPlayground (Playground).xcscheme isShown orderHint - 2 + 0 assignment.xcscheme_^#shared#^_ diff --git a/assignment/assignment/AppDelegate.swift b/assignment/assignment/AppDelegate.swift index 3183abf..1f388a6 100644 --- a/assignment/assignment/AppDelegate.swift +++ b/assignment/assignment/AppDelegate.swift @@ -9,14 +9,17 @@ import UIKit @main class AppDelegate: UIResponder, UIApplicationDelegate { - - - + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - // Override point for customization after application launch. + let window = UIWindow(frame: UIScreen.main.bounds) + let welcomeVC = LoginViewController() + let navigationController = UINavigationController(rootViewController: welcomeVC) + window.rootViewController = navigationController + window.makeKeyAndVisible() return true } + // MARK: UISceneSession Lifecycle func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { diff --git a/assignment/assignment/Main/MainViewController.swift b/assignment/assignment/Main/MainVC.swift similarity index 78% rename from assignment/assignment/Main/MainViewController.swift rename to assignment/assignment/Main/MainVC.swift index 14f1c59..63c64ae 100644 --- a/assignment/assignment/Main/MainViewController.swift +++ b/assignment/assignment/Main/MainVC.swift @@ -13,12 +13,9 @@ import Moya class MainViewController: UIViewController { //MARK: - Properties - var mainCollectionView : UICollectionView! - //private var dataSource = MainModel.dummy() // 나와라 더미데이터 + var mainCollectionView : UICollectionView! - var provider = MoyaProvider(plugins: [NetworkLoggerPlugin()]) - var dataSource = MainModel(sections: []) //더미데이터에서 영화사이트에서 데이터 직접 가져오기로 변경 - + var dataSource = MainModel(sections: []) private func setupCompositionalLayout() -> UICollectionViewLayout { return UICollectionViewCompositionalLayout { sectionIndex, _ -> NSCollectionLayoutSection? in @@ -29,41 +26,17 @@ class MainViewController: UIViewController { return section } } - + // MARK: - ViewDidLoad override func viewDidLoad() { super.viewDidLoad() setupCollectionView() - fetchMovies() - - } - - func fetchMovies() { - DispatchQueue.global(qos: .userInitiated).async { - self.provider.request(.dailyBoxOffice(key: "63adcda43f0b97ae5d966b40878b62fb", targetDate: "20240505")) { [weak self] result in - DispatchQueue.main.async { - switch result { - case .success(let response): - let responseDataString = String(data: response.data, encoding: .utf8) ?? "Invalid data" - print("Response Data: \(responseDataString)") - do { - let results = try JSONDecoder().decode(BoxOfficeResponse.self, from: response.data) - self?.updateMainModel(with: results.boxOfficeResult.dailyBoxOfficeList) - self?.mainCollectionView.reloadData() - } catch { - print("Error decoding: \(error)") - } - case .failure(let error): - print("Error in fetching data: \(error)") - } - } - } - } + } -// 전체 UI의 일부분만 API로 업데이트 해서 생긴 로직 + // 전체 UI의 일부분만 API로 업데이트 해서 생긴 로직 func updateMainModel(with movies: [Movie]) { let newContents = movies.map { movie in Content(image: UIImage(named: "contents1") ?? UIImage(), title: movie.movieNm) @@ -82,21 +55,21 @@ class MainViewController: UIViewController { // 데이터 소스 업데이트 dataSource.sections = updatedSections } - + //섹션 레이아웃 설정 func setupCollectionView() { let layout = UICollectionViewCompositionalLayout { [weak self] (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in guard let self = self, sectionIndex < self.dataSource.sections.count else { return nil } - + let sectionType = self.dataSource.sections[sectionIndex] - + let footerSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(50)) let footer = NSCollectionLayoutBoundarySupplementaryItem( layoutSize: footerSize, elementKind: UICollectionView.elementKindSectionFooter, alignment: .bottom) - + switch sectionType { case .headContent(let contents): return self.getLayoutHeaderSection(contents: contents) @@ -111,7 +84,7 @@ class MainViewController: UIViewController { return nil } } - + mainCollectionView = UICollectionView(frame: .zero, collectionViewLayout: layout).then { $0.isScrollEnabled = true $0.showsHorizontalScrollIndicator = false @@ -125,10 +98,10 @@ class MainViewController: UIViewController { $0.register(DoosanFooterViewCollectionViewCell.self, forCellWithReuseIdentifier: DoosanFooterViewCollectionViewCell.identifier) $0.register(CustomFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: CustomFooterView.identifier) $0.register(LiveContentCell.self, forCellWithReuseIdentifier: LiveContentCell.identifier) - + $0.dataSource = self } - + self.view.addSubview(mainCollectionView) mainCollectionView.snp.makeConstraints { $0.edges.equalToSuperview() } } @@ -201,40 +174,40 @@ class MainViewController: UIViewController { widthDimension: .fractionalWidth(1/3), heightDimension: .fractionalHeight(0.46) ) - + let item = NSCollectionLayoutItem(layoutSize: itemSize) item.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4) - + // Group s let groupSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(0.2) ) - + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) group.interItemSpacing = .fixed(7) // 그룹간 거리 7로 고정 - + // Section let section = NSCollectionLayoutSection(group: group) section.orthogonalScrollingBehavior = .continuous - + // Header let headerSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), heightDimension: .absolute(40) ) - + let header = NSCollectionLayoutBoundarySupplementaryItem( layoutSize: headerSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top ) - + section.boundarySupplementaryItems = [header] - + return section } - + //MARK: - DoosanCell // Function to get the layout for the Doosan section private func getLayoutDoosanSection(contents: [DoosanContent]) -> NSCollectionLayoutSection { @@ -243,27 +216,24 @@ class MainViewController: UIViewController { widthDimension: .fractionalWidth(1/2), heightDimension: .fractionalHeight(0.8) ) - + let item = NSCollectionLayoutItem(layoutSize: itemSize) item.contentInsets = NSDirectionalEdgeInsets(top: 4, leading: 4, bottom: 4, trailing: 4) - + // Group let groupSize = NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), heightDimension: .absolute(100) ) - + let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item, item]) - + // Section let section = NSCollectionLayoutSection(group: group) section.orthogonalScrollingBehavior = .continuous - + return section } - - - } // MARK: - UICollectionViewDataSource @@ -275,7 +245,7 @@ extension MainViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { let sectionType = dataSource.sections[section] - + switch sectionType { case .headContent(let contents): return contents.count @@ -296,7 +266,7 @@ extension MainViewController: UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let sectionType = dataSource.sections[indexPath.section] - + switch sectionType { case .headContent(let contents): let content = contents[indexPath.item] @@ -328,39 +298,39 @@ extension MainViewController: UICollectionViewDataSource { cell.configures(content: content) return cell case .DoosanContent(let contents): - let content = contents[indexPath.item] - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DoosanFooterViewCollectionViewCell.identifier, for: indexPath) as! DoosanFooterViewCollectionViewCell - cell.configure(image: content.image) - return cell - } + let content = contents[indexPath.item] + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: DoosanFooterViewCollectionViewCell.identifier, for: indexPath) as! DoosanFooterViewCollectionViewCell + cell.configure(image: content.image) + return cell + } } - - - //Header, Footer 지정 - func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { - switch kind { - case UICollectionView.elementKindSectionHeader: - let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "TitleHeaderViewCollectionViewCell", for: indexPath) as! TitleHeaderViewCollectionViewCell - let sectionType = dataSource.sections[indexPath.section] - switch sectionType { - case .mainContents(_, let title), - .freeContents(_, let title), - .magicContents(_, let title), - .live(_, let title): - header.prepare(titleText: title, subtitleText: "전체보기") - default: - header.prepare(titleText: "", subtitleText: "") - } - return header - - case UICollectionView.elementKindSectionFooter: - let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CustomFooterView.identifier, for: indexPath) as! CustomFooterView - footer.configure(numberOfPages: collectionView.numberOfItems(inSection: indexPath.section), currentPage: indexPath.row) - return footer - + + + //Header, Footer 지정 + func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView { + switch kind { + case UICollectionView.elementKindSectionHeader: + let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "TitleHeaderViewCollectionViewCell", for: indexPath) as! TitleHeaderViewCollectionViewCell + let sectionType = dataSource.sections[indexPath.section] + switch sectionType { + case .mainContents(_, let title), + .freeContents(_, let title), + .magicContents(_, let title), + .live(_, let title): + header.prepare(titleText: title, subtitleText: "전체보기") default: - fatalError("Unexpected element kind") + header.prepare(titleText: "", subtitleText: "") } + return header + + case UICollectionView.elementKindSectionFooter: + let footer = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: CustomFooterView.identifier, for: indexPath) as! CustomFooterView + footer.configure(numberOfPages: collectionView.numberOfItems(inSection: indexPath.section), currentPage: indexPath.row) + return footer + + default: + fatalError("Unexpected element kind") + } } } diff --git a/assignment/assignment/Main/Model/mainModel.swift b/assignment/assignment/Main/Model/mainModel.swift index 42463b5..a44ce7e 100644 --- a/assignment/assignment/Main/Model/mainModel.swift +++ b/assignment/assignment/Main/Model/mainModel.swift @@ -97,23 +97,3 @@ struct MainModel { } } - -struct BoxOfficeResponse: Codable { - let boxOfficeResult: BoxOfficeResult -} - -struct BoxOfficeResult: Codable { - let boxofficeType: String - let showRange: String - let dailyBoxOfficeList: [Movie] -} - -struct Movie: Codable { - let rnum: String - let rank: String - let movieCd: String - let movieNm: String - let openDt: String - let salesAmt: String - let audiCnt: String -} diff --git a/assignment/assignment/Main/RootCVC.swift b/assignment/assignment/Main/RootCVC.swift new file mode 100644 index 0000000..2bcc2d1 --- /dev/null +++ b/assignment/assignment/Main/RootCVC.swift @@ -0,0 +1,111 @@ +// +// RootCollectionViewController.swift +// assignment +// +// Created by 이지훈 on 4/29/24. +// + +import UIKit +import SnapKit +import Then + +class RootCollectionViewController: UIViewController { + + private let viewModel = RootViewModel() + + private lazy var segmentedControl = UnderlineSegmentedControl(items: viewModel.getSegmentTitles()).then { + $0.addTarget(self, action: #selector(changeValue(control:)), for: .valueChanged) + $0.backgroundColor = .clear + } + + private lazy var pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil).then { + $0.delegate = viewModel + $0.dataSource = viewModel + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .black + setupUI() + viewModelBinding() + } + + private func viewModelBinding() { + viewModel.onPageChanged = { [weak self] index in + self?.segmentedControl.selectedSegmentIndex = index + } + + DispatchQueue.main.async { [weak self] in + self?.setupCollectionViewDelegate() + } + + viewModel.updatePage(to: 0) { [weak self] viewController, direction in + self?.pageViewController.setViewControllers([viewController], direction: direction, animated: false, completion: nil) + } + } + + private func setupUI() { + setupNavigationBar() + setupSegmentsAndPageView() + setupConstraints() + } + + private func setupNavigationBar() { + let leftImage = UIImage(named: "leftTving") + let leftButtonItem = UIBarButtonItem(image: leftImage, style: .plain, target: self, action: nil) + navigationItem.leftBarButtonItem = leftButtonItem + + let rightImage = UIImage(named: "rightImage") + let rightButtonItem = UIBarButtonItem(image: rightImage, style: .plain, target: self, action: nil) + navigationItem.rightBarButtonItem = rightButtonItem + } + + private func setupSegmentsAndPageView() { + view.addSubview(segmentedControl) + addChild(pageViewController) + view.addSubview(pageViewController.view) + pageViewController.didMove(toParent: self) + view.bringSubviewToFront(segmentedControl) + } + + private func setupConstraints() { + segmentedControl.snp.makeConstraints { + $0.leading.trailing.equalToSuperview() + $0.top.equalTo(self.view.safeAreaLayoutGuide.snp.top) + $0.height.equalTo(50) + } + + pageViewController.view.snp.makeConstraints { + $0.leading.trailing.equalToSuperview() + $0.top.equalTo(view.snp.top) + $0.bottom.equalToSuperview() + } + } + + private func setupCollectionViewDelegate() { + if let mainvc = viewModel.mainViewController() { + mainvc.mainCollectionView.delegate = self + } + } + + @objc private func changeValue(control: UISegmentedControl) { + viewModel.setSelectedSegmentIndex(control.selectedSegmentIndex) + viewModel.updatePage(to: control.selectedSegmentIndex) { [weak self] viewController, direction in + self?.pageViewController.setViewControllers([viewController], direction: direction, animated: true, completion: nil) + } + } +} + +extension RootCollectionViewController: UICollectionViewDelegate { + func scrollViewDidScroll(_ scrollView: UIScrollView) { + viewModel.scrollViewDidScroll(scrollView, navigationController: navigationController) + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + viewModel.scrollViewDidEndDecelerating(scrollView) { visibleIndexPath, numberOfItems, currentPage in + if let mainvc = viewModel.mainViewController(), let footerView = mainvc.mainCollectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionFooter, at: IndexPath(item: 0, section: visibleIndexPath.section)) as? CustomFooterView { + footerView.configure(numberOfPages: numberOfItems, currentPage: currentPage) + } + } + } +} diff --git a/assignment/assignment/Main/RootCollectionViewController.swift b/assignment/assignment/Main/RootCollectionViewController.swift deleted file mode 100644 index 0ec631e..0000000 --- a/assignment/assignment/Main/RootCollectionViewController.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// RootCollectionViewController.swift -// assignment -// -// Created by 이지훈 on 4/29/24. -// -import UIKit -import SnapKit -import Then - -class RootCollectionViewController: UIViewController { - - private let segmentedControl = UnderlineSegmentedControl(items: ["홈", "실시간", "TV프로그램", "영화", "파라마운트+"]).then { - $0.addTarget(self, action: #selector(changeValue(control:)), for: .valueChanged) - $0.backgroundColor = .clear - } - - private lazy var pageViewController: UIPageViewController = { - let vc = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil) - vc.setViewControllers([self.dataViewControllers[0]], direction: .forward, animated: true) - vc.delegate = self - vc.dataSource = self - return vc - }() - - private let mainvc = MainViewController() - private let vc2 = UIViewController().then { $0.view.backgroundColor = .green } - private let vc3 = UIViewController().then { $0.view.backgroundColor = .blue } - private let vc4 = UIViewController().then { $0.view.backgroundColor = .white } - private let vc5 = UIViewController().then { $0.view.backgroundColor = .black } - - var dataViewControllers: [UIViewController] { - [self.mainvc, self.vc2, self.vc3, self.vc4, self.vc5] - } - - var currentPage: Int = 0 { - didSet { - let direction: UIPageViewController.NavigationDirection = oldValue <= self.currentPage ? .forward : .reverse - self.pageViewController.setViewControllers([dataViewControllers[self.currentPage]], direction: direction, animated: true, completion: nil) - } - } - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .black - - setupNavigationBar() - setupSegmentsAndPageView() - setupConstraints() - - setupInitialState() - setupCollectionViewDelegate() - - } - - private func setupNavigationBar() { - let leftImage = UIImage(named: "leftTving") - let leftButtonItem = UIBarButtonItem(image: leftImage, style: .plain, target: self, action: nil) - navigationItem.leftBarButtonItem = leftButtonItem - - let rightImage = UIImage(named: "rightImage") - let rightButtonItem = UIBarButtonItem(image: rightImage, style: .plain, target: self, action: nil) - navigationItem.rightBarButtonItem = rightButtonItem - } - - private func setupSegmentsAndPageView() { - view.addSubview(segmentedControl) - addChild(pageViewController) - view.addSubview(pageViewController.view) - pageViewController.didMove(toParent: self) - - view.bringSubviewToFront(segmentedControl) // 앞으로 땡겨오기 - } - - private func setupConstraints() { - segmentedControl.snp.makeConstraints { - $0.leading.trailing.equalToSuperview() - $0.top.equalTo(self.view.safeAreaLayoutGuide.snp.top) - $0.height.equalTo(50) - } - - pageViewController.view.snp.makeConstraints { - $0.leading.trailing.equalToSuperview() - $0.top.equalTo(view.snp.top) - $0.bottom.equalToSuperview() - } - } - - private func setupInitialState() { - segmentedControl.selectedSegmentIndex = 0 - changeValue(control: segmentedControl) - } - - private func setupCollectionViewDelegate() { - mainvc.mainCollectionView.delegate = self - } - - @objc private func changeValue(control: UISegmentedControl) { - self.currentPage = control.selectedSegmentIndex - } -} - -extension RootCollectionViewController: UIPageViewControllerDataSource, UIPageViewControllerDelegate { - func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { - - guard let index = self.dataViewControllers.firstIndex(of: viewController), index - 1 >= 0 else { return nil } - return self.dataViewControllers[index - 1] - } - - func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { - guard let index = self.dataViewControllers.firstIndex(of: viewController), index + 1 < self.dataViewControllers.count else { return nil } - return self.dataViewControllers[index + 1] - } - - func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { - guard let viewController = pageViewController.viewControllers?[0], let index = self.dataViewControllers.firstIndex(of: viewController) else { return } - self.currentPage = index - self.segmentedControl.selectedSegmentIndex = index - } -} - -extension RootCollectionViewController: UICollectionViewDelegate { - func scrollViewDidScroll(_ scrollView: UIScrollView) { - let offset = scrollView.contentOffset.y - let isScrollingDown = offset > 0 - - navigationController?.setNavigationBarHidden(isScrollingDown, animated: true) - } - - func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { - guard let visibleIndexPath = mainvc.mainCollectionView.indexPathForItem(at: CGPoint(x: mainvc.mainCollectionView.contentOffset.x + mainvc.mainCollectionView.bounds.width / 2, y: mainvc.mainCollectionView.contentOffset.y + mainvc.mainCollectionView.bounds.height / 2)) else { return } - - if let footerView = mainvc.mainCollectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionFooter, at: IndexPath(item: 0, section: visibleIndexPath.section)) as? CustomFooterView { - footerView.configure(numberOfPages: mainvc.mainCollectionView.numberOfItems(inSection: visibleIndexPath.section), currentPage: visibleIndexPath.item) - } - } -} diff --git a/assignment/assignment/Main/Cell/ContentCell.swift b/assignment/assignment/Main/View/Cell/ContentCell.swift similarity index 100% rename from assignment/assignment/Main/Cell/ContentCell.swift rename to assignment/assignment/Main/View/Cell/ContentCell.swift diff --git a/assignment/assignment/Main/Cell/CustomFooterView.swift b/assignment/assignment/Main/View/Cell/CustomFooterView.swift similarity index 99% rename from assignment/assignment/Main/Cell/CustomFooterView.swift rename to assignment/assignment/Main/View/Cell/CustomFooterView.swift index f93dec6..fbeb894 100644 --- a/assignment/assignment/Main/Cell/CustomFooterView.swift +++ b/assignment/assignment/Main/View/Cell/CustomFooterView.swift @@ -6,6 +6,7 @@ // import UIKit + import SnapKit import Then diff --git a/assignment/assignment/Main/Cell/DoosanFooterViewCollectionViewCell.swift b/assignment/assignment/Main/View/Cell/DoosanFooterViewCollectionViewCell.swift similarity index 100% rename from assignment/assignment/Main/Cell/DoosanFooterViewCollectionViewCell.swift rename to assignment/assignment/Main/View/Cell/DoosanFooterViewCollectionViewCell.swift diff --git a/assignment/assignment/Main/Cell/LiveCell.swift b/assignment/assignment/Main/View/Cell/LiveCell.swift similarity index 100% rename from assignment/assignment/Main/Cell/LiveCell.swift rename to assignment/assignment/Main/View/Cell/LiveCell.swift diff --git a/assignment/assignment/Main/Cell/TitleHeaderViewCollectionViewCell.swift b/assignment/assignment/Main/View/Cell/TitleHeaderViewCollectionViewCell.swift similarity index 100% rename from assignment/assignment/Main/Cell/TitleHeaderViewCollectionViewCell.swift rename to assignment/assignment/Main/View/Cell/TitleHeaderViewCollectionViewCell.swift diff --git a/assignment/assignment/Main/Cell/UnderlineSegment.swift b/assignment/assignment/Main/View/Cell/UnderlineSegment.swift similarity index 100% rename from assignment/assignment/Main/Cell/UnderlineSegment.swift rename to assignment/assignment/Main/View/Cell/UnderlineSegment.swift diff --git a/assignment/assignment/Main/Model/Config.swift b/assignment/assignment/Main/View/Network/Config.swift similarity index 100% rename from assignment/assignment/Main/Model/Config.swift rename to assignment/assignment/Main/View/Network/Config.swift diff --git a/assignment/assignment/Main/View/Network/Foundation/NetworkError.swift b/assignment/assignment/Main/View/Network/Foundation/NetworkError.swift new file mode 100644 index 0000000..d56c5e9 --- /dev/null +++ b/assignment/assignment/Main/View/Network/Foundation/NetworkError.swift @@ -0,0 +1,29 @@ +// +// NetworkError.swift +// assignment +// +// Created by 이지훈 on 6/4/24. +// + +import Foundation + +import Moya + +enum MovieError: Error { + case networkError(MoyaError) + case decodingError + case unknownError +} + +extension MovieError: LocalizedError { + var errorDescription: String? { + switch self { + case .networkError(let error): + return error.localizedDescription + case .decodingError: + return "Failed to decode the response." + case .unknownError: + return "An unknown error occurred." + } + } +} diff --git a/assignment/assignment/Main/View/Network/Manager/MovieManager.swift b/assignment/assignment/Main/View/Network/Manager/MovieManager.swift new file mode 100644 index 0000000..850d0f1 --- /dev/null +++ b/assignment/assignment/Main/View/Network/Manager/MovieManager.swift @@ -0,0 +1,31 @@ +// +// MovieManager.swift +// assignment +// +// Created by 이지훈 on 6/4/24. +// + +import Foundation + +import Moya + +class MovieManager { + private let provider = MoyaProvider() + + func fetchDailyBoxOffice(key: String, targetDate: String, completion: @escaping (Result<[Movie], MovieError>) -> Void) { + provider.request(.dailyBoxOffice(key: key, targetDate: targetDate)) { result in + switch result { + case .success(let response): + do { + let decoder = JSONDecoder() + let dailyBoxOfficeResponse = try decoder.decode(BoxOfficeResponse.self, from: response.data) + completion(.success(dailyBoxOfficeResponse.boxOfficeResult.dailyBoxOfficeList)) + } catch { + completion(.failure(.decodingError)) + } + case .failure(let error): + completion(.failure(.networkError(error))) + } + } + } +} diff --git a/assignment/assignment/Main/Model/MovieAPI.swift b/assignment/assignment/Main/View/Network/MovieAPI.swift similarity index 100% rename from assignment/assignment/Main/Model/MovieAPI.swift rename to assignment/assignment/Main/View/Network/MovieAPI.swift diff --git a/assignment/assignment/Main/View/Network/Response/BoxofficeDTO.swift b/assignment/assignment/Main/View/Network/Response/BoxofficeDTO.swift new file mode 100644 index 0000000..554c3a4 --- /dev/null +++ b/assignment/assignment/Main/View/Network/Response/BoxofficeDTO.swift @@ -0,0 +1,28 @@ +// +// BoxofficeDTO.swift +// assignment +// +// Created by 이지훈 on 6/4/24. +// + +import Foundation + +struct BoxOfficeResponse: Codable { + let boxOfficeResult: BoxOfficeResult +} + +struct BoxOfficeResult: Codable { + let boxofficeType: String + let showRange: String + let dailyBoxOfficeList: [Movie] +} + +struct Movie: Codable { + let rnum: String + let rank: String + let movieCd: String + let movieNm: String + let openDt: String + let salesAmt: String + let audiCnt: String +} diff --git a/assignment/assignment/Main/ViewModel/RootVM.swift b/assignment/assignment/Main/ViewModel/RootVM.swift new file mode 100644 index 0000000..47eab83 --- /dev/null +++ b/assignment/assignment/Main/ViewModel/RootVM.swift @@ -0,0 +1,91 @@ +// +// MainVM.swift +// assignment +// +// Created by 이지훈 on 6/3/24. +// + +import UIKit + +class RootViewModel: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate { + private let segmentTitles: [String] = ["홈", "실시간", "TV프로그램", "영화", "파라마운트+"] + private let viewControllers: [UIViewController] = [ + MainViewController(), + UIViewController().then { $0.view.backgroundColor = .green }, + UIViewController().then { $0.view.backgroundColor = .blue }, + UIViewController().then { $0.view.backgroundColor = .white }, + UIViewController().then { $0.view.backgroundColor = .black } + ] + + var onPageChanged: ((Int) -> Void)? + + var currentPage: Int = 0 { + didSet { + onPageChanged?(currentPage) + } + } + var numberOfSegments: Int { + return segmentTitles.count + } + + func getSegmentTitles() -> [String] { + return segmentTitles + } + + func viewControllerForSegment(at index: Int) -> UIViewController { + return viewControllers[index] + } + + func setSelectedSegmentIndex(_ index: Int) { + currentPage = index + } + + func mainViewController() -> MainViewController? { + return viewControllers[0] as? MainViewController + } + + func viewControllerBefore(viewController: UIViewController) -> UIViewController? { + guard let index = viewControllers.firstIndex(of: viewController), index - 1 >= 0 else { return nil } + return viewControllers[index - 1] + } + + func viewControllerAfter(viewController: UIViewController) -> UIViewController? { + guard let index = viewControllers.firstIndex(of: viewController), index + 1 < viewControllers.count else { return nil } + return viewControllers[index + 1] + } + + func updatePage(to index: Int, completion: @escaping (UIViewController, UIPageViewController.NavigationDirection) -> Void) { + let direction: UIPageViewController.NavigationDirection = currentPage <= index ? .forward : .reverse + let viewController = viewControllers[index] + currentPage = index + completion(viewController, direction) + } + + func scrollViewDidScroll(_ scrollView: UIScrollView, navigationController: UINavigationController?) { + let offset = scrollView.contentOffset.y + let isScrollingDown = offset > 0 + navigationController?.setNavigationBarHidden(isScrollingDown, animated: true) + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView, footerViewUpdateHandler: (IndexPath, Int, Int) -> Void) { + if let mainvc = mainViewController(), let visibleIndexPath = mainvc.mainCollectionView.indexPathForItem(at: CGPoint(x: mainvc.mainCollectionView.contentOffset.x + mainvc.mainCollectionView.bounds.width / 2, y: mainvc.mainCollectionView.contentOffset.y + mainvc.mainCollectionView.bounds.height / 2)) { + let numberOfItems = mainvc.mainCollectionView.numberOfItems(inSection: visibleIndexPath.section) + footerViewUpdateHandler(visibleIndexPath, numberOfItems, visibleIndexPath.item) + } + } + + // MARK: - UIPageViewControllerDataSource + func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { + return viewControllerBefore(viewController: viewController) + } + + func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { + return viewControllerAfter(viewController: viewController) + } + + // MARK: - UIPageViewControllerDelegate + func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { + guard completed, let viewController = pageViewController.viewControllers?.first, let index = viewControllers.firstIndex(of: viewController) else { return } + setSelectedSegmentIndex(index) + } +} diff --git a/assignment/assignment/Main/extension.swift b/assignment/assignment/Main/extension.swift new file mode 100644 index 0000000..90bccdb --- /dev/null +++ b/assignment/assignment/Main/extension.swift @@ -0,0 +1,26 @@ +// +// extension.swift +// assignment +// +// Created by 이지훈 on 6/4/24. +// + +import Foundation + +extension Bundle { + var boxofficeKey: String { + guard let file = self.path(forResource: "privacyInfo", ofType: "plist") else { + fatalError("Couldn't find privacyInfo.plist in main bundle.") + } + + guard let resource = NSDictionary(contentsOfFile: file) else { + fatalError("Couldn't load contents of privacyInfo.plist.") + } + + guard let boxofficeKey = resource["BoxOfficeKey"] as? String else { + fatalError("Couldn't find key 'BoxOfficeKey' in privacyInfo.plist.") + } + + return boxofficeKey + } +} diff --git a/assignment/assignment/SceneDelegate.swift b/assignment/assignment/SceneDelegate.swift index 1b07c03..ab1a10d 100644 --- a/assignment/assignment/SceneDelegate.swift +++ b/assignment/assignment/SceneDelegate.swift @@ -19,7 +19,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // 2. self.window = UIWindow(windowScene: windowScene) // 3. - let navigationController = UINavigationController(rootViewController: RootCollectionViewController()) + let navigationController = UINavigationController(rootViewController: LoginViewController()) self.window?.rootViewController = navigationController // 4. self.window?.makeKeyAndVisible() diff --git a/assignment/assignment/onboarding/Login/LoginModel.swift b/assignment/assignment/onboarding/Login/LoginModel.swift new file mode 100644 index 0000000..bbfd80b --- /dev/null +++ b/assignment/assignment/onboarding/Login/LoginModel.swift @@ -0,0 +1,13 @@ +// +// LoginModel.swift +// assignment +// +// Created by 이지훈 on 5/27/24. +// + +import Foundation + +struct LoginModel { + let id: String + let password: String +} diff --git a/assignment/assignment/onboarding/Login/LoginVC.swift b/assignment/assignment/onboarding/Login/LoginVC.swift new file mode 100644 index 0000000..685dfad --- /dev/null +++ b/assignment/assignment/onboarding/Login/LoginVC.swift @@ -0,0 +1,87 @@ +// +// ViewController.swift +// assignment +// +// Created by 이지훈 on 4/8/24. +// +import UIKit +import SnapKit +import Then + +class LoginViewController: UIViewController, UITextFieldDelegate { + + var nickname: String? + + private var viewModel = LoginViewModel() + private var loginView = LoginView() + + override func viewDidLoad() { + super.viewDidLoad() + setupLoginView() + bindViewModel() + } + + private func setupLoginView() { + view.addSubview(loginView) + loginView.frame = view.bounds + loginView.idTextFieldView.delegate = self + loginView.passwordTextFieldView.delegate = self + + loginView.idTextFieldView.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged) + loginView.passwordTextFieldView.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged) + loginView.xCircleButton.addTarget(self, action: #selector(handleXCircleButtonTap), for: .touchUpInside) + loginView.makeAccount.addTarget(self, action: #selector(presentModalView), for: .touchUpInside) + loginView.loginButton.addTarget(self, action: #selector(tryLogin), for: .touchUpInside) + } + + private func bindViewModel() { + viewModel.isLoginButtonEnabled.bind { [weak self] isEnabled in + self?.loginView.loginButton.isEnabled = isEnabled + } + + viewModel.loginSuccess.bind { [weak self] success in + if success { + self?.navigateToWelcomeScreen() + } else { + self?.showError("로그인에 실패하였습니다.") + } + } + + viewModel.errorMessage.bind { [weak self] message in + if let msg = message { + self?.showError(msg) + } + } + } + + @objc func textFieldDidChange(_ textField: UITextField) { + viewModel.checkValid(id: loginView.idTextFieldView.text, password: loginView.passwordTextFieldView.text) + } + + @objc func handleXCircleButtonTap() { + loginView.idTextFieldView.text = "" + loginView.passwordTextFieldView.text = "" + } + + @objc func presentModalView() { + let modalViewController = NicknameViewController() + present(modalViewController, animated: true, completion: nil) + modalViewController.onSaveNickname = { [weak self] nickname in + self?.nickname = nickname + print("닉네임 저장됨: \(nickname)") + } + } + + @objc func tryLogin() { + viewModel.login() + } + + func navigateToWelcomeScreen() { + let welcomeVC = WelcomeViewController() + present(welcomeVC, animated: true, completion: nil) + } + + func showError(_ message: String) { + print(message) + } +} diff --git a/assignment/assignment/onboarding/Login/LoginVM.swift b/assignment/assignment/onboarding/Login/LoginVM.swift new file mode 100644 index 0000000..e3e19dc --- /dev/null +++ b/assignment/assignment/onboarding/Login/LoginVM.swift @@ -0,0 +1,82 @@ +// +// LoginVm.swift +// assignment +// +// Created by 이지훈 on 5/27/24. +// +import Foundation + +class LoginViewModel { + + var id = ObservablePattern("") + var password = ObservablePattern("") + var nickname: String? + + var isLoginButtonEnabled = ObservablePattern(false) + var loginSuccess = ObservablePattern(false) + var errorMessage = ObservablePattern(nil) + + init() { + bindInputs() + } + + private func bindInputs() { + id.bind { [weak self] id in + self?.validateCredentials() + } + + password.bind { [weak self] password in + self?.validateCredentials() + } + } + + private func validateCredentials() { + let isValidId = validateId(id.value) + let isValidPassword = validatePassword(password.value) + + isLoginButtonEnabled.value = (isValidId == nil && isValidPassword == nil) + } + + func login() { + if validateId(id.value) == nil && validatePassword(password.value) == nil { + loginSuccess.value = true + errorMessage.value = nil + nickname = id.value // 로그인 성공 시 사용자 아이디를 닉네임으로 설정 + } else { + errorMessage.value = "아이디 또는 비밀번호 형식이 올바르지 않습니다." + loginSuccess.value = false + } + } + + func checkValid(id: String?, password: String?) { + guard let id = id, !id.isEmpty, let password = password, !password.isEmpty else { + errorMessage.value = "ID와 비밀번호를 모두 입력해주세요." + return + } + + let validId = validateId(id) + let validPassword = validatePassword(password) + + if validId == nil && validPassword == nil { + loginSuccess.value = true + errorMessage.value = nil + } else { + errorMessage.value = "입력된 ID 또는 비밀번호가 형식에 맞지 않습니다." + loginSuccess.value = false + } + } + + private func validateId(_ id: String) -> String? { + guard id.range(of: "[A-Za-z0-9]{5,13}", options: .regularExpression) != nil else { + return "아이디가 유효하지 않습니다." + } + return nil + } + + private func validatePassword(_ password: String) -> String? { + guard password.range(of: "[A-Za-z0-9!_@$%^&+=]{8,20}", options: .regularExpression) != nil else { + return "비밀번호가 유효하지 않습니다." + } + return nil + } +} diff --git a/assignment/assignment/onboarding/Login/View.swift b/assignment/assignment/onboarding/Login/View.swift new file mode 100644 index 0000000..b68d8bd --- /dev/null +++ b/assignment/assignment/onboarding/Login/View.swift @@ -0,0 +1,174 @@ +// +// View.swift +// assignment +// +// Created by 이지훈 on 7/3/24. +// + +import UIKit +import SnapKit +import Then + +class LoginView: UIView { + + let loginLabel = UILabel().then { + $0.text = "TVING ID 로그인" + $0.textColor = UIColor(named: "gray84") + $0.font = UIFont(name: "Pretendard-Medium", size: 24) + } + + let idTextFieldView = UITextField().then { + $0.backgroundColor = UIColor(named: "gray4") + $0.layer.cornerRadius = 3 + $0.attributedPlaceholder = NSAttributedString(string: "아이디", attributes: [NSAttributedString.Key.foregroundColor: UIColor(named: "gray2")]) + } + + let passwordTextFieldView = UITextField().then { + $0.backgroundColor = UIColor(named: "gray4") + $0.layer.cornerRadius = 3 + $0.attributedPlaceholder = NSAttributedString(string: "비밀번호", attributes: [NSAttributedString.Key.foregroundColor: UIColor(named: "gray2")]) + $0.isSecureTextEntry = true + } + + let loginButton = UIButton().then { + $0.backgroundColor = UIColor.clear + $0.layer.borderColor = UIColor(named: "gray4")?.cgColor + $0.layer.borderWidth = 1 + $0.setTitle("로그인하기", for: .normal) + $0.layer.cornerRadius = 3 + $0.isEnabled = false + } + + let findId = UILabel().then { + $0.text = "아이디 찾기" + $0.textColor = UIColor(named: "gray2") + $0.font = UIFont(name: "Pretendard-SemiBold", size: 14) + } + + let findPw = UILabel().then { + $0.text = "비밀번호 찾기" + $0.textColor = UIColor(named: "gray2") + $0.font = UIFont(name: "Pretendard-SemiBold", size: 14) + } + + let noAccount = UILabel().then { + $0.text = "아직계정이 없으신가요?" + $0.textColor = UIColor(named: "gray3") + $0.font = UIFont(name: "Pretendard-SemiBold", size: 14) + } + + let makeAccount = UIButton().then { + let title = "닉네임 만들러 가기" + let attributes: [NSAttributedString.Key: Any] = [ + .font: UIFont(name: "Pretendard-SemiBold", size: 14)!, + .foregroundColor: UIColor(named: "gray2")!, + .underlineStyle: NSUnderlineStyle.single.rawValue + ] + + let attributedTitle = NSMutableAttributedString(string: title, attributes: attributes) + $0.setAttributedTitle(attributedTitle, for: .normal) + } + + let spaceView = UIView().then { + $0.backgroundColor = UIColor(named: "gray2") + } + + let eyeButton = UIButton(type: .custom).then { + $0.setImage(UIImage(named: "eyeIcon"), for: .normal) + } + + let xCircleButton = UIButton(type: .custom).then { + $0.setImage(UIImage(named: "xCircle"), for: .normal) + } + + override init(frame: CGRect) { + super.init(frame: frame) + setupSubviews() + setupConstraints() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupSubviews() { + addSubview(loginLabel) + addSubview(idTextFieldView) + addSubview(passwordTextFieldView) + addSubview(loginButton) + addSubview(findId) + addSubview(findPw) + addSubview(noAccount) + addSubview(makeAccount) + addSubview(spaceView) + addSubview(eyeButton) + addSubview(xCircleButton) + } + + private func setupConstraints() { + loginLabel.snp.makeConstraints { make in + make.top.equalTo(safeAreaLayoutGuide).offset(90) + make.centerX.equalToSuperview() + } + + idTextFieldView.snp.makeConstraints { make in + make.top.equalTo(loginLabel.snp.bottom).offset(20) + make.centerX.equalToSuperview() + make.width.equalTo(335) + make.height.equalTo(52) + } + + passwordTextFieldView.snp.makeConstraints { make in + make.top.equalTo(idTextFieldView.snp.bottom).offset(20) + make.centerX.equalToSuperview() + make.width.equalTo(335) + make.height.equalTo(52) + } + + loginButton.snp.makeConstraints { make in + make.top.equalTo(passwordTextFieldView.snp.bottom).offset(21) + make.centerX.equalToSuperview() + make.width.equalTo(335) + make.height.equalTo(52) + } + + findId.snp.makeConstraints { make in + make.top.equalTo(loginButton.snp.bottom).offset(31) + make.leading.equalToSuperview().offset(85) + } + + spaceView.snp.makeConstraints { make in + make.centerX.equalToSuperview() + make.top.equalTo(loginButton.snp.bottom).offset(31) + make.width.equalTo(2) + make.height.equalTo(14) + } + + findPw.snp.makeConstraints { make in + make.top.equalTo(loginButton.snp.bottom).offset(31) + make.trailing.equalToSuperview().offset(-86) + } + + noAccount.snp.makeConstraints { make in + make.top.equalTo(findId.snp.bottom).offset(12) + make.leading.equalToSuperview().offset(51) + } + + makeAccount.snp.makeConstraints { make in + make.top.equalTo(findPw.snp.bottom).offset(12) + make.trailing.equalToSuperview().offset(-43) + } + + eyeButton.snp.makeConstraints { make in + make.trailing.equalTo(passwordTextFieldView.snp.trailing).offset(-20) + make.centerY.equalTo(passwordTextFieldView) + make.width.height.equalTo(24) + } + + xCircleButton.snp.makeConstraints { make in + make.trailing.equalTo(eyeButton.snp.leading).offset(-20) + make.centerY.equalTo(passwordTextFieldView) + make.width.height.equalTo(24) + } + } +} diff --git a/assignment/assignment/onboarding/LoginViewController.swift b/assignment/assignment/onboarding/LoginViewController.swift deleted file mode 100644 index 42ff65c..0000000 --- a/assignment/assignment/onboarding/LoginViewController.swift +++ /dev/null @@ -1,307 +0,0 @@ -// -// ViewController.swift -// assignment -// -// Created by 이지훈 on 4/8/24. -// - -import UIKit -import SnapKit -import Then - - -class LoginViewController: UIViewController, UITextFieldDelegate, WelcomeViewControllerDelegate { - - func didLoginWithId(id: String) { - print(1) - } - - //MARK: - Properties - var nickname: String? // 클로저로 받을 닉네임 - - let loginLabel = UILabel().then { - $0.text = "TVING ID 로그인" - $0.textColor = UIColor(named: "gray84") - $0.font = UIFont(name: "Pretendard-Medium", size: 24) - } - - let idTextFieldView = UITextField().then { - $0.backgroundColor = UIColor(named: "gray4") - $0.layer.cornerRadius = 3 - $0.attributedPlaceholder = NSAttributedString(string: "아이디", attributes: [NSAttributedString.Key.foregroundColor: UIColor(named: "gray2")]) - } - - let passwordTextFieldView = UITextField().then { - $0.backgroundColor = UIColor(named: "gray4") - $0.layer.cornerRadius = 3 - $0.attributedPlaceholder = NSAttributedString(string: "비밀번호", attributes: [NSAttributedString.Key.foregroundColor: UIColor(named: "gray2")]) - $0.isSecureTextEntry = true - } - - let loginButton = UIButton().then { - $0.backgroundColor = UIColor.clear - $0.layer.borderColor = UIColor(named: "gray4")?.cgColor - $0.layer.borderWidth = 1 - $0.setTitle("로그인하기", for: .normal) - $0.layer.cornerRadius = 3 - $0.isEnabled = false - } - - let findId = UILabel().then { - $0.text = "아이디 찾기" - $0.textColor = UIColor(named: "gray2") - $0.font = UIFont(name: "Pretendard-SemiBold", size: 14) - } - - let findPw = UILabel().then { - $0.text = "비밀번호 찾기" - $0.textColor = UIColor(named: "gray2") - $0.font = UIFont(name: "Pretendard-SemiBold", size: 14) - } - - let noAccount = UILabel().then { - $0.text = "아직계정이 없으신가요?" - $0.textColor = UIColor(named: "gray3") - $0.font = UIFont(name: "Pretendard-SemiBold", size: 14) - } - - let makeAccount = UIButton().then { - let title = "닉네임 만들러 가기" - let attributes: [NSAttributedString.Key: Any] = [ - .font: UIFont(name: "Pretendard-SemiBold", size: 14)!, - .foregroundColor: UIColor(named: "gray2")!, - .underlineStyle: NSUnderlineStyle.single.rawValue - ] - - let attributedTitle = NSMutableAttributedString(string: title, attributes: attributes) - $0.setAttributedTitle(attributedTitle, for: .normal) - } - - - let spaceView = UIView().then { - $0.backgroundColor = UIColor(named: "gray2") - } - - - let eyeButton = UIButton(type: .custom) - let xCircleButton = UIButton(type: .custom) - - - //MARK: - ViewDidLoad - override func viewDidLoad() { - super.viewDidLoad() - - view.backgroundColor = .black - idTextFieldView.delegate = self - passwordTextFieldView.delegate = self - - addSubViews() - setConstraints() - - // 패스워드 텍스트 필드 설정 - eyeButton.setImage(UIImage(named: "eyeIcon"), for: .normal) - eyeButton.addTarget(self, action: #selector(togglePasswordView), for: .touchUpInside) - eyeButton.snp.makeConstraints { make in - make.trailing.equalTo(passwordTextFieldView.snp.trailing).offset(-20) - make.centerY.equalTo(passwordTextFieldView) - make.width.height.equalTo(24) - } - - xCircleButton.setImage(UIImage(named: "xCircle"), for: .normal) - xCircleButton.snp.makeConstraints { make in - make.trailing.equalTo(eyeButton.snp.leading).offset(-20) - make.centerY.equalTo(passwordTextFieldView) - make.width.height.equalTo(24) - } - - idTextFieldView.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged) - passwordTextFieldView.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged) - xCircleButton.addTarget(self, action: #selector(handleXCircleButtonTap), for: .touchUpInside) - - loginButton.addTarget(self, action: #selector(handleLogin), for: .touchUpInside) - makeAccount.addTarget(self, action: #selector(presentModalView), for: .touchUpInside) - - - - } - - //MARK: - AddSubview - func addSubViews() { - let views = [ - loginLabel, - idTextFieldView, - passwordTextFieldView, - loginButton, - findId, - findPw, - noAccount, - makeAccount, - spaceView, - eyeButton, - xCircleButton - ] - views.forEach { - view.addSubview($0) - } - } - - - //MARK: - layout - func setConstraints() { - loginLabel.snp.makeConstraints { make in - make.top.equalTo(view.safeAreaLayoutGuide).offset(90) - make.centerX.equalTo(view) - } - - idTextFieldView.snp.makeConstraints { make in - make.top.equalTo(loginLabel.snp.bottom).offset(20) - make.centerX.equalTo(view) - make.width.equalTo(335) - make.height.equalTo(52) - - } - - passwordTextFieldView.snp.makeConstraints { make in - make.top.equalTo(idTextFieldView.snp.bottom).offset(20) - - make.centerX.equalTo(view) - make.width.equalTo(335) - make.height.equalTo(52) - } - - loginButton.snp.makeConstraints { make in - make.top.equalTo(passwordTextFieldView.snp.bottom).offset(21) - - make.centerX.equalTo(view) - make.width.equalTo(335) - make.height.equalTo(52) - } - - findId.snp.makeConstraints { make in - make.top.equalTo(loginButton.snp.bottom).offset(31) - make.leading.equalTo(view).offset(85) - } - - spaceView.snp.makeConstraints { make in - make.centerX.equalTo(view) - make.top.equalTo(loginButton.snp.bottom).offset(31) - make.width.equalTo(2) - make.height.equalTo(14) - } - - findPw.snp.makeConstraints { make in - make.top.equalTo(loginButton.snp.bottom).offset(31) - make.trailing.equalTo(view).offset(-86) - } - - noAccount.snp.makeConstraints { make in - make.top.equalTo(findId.snp.bottom).offset(12) - make.leading.equalTo(view.snp.leading).offset(51) - } - - makeAccount.snp.makeConstraints { make in - make.top.equalTo(findPw.snp.bottom).offset(12) - make.trailing.equalTo(view.snp.trailing).offset(-43) - } - - //PlaceHolder 왼쪽공간 띄우기 - let spacerView = UIView(frame:CGRect(x:0, y:0, width:22, height:10)) - idTextFieldView.leftViewMode = .always - idTextFieldView.leftView = spacerView - - let spacerViewForPassword = UIView(frame:CGRect(x:0, y:0, width:22, height:10)) - passwordTextFieldView.leftViewMode = .always - passwordTextFieldView.leftView = spacerViewForPassword - - findId.snp.makeConstraints { make in - make.top.equalTo(loginButton.snp.bottom).offset(31) - - } - - //오른쪽 버튼 공간 띄우기 - eyeButton.setImage(UIImage(named: "eyeIcon"), for: .normal) - eyeButton.addTarget(self, action: #selector(togglePasswordView), for: .touchUpInside) - eyeButton.snp.makeConstraints { make in - make.trailing.equalTo(passwordTextFieldView.snp.trailing).offset(-20) - make.centerY.equalTo(passwordTextFieldView) - make.width.height.equalTo(24) - } - - xCircleButton.setImage(UIImage(named: "xcircle"), for: .normal) - xCircleButton.snp.makeConstraints { make in - make.trailing.equalTo(eyeButton.snp.leading).offset(-20) - make.centerY.equalTo(passwordTextFieldView) - make.width.height.equalTo(24) - } - - } - - // TF 포커스 호출 - func textFieldDidBeginEditing(_ textField: UITextField) { - // 테두리 색상 변경 - textField.layer.borderColor = CGColor(gray: 1, alpha: 1) - textField.layer.borderWidth = 1.0 - } - - // TF 포커스 해제 - func textFieldDidEndEditing(_ textField: UITextField) { - textField.layer.borderColor = UIColor.clear.cgColor - textField.layer.borderWidth = 0.0 - } - - // 텍스트가 변경될 때 호출되는 메서드 - func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { - textField.clearButtonMode = .whileEditing - return true - } - - //비밀번호 보안처리 - @objc func togglePasswordView() { - passwordTextFieldView.isSecureTextEntry.toggle() - } - - //텍스트 필드 채워졌는지 확인 - @objc func textFieldDidChange(_ textField: UITextField) { - let isBothFilled = !(idTextFieldView.text?.isEmpty ?? true) && !(passwordTextFieldView.text?.isEmpty ?? true) - loginButton.isEnabled = isBothFilled - loginButton.backgroundColor = isBothFilled ? UIColor(named: "red"): .clear - } - - //지우기 버튼 눌럿을때 동작 - @objc func handleXCircleButtonTap() { - idTextFieldView.text = "" - passwordTextFieldView.text = "" - } - - //로그인 화면 전환 - @objc func handleLogin() { - let welcomeVC = WelcomeViewController() - welcomeVC.delegate = self - welcomeVC.id = idTextFieldView.text ?? "" - welcomeVC.nickname = self.nickname - welcomeVC.modalPresentationStyle = .fullScreen - - present(welcomeVC, animated: true, completion: nil) - } - - //닉네임 만들기 - @objc func presentModalView() { - let modalViewController = NicknameViewController() - - if let nicknameVC = modalViewController.presentationController as? UISheetPresentationController { - nicknameVC.detents = [.medium()] - nicknameVC.prefersGrabberVisible = true - - } - modalViewController.onSaveNickname = { [weak self] nickname in - self?.nickname = nickname // 닉네임 저장 - print("닉네임 저장됨: \(nickname)") - } - present(modalViewController, animated: true, completion: nil) - } -} - - -//#Preview{ -// LoginViewController() -//} diff --git a/assignment/assignment/onboarding/NicknameViewController.swift b/assignment/assignment/onboarding/Nickname/NicknameVC.swift similarity index 65% rename from assignment/assignment/onboarding/NicknameViewController.swift rename to assignment/assignment/onboarding/Nickname/NicknameVC.swift index 623d517..8d7a96f 100644 --- a/assignment/assignment/onboarding/NicknameViewController.swift +++ b/assignment/assignment/onboarding/Nickname/NicknameVC.swift @@ -1,4 +1,3 @@ -// // NicknameViewController.swift // assignment // @@ -6,14 +5,16 @@ // import UIKit + import SnapKit import Then class NicknameViewController: UIViewController, UITextFieldDelegate { - //MARK: - Properties + // MARK: - Properties var onSaveNickname: ((String) -> Void)? + private var viewModel: NicknameViewModelType = NicknameViewModel() let nicknameLabel = UILabel().then { $0.text = "닉네임을 입력해주세요" @@ -38,7 +39,6 @@ class NicknameViewController: UIViewController, UITextFieldDelegate { $0.backgroundColor = UIColor(named: "red") $0.setTitle("저장하기", for: .normal) $0.layer.cornerRadius = 3 - $0.addTarget(self, action: #selector(saveNickname), for: .touchUpInside) } override func viewDidLoad() { @@ -48,11 +48,29 @@ class NicknameViewController: UIViewController, UITextFieldDelegate { nicknameTextField.delegate = self addSubViews() - layouts() + setConstraints() + bindViewModel() setupActions() } - //MARK: - AddSubViews + // MARK: - Bind ViewModel + private func bindViewModel() { + viewModel.nickname.bind { [weak self] nickname in + self?.nicknameTextField.text = nickname + } + + viewModel.isValid.bind { [weak self] isValid in + self?.saveBtn.isEnabled = isValid + } + + viewModel.errorMessage.bind { [weak self] errorMessage in + if let errorMessage = errorMessage { + print("Error: \(errorMessage)") + } + } + } + + // MARK: - AddSubViews func addSubViews() { let views = [ nicknameLabel, @@ -64,8 +82,8 @@ class NicknameViewController: UIViewController, UITextFieldDelegate { } } - //MARK: - Layouts - func layouts() { + // MARK: - Layouts + func setConstraints() { nicknameLabel.snp.makeConstraints { $0.top.equalToSuperview().offset(50) $0.leading.equalToSuperview().offset(20) @@ -86,35 +104,38 @@ class NicknameViewController: UIViewController, UITextFieldDelegate { } } + // MARK: - Setup Actions func setupActions() { saveBtn.addTarget(self, action: #selector(saveNickname), for: .touchUpInside) + nicknameTextField.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged) } - - //정규식 + // MARK: - Text Field Delegate func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { let currentText = textField.text ?? "" let prospectiveText = (currentText as NSString).replacingCharacters(in: range, with: string) if string.isEmpty { return true } - //입력하는중에는 길이만 체크 return prospectiveText.count <= 10 } - @objc func saveNickname() { - if let text = nicknameTextField.text, !text.isEmpty, text.range(of: "^[가-힣]{1,10}$", options: .regularExpression) != nil { - onSaveNickname?(text) // 클로저로 전달 - dismiss(animated: true, completion: nil) - print(text) - } else { - print("닉네임을 한글 1~10자로 입력해주세요.") + @objc func textFieldDidChange(_ textField: UITextField) { + if let text = textField.text { + viewModel.updateNickname(text) } } - - + @objc func saveNickname() { + print("Save Button Pressed") // 추가한 로그 + viewModel.saveNickname { [weak self] nickname in + guard let nickname = nickname else { + return + } + self?.onSaveNickname?(nickname) + + let welcomeVC = WelcomeViewController() + // welcomeVC.configureViewModel(id: "", nickname: nickname) + welcomeVC.modalPresentationStyle = .fullScreen + self?.present(welcomeVC, animated: true, completion: nil) + } + } } - -//#Preview{ -// NicknameViewController() -//} - diff --git a/assignment/assignment/onboarding/Nickname/NicknameVM.swift b/assignment/assignment/onboarding/Nickname/NicknameVM.swift new file mode 100644 index 0000000..181f7e4 --- /dev/null +++ b/assignment/assignment/onboarding/Nickname/NicknameVM.swift @@ -0,0 +1,47 @@ +// NicknameVM.swift +// assignment +// +// Created by 이지훈 on 5/27/24. +// + +import Foundation + +protocol NicknameViewModelType { + var nickname: ObservablePattern { get } + var isValid: ObservablePattern { get } + var errorMessage: ObservablePattern { get } + + func updateNickname(_ nickname: String) + func saveNickname(completion: (String?) -> Void) +} + +final class NicknameViewModel: NicknameViewModelType { + var nickname: ObservablePattern = ObservablePattern(nil) + var isValid: ObservablePattern = ObservablePattern(false) + var errorMessage: ObservablePattern = ObservablePattern(nil) + + private let validNicknameRegex = "^[가-힣]{1,10}$" + + func updateNickname(_ nickname: String) { + self.nickname.value = nickname + validateNickname(nickname) + } + + func saveNickname(completion: (String?) -> Void) { + guard let nickname = nickname.value, validateNickname(nickname) else { + errorMessage.value = "닉네임을 한글 1~10자로 입력해주세요." + completion(nil) + return + } + errorMessage.value = nil + completion(nickname) + } + + @discardableResult + private func validateNickname(_ nickname: String) -> Bool { + let isValid = nickname.range(of: validNicknameRegex, options: .regularExpression) != nil + self.isValid.value = isValid + return isValid + } +} + diff --git a/assignment/assignment/onboarding/Util/BindingTextField.swift b/assignment/assignment/onboarding/Util/BindingTextField.swift new file mode 100644 index 0000000..a183930 --- /dev/null +++ b/assignment/assignment/onboarding/Util/BindingTextField.swift @@ -0,0 +1,41 @@ +// +// BindingTextField.swift +// assignment +// +// Created by 이지훈 on 7/3/24. +// + +import Foundation +import UIKit + +class BindingTextField : UITextField { + + override init(frame: CGRect) { + super.init(frame:frame) + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + //addtarget에서 메서드 호출 + private func bindViewModel() { + self.addTarget(self, action: #selector(textFieldDidChange), for: .editingChanged) + } + + // 글자 변경시마다 메서드 호출하기 + @objc func textFieldDidChange(_ textfleld: UITextField) { + if let text = textfleld.text { + textChanged(text) + } + } + + //글자 변경시에 클로저 호출 + private var textChanged: (String) -> Void = { _ in } + + func binding(callback: @escaping (String) -> Void) { + self.textChanged = callback + bindViewModel() + } +} diff --git a/assignment/assignment/onboarding/Util/Observable.swift b/assignment/assignment/onboarding/Util/Observable.swift new file mode 100644 index 0000000..0f18150 --- /dev/null +++ b/assignment/assignment/onboarding/Util/Observable.swift @@ -0,0 +1,27 @@ +// +// Observable.swift +// assignment +// +// Created by 이지훈 on 5/27/24. +// + +import Foundation + +class ObservablePattern { + var value: T { + didSet { + listener?(value) + } + } + + private var listener: ((T) -> Void)? + + init(_ value: T) { + self.value = value + } + + func bind(_ listener: @escaping (T) -> Void) { + self.listener = listener + listener(value) + } +} diff --git a/assignment/assignment/onboarding/WelcomeViewController.swift b/assignment/assignment/onboarding/Welcome/WelcomeVC.swift similarity index 59% rename from assignment/assignment/onboarding/WelcomeViewController.swift rename to assignment/assignment/onboarding/Welcome/WelcomeVC.swift index eb5b22f..2bf354a 100644 --- a/assignment/assignment/onboarding/WelcomeViewController.swift +++ b/assignment/assignment/onboarding/Welcome/WelcomeVC.swift @@ -1,103 +1,100 @@ -// // WelcomeViewController.swift // assignment // // Created by 이지훈 on 4/12/24. - import UIKit -import Then -import SnapKit -protocol WelcomeViewControllerDelegate: AnyObject { - func didLoginWithId(id: String) -} +import SnapKit +import Then +import RxSwift +import RxCocoa class WelcomeViewController: UIViewController { - - weak var delegate: WelcomeViewControllerDelegate? - //delegate에서 받은값 - var id: String = "" - //closure에서 받은값 - var nickname: String? - + private var viewModel: WelcomeViewModelType? + private let disposeBag = DisposeBag() + let imageView = UIImageView().then { $0.image = UIImage(named: "tving") $0.contentMode = .scaleToFill } - + let welcomeLabel = UILabel().then { $0.textColor = UIColor(named: "gray84") $0.font = UIFont(name: "Pretendard-Bold", size: 23) $0.textAlignment = .center $0.numberOfLines = 2 - } - + let backButton = UIButton().then { $0.setTitle("메인으로", for: .normal) $0.backgroundColor = UIColor(named: "red") $0.layer.cornerRadius = 3 } - - + override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .black - - // presentNicknameViewController() - configureLabel() - backButton.addTarget(self, action: #selector(backToMain), for: .touchUpInside) + addSubViews() - Layouts() + setLayouts() + + if let viewModel = viewModel { + bindViewModel(viewModel) + } - print(id) - print(nickname) + backButton.addTarget(self, action: #selector(backToMain), for: .touchUpInside) } - - func configureLabel() { - if let nickname = nickname { - welcomeLabel.text = "\(nickname)님\n 반가워요!" - - } else { - welcomeLabel.text = "\(id)님\n 반가워요!" - } + + func configureViewModel(id: String, nickname: String) { + let viewModel = WelcomeViewModel() + viewModel.id.accept(id) + viewModel.nickname.accept(nickname) + self.viewModel = viewModel + bindViewModel() + print("id: \(id)") + print("nickname : \(nickname)") } - + + private func bindViewModel() { + viewModel?.welcomeMessage + .bind(to: welcomeLabel.rx.text) + .disposed(by: disposeBag) + } + + + private func bindViewModel(_ viewModel: WelcomeViewModelType) { + viewModel.welcomeMessage + .bind(to: welcomeLabel.rx.text) + .disposed(by: disposeBag) + } + func addSubViews() { let views = [imageView, welcomeLabel, backButton] views.forEach { view.addSubview($0) } } - - func Layouts() { + + func setLayouts() { imageView.snp.makeConstraints { $0.top.equalTo(view.snp.top).offset(58) $0.leading.trailing.equalToSuperview() $0.height.equalTo(200) } - + welcomeLabel.snp.makeConstraints { $0.top.equalTo(imageView.snp.bottom).offset(67) $0.leading.trailing.equalTo(view).inset(20) } - + backButton.snp.makeConstraints { $0.bottom.equalTo(view.snp.bottom).offset(-66) $0.leading.trailing.equalTo(view).inset(20) $0.height.equalTo(52) } } - - @objc func backToMain() { - let mainVC = MainViewController() - - if let navigationController = self.navigationController { - navigationController.pushViewController(mainVC, animated: true) - - } + + @objc func backToMain() { + let mainVC = RootCollectionViewController() + mainVC.modalPresentationStyle = .fullScreen + self.present(mainVC, animated: true, completion: nil) } - } -// -//#Preview { -// WelcomeViewController() -//} diff --git a/assignment/assignment/onboarding/Welcome/WelcomeVM.swift b/assignment/assignment/onboarding/Welcome/WelcomeVM.swift new file mode 100644 index 0000000..50e5f97 --- /dev/null +++ b/assignment/assignment/onboarding/Welcome/WelcomeVM.swift @@ -0,0 +1,45 @@ +// +// WelcomeVM.swift +// assignment +// +// Created by 이지훈 on 5/27/24. +// +import Foundation + +import RxSwift +import RxCocoa + +protocol WelcomeViewModelType { + var id: BehaviorRelay { get } + var nickname: BehaviorRelay { get } + var welcomeMessage: BehaviorRelay { get } + + func configureWelcomeMessage() +} + +class WelcomeViewModel: WelcomeViewModelType { + var id = BehaviorRelay(value: "") + var nickname = BehaviorRelay(value: "") + var welcomeMessage = BehaviorRelay(value: "Loading...") + + private let disposeBag = DisposeBag() + + init() { + Observable.combineLatest(id.asObservable(), nickname.asObservable()) + .map { id, nickname in + if let nickname = nickname { + return "\(nickname)님\n 반가워요!" + } else { + return "\(id)님\n 반가워요!" + + } + } + .bind(to: welcomeMessage) + .disposed(by: disposeBag) + } + + func configureWelcomeMessage() { + let message = nickname.value.map { "\($0)님\n 반가워요!" } ?? "\(id.value)님\n 반가워요!" + welcomeMessage.accept(message) + } +}