├── .gitignore ├── DailyQuest ├── DailyQuest.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── DailyQuest │ ├── Application │ │ ├── AppAppearance.swift │ │ ├── AppCoordinator.swift │ │ ├── AppDelegate.swift │ │ ├── Container │ │ │ ├── Repository │ │ │ │ └── RepositoryKey.swift │ │ │ ├── Service │ │ │ │ └── ServiceKey.swift │ │ │ ├── Storage │ │ │ │ └── StroageKey.swift │ │ │ └── UseCase │ │ │ │ └── UseCaseKey.swift │ │ ├── DIContainer │ │ │ ├── AppDIContainer.swift │ │ │ ├── BrowseSceneDIContainer.swift │ │ │ ├── HomeSceneDIContainer.swift │ │ │ └── SettingsSceneDIContainer.swift │ │ ├── Protocols │ │ │ ├── Coordinator.swift │ │ │ └── TabCoordinator.swift │ │ ├── SceneDelegate.swift │ │ └── SyncManager.swift │ ├── Data │ │ ├── DataConfigure.swift │ │ ├── Network │ │ │ └── DataMapping │ │ │ │ ├── BrowseQuestDTO+Mapping.swift │ │ │ │ ├── DTO.swift │ │ │ │ ├── QuestDTO+Mapping.swift │ │ │ │ └── UserDTO+Mapping.swift │ │ ├── PersistentStorages │ │ │ ├── BrowseQuestsStorage │ │ │ │ ├── BrowseQuestsStorage.swift │ │ │ │ └── RealmStorage │ │ │ │ │ ├── EntityMapping │ │ │ │ │ ├── BrowseQuestEntity+Mapping.swift │ │ │ │ │ └── SubQuestEntity+Mapping.swift │ │ │ │ │ └── RealmBrowseQuestsStorage.swift │ │ │ ├── QuestsStorage │ │ │ │ ├── QuestsStorage.swift │ │ │ │ └── RealmStorage │ │ │ │ │ ├── EntityMapping │ │ │ │ │ └── QuestEntity+Mapping.swift │ │ │ │ │ └── RealmQuestsStorage.swift │ │ │ ├── RealmStorage │ │ │ │ ├── Entities │ │ │ │ │ ├── BrowseQuestEntity.swift │ │ │ │ │ ├── QuestEntity.swift │ │ │ │ │ ├── SubQuestEntity.swift │ │ │ │ │ └── UserInfoEntity.swift │ │ │ │ └── RealmStorage.swift │ │ │ └── UserInfoStorage │ │ │ │ ├── RealmStorage │ │ │ │ ├── EntityMapping │ │ │ │ │ └── UserInfoEntity+Mapping.swift │ │ │ │ └── RealmUserInfoStorage.swift │ │ │ │ └── UserInfoStorage.swift │ │ └── Repositories │ │ │ ├── DefaultAuthRepository.swift │ │ │ ├── DefaultBrowseRepository.swift │ │ │ ├── DefaultQuestsRepository.swift │ │ │ ├── DefaultUserRepository.swift │ │ │ └── RepositoryManager.swift │ ├── Domain │ │ ├── Entities │ │ │ ├── BrowseQuest.swift │ │ │ ├── DailyQuestCompletion.swift │ │ │ ├── Quest.swift │ │ │ └── User.swift │ │ ├── Interfaces │ │ │ └── Repositories │ │ │ │ ├── AuthRepository.swift │ │ │ │ ├── BrowseRepository.swift │ │ │ │ ├── ProtectedUserRepository.swift │ │ │ │ ├── QuestsRepository.swift │ │ │ │ └── UserRepository.swift │ │ └── UseCases │ │ │ ├── Browse │ │ │ ├── DefaultBrowseUseCase.swift │ │ │ └── Protocols │ │ │ │ └── BrowseUseCase.swift │ │ │ ├── Common │ │ │ ├── DefaultFriendCalendarUseCase.swift │ │ │ ├── DefaultFriendQuestUseCase.swift │ │ │ └── Protocols │ │ │ │ └── FriendQuestUseCase.swift │ │ │ ├── Home │ │ │ ├── DefaultEnrollUseCase.swift │ │ │ ├── DefaultQuestUseCase.swift │ │ │ ├── DefaultUserUseCase.swift │ │ │ ├── HomeCalendarUseCase.swift │ │ │ └── Protocols │ │ │ │ ├── CalendarUseCase.swift │ │ │ │ ├── EnrollUseCase.swift │ │ │ │ ├── QuestUseCase.swift │ │ │ │ └── UserUseCase.swift │ │ │ └── Settings │ │ │ ├── DefaultAuthUseCase.swift │ │ │ ├── DefaultSettingsUseCase.swift │ │ │ └── Protocols │ │ │ ├── AuthUseCase.swift │ │ │ └── SettingsUseCase.swift │ ├── Infrastructure │ │ ├── FirebaseService │ │ │ └── FirebaseService.swift │ │ ├── NetworkConfigure.swift │ │ └── NetworkService.swift │ ├── Presentation │ │ ├── Browse │ │ │ ├── Flow │ │ │ │ └── BrowseCoordinator.swift │ │ │ ├── View │ │ │ │ ├── BrowseCell.swift │ │ │ │ └── UserInfoView.swift │ │ │ ├── ViewController │ │ │ │ └── BrowseViewController.swift │ │ │ └── ViewModel │ │ │ │ ├── BrowseItemViewModel.swift │ │ │ │ └── BrowseViewModel.swift │ │ ├── Common │ │ │ ├── CalendarView.swift │ │ │ ├── Cells │ │ │ │ ├── CalendarCell.swift │ │ │ │ ├── FollowingCell.swift │ │ │ │ ├── LastFollowingCell.swift │ │ │ │ ├── QuestCell.swift │ │ │ │ └── UserInfoCell.swift │ │ │ ├── CircleCheckView.swift │ │ │ ├── CustomProgressBar.swift │ │ │ ├── TextFieldForm.swift │ │ │ ├── View │ │ │ │ ├── FollowingView.swift │ │ │ │ ├── FriendStatusView.swift │ │ │ │ ├── QuestView.swift │ │ │ │ └── QuestViewHeader.swift │ │ │ ├── ViewController │ │ │ │ ├── FriendViewController.swift │ │ │ │ └── LaunchViewController.swift │ │ │ └── ViewModel │ │ │ │ └── FriendViewModel.swift │ │ ├── Home │ │ │ ├── Flow │ │ │ │ └── HomeCoordinator.swift │ │ │ ├── View │ │ │ │ ├── DayNamePickerView.swift │ │ │ │ ├── MessageBubble.swift │ │ │ │ ├── PlanDatePickerView.swift │ │ │ │ ├── QuantityView.swift │ │ │ │ ├── StatusView.swift │ │ │ │ └── UserImageView.swift │ │ │ ├── ViewController │ │ │ │ ├── EnrollViewController.swift │ │ │ │ ├── HomeViewController.swift │ │ │ │ └── ProfileViewController.swift │ │ │ └── ViewModel │ │ │ │ ├── EnrollViewModel.swift │ │ │ │ ├── HomeViewModel.swift │ │ │ │ └── ProfileViewModel.swift │ │ └── Settings │ │ │ ├── Flow │ │ │ └── SettingsCoordinator.swift │ │ │ ├── View │ │ │ ├── CommonField.swift │ │ │ ├── NavigateField │ │ │ │ ├── NavigateCell.swift │ │ │ │ ├── NavigateField.swift │ │ │ │ └── NavigateItemViewModel.swift │ │ │ ├── PlainField │ │ │ │ ├── PlainCell.swift │ │ │ │ ├── PlainField.swift │ │ │ │ └── PlainItemViewModel.swift │ │ │ └── ToggleField │ │ │ │ ├── ToggleCell.swift │ │ │ │ ├── ToggleField.swift │ │ │ │ └── ToggleItemViewModel.swift │ │ │ ├── ViewController │ │ │ ├── LoginViewController.swift │ │ │ ├── SettingsViewController.swift │ │ │ └── SignUpViewController.swift │ │ │ └── ViewModel │ │ │ ├── LoginViewModel.swift │ │ │ ├── SettingsViewModel.swift │ │ │ └── SignUpViewModel.swift │ ├── Resource │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── AppIcon (1).png │ │ │ │ └── Contents.json │ │ │ ├── ColorSet │ │ │ │ ├── Contents.json │ │ │ │ ├── MaxDarkYellow.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── MaxGreen.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── MaxLightBlue.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── MaxLightGrey.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── MaxLightYellow.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── MaxRed.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── MaxViolet.colorset │ │ │ │ │ └── Contents.json │ │ │ │ └── MaxYellow.colorset │ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── NoMoreQuests.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── NoMoreQuests.png │ │ │ ├── StatusMax.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── Max 7.png │ │ │ └── defaultBackground.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── blur.png │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ ├── Info.plist │ │ ├── ko.lproj │ │ │ └── LaunchScreen.strings │ │ └── max.lottie.json │ └── Utils │ │ ├── Alertable.swift │ │ ├── Date+.swift │ │ ├── Notification+.swift │ │ ├── StatusView+.swift │ │ ├── String+.swift │ │ ├── SwiftUIPreview.swift │ │ ├── UIButton+.swift │ │ ├── UIColor+.swift │ │ └── UIImageView+.swift ├── DailyQuestTests │ ├── Data │ │ └── QuestsRepositoryTests.swift │ ├── Domain │ │ ├── Mocks │ │ │ ├── BrowseRepositoryMock.swift │ │ │ └── QuestRepositoryMock.swift │ │ └── UseCases │ │ │ ├── BrowseUseCaseTests.swift │ │ │ └── QuestUseCaseTests.swift │ ├── Mocks │ │ ├── BrowseQuest+Stub.swift │ │ ├── Quest+Stub.swift │ │ └── User+Stub.swift │ └── Presentation │ │ ├── Mocks │ │ ├── BrowseUseCaseMock.swift │ │ └── QuestUseCaseMock.swift │ │ └── ViewModel │ │ ├── BrowseViewModelTests.swift │ │ └── QuestViewModelTests.swift ├── DailyQuestUITests │ ├── DailyQuestUITests.swift │ └── DailyQuestUITestsLaunchTests.swift └── SubFrameworks │ └── DailyContainer │ ├── DailyContainer.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── DailyContainer │ ├── Container.swift │ ├── DailyContainer.docc │ └── DailyContainer.md │ ├── DailyContainer.h │ ├── Injected.swift │ └── InjectionKey.swift ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── README.md └── keynotes ├── week01 ├── DailyQuest01_기획공유.key └── DailyQuest01_기획공유.pdf └── week02 └── DailyQuest02_데모발표.pdf /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/xcode,macos,firebase 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=xcode,macos,firebase 3 | 4 | ### Firebase ### 5 | .idea 6 | **/node_modules/* 7 | **/.firebaserc 8 | GoogleService-Info.plist 9 | 10 | ### Firebase Patch ### 11 | .runtimeconfig.json 12 | .firebase/ 13 | 14 | ### macOS ### 15 | # General 16 | .DS_Store 17 | .AppleDouble 18 | .LSOverride 19 | 20 | # Icon must end with two \r 21 | Icon 22 | 23 | 24 | # Thumbnails 25 | ._* 26 | 27 | # Files that might appear in the root of a volume 28 | .DocumentRevisions-V100 29 | .fseventsd 30 | .Spotlight-V100 31 | .TemporaryItems 32 | .Trashes 33 | .VolumeIcon.icns 34 | .com.apple.timemachine.donotpresent 35 | 36 | # Directories potentially created on remote AFP share 37 | .AppleDB 38 | .AppleDesktop 39 | Network Trash Folder 40 | Temporary Items 41 | .apdisk 42 | 43 | ### macOS Patch ### 44 | # iCloud generated files 45 | *.icloud 46 | 47 | ### Xcode ### 48 | ## User settings 49 | xcuserdata/ 50 | 51 | ## Xcode 8 and earlier 52 | *.xcscmblueprint 53 | *.xccheckout 54 | 55 | ### Xcode Patch ### 56 | ## *.xcodeproj/* 57 | !*.xcodeproj/project.pbxproj 58 | !*.xcodeproj/xcshareddata/ 59 | !*.xcodeproj/project.xcworkspace/ 60 | !*.xcworkspace/contents.xcworkspacedata 61 | /*.gcno 62 | **/xcshareddata/WorkspaceSettings.xcsettings 63 | 64 | # End of https://www.toptal.com/developers/gitignore/api/xcode,macos,firebase 65 | 66 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Application/AppAppearance.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppAppearance.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | final class AppAppearance { 11 | static func setupAppearance() { 12 | UITabBar.appearance().backgroundColor = .white 13 | UITabBar.appearance().tintColor = .maxGreen 14 | 15 | UITableViewCell.appearance().selectionStyle = .none 16 | UITableView.appearance().separatorStyle = .none 17 | 18 | UISwitch.appearance().tintColor = .maxLightGrey 19 | UISwitch.appearance().onTintColor = .maxYellow 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Application/AppCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppCoordinator.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | final class AppCoordinator: NSObject, TabCoordinator, UITabBarControllerDelegate { 11 | weak var finishDelegate: CoordinatorFinishDelegate? 12 | var tabBarController: UITabBarController 13 | var childCoordinators: [Coordinator] = [] 14 | let appDIContainer: AppDIContainer 15 | 16 | init(tabBarController: UITabBarController, 17 | appDIContainer: AppDIContainer) { 18 | self.tabBarController = tabBarController 19 | self.appDIContainer = appDIContainer 20 | } 21 | 22 | func start() { 23 | let pages: [TabBarPage] = [.home, .browse, .settings] 24 | let controllers: [UINavigationController] = pages.map(getTabController(_:)) 25 | 26 | prepareTabBarController(withTabControllers: controllers) 27 | } 28 | 29 | private func prepareTabBarController(withTabControllers tabControllers: [UIViewController]) { 30 | tabBarController.delegate = self 31 | tabBarController.setViewControllers(tabControllers, animated: true) 32 | tabBarController.selectedIndex = TabBarPage.home.pageOrderNumber 33 | tabBarController.tabBar.isTranslucent = false 34 | 35 | } 36 | 37 | private func getTabController(_ page: TabBarPage) -> UINavigationController { 38 | let navController = UINavigationController() 39 | navController.setNavigationBarHidden(false, animated: false) 40 | 41 | navController.tabBarItem = UITabBarItem.init(title: page.pageTitleValue, 42 | image: page.pageIcon, 43 | tag: page.pageOrderNumber) 44 | 45 | switch page { 46 | case .home: 47 | let homeSceneDIContainer = appDIContainer.makeHomeSceneDIContainer() 48 | let homeCoordinator = homeSceneDIContainer.makeHomeCoordinator(navigationController: navController, 49 | homeSceneDIContainer: homeSceneDIContainer) 50 | homeCoordinator.start() 51 | childCoordinators.append(homeCoordinator) 52 | break 53 | case .browse: 54 | let browseSceneDIContainer = appDIContainer.makeBrowseSceneDIContainer() 55 | let browseCoordinator = browseSceneDIContainer.makeBrowseCoordinator(navigationController: navController, 56 | browseSceneDIContainer: browseSceneDIContainer) 57 | browseCoordinator.start() 58 | childCoordinators.append(browseCoordinator) 59 | case .settings: 60 | let settingsSceneDIContainer = appDIContainer.makeSettingsSceneDIContainer() 61 | let settingsCoordinator = settingsSceneDIContainer.makeSettingsCoordinator(navigationController: navController, 62 | settingsSceneDIContainer: settingsSceneDIContainer) 63 | settingsCoordinator.start() 64 | childCoordinators.append(settingsCoordinator) 65 | } 66 | 67 | return navController 68 | } 69 | } 70 | 71 | extension AppCoordinator: CoordinatorFinishDelegate { 72 | func coordinatorDidFinish(childCoordinator: Coordinator) { 73 | 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Application/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/11. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Application/Container/Repository/RepositoryKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepositoryKey.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/12/12. 6 | // 7 | 8 | import Foundation 9 | import DailyContainer 10 | 11 | struct QuestRepositoryKey: InjectionKey { 12 | typealias Value = QuestsRepository 13 | } 14 | 15 | struct AuthRepositoryKey: InjectionKey { 16 | typealias Value = AuthRepository 17 | } 18 | 19 | struct BrowseRepositoryKey: InjectionKey { 20 | typealias Value = BrowseRepository 21 | } 22 | 23 | struct UserRepositoryKey: InjectionKey { 24 | typealias Value = UserRepository 25 | } 26 | 27 | struct ProtectedUserRepositoryKey: InjectionKey { 28 | typealias Value = ProtectedUserRepository 29 | } 30 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Application/Container/Service/ServiceKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceKey.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/12/12. 6 | // 7 | 8 | import Foundation 9 | import DailyContainer 10 | 11 | struct ServiceKey: InjectionKey { 12 | typealias Value = NetworkService 13 | } 14 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Application/Container/Storage/StroageKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StroageKey.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/12/12. 6 | // 7 | 8 | import Foundation 9 | import DailyContainer 10 | 11 | struct QuestStorageKey: InjectionKey { 12 | typealias Value = QuestsStorage 13 | } 14 | 15 | struct BrowseQuestStorageKey: InjectionKey { 16 | typealias Value = BrowseQuestsStorage 17 | } 18 | 19 | struct UserInfoStorageKey: InjectionKey { 20 | typealias Value = UserInfoStorage 21 | } 22 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Application/Container/UseCase/UseCaseKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UseCaseKey.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/12/12. 6 | // 7 | 8 | import Foundation 9 | import DailyContainer 10 | 11 | // MARK: - Home Scene 12 | struct QuestUseCaseKey: InjectionKey { 13 | typealias Value = QuestUseCase 14 | } 15 | 16 | struct EnrollUseCaseKey: InjectionKey { 17 | typealias Value = EnrollUseCase 18 | } 19 | 20 | struct UserUseCaseKey: InjectionKey { 21 | typealias Value = UserUseCase 22 | } 23 | 24 | struct CalendarUseCaseKey: InjectionKey { 25 | typealias Value = CalendarUseCase 26 | } 27 | 28 | // MARK: - Browse Scene 29 | 30 | // MARK: - Settings Scene 31 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Application/DIContainer/AppDIContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDIContainer.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/16. 6 | // 7 | 8 | import Foundation 9 | import DailyContainer 10 | 11 | final class AppDIContainer { 12 | 13 | init() { 14 | registerService() 15 | registerStorage() 16 | registerRepository() 17 | } 18 | 19 | func makeHomeSceneDIContainer() -> HomeSceneDIContainer { 20 | return HomeSceneDIContainer() 21 | } 22 | 23 | func makeBrowseSceneDIContainer() -> BrowseSceneDIContainer { 24 | return BrowseSceneDIContainer() 25 | } 26 | 27 | func makeSettingsSceneDIContainer() -> SettingsSceneDIContainer { 28 | return SettingsSceneDIContainer() 29 | } 30 | } 31 | 32 | private extension AppDIContainer { 33 | func registerService() { 34 | Container.shared.register { 35 | Module(ServiceKey.self) { FirebaseService.shared } 36 | } 37 | } 38 | 39 | func registerStorage() { 40 | Container.shared.register { 41 | Module(QuestStorageKey.self) { RealmQuestsStorage() } 42 | Module(BrowseQuestStorageKey.self) { RealmBrowseQuestsStorage() } 43 | Module(UserInfoStorageKey.self) { RealmUserInfoStorage() } 44 | } 45 | } 46 | 47 | func registerRepository() { 48 | Container.shared.register { 49 | Module(QuestRepositoryKey.self) { 50 | @Injected(QuestStorageKey.self) 51 | var questStorage: QuestsStorage 52 | return DefaultQuestsRepository(persistentStorage: questStorage) 53 | } 54 | 55 | Module(AuthRepositoryKey.self) { 56 | @Injected(QuestStorageKey.self) 57 | var questStorage: QuestsStorage 58 | 59 | @Injected(UserInfoStorageKey.self) 60 | var userInfoStorage: UserInfoStorage 61 | 62 | return DefaultAuthRepository(persistentQuestsStorage: questStorage, 63 | persistentUserStorage: userInfoStorage) 64 | } 65 | 66 | Module(BrowseRepositoryKey.self) { 67 | @Injected(BrowseQuestStorageKey.self) 68 | var browseQuestStroage: BrowseQuestsStorage 69 | 70 | @Injected(ServiceKey.self) 71 | var networkService: NetworkService 72 | 73 | return DefaultBrowseRepository(persistentStorage: browseQuestStroage, 74 | networkService: networkService) 75 | } 76 | 77 | /** 78 | Networ service instance needed. 79 | */ 80 | Module(UserRepositoryKey.self) { 81 | @Injected(UserInfoStorageKey.self) 82 | var userInfoStorage: UserInfoStorage 83 | 84 | return DefaultUserRepository(persistentStorage: userInfoStorage) 85 | } 86 | 87 | /** 88 | Protected User Repository Injection 89 | goes here. 90 | */ 91 | } 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Application/DIContainer/BrowseSceneDIContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowseSceneDIContainer.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | final class BrowseSceneDIContainer { 11 | 12 | lazy var browseQuestsStorage: BrowseQuestsStorage = RealmBrowseQuestsStorage() 13 | lazy var questsStorage: QuestsStorage = RealmQuestsStorage() 14 | 15 | // MARK: - Repositories 16 | func makeBrowseRepository() -> BrowseRepository { 17 | return DefaultBrowseRepository(persistentStorage: browseQuestsStorage) 18 | } 19 | 20 | func makeQuestsRepository() -> QuestsRepository { 21 | return DefaultQuestsRepository(persistentStorage: questsStorage) 22 | } 23 | 24 | // MARK: - Use Cases 25 | func makeBrowseUseCase() -> BrowseUseCase { 26 | return DefaultBrowseUseCase(browseRepository: makeBrowseRepository()) 27 | } 28 | 29 | func makeFriendQuestUseCase() -> FriendQuestUseCase { 30 | return DefaultFriendUseCase(questsRepository: makeQuestsRepository()) 31 | } 32 | 33 | func makeFriendCalendarUseCase(with user: User) -> CalendarUseCase { 34 | return DefaultFriendCalendarUseCase(user: user, questsRepository: makeQuestsRepository()) 35 | } 36 | 37 | // MARK: - View Models 38 | func makeBrowseViewModel() -> BrowseViewModel { 39 | return BrowseViewModel(browseUseCase: makeBrowseUseCase()) 40 | } 41 | 42 | func makeFriendViewModel(with user: User) -> FriendViewModel { 43 | return FriendViewModel(user: user, 44 | friendQuestUseCase: makeFriendQuestUseCase(), 45 | friendCalendarUseCase: makeFriendCalendarUseCase(with: user)) 46 | } 47 | 48 | // MARK: - View Controller 49 | func makeBrowseViewController() -> BrowseViewController { 50 | return BrowseViewController.create(with: makeBrowseViewModel()) 51 | } 52 | 53 | func makeFriendViewController(with user: User) -> FriendViewController { 54 | return FriendViewController.create(with: makeFriendViewModel(with: user)) 55 | } 56 | 57 | // MARK: - Flow 58 | func makeBrowseCoordinator(navigationController: UINavigationController, 59 | browseSceneDIContainer: BrowseSceneDIContainer) -> BrowseCoordinator { 60 | return DefaultBrowseCoordinator(navigationController: navigationController, 61 | browseSceneDIContainer: browseSceneDIContainer) 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Application/DIContainer/HomeSceneDIContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeSceneDIContainer.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/16. 6 | // 7 | 8 | import UIKit 9 | import DailyContainer 10 | 11 | final class HomeSceneDIContainer { 12 | 13 | init() { 14 | registerUseCase() 15 | } 16 | 17 | // MARK: - View Models 18 | func makeHomeViewModel() -> HomeViewModel { 19 | @Injected(QuestUseCaseKey.self) 20 | var questUseCase: QuestUseCase 21 | 22 | @Injected(UserUseCaseKey.self) 23 | var userUseCase: UserUseCase 24 | 25 | @Injected(CalendarUseCaseKey.self) 26 | var calendarUseCase: CalendarUseCase 27 | 28 | return HomeViewModel(userUseCase: userUseCase, 29 | questUseCase: questUseCase, 30 | calendarUseCase: calendarUseCase) 31 | } 32 | 33 | func makeEnrollViewModel() -> EnrollViewModel { 34 | @Injected(EnrollUseCaseKey.self) 35 | var enrollUseCase: EnrollUseCase 36 | 37 | return EnrollViewModel(enrollUseCase: enrollUseCase) 38 | } 39 | 40 | func makeProfileViewModel() -> ProfileViewModel { 41 | @Injected(UserUseCaseKey.self) 42 | var userUseCase: UserUseCase 43 | 44 | return ProfileViewModel(userUseCase: userUseCase) 45 | } 46 | 47 | // MARK: - View Controller 48 | func makeHomeViewController() -> HomeViewController { 49 | return HomeViewController.create(with: makeHomeViewModel()) 50 | } 51 | 52 | func makeEnrollViewController() -> EnrollViewController { 53 | return EnrollViewController.create(with: makeEnrollViewModel()) 54 | } 55 | 56 | func makeProfileViewController() -> ProfileViewController { 57 | return ProfileViewController.create(with: makeProfileViewModel()) 58 | } 59 | 60 | // MARK: - Flow 61 | func makeHomeCoordinator(navigationController: UINavigationController, 62 | homeSceneDIContainer: HomeSceneDIContainer) -> HomeCoordinator { 63 | return DefaultHomeCoordinator(navigationController: navigationController, 64 | homeSceneDIContainer: homeSceneDIContainer) 65 | } 66 | } 67 | 68 | private extension HomeSceneDIContainer { 69 | func registerUseCase() { 70 | Container.shared.register { 71 | Module(QuestUseCaseKey.self) { 72 | @Injected(QuestRepositoryKey.self) 73 | var questRepository: QuestsRepository 74 | 75 | return DefaultQuestUseCase(questsRepository: questRepository) 76 | } 77 | 78 | Module(EnrollUseCaseKey.self) { 79 | @Injected(QuestRepositoryKey.self) 80 | var questRepository: QuestsRepository 81 | 82 | return DefaultEnrollUseCase(questsRepository: questRepository) 83 | } 84 | 85 | Module(UserUseCaseKey.self) { 86 | @Injected(UserRepositoryKey.self) 87 | var userRepository: UserRepository 88 | 89 | return DefaultUserUseCase(userRepository: userRepository) 90 | } 91 | 92 | Module(CalendarUseCaseKey.self) { 93 | @Injected(QuestRepositoryKey.self) 94 | var questRepository: QuestsRepository 95 | 96 | return HomeCalendarUseCase(questsRepository: questRepository) 97 | } 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Application/DIContainer/SettingsSceneDIContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsSceneDIContainer.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | final class SettingsSceneDIContainer { 11 | 12 | lazy var userInfoStorage: UserInfoStorage = RealmUserInfoStorage() 13 | lazy var questsStorage: QuestsStorage = RealmQuestsStorage() 14 | 15 | // MARK: - Repositories 16 | func makeAuthRepository() -> AuthRepository { 17 | return DefaultAuthRepository(persistentQuestsStorage: questsStorage, 18 | persistentUserStorage: userInfoStorage) 19 | } 20 | 21 | func makeUserRepository() -> UserRepository { 22 | return DefaultUserRepository(persistentStorage: userInfoStorage) 23 | } 24 | 25 | // MARK: - Use Cases 26 | func makeAuthUseCase() -> AuthUseCase { 27 | return DefaultAuthUseCase(authRepository: makeAuthRepository()) 28 | } 29 | 30 | func makeSettingsUseCase() -> SettingsUseCase { 31 | return DefaultSettingsUseCase(userRepository: makeUserRepository(), 32 | authRepository: makeAuthRepository()) 33 | } 34 | 35 | // MARK: - View Models 36 | func makeLoginViewModel() -> LoginViewModel { 37 | return LoginViewModel(authUseCase: makeAuthUseCase()) 38 | } 39 | 40 | func makeSignUpViewModel() -> SignUpViewModel { 41 | return SignUpViewModel(authUseCase: makeAuthUseCase()) 42 | } 43 | 44 | func makeSettingsViewModel() -> SettingsViewModel { 45 | return SettingsViewModel(settingsUseCase: makeSettingsUseCase()) 46 | } 47 | 48 | // MARK: - View Controller 49 | func makeLoginViewController() -> LoginViewController { 50 | return LoginViewController.create(with: makeLoginViewModel()) 51 | } 52 | 53 | func makeSignUpViewController() -> SignUpViewController { 54 | return SignUpViewController.create(with: makeSignUpViewModel()) 55 | } 56 | 57 | func makeSettingsViewController() -> SettingsViewController { 58 | return SettingsViewController.create(with: makeSettingsViewModel()) 59 | } 60 | 61 | // MARK: - Flow 62 | func makeSettingsCoordinator(navigationController: UINavigationController, 63 | settingsSceneDIContainer: SettingsSceneDIContainer) -> SettingsCoordinator { 64 | return DefaultSettingsCoordinator(navigationController: navigationController, 65 | settingsSceneDIContainer: settingsSceneDIContainer) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Application/Protocols/Coordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinator.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol Coordinator: AnyObject { 11 | var finishDelegate: CoordinatorFinishDelegate? { get set } 12 | var childCoordinators: [Coordinator] { get set } 13 | 14 | func start() 15 | func finish() 16 | } 17 | 18 | extension Coordinator { 19 | func finish() { 20 | childCoordinators.removeAll() 21 | finishDelegate?.coordinatorDidFinish(childCoordinator: self) 22 | } 23 | } 24 | 25 | protocol CoordinatorFinishDelegate: AnyObject { 26 | func coordinatorDidFinish(childCoordinator: Coordinator) 27 | } 28 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Application/Protocols/TabCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabCoordinator.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol TabCoordinator: Coordinator { 11 | var tabBarController: UITabBarController { get set } 12 | } 13 | 14 | enum TabBarPage { 15 | case home 16 | case browse 17 | case settings 18 | 19 | init?(index: Int) { 20 | switch index { 21 | case 0: 22 | self = .home 23 | case 1: 24 | self = .browse 25 | case 2: 26 | self = .settings 27 | default: 28 | return nil 29 | } 30 | } 31 | 32 | var pageTitleValue: String { 33 | switch self { 34 | case .home: 35 | return "홈" 36 | case .browse: 37 | return "둘러보기" 38 | case .settings: 39 | return "더보기" 40 | } 41 | } 42 | 43 | var pageOrderNumber: Int { 44 | switch self { 45 | case .home: 46 | return 0 47 | case .browse: 48 | return 1 49 | case .settings: 50 | return 2 51 | } 52 | } 53 | 54 | var pageIcon: UIImage? { 55 | switch self { 56 | case .home: 57 | return UIImage(systemName: "house") 58 | case .browse: 59 | return UIImage(systemName: "leaf.fill") 60 | case .settings: 61 | return UIImage(systemName: "ellipsis") 62 | } 63 | } 64 | } 65 | 66 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Application/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/11. 6 | // 7 | 8 | import UIKit 9 | import RxSwift 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | var appCoordinator: AppCoordinator? 15 | let appDIContainer = AppDIContainer() 16 | 17 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 18 | 19 | guard let windowScene = (scene as? UIWindowScene) else { return } 20 | window = UIWindow(windowScene: windowScene) 21 | let viewController = LaunchViewController() 22 | let navigation = UINavigationController(rootViewController: viewController) 23 | self.window?.rootViewController = navigation 24 | self.window?.makeKeyAndVisible() 25 | 26 | return 27 | } 28 | 29 | func sceneDidDisconnect(_ scene: UIScene) { 30 | // Called as the scene is being released by the system. 31 | // This occurs shortly after the scene enters the background, or when its session is discarded. 32 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 33 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 34 | } 35 | 36 | func sceneDidBecomeActive(_ scene: UIScene) { 37 | // Called when the scene has moved from an inactive state to an active state. 38 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 39 | } 40 | 41 | func sceneWillResignActive(_ scene: UIScene) { 42 | // Called when the scene will move from an active state to an inactive state. 43 | // This may occur due to temporary interruptions (ex. an incoming phone call). 44 | } 45 | 46 | func sceneWillEnterForeground(_ scene: UIScene) { 47 | // Called as the scene transitions from the background to the foreground. 48 | // Use this method to undo the changes made on entering the background. 49 | } 50 | 51 | func sceneDidEnterBackground(_ scene: UIScene) { 52 | // Called as the scene transitions from the foreground to the background. 53 | // Use this method to save data, release shared resources, and store enough scene-specific state information 54 | // to restore the scene back to its current state. 55 | 56 | let syncManager = SyncManager() 57 | syncManager.sync() 58 | } 59 | 60 | func switchRoot() { 61 | AppAppearance.setupAppearance() 62 | let tabbarController = UITabBarController() 63 | self.window?.rootViewController = tabbarController 64 | self.appCoordinator = AppCoordinator(tabBarController: tabbarController, 65 | appDIContainer: self.appDIContainer) 66 | self.appCoordinator?.start() 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Application/SyncManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyncManager.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/12/09. 6 | // 7 | 8 | import UIKit 9 | import RxSwift 10 | 11 | final class SyncManager { 12 | private let app = UIApplication.shared 13 | private let persistentStorage: RealmStorage = RealmStorage.shared 14 | private let networkStorage: NetworkService = FirebaseService.shared 15 | 16 | private let disposeBag = DisposeBag() 17 | 18 | func sync(){ 19 | guard FirebaseService.shared.uid.value != nil else {return } 20 | let taskId = app.beginBackgroundTask() 21 | self.qusetsSync() 22 | .timeout(.seconds(20), scheduler: MainScheduler.instance) 23 | .subscribe(onSuccess:{ res in 24 | print("✅",res) 25 | self.app.endBackgroundTask(taskId) 26 | },onFailure: { error in 27 | print("❌",error) 28 | self.app.endBackgroundTask(taskId) 29 | }, onDisposed: { 30 | print("disposed") 31 | }) 32 | .disposed(by: disposeBag) 33 | } 34 | 35 | private func qusetsSync() -> Single { 36 | self.fetchQuests() 37 | .flatMap { [weak self] persistentStorageQuest in 38 | guard let self = self else { return Single<[Quest]>.just([]) } 39 | return self.networkStorage.read(type: QuestDTO.self, userCase: .currentUser, access: .quests, filter: nil) 40 | .map { $0.toDomain() } 41 | .toArray() 42 | .map { networkServiceQuests in 43 | let networkServiceQuestsDict = Dictionary(uniqueKeysWithValues: networkServiceQuests.map { ($0.uuid, $0) }) 44 | let quests = persistentStorageQuest.filter { 45 | guard let dictQuest = networkServiceQuestsDict[$0.uuid] else { return true } 46 | return dictQuest != $0 47 | } 48 | return quests 49 | } 50 | } 51 | .flatMap{ [weak self] syncQuests in 52 | guard let self = self else { return Single.just(false)} 53 | return Observable.from(syncQuests) 54 | .flatMap { quest in 55 | self.networkStorage.create(userCase: .currentUser, access: .quests, dto: quest.toDTO()) 56 | } 57 | .map{$0.toDomain()} 58 | .toArray() 59 | .map{ _ in true} 60 | } 61 | } 62 | 63 | private func fetchQuests() -> Single<[Quest]> { 64 | return Single.create { [weak self] single in 65 | guard let persistentStorage = self?.persistentStorage else { 66 | single(.failure(RealmStorageError.noDataError)) 67 | return Disposables.create() 68 | } 69 | 70 | do { 71 | let quests = try persistentStorage 72 | .readEntities(type: QuestEntity.self, filter: nil) 73 | .compactMap { $0.toDomain() } 74 | single(.success(quests)) 75 | } catch let error { 76 | single(.failure(error)) 77 | } 78 | 79 | return Disposables.create() 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/DataConfigure.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataConfigure.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/12/05. 6 | // 7 | 8 | import Foundation 9 | 10 | enum DateFilter { 11 | case today(_ date: Date) 12 | case month(_ date: Date) 13 | case year(_date: Date) 14 | } 15 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/Network/DataMapping/BrowseQuestDTO+Mapping.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowseQuestDTO.swift 3 | // DailyQuest 4 | // 5 | // Created by 이다연 on 2022/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | struct BrowseQuestDTO { 11 | let user: UserDTO 12 | let quests: [QuestDTO] 13 | 14 | init() { 15 | self.user = UserDTO() 16 | self.quests = [QuestDTO()] 17 | } 18 | } 19 | 20 | extension BrowseQuestDTO { 21 | func toDomain() -> BrowseQuest { 22 | return BrowseQuest(user: user.toDomain(), quests: quests.compactMap { $0.toDomain() }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/Network/DataMapping/DTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DTO.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol DTO: Codable { 11 | var uuid: String { get } 12 | } 13 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/Network/DataMapping/QuestDTO+Mapping.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuestDTO+Mapping.swift 3 | // DailyQuest 4 | // 5 | // Created by 이다연 on 2022/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | struct QuestDTO: DTO { 11 | let groupId: UUID 12 | let uuid: String 13 | let date: String 14 | let title: String 15 | let currentCount: Int 16 | let totalCount: Int 17 | 18 | init() { 19 | self.groupId = UUID() 20 | self.uuid = "" 21 | self.date = "" 22 | self.title = "" 23 | self.currentCount = 0 24 | self.totalCount = 0 25 | } 26 | 27 | init(groupId: UUID, uuid: String, date: String, title: String, currentCount: Int, totalCount: Int) { 28 | self.groupId = groupId 29 | self.uuid = uuid 30 | self.date = date 31 | self.title = title 32 | self.currentCount = currentCount 33 | self.totalCount = totalCount 34 | } 35 | } 36 | 37 | extension QuestDTO { 38 | func toDomain() -> Quest { 39 | return Quest(groupId: groupId, 40 | uuid: UUID(uuidString: uuid)!, 41 | date: date.toDate()!, 42 | title: title, 43 | currentCount: currentCount, 44 | totalCount: totalCount) 45 | } 46 | } 47 | 48 | extension Quest { 49 | func toDTO() -> QuestDTO { 50 | return QuestDTO(groupId: groupId, 51 | uuid: uuid.uuidString, 52 | date: date.toString, 53 | title: title, 54 | currentCount: currentCount, 55 | totalCount: totalCount) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/Network/DataMapping/UserDTO+Mapping.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDTO+Mapping.swift 3 | // DailyQuest 4 | // 5 | // Created by 이다연 on 2022/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | struct UserDTO: DTO { 11 | let uuid: String 12 | let nickName: String 13 | let profileURL: String 14 | let backgroundImageURL: String 15 | let introduce: String 16 | let allow: Bool 17 | 18 | init() { 19 | self.uuid = "" 20 | self.nickName = "" 21 | self.profileURL = "" 22 | self.backgroundImageURL = "" 23 | self.introduce = "" 24 | self.allow = false 25 | } 26 | 27 | init(user: User) { 28 | self.uuid = user.uuid 29 | self.nickName = user.nickName 30 | self.profileURL = user.profileURL 31 | self.backgroundImageURL = user.backgroundImageURL 32 | self.introduce = user.introduce 33 | self.allow = user.allow 34 | } 35 | 36 | init(uuid: String, userDto: UserDTO) { 37 | self.uuid = uuid 38 | self.nickName = userDto.nickName 39 | self.profileURL = userDto.profileURL 40 | self.backgroundImageURL = userDto.backgroundImageURL 41 | self.introduce = userDto.introduce 42 | self.allow = userDto.allow 43 | } 44 | } 45 | 46 | extension UserDTO { 47 | func toDomain() -> User { 48 | User(uuid: uuid, 49 | nickName: nickName, 50 | profileURL: profileURL, 51 | backgroundImageURL: backgroundImageURL, 52 | introduce: introduce, 53 | allow: allow) 54 | } 55 | } 56 | 57 | extension User { 58 | func toDTO() -> UserDTO { 59 | UserDTO(user: self) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/PersistentStorages/BrowseQuestsStorage/BrowseQuestsStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowseQuestsStorage.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/11/15. 6 | // 7 | 8 | import RxSwift 9 | 10 | protocol BrowseQuestsStorage { 11 | func fetchBrowseQuests() -> Single<[BrowseQuest]> 12 | func saveBrowseQuest(browseQuest: BrowseQuest) -> Single 13 | func deleteBrowseQuests() -> Single 14 | } 15 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/PersistentStorages/BrowseQuestsStorage/RealmStorage/EntityMapping/BrowseQuestEntity+Mapping.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowseQuestEntity+Mapping.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/11/15. 6 | // 7 | 8 | import Foundation 9 | 10 | extension BrowseQuestEntity { 11 | convenience init(browseQuest: BrowseQuest) { 12 | let questsEntities = browseQuest.quests.compactMap { SubQuestEntity(quest: $0) } 13 | 14 | self.init(uuid: browseQuest.user.uuid, nickName: browseQuest.user.nickName, profileImageURL: browseQuest.user.profileURL, quests: questsEntities) 15 | } 16 | } 17 | 18 | extension BrowseQuestEntity { 19 | func toDomain() -> BrowseQuest { 20 | let quests = Array(quests).compactMap { $0.toDomain() } 21 | return BrowseQuest(user: User(uuid: uuid, 22 | nickName: nickName, 23 | profileURL: profileImageURL, 24 | backgroundImageURL: "", 25 | introduce: "", 26 | allow: false), 27 | quests: quests) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/PersistentStorages/BrowseQuestsStorage/RealmStorage/EntityMapping/SubQuestEntity+Mapping.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SubQuestEntity+Mapping.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/11/30. 6 | // 7 | 8 | import Foundation 9 | 10 | extension SubQuestEntity { 11 | convenience init(quest: Quest) { 12 | self.init( 13 | groupId: quest.groupId, 14 | uuid: quest.uuid, 15 | date: quest.date.toString, 16 | title: quest.title, 17 | currentCount: quest.currentCount, 18 | totalCount: quest.totalCount) 19 | } 20 | } 21 | 22 | extension SubQuestEntity { 23 | func toDomain() -> Quest { 24 | return Quest(groupId: groupId, 25 | uuid: uuid, 26 | date: date.toDate() ?? Date(), 27 | title: title, 28 | currentCount: currentCount, 29 | totalCount: totalCount) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/PersistentStorages/BrowseQuestsStorage/RealmStorage/RealmBrowseQuestsStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealmBrowseQuestsStorage.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/11/15. 6 | // 7 | 8 | import RxSwift 9 | 10 | final class RealmBrowseQuestsStorage { 11 | 12 | private let realmStorage: RealmStorage 13 | 14 | init(realmStorage: RealmStorage = RealmStorage.shared) { 15 | self.realmStorage = realmStorage 16 | } 17 | } 18 | 19 | extension RealmBrowseQuestsStorage: BrowseQuestsStorage { 20 | func fetchBrowseQuests() -> Single<[BrowseQuest]> { 21 | return Single<[BrowseQuest]>.create { [weak self] single in 22 | guard let realmStorage = self?.realmStorage else { 23 | return Disposables.create() 24 | } 25 | 26 | do { 27 | let browseQuests = try realmStorage.readEntities(type: BrowseQuestEntity.self) 28 | .compactMap { $0.toDomain() } 29 | single(.success(browseQuests)) 30 | } catch let error { 31 | single(.failure(error)) 32 | } 33 | 34 | return Disposables.create() 35 | } 36 | } 37 | 38 | func saveBrowseQuest(browseQuest: BrowseQuest) -> Single { 39 | return Single.create { [weak self] single in 40 | guard let realmStorage = self?.realmStorage else { 41 | return Disposables.create() 42 | } 43 | 44 | let browseQuestEntity = BrowseQuestEntity(browseQuest: browseQuest) 45 | 46 | do { 47 | try realmStorage.createEntity(entity: browseQuestEntity) 48 | single(.success(browseQuest)) 49 | } catch let error { 50 | single(.failure(RealmStorageError.saveError(error))) 51 | } 52 | 53 | return Disposables.create() 54 | } 55 | } 56 | 57 | func deleteBrowseQuests() -> Single { 58 | return Single.create { [weak self] single in 59 | guard let realmStorage = self?.realmStorage else { 60 | return Disposables.create() 61 | } 62 | 63 | do { 64 | try realmStorage.deleteAllEntity(type: BrowseQuestEntity.self) 65 | try realmStorage.deleteAllEntity(type: SubQuestEntity.self) 66 | single(.success(true)) 67 | } catch let error { 68 | single(.failure(RealmStorageError.deleteError(error))) 69 | } 70 | 71 | return Disposables.create() 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/PersistentStorages/QuestsStorage/QuestsStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuestsStorage.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/11/14. 6 | // 7 | 8 | import Foundation 9 | import RxSwift 10 | 11 | protocol QuestsStorage { 12 | func saveQuests(with quests: [Quest]) -> Single<[Quest]> 13 | func fetchQuests(by date: Date) -> Single<[Quest]> 14 | func updateQuest(with quest: Quest) -> Single 15 | func deleteQuest(with questId: UUID) -> Single 16 | func deleteQuestGroup(with groupId: UUID) -> Single<[Quest]> 17 | func deleteAllQuests() -> Single<[Quest]> 18 | func fetchQuests() -> Single<[Quest]> 19 | } 20 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/PersistentStorages/QuestsStorage/RealmStorage/EntityMapping/QuestEntity+Mapping.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuestEntity+Mapping.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/11/14. 6 | // 7 | 8 | import Foundation 9 | 10 | extension QuestEntity { 11 | convenience init(quest: Quest) { // Quest에 date 들어가면 수정 12 | self.init( 13 | groupId: quest.groupId, 14 | uuid: quest.uuid, 15 | date: quest.date.toString, 16 | title: quest.title, 17 | currentCount: quest.currentCount, 18 | totalCount: quest.totalCount) 19 | } 20 | } 21 | 22 | extension QuestEntity { 23 | func toDomain() -> Quest { 24 | return Quest(groupId: groupId, 25 | uuid: uuid, 26 | date: date.toDate() ?? Date(), 27 | title: title, 28 | currentCount: currentCount, 29 | totalCount: totalCount) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/PersistentStorages/RealmStorage/Entities/BrowseQuestEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowseQuestEntity.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/11/15. 6 | // 7 | 8 | import Foundation 9 | import RealmSwift 10 | 11 | final class BrowseQuestEntity: Object { 12 | @Persisted var uuid: String 13 | @Persisted var nickName: String 14 | @Persisted var profileImageURL: String 15 | @Persisted var quests: List 16 | 17 | override init() { } 18 | 19 | init(uuid: String, nickName: String, profileImageURL: String, quests: [SubQuestEntity]) { 20 | self.uuid = uuid 21 | self.nickName = nickName 22 | self.profileImageURL = profileImageURL 23 | let realmList = List() 24 | realmList.append(objectsIn: quests) 25 | self.quests = realmList 26 | } 27 | 28 | override class func primaryKey() -> String? { 29 | "uuid" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/PersistentStorages/RealmStorage/Entities/QuestEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserQuestEntity.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/11/14. 6 | // 7 | 8 | import Foundation 9 | import RealmSwift 10 | 11 | final class QuestEntity: Object { 12 | @Persisted var groupId: UUID 13 | @Persisted var uuid: UUID 14 | @Persisted var date: String 15 | @Persisted var title: String 16 | @Persisted var currentCount: Int 17 | @Persisted var totalCount: Int 18 | 19 | override init() { } 20 | 21 | init(groupId: UUID, uuid: UUID, date: String, title: String, currentCount: Int, totalCount: Int) { 22 | self.groupId = groupId 23 | self.uuid = uuid 24 | self.date = date 25 | self.title = title 26 | self.currentCount = currentCount 27 | self.totalCount = totalCount 28 | } 29 | 30 | override class func primaryKey() -> String? { 31 | "uuid" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/PersistentStorages/RealmStorage/Entities/SubQuestEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SubQuestEntity.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/11/30. 6 | // 7 | 8 | import Foundation 9 | import RealmSwift 10 | 11 | final class SubQuestEntity: Object { 12 | @Persisted var groupId: UUID 13 | @Persisted var uuid: UUID 14 | @Persisted var date: String 15 | @Persisted var title: String 16 | @Persisted var currentCount: Int 17 | @Persisted var totalCount: Int 18 | 19 | override init() { } 20 | 21 | init(groupId: UUID, uuid: UUID, date: String, title: String, currentCount: Int, totalCount: Int) { 22 | self.groupId = groupId 23 | self.uuid = uuid 24 | self.date = date 25 | self.title = title 26 | self.currentCount = currentCount 27 | self.totalCount = totalCount 28 | } 29 | 30 | override class func primaryKey() -> String? { 31 | "uuid" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/PersistentStorages/RealmStorage/Entities/UserInfoEntity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserInfoEntity.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/11/15. 6 | // 7 | 8 | import Foundation 9 | import RealmSwift 10 | 11 | final class UserInfoEntity: Object { 12 | @Persisted var uuid: String 13 | @Persisted var nickName: String 14 | @Persisted var profileURL: String 15 | @Persisted var backgroundImageURL: String 16 | @Persisted var introduce: String 17 | @Persisted var allow: Bool 18 | 19 | override init() { } 20 | 21 | init(uuid: String, nickName: String, profileURL: String, backgroundImageURL: String, introduce: String, allow: Bool) { 22 | self.uuid = uuid 23 | self.nickName = nickName 24 | self.profileURL = profileURL 25 | self.backgroundImageURL = backgroundImageURL 26 | self.introduce = introduce 27 | self.allow = allow 28 | } 29 | 30 | override class func primaryKey() -> String? { 31 | "uuid" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/PersistentStorages/RealmStorage/RealmStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealmStorage.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/11/14. 6 | // 7 | 8 | import RealmSwift 9 | import Foundation 10 | 11 | enum RealmStorageError: Error { 12 | case realmObjectError 13 | case noDataError 14 | case saveError(Error) 15 | case readError(Error) 16 | case updateError(Error) 17 | case deleteError(Error) 18 | } 19 | 20 | final class RealmStorage { 21 | static let shared = RealmStorage() 22 | private let persistentContainer = try? Realm() 23 | 24 | private init() { 25 | // Realm file path 26 | #if DEBUG 27 | print(Realm.Configuration.defaultConfiguration.fileURL!) 28 | #endif 29 | } 30 | } 31 | 32 | extension RealmStorage { 33 | @discardableResult 34 | func createEntity(entity: O) throws -> O { 35 | guard let persistentContainer = persistentContainer else { 36 | throw RealmStorageError.realmObjectError 37 | } 38 | try persistentContainer.write { 39 | persistentContainer.add(entity, update: .modified) 40 | } 41 | return entity 42 | } 43 | 44 | func readEntities(type: O.Type, filter: NSPredicate? = nil) throws -> [O] { 45 | guard let persistentContainer = persistentContainer else { 46 | throw RealmStorageError.realmObjectError 47 | } 48 | if let filter = filter { 49 | return Array(persistentContainer.objects(type).filter(filter)) 50 | } else { 51 | return Array(persistentContainer.objects(type)) 52 | } 53 | } 54 | 55 | @discardableResult 56 | func updateEntity(entity: O) throws -> O { 57 | guard let persistentContainer = persistentContainer else { 58 | throw RealmStorageError.realmObjectError 59 | } 60 | try persistentContainer.write { 61 | persistentContainer.add(entity, update: .modified) 62 | } 63 | return entity 64 | } 65 | 66 | @discardableResult 67 | func deleteEntity(entity: O) throws -> O { 68 | guard let persistentContainer = persistentContainer else { 69 | throw RealmStorageError.realmObjectError 70 | } 71 | try persistentContainer.write { 72 | persistentContainer.delete(entity) 73 | } 74 | 75 | return entity 76 | } 77 | 78 | @discardableResult 79 | func deleteAllEntity(type: O.Type) throws -> [O] { 80 | guard let persistentContainer = persistentContainer else { 81 | throw RealmStorageError.realmObjectError 82 | } 83 | for entity in Array(persistentContainer.objects(type)) { 84 | try persistentContainer.write { 85 | persistentContainer.delete(entity) 86 | } 87 | } 88 | return Array(persistentContainer.objects(type)) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/PersistentStorages/UserInfoStorage/RealmStorage/EntityMapping/UserInfoEntity+Mapping.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserInfoEntity+Mapping.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/11/15. 6 | // 7 | 8 | import Foundation 9 | 10 | // DomainObject -> RealmObject 11 | extension UserInfoEntity { 12 | convenience init(user: User) { 13 | self.init(uuid: user.uuid, 14 | nickName: user.nickName, 15 | profileURL: user.profileURL, 16 | backgroundImageURL: user.backgroundImageURL, 17 | introduce: user.introduce, 18 | allow: user.allow) 19 | } 20 | } 21 | 22 | // RealmObject -> DomainObject 23 | extension UserInfoEntity { 24 | func toDomain() -> User { 25 | return User(uuid: uuid, 26 | nickName: nickName, 27 | profileURL: profileURL, 28 | backgroundImageURL: backgroundImageURL, 29 | introduce: introduce, 30 | allow: allow) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/PersistentStorages/UserInfoStorage/RealmStorage/RealmUserInfoStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealmUserInfoStorage.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/11/15. 6 | // 7 | 8 | import RxSwift 9 | 10 | final class RealmUserInfoStorage { 11 | 12 | private let realmStorage: RealmStorage 13 | 14 | init(realmStorage: RealmStorage = RealmStorage.shared) { 15 | self.realmStorage = realmStorage 16 | } 17 | } 18 | 19 | extension RealmUserInfoStorage: UserInfoStorage { 20 | func fetchUserInfo() -> Single { 21 | return Single.create { [weak self] single in 22 | do { 23 | guard let realmStorage = self?.realmStorage else { throw RealmStorageError.realmObjectError } 24 | guard let userInfoEntity = try realmStorage.readEntities(type: UserInfoEntity.self) 25 | .first else { 26 | throw RealmStorageError.noDataError 27 | } 28 | single(.success(userInfoEntity.toDomain())) 29 | } catch let error { 30 | single(.failure(error)) 31 | } 32 | return Disposables.create() 33 | } 34 | } 35 | 36 | func updateUserInfo(user: User) -> Single { 37 | return Single.create { [weak self] single in 38 | let userInfo = UserInfoEntity(user: user) 39 | do { 40 | guard let realmStorage = self?.realmStorage else { throw RealmStorageError.realmObjectError } 41 | // update 성공했을 경우, success(user) 42 | try realmStorage.updateEntity(entity: userInfo) 43 | single(.success(user)) 44 | } catch let error { 45 | // update 성공하지 못했을 경우, failure(error) 46 | single(.failure(RealmStorageError.saveError(error))) 47 | } 48 | 49 | return Disposables.create() 50 | } 51 | } 52 | 53 | func deleteUserInfo() -> Single { 54 | return Single.create { [weak self] single in 55 | do { 56 | guard let realmStorage = self?.realmStorage else { throw RealmStorageError.realmObjectError } 57 | guard let user = try realmStorage.deleteAllEntity(type: UserInfoEntity.self).first?.toDomain() else { 58 | throw RealmStorageError.noDataError 59 | } 60 | single(.success(user)) 61 | } catch let error { 62 | // update 성공하지 못했을 경우, failure(error) 63 | single(.failure(RealmStorageError.deleteError(error))) 64 | } 65 | return Disposables.create() 66 | } 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/PersistentStorages/UserInfoStorage/UserInfoStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserInfoStorage.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/11/15. 6 | // 7 | 8 | import RxSwift 9 | 10 | protocol UserInfoStorage { 11 | func fetchUserInfo() -> Single 12 | func updateUserInfo(user: User) -> Single 13 | func deleteUserInfo() -> Single 14 | } 15 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/Repositories/DefaultAuthRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultAuthRepository.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/11/28. 6 | // 7 | 8 | import RxSwift 9 | import Foundation 10 | 11 | final class DefaultAuthRepository { 12 | private let persistentQuestsStorage: QuestsStorage 13 | private let persistentUserStorage: UserInfoStorage 14 | private let networkService: NetworkService 15 | 16 | private let disposeBag = DisposeBag() 17 | 18 | init(persistentQuestsStorage: QuestsStorage, 19 | persistentUserStorage: UserInfoStorage, 20 | networkService: NetworkService = FirebaseService.shared) { 21 | self.persistentUserStorage = persistentUserStorage 22 | self.persistentQuestsStorage = persistentQuestsStorage 23 | self.networkService = networkService 24 | } 25 | } 26 | 27 | extension DefaultAuthRepository: AuthRepository { 28 | func signIn(email: String, password: String) -> Single { 29 | return self.networkService.signIn(email: email, password: password) 30 | .do(onSuccess: { [weak self] result in 31 | if let self = self, result { 32 | self.networkService.read(type: UserDTO.self, 33 | userCase: .currentUser, 34 | access: .userInfo, 35 | filter: nil) 36 | .map { $0.toDomain() } 37 | .flatMap(self.persistentUserStorage.updateUserInfo(user:)) 38 | .map { _ in true } 39 | .catchAndReturn(false) 40 | .flatMap { _ in 41 | self.networkService.read(type: QuestDTO.self, 42 | userCase: .currentUser, 43 | access: .quests, 44 | filter: nil) 45 | .map { $0.toDomain() } 46 | .toArray() 47 | .flatMap(self.persistentQuestsStorage.saveQuests(with:)) 48 | .do(afterSuccess: { quests in 49 | let dates = quests.map { $0.date } 50 | NotificationCenter.default.post(name: .userUpdated, object: dates) 51 | }) 52 | } 53 | .subscribe() 54 | .disposed(by: self.disposeBag) 55 | } 56 | }) 57 | } 58 | 59 | func signOut() -> Single { 60 | return self.networkService.signOut() 61 | .do(afterSuccess: { [weak self] result in 62 | if let self = self, result { 63 | self.persistentUserStorage.deleteUserInfo() 64 | .map { _ in true } 65 | .catchAndReturn(false) 66 | .flatMap { _ in 67 | self.persistentQuestsStorage.deleteAllQuests() 68 | } 69 | .do(afterSuccess: { quests in 70 | let dates = quests.map { $0.date } 71 | NotificationCenter.default.post(name: .userUpdated, object: dates) 72 | }) 73 | .subscribe() 74 | .disposed(by: self.disposeBag) 75 | } 76 | }) 77 | } 78 | 79 | func signUp(email: String, password: String, user: User) -> Single { 80 | return self.networkService.signUp(email: email, password: password, userDto: user.toDTO()) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/Repositories/DefaultBrowseRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultBrowseRepository.swift 3 | // DailyQuest 4 | // 5 | // Created by 이다연 on 2022/11/29. 6 | // 7 | 8 | import Foundation 9 | import RxSwift 10 | 11 | final class DefaultBrowseRepository { 12 | 13 | private let persistentStorage: BrowseQuestsStorage 14 | private let networkService: NetworkService 15 | private let disposeBag: DisposeBag = DisposeBag() 16 | 17 | init(persistentStorage: BrowseQuestsStorage, 18 | networkService: NetworkService = FirebaseService.shared) { 19 | self.persistentStorage = persistentStorage 20 | self.networkService = networkService 21 | } 22 | } 23 | 24 | extension DefaultBrowseRepository: BrowseRepository { 25 | 26 | /// Fetch BrowseQuests 27 | /// Firebase 우선, 실패시 persistentStorage, persistentStorage도 실패시 Error반환 28 | /// - Returns: Observable<[BrowseQuest]> 29 | func fetch() -> Single<[BrowseQuest]> { 30 | return networkService.getAllowUsers(limit: 10) 31 | .map { $0.toDomain() } 32 | .flatMap(fetchBrowseQuestNetworkService(user:)) 33 | .filter { !$0.quests.isEmpty } 34 | .toArray() 35 | .do(afterSuccess: { [weak self] browseQuests in 36 | guard let self = self else { return } 37 | self.persistentStorage.deleteBrowseQuests() 38 | .asObservable() 39 | .concatMap { _ in 40 | Observable.from(browseQuests) 41 | .flatMap (self.saveBrowseQuestPersistentStorage(browseQuest:)) 42 | } 43 | .subscribe() 44 | .disposed(by: self.disposeBag) 45 | }) 46 | .timeout(.seconds(5), scheduler: MainScheduler.instance) 47 | .catch { [weak self] _ in 48 | guard let self = self else { return Single.just([])} 49 | return self.persistentStorage.fetchBrowseQuests() 50 | } 51 | } 52 | } 53 | 54 | private extension DefaultBrowseRepository { 55 | func fetchBrowseQuestNetworkService(user: User) -> Single { 56 | networkService 57 | .read(type: QuestDTO.self, userCase: .anotherUser(user.uuid), access: .quests, filter: .today(Date())) 58 | .map { $0.toDomain() } 59 | .toArray() 60 | .map { BrowseQuest(user: user, quests: Array($0.prefix(3))) } 61 | } 62 | 63 | func saveBrowseQuestPersistentStorage(browseQuest: BrowseQuest) -> Observable { 64 | persistentStorage.saveBrowseQuest(browseQuest: browseQuest) 65 | .asObservable() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Data/Repositories/RepositoryManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RepositoryManager.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/12/05. 6 | // 7 | 8 | import RxSwift 9 | 10 | final class RepositoryManager { 11 | static let shared = RepositoryManager() 12 | private init() { } 13 | 14 | var disposeBag: DisposeBag = DisposeBag() 15 | } 16 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/Entities/BrowseQuest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowseQuest.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/11/15. 6 | // 7 | 8 | import Foundation 9 | 10 | // Domain-Entities 이동 11 | struct BrowseQuest { 12 | let user: User 13 | let quests: [Quest] 14 | } 15 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/Entities/DailyQuestCompletion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DailyQuestCompletion.swift 3 | // DailyQuest 4 | // 5 | // Created by wickedRun on 2022/12/06. 6 | // 7 | 8 | import Foundation 9 | 10 | struct DailyQuestCompletion: Hashable { 11 | 12 | enum State: Hashable { 13 | case hidden 14 | case normal 15 | case notDone(Int) 16 | case done 17 | } 18 | 19 | let day: Date 20 | let state: State 21 | let isSelected: Bool 22 | } 23 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/Entities/Quest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Quest.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/14. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Quest: Equatable { 11 | let groupId: UUID 12 | let uuid: UUID 13 | let date: Date 14 | let title: String 15 | var currentCount: Int 16 | let totalCount: Int 17 | 18 | var state: Bool { 19 | return currentCount == totalCount 20 | } 21 | 22 | /** 23 | 현재 목표달성량(currentCount)에 인자값만큼 더합니다. 24 | Note. 현재 목표달성량은 전체량(totalCount)를 넘을 수 없습니다. 25 | 26 | - Parameters: 27 | - value: 0보다 큰 정수값입니다. 기본값은 1입니다. 28 | */ 29 | // mutating func increaseCount(with value: Int=1) { 30 | // guard currentCount + value <= totalCount else { 31 | // self.currentCount = totalCount 32 | // return 33 | // } 34 | // self.currentCount += value 35 | // } 36 | func increaseCount(with value: Int=1) -> Self? { 37 | guard currentCount + value <= totalCount else { 38 | return nil 39 | } 40 | return .init(groupId: groupId, uuid: uuid, date: date, title: title, currentCount: currentCount+value, totalCount: totalCount) 41 | } 42 | 43 | /** 44 | 현재 목표달성량(currentCount)에 인자값만큼 뺍니다. 45 | Note. 현재 목표달성량은 0보다 작을 수 없습니다. 46 | 47 | - Parameters: 48 | - value: 0보다 큰 정수값입니다. 기본값은 1입니다. 49 | */ 50 | func decreaseCount(with value: Int=1) -> Self? { 51 | guard currentCount - value >= 0 else { 52 | return nil 53 | } 54 | return .init(groupId: groupId, uuid: uuid, date: date, title: title, currentCount: currentCount-value, totalCount: totalCount) 55 | } 56 | 57 | static func == (lhs: Self, rhs: Self) -> Bool { 58 | lhs.groupId == rhs.groupId && 59 | lhs.uuid == rhs.uuid && 60 | lhs.date == rhs.date && 61 | lhs.title == rhs.title && 62 | lhs.currentCount == rhs.currentCount && 63 | lhs.totalCount == rhs.totalCount 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/Entities/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/14. 6 | // 7 | 8 | import Foundation 9 | 10 | struct User { 11 | let uuid: String 12 | let nickName: String 13 | let profileURL: String 14 | let backgroundImageURL: String 15 | let introduce: String 16 | let allow: Bool 17 | 18 | init() { 19 | self.uuid = "" 20 | self.nickName = "" 21 | self.profileURL = "" 22 | self.backgroundImageURL = "" 23 | self.introduce = "" 24 | self.allow = false 25 | } 26 | 27 | init(nickName: String) { 28 | self.uuid = "" 29 | self.nickName = nickName 30 | self.profileURL = "" 31 | self.backgroundImageURL = "" 32 | self.introduce = "" 33 | self.allow = true 34 | } 35 | 36 | init(uuid: String, nickName: String, profileURL: String, backgroundImageURL: String, introduce: String, allow: Bool) { 37 | self.uuid = uuid 38 | self.nickName = nickName 39 | self.profileURL = profileURL 40 | self.backgroundImageURL = backgroundImageURL 41 | self.introduce = introduce 42 | self.allow = allow 43 | } 44 | } 45 | 46 | extension User { 47 | func setAllow(allow: Bool) -> User { 48 | return User(uuid: self.uuid, 49 | nickName: self.nickName, 50 | profileURL: self.profileURL, 51 | backgroundImageURL: self.backgroundImageURL, 52 | introduce: self.introduce, 53 | allow: allow) 54 | } 55 | 56 | func setProfileImageURL(profileURL: String) -> User { 57 | return User(uuid: self.uuid, 58 | nickName: self.nickName, 59 | profileURL: profileURL, 60 | backgroundImageURL: self.backgroundImageURL, 61 | introduce: self.introduce, 62 | allow: self.allow) 63 | } 64 | 65 | func setBackgroundImageURL(backgroundImageURL: String) -> User { 66 | return User(uuid: self.uuid, 67 | nickName: self.nickName, 68 | profileURL: self.profileURL, 69 | backgroundImageURL: backgroundImageURL, 70 | introduce: self.introduce, 71 | allow: self.allow) 72 | } 73 | 74 | func setIntroduce(introduce: String) -> User { 75 | return User(uuid: self.uuid, 76 | nickName: self.nickName, 77 | profileURL: self.profileURL, 78 | backgroundImageURL: self.backgroundImageURL, 79 | introduce: introduce, 80 | allow: self.allow) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/Interfaces/Repositories/AuthRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthRepository.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/28. 6 | // 7 | 8 | import RxSwift 9 | 10 | protocol AuthRepository { 11 | /// 로그인 동작을 수행합니다. 12 | /// 13 | /// - Parameters: 14 | /// - email: 사용자의 email 입니다. 15 | /// - password: 사용자의 password입니다. 16 | /// - Returns: 성공하면 true를, 실패하면 error를 방출하는 Observable을 반환합니다. 17 | func signIn(email: String, password: String) -> Single 18 | 19 | /// 로그아웃 동작을 수행합니다. 20 | /// - Returns: 성공하면 true를, 실패하면 error을 방출하는 Observable을 반환합니다. 21 | func signOut() -> Single 22 | 23 | /// 회원가입 동작을 수행합니다. 24 | /// - Parameters: 25 | /// - email: 사용자의 email 입니다. 26 | /// - password: 사용자의 password입니다. 27 | /// - user: 사용자의 정보입니다. 28 | /// - Returns: 성공하면 true를, 실패하면 error를 방출하는 Observable을 반환합니다. 29 | func signUp(email: String, password: String, user: User) -> Single 30 | } 31 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/Interfaces/Repositories/BrowseRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowseRepository.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/29. 6 | // 7 | 8 | import RxSwift 9 | 10 | protocol BrowseRepository { 11 | // TODO: 무한 스크롤을 위한 페이징 추가 12 | func fetch() -> Single<[BrowseQuest]> 13 | } 14 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/Interfaces/Repositories/ProtectedUserRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProtectedUserRepository.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/12/05. 6 | // 7 | 8 | import RxSwift 9 | 10 | protocol ProtectedUserRepository { 11 | /// 유저정보를 삭제합니다. 12 | /// - Returns: 삭제 성공 여부를 방출하는 Observable입니다. 13 | func deleteUser() -> Single 14 | } 15 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/Interfaces/Repositories/QuestsRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuestsRepository.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol QuestsRepository { 13 | /** 14 | 해당 퀘스트를 저장합니다. 15 | 16 | - Parameters: 17 | - quest: 저장할 퀘스트입니다. 18 | - Returns: 성공시 Quest를, 실패시, error를 방출하는 Observable입니다. 19 | */ 20 | func save(with quest: [Quest]) -> Single<[Quest]> 21 | 22 | /** 23 | 해당 날짜의 퀘스트 배열을 받아옵니다. 24 | 25 | - Parameters: 26 | - date: 받아오길 원하는 날짜입니다. 27 | - Returns: Quest의 배열을 방출하는 Observable입니다. 비어있다면 비어있는 배열을 방출합니다. 28 | */ 29 | func fetch(by date: Date) -> Single<[Quest]> 30 | 31 | /** 32 | 해당 날짜의 퀘스트 값(`currentCount`)을 업데이트 합니다. 33 | 34 | - Parameters: 35 | - quest: 업데이트될 퀘스트입니다. 36 | - Returns: 성공시 Quest를, 실패시 error를 방출하는 Observable입니다. 37 | */ 38 | func update(with quest: Quest) -> Single 39 | 40 | /** 41 | 해당 날짜에 해당하는 퀘스트의 값을 업데이트 합니다. 42 | 43 | - Parameters: 44 | - questId: 삭제하고자 하는 퀘스트의 id입니다. 45 | - Returns: 성공시 해당 Quest를, 실패시 error를 방출하는 Observable입니다. 46 | */ 47 | func delete(with questId: UUID) -> Single 48 | 49 | /** 50 | 해당 group의 Quest를 모두 삭제합니다. 51 | 52 | 53 | - Parameters: 54 | - groupId: 삭제하고자 하는 퀘스트 그룹의 id입니다. 55 | - Returns: 성공시 해당 Quest 배열을, 실패시 error를 방출하는 Observable입니다. 56 | */ 57 | func deleteAll(with groupId: UUID) -> Single<[Quest]> 58 | 59 | func fetch(by uuid: String, date: Date) -> Single<[Quest]> 60 | 61 | func fetch(by uuid: String, date: Date, filter: Date) -> Single<[Date: [Quest]]> 62 | } 63 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/Interfaces/Repositories/UserRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserRepository.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/12/01. 6 | // 7 | 8 | import Foundation 9 | import RxSwift 10 | import RxRelay 11 | 12 | protocol UserRepository { 13 | /// 로그인되어 있는지 확인합니다. 14 | /// - Returns: 성공하면 uid정보를 방출하는 relay입니다. 15 | func isLoggedIn() -> BehaviorRelay 16 | 17 | /// 유저정보를 불러옵니다. 18 | /// - Returns: 유저의 정보를 방출하는 Observable을 반환합니다. 19 | func readUser() -> Single 20 | 21 | /// 유저정보를 업데이트합니다. 22 | /// - Parameter user: 바뀐 유저 정보입니다. 23 | /// - Returns: 성공적으로 바뀐 유저의 정보를 방출하는 Observable입니다. 24 | func updateUser(by user: User) -> Single 25 | 26 | func saveProfileImage(data: Data) -> Single 27 | 28 | func saveBackgroundImage(data: Data) -> Single 29 | 30 | func fetchUser(by uuid: String) -> Single 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/UseCases/Browse/DefaultBrowseUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultBrowseUseCase.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class DefaultBrowseUseCase { 13 | private let browseRepository: BrowseRepository 14 | 15 | init(browseRepository: BrowseRepository) { 16 | self.browseRepository = browseRepository 17 | } 18 | } 19 | 20 | extension DefaultBrowseUseCase: BrowseUseCase { 21 | func excute() -> Single<[BrowseQuest]> { 22 | return browseRepository.fetch() 23 | } 24 | } 25 | 26 | final class BrowseMockRepo: BrowseRepository { 27 | func fetch() -> Single<[BrowseQuest]> { 28 | return .just([BrowseQuest(user: User(uuid: "", nickName: "test", profileURL: "", backgroundImageURL: "", introduce: "", allow: false), quests: [])]) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/UseCases/Browse/Protocols/BrowseUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowseUseCase.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol BrowseUseCase { 13 | func excute() -> Single<[BrowseQuest]> 14 | } 15 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/UseCases/Common/DefaultFriendQuestUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultFriendQuestUseCase.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/12/08. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class DefaultFriendUseCase { 13 | private let questsRepository: QuestsRepository 14 | 15 | init(questsRepository: QuestsRepository) { 16 | self.questsRepository = questsRepository 17 | } 18 | } 19 | 20 | extension DefaultFriendUseCase: FriendQuestUseCase { 21 | func fetch(with uuid: String, by date: Date) -> Single<[Quest]> { 22 | return questsRepository.fetch(by: uuid, date: date) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/UseCases/Common/Protocols/FriendQuestUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FriendQuestUseCase.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/12/08. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol FriendQuestUseCase { 13 | func fetch(with uuid: String, by date: Date) -> Single<[Quest]> 14 | } 15 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/UseCases/Home/DefaultEnrollUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultEnrollUseCase.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/12/05. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class DefaultEnrollUseCase { 13 | private let questsRepository: QuestsRepository 14 | 15 | init(questsRepository: QuestsRepository) { 16 | self.questsRepository = questsRepository 17 | } 18 | } 19 | 20 | extension DefaultEnrollUseCase: EnrollUseCase { 21 | func save(with quests: [Quest]) -> Single { 22 | return questsRepository 23 | .save(with: quests) 24 | .map { _ in 25 | true 26 | } 27 | .catchAndReturn(false) 28 | .do(onSuccess: { _ in 29 | let dates = quests.map { $0.date } 30 | NotificationCenter.default.post(name: .updated, object: dates) 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/UseCases/Home/DefaultQuestUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultQuestUseCase.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class DefaultQuestUseCase { 13 | private let questsRepository: QuestsRepository 14 | 15 | init(questsRepository: QuestsRepository) { 16 | self.questsRepository = questsRepository 17 | } 18 | } 19 | 20 | extension DefaultQuestUseCase: QuestUseCase { 21 | func fetch(by date: Date) -> Single<[Quest]> { 22 | return questsRepository.fetch(by: date) 23 | } 24 | 25 | func update(with quest: Quest) -> Single { 26 | return questsRepository 27 | .update(with: quest) 28 | .do(onSuccess: { quest in 29 | NotificationCenter.default.post(name: .questStateChanged, object: quest.date) 30 | }) 31 | .map { _ in true } 32 | .catchAndReturn(false) 33 | } 34 | 35 | func delete(with quest: Quest) -> Single { 36 | return questsRepository 37 | .delete(with: quest.uuid) 38 | .do(onSuccess: { quest in 39 | NotificationCenter.default.post(name: .questStateChanged, object: quest.date) 40 | }) 41 | .map { _ in true } 42 | .catchAndReturn(false) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/UseCases/Home/DefaultUserUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultUserUseCase.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/12/07. 6 | // 7 | 8 | import Foundation 9 | import RxSwift 10 | 11 | final class DefaultUserUseCase { 12 | private let userRepository: UserRepository 13 | 14 | init(userRepository: UserRepository) { 15 | self.userRepository = userRepository 16 | } 17 | } 18 | 19 | extension DefaultUserUseCase: UserUseCase { 20 | func isLoggedIn() -> Observable { 21 | return userRepository 22 | .isLoggedIn() 23 | .map { id in 24 | guard let _ = id else { return false } 25 | return true 26 | } 27 | .asObservable() 28 | } 29 | 30 | func fetch() -> Single { 31 | return userRepository.readUser() 32 | .catchAndReturn(User()) 33 | 34 | } 35 | 36 | func save(with user: User) -> Single { 37 | return userRepository.updateUser(by: user) 38 | .catchAndReturn(User()) 39 | } 40 | 41 | func saveProfileImage(data: Data) -> Single { 42 | return userRepository.saveProfileImage(data: data) 43 | .catchAndReturn(false) 44 | } 45 | 46 | func saveBackgroundImage(data: Data) -> Single { 47 | return userRepository.saveBackgroundImage(data: data) 48 | .catchAndReturn(false) 49 | } 50 | 51 | func delete() -> Single { 52 | guard let userRepository = userRepository as? ProtectedUserRepository else { return Single.just(false) } 53 | return userRepository.deleteUser() 54 | .catchAndReturn(false) 55 | } 56 | 57 | func updateIntroduce(introduce: String) -> Single { 58 | userRepository.readUser() 59 | .map { $0.setIntroduce(introduce: introduce) } 60 | .flatMap(userRepository.updateUser(by:)) 61 | .map { _ in true } 62 | .catchAndReturn(false) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/UseCases/Home/Protocols/CalendarUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CalendarUseCase.swift 3 | // DailyQuest 4 | // 5 | // Created by wickedRun on 2022/12/05. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol CalendarUseCase { 13 | 14 | var currentMonth: BehaviorSubject { get } 15 | var completionOfMonths: BehaviorSubject<[[DailyQuestCompletion]]> { get } 16 | var selectedDate: BehaviorSubject { get } 17 | 18 | func fetchNextMontlyCompletion() 19 | func fetchLastMontlyCompletion() 20 | func setupMonths() 21 | func refreshMontlyCompletion(for date: Date) 22 | func selectDate(_ date: Date) 23 | } 24 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/UseCases/Home/Protocols/EnrollUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EnrollUseCase.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/12/05. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol EnrollUseCase { 13 | func save(with quests: [Quest]) -> Single 14 | } 15 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/UseCases/Home/Protocols/QuestUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuestUseCase.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol QuestUseCase { 13 | func fetch(by date: Date) -> Single<[Quest]> 14 | func update(with quest: Quest) -> Single 15 | func delete(with quest: Quest) -> Single 16 | } 17 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/UseCases/Home/Protocols/UserUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserUseCase.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/12/07. 6 | // 7 | 8 | import Foundation 9 | import RxSwift 10 | 11 | protocol UserUseCase { 12 | func isLoggedIn() -> Observable 13 | func fetch() -> Single 14 | func save(with user: User) -> Single 15 | 16 | func saveProfileImage(data: Data) -> Single 17 | func saveBackgroundImage(data: Data) -> Single 18 | 19 | func delete() -> Single 20 | 21 | func updateIntroduce(introduce: String) -> Single 22 | } 23 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/UseCases/Settings/DefaultAuthUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultAuthUseCase.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/28. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class DefaultAuthUseCase { 13 | private let authRepository: AuthRepository 14 | 15 | init(authRepository: AuthRepository) { 16 | self.authRepository = authRepository 17 | } 18 | } 19 | 20 | extension DefaultAuthUseCase: AuthUseCase { 21 | func signIn(email: String, password: String) -> Observable { 22 | return authRepository 23 | .signIn(email: email, password: password) 24 | .catch { _ in 25 | return .just(false) 26 | } 27 | .asObservable() 28 | } 29 | 30 | func signOut() -> Observable { 31 | return authRepository 32 | .signOut() 33 | .catchAndReturn(false) 34 | .asObservable() 35 | } 36 | 37 | func signUp(email: String, password: String, user: User) -> Observable { 38 | return authRepository 39 | .signUp(email: email, password: password, user: user) 40 | .catch { _ in 41 | return .just(false) 42 | } 43 | .asObservable() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/UseCases/Settings/DefaultSettingsUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultSettingsUseCase.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/12/06. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class DefaultSettingsUseCase { 13 | private let userRepository: UserRepository 14 | private let authRepository: AuthRepository 15 | 16 | init(userRepository: UserRepository, authRepository: AuthRepository) { 17 | self.userRepository = userRepository 18 | self.authRepository = authRepository 19 | } 20 | } 21 | 22 | extension DefaultSettingsUseCase: SettingsUseCase { 23 | func isLoggedIn() -> Observable { 24 | return userRepository 25 | .isLoggedIn() 26 | .map { id in 27 | guard let _ = id else { return false } 28 | return true 29 | } 30 | .asObservable() 31 | } 32 | 33 | func signOut() -> Observable { 34 | return authRepository 35 | .signOut() 36 | .map { _ in true } 37 | .catchAndReturn(false) 38 | .asObservable() 39 | } 40 | 41 | func updateAllow(allow: Bool) -> Single { 42 | userRepository.readUser() 43 | .map { $0.setAllow(allow: allow) } 44 | .flatMap(userRepository.updateUser(by:)) 45 | .map { _ in true } 46 | .catchAndReturn(false) 47 | } 48 | 49 | func fetchAllow() -> Single { 50 | userRepository.readUser() 51 | .map { $0.allow } 52 | .catchAndReturn(nil) 53 | } 54 | 55 | func delete() -> Single { 56 | guard let userRepository = userRepository as? ProtectedUserRepository else { return Single.just(false) } 57 | return userRepository.deleteUser() 58 | .catchAndReturn(false) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/UseCases/Settings/Protocols/AuthUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthUseCase.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/28. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol AuthUseCase { 13 | func signIn(email: String, password: String) -> Observable 14 | func signOut() -> Observable 15 | func signUp(email: String, password: String, user: User) -> Observable 16 | } 17 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Domain/UseCases/Settings/Protocols/SettingsUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsUseCase.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/12/06. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol SettingsUseCase { 13 | func isLoggedIn() -> Observable 14 | func signOut() -> Observable 15 | 16 | func updateAllow(allow: Bool) -> Single 17 | func fetchAllow() -> Single 18 | 19 | func delete() -> Single 20 | } 21 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Infrastructure/NetworkConfigure.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkConfigure.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | enum UserCase { 11 | case currentUser 12 | case anotherUser(_ uid: String) 13 | 14 | var path: String { 15 | return "users" 16 | } 17 | } 18 | 19 | enum Access: String { 20 | case quests 21 | case receiveQuests 22 | case userInfo 23 | 24 | var path: String { 25 | return self.rawValue 26 | } 27 | } 28 | 29 | enum CRUD { 30 | case create 31 | case read 32 | case update 33 | case delete 34 | } 35 | 36 | enum StoragePath { 37 | case profileImages 38 | case backgroundImages 39 | case another(_ path: String) 40 | 41 | var path: String { 42 | switch self { 43 | case .profileImages: 44 | return "profileImages" 45 | case .backgroundImages: 46 | return "backgroundImages" 47 | case .another(let path): 48 | return path 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Infrastructure/NetworkService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkService.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/11/17. 6 | // 7 | 8 | import RxSwift 9 | import RxRelay 10 | import Foundation 11 | 12 | enum NetworkServiceError: Error { 13 | case noNetworkService // NetworkService X 14 | case noAuthError // uid X 15 | case permissionDenied // wrong access 16 | case needFilterError 17 | case noUrlError 18 | case noDataError 19 | } 20 | 21 | protocol NetworkService { 22 | var uid: BehaviorRelay { get } 23 | 24 | func signIn(email: String, password: String) -> Single 25 | func signOut() -> Single 26 | func signUp(email: String, password: String, userDto: UserDTO) -> Single 27 | func deleteUser() -> Single 28 | 29 | func create(userCase: UserCase, access: Access, dto: T) -> Single 30 | func read(type: T.Type, userCase: UserCase, access: Access, filter: DateFilter?) -> Observable 31 | func update(userCase: UserCase, access: Access, dto: T) -> Single 32 | func delete(userCase: UserCase, access: Access, dto: T) -> Single 33 | 34 | func uploadDataStorage(data: Data, path: StoragePath) -> Single 35 | func downloadDataStorage(fileName: String) -> Single 36 | func deleteDataStorage(forUrl: String) -> Single 37 | 38 | func getAllowUsers(limit: Int) -> Observable 39 | } 40 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Browse/Flow/BrowseCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowseCoordinator.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | import RxSwift 11 | 12 | protocol BrowseCoordinator: Coordinator { 13 | func showFriendFlow(with user: User) 14 | } 15 | 16 | final class DefaultBrowseCoordinator: BrowseCoordinator { 17 | private var disposableBag = DisposeBag() 18 | 19 | var finishDelegate: CoordinatorFinishDelegate? 20 | var childCoordinators: [Coordinator] = [] 21 | var navigationController: UINavigationController 22 | let browseSceneDIContainer: BrowseSceneDIContainer 23 | 24 | init(navigationController: UINavigationController, 25 | browseSceneDIContainer: BrowseSceneDIContainer) { 26 | self.navigationController = navigationController 27 | self.browseSceneDIContainer = browseSceneDIContainer 28 | } 29 | 30 | func start() { 31 | let browseViewController = browseSceneDIContainer.makeBrowseViewController() 32 | navigationController.pushViewController(browseViewController, animated: false) 33 | navigationController.isNavigationBarHidden = true 34 | 35 | browseViewController 36 | .coordinatorPublisher 37 | .subscribe(onNext: showFriendFlow(with:)) 38 | .disposed(by: disposableBag) 39 | } 40 | 41 | func showFriendFlow(with user: User) { 42 | let friendViewController = browseSceneDIContainer.makeFriendViewController(with: user) 43 | navigationController.present(friendViewController, animated: true) 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Browse/View/BrowseCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowseCell.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/15. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | final class BrowseCell: UITableViewCell { 13 | var viewModel: BrowseItemViewModel! 14 | 15 | /// dequeuResusable을 위한 아이덴티파이어입니다. 16 | static let reuseIdentifier = "BrowseCell" 17 | 18 | // MARK: - Components 19 | private lazy var header: UserInfoView = { 20 | return UserInfoView() 21 | }() 22 | 23 | private lazy var questTableView: UITableView = { 24 | let questTableView = UITableView() 25 | questTableView.backgroundColor = .maxLightBlue 26 | questTableView.layer.cornerRadius = 15 27 | 28 | return questTableView 29 | }() 30 | 31 | // MARK: - Methods 32 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 33 | super.init(style: style, reuseIdentifier: reuseIdentifier) 34 | 35 | questTableView.register(QuestCell.self, forCellReuseIdentifier: QuestCell.reuseIdentifier) 36 | questTableView.delegate = self 37 | questTableView.dataSource = self 38 | 39 | questTableView.rowHeight = 75 // the cell size 40 | 41 | questTableView.allowsSelection = false 42 | questTableView.sectionHeaderTopPadding = 0 43 | 44 | configureUI() 45 | } 46 | 47 | required init?(coder: NSCoder) { 48 | fatalError("init(coder:) has not been implemented") 49 | } 50 | 51 | private func configureUI() { 52 | addSubview(questTableView) 53 | 54 | questTableView.snp.makeConstraints { make in 55 | make.edges.equalToSuperview().inset(10) 56 | } 57 | } 58 | 59 | /** 60 | 인자로 viewModel을 받아, 테이블뷰를 reload합니다. 61 | 62 | - Parameters: 63 | - viewModel: `BrowseItemViewModel` 타입입니다. User의 인스턴스와 Quest 인스턴스의 배열을 가지고 있습니다. 64 | */ 65 | func setup(with viewModel: BrowseItemViewModel) { 66 | self.viewModel = viewModel 67 | header.setup(with: viewModel.user) 68 | 69 | questTableView.reloadData() 70 | } 71 | } 72 | 73 | extension BrowseCell: UITableViewDelegate { 74 | func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 75 | return header 76 | } 77 | 78 | func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 79 | return 75 80 | } 81 | } 82 | 83 | extension BrowseCell: UITableViewDataSource { 84 | /** 85 | 테이블 뷰안에 들어갈 QuestCell의 개수를 구합니다. 86 | Note. 이 메서드가 최초로 실행되는 시점에는 viewModel이 nil입니다. 87 | 데이터소스를 통해 값이 삽입되는 시점에는 그렇지 않으므로, 예외처리를 통해 해결했습니다. 88 | */ 89 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 90 | guard let count = viewModel?.quests.count else { return 0 } 91 | return count 92 | } 93 | 94 | /** 95 | QuestCell을 생성합니다. 96 | */ 97 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 98 | guard let cell = questTableView.dequeueReusableCell(withIdentifier: QuestCell.reuseIdentifier, for: indexPath) as? QuestCell else { 99 | assertionFailure("Cannot deque reuseable cell.") 100 | return UITableViewCell() 101 | } 102 | cell.setup(with: viewModel.quests[indexPath.row]) 103 | 104 | return cell 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Browse/View/UserInfoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserInfoView.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/16. 6 | // 7 | 8 | import UIKit 9 | import Kingfisher 10 | import SnapKit 11 | 12 | final class UserInfoView: UIStackView { 13 | 14 | // MARK: - Components 15 | private lazy var userImage: UIImageView = { 16 | let userImage = UIImageView() 17 | userImage.image = UIImage(named: "StatusMax") 18 | userImage.clipsToBounds = true 19 | userImage.backgroundColor = .white 20 | 21 | return userImage 22 | }() 23 | 24 | private lazy var welcomeLabel: UILabel = { 25 | let welcomeLabel = UILabel() 26 | welcomeLabel.textColor = .white 27 | welcomeLabel.font = UIFont.boldSystemFont(ofSize: 22) 28 | 29 | return welcomeLabel 30 | }() 31 | 32 | // MARK: - Methods 33 | convenience init() { 34 | self.init(frame: .zero) 35 | 36 | axis = .horizontal 37 | alignment = .center 38 | spacing = 10 39 | isLayoutMarginsRelativeArrangement = true 40 | layoutMargins = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10) 41 | 42 | configureUI() 43 | } 44 | 45 | override func layoutSubviews() { 46 | super.layoutSubviews() 47 | userImage.layer.cornerRadius = userImage.frame.height / 2 48 | } 49 | 50 | private func configureUI() { 51 | addArrangedSubview(userImage) 52 | addArrangedSubview(welcomeLabel) 53 | 54 | userImage.snp.makeConstraints { make in 55 | make.height.equalToSuperview().offset(-40) 56 | make.width.equalTo(userImage.snp.height) 57 | } 58 | } 59 | 60 | func setup(with user: User) { 61 | welcomeLabel.text = user.nickName + "님의 퀘스트" 62 | userImage.setImage(with: user.profileURL) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Browse/ViewController/BrowseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowseViewController.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/15. 6 | // 7 | 8 | import UIKit 9 | 10 | import RxSwift 11 | import RxCocoa 12 | 13 | final class BrowseViewController: UITableViewController { 14 | 15 | var coordinatorPublisher = PublishSubject() 16 | 17 | private var viewModel: BrowseViewModel! 18 | private var disposableBag = DisposeBag() 19 | 20 | lazy var activityIndicator: UIActivityIndicatorView = { 21 | // Create an indicator. 22 | let activityIndicator = UIActivityIndicatorView() 23 | activityIndicator.color = .maxDarkYellow 24 | 25 | let transfrom = CGAffineTransform.init(scaleX: 2, y: 2) 26 | activityIndicator.transform = transfrom 27 | 28 | activityIndicator.startAnimating() 29 | return activityIndicator 30 | }() 31 | 32 | // MARK: - Life Cycle 33 | static func create(with viewModel: BrowseViewModel) -> BrowseViewController { 34 | let view = BrowseViewController() 35 | view.viewModel = viewModel 36 | return view 37 | } 38 | 39 | override func viewDidLoad() { 40 | super.viewDidLoad() 41 | 42 | configure() 43 | configureIndicatorBar() 44 | bind() 45 | } 46 | 47 | private func configureIndicatorBar() { 48 | self.view.addSubview(activityIndicator) 49 | activityIndicator.snp.makeConstraints { make in 50 | make.width.height.equalTo(50) 51 | make.centerX.centerY.equalToSuperview() 52 | } 53 | } 54 | 55 | /** 56 | table view의 기본 정보를 설정합니다. 57 | */ 58 | private func configure() { 59 | // 델리게이트와 데이터소스를 rx로 재설정합니다. 60 | tableView.delegate = nil 61 | tableView.dataSource = nil 62 | tableView.rx.setDelegate(self).disposed(by: disposableBag) 63 | 64 | // BrowseCell을 등록합니다. 65 | tableView.register(BrowseCell.self, forCellReuseIdentifier: BrowseCell.reuseIdentifier) 66 | } 67 | 68 | private func bind() { 69 | let output = viewModel.transform(input: BrowseViewModel.Input(viewDidLoad: .just(()).asObservable())) 70 | 71 | output 72 | .data 73 | .do{ [weak self] _ in 74 | self?.activityIndicator.stopAnimating() 75 | } 76 | .drive(tableView.rx.items(cellIdentifier: BrowseCell.reuseIdentifier, cellType: BrowseCell.self)) { row, item, cell in 77 | cell.setup(with: item) 78 | } 79 | .disposed(by: disposableBag) 80 | 81 | tableView 82 | .rx 83 | .modelSelected(BrowseItemViewModel.self) 84 | .map { $0.user } 85 | .bind(to: coordinatorPublisher) 86 | .disposed(by: disposableBag) 87 | } 88 | } 89 | 90 | extension BrowseViewController { 91 | /** 92 | 하나의 BrowseCell의 크기를 결정합니다. BrowseCell내의 QuestCell의 개수만큼 크기가 늘어납니다. 93 | +20은 margins으로 인해 추가된 값입니다. 94 | */ 95 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 96 | return (75.0 * CGFloat(viewModel.cellCount[indexPath.row])) + 20 + 75 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Browse/ViewModel/BrowseItemViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowseCellItemModel.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/15. 6 | // 7 | 8 | import Foundation 9 | 10 | final class BrowseItemViewModel { 11 | let user: User 12 | let quests: [Quest] 13 | 14 | init(user: User, quests: [Quest]) { 15 | self.user = user 16 | self.quests = quests 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Browse/ViewModel/BrowseViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowseViewModel.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/15. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | import RxCocoa 12 | 13 | final class BrowseViewModel { 14 | private let browseUseCase: BrowseUseCase 15 | private(set) var cellCount: [Int] = [] 16 | 17 | init(browseUseCase: BrowseUseCase) { 18 | self.browseUseCase = browseUseCase 19 | } 20 | 21 | struct Input { 22 | let viewDidLoad: Observable 23 | } 24 | 25 | struct Output { 26 | let data: Driver<[BrowseItemViewModel]> 27 | } 28 | 29 | func transform(input: Input) -> Output { 30 | let data = input 31 | .viewDidLoad 32 | .flatMap { _ in 33 | self.browseUseCase.excute() 34 | } 35 | .map(transform(with:)) 36 | .do(onNext: { [weak self] items in 37 | self?.cellCount = items.map({ $0.quests.count }) 38 | }) 39 | .asDriver(onErrorJustReturn: []) 40 | 41 | return Output(data: data) 42 | } 43 | 44 | private func transform(with browseQuests: [BrowseQuest]) -> [BrowseItemViewModel] { 45 | return browseQuests.map { browseQuest in 46 | BrowseItemViewModel(user: browseQuest.user, quests: browseQuest.quests) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Common/Cells/CalendarCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CalendarCell.swift 3 | // DailyQuest 4 | // 5 | // Created by wickedRun on 2022/11/14. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | final class CalendarCell: UICollectionViewCell { 12 | 13 | /// 재사용 식별자 14 | static let reuseIdentifier = "CalendarCell" 15 | 16 | // MARK: - Sub Views 17 | 18 | private lazy var circleCheckView: CircleCheckView = { 19 | let view = CircleCheckView() 20 | return view 21 | }() 22 | 23 | private lazy var dayLabel: UILabel = { 24 | let view = UILabel() 25 | view.textAlignment = .center 26 | view.textColor = .gray 27 | view.adjustsFontSizeToFitWidth = true 28 | return view 29 | }() 30 | 31 | override var isSelected: Bool { 32 | didSet { 33 | if isSelected { 34 | self.circleCheckView.layer.borderWidth = 3.0 35 | self.circleCheckView.layer.borderColor = UIColor.maxDarkYellow.cgColor 36 | } else { 37 | self.circleCheckView.layer.borderWidth = 0.0 38 | self.circleCheckView.layer.borderColor = nil 39 | } 40 | } 41 | } 42 | 43 | override init(frame: CGRect) { 44 | super.init(frame: frame) 45 | 46 | addSubviews() 47 | setupContstraints() 48 | } 49 | 50 | required init?(coder: NSCoder) { 51 | fatalError("init(coder:) has not been implemented") 52 | } 53 | 54 | // MARK: - Configuration View 55 | 56 | private func addSubviews() { 57 | self.contentView.addSubview(circleCheckView) 58 | self.contentView.addSubview(dayLabel) 59 | } 60 | 61 | private func setupContstraints() { 62 | circleCheckView.snp.makeConstraints { make in 63 | make.top.equalToSuperview().inset(5) 64 | make.centerX.equalToSuperview().priority(.high) 65 | make.width.equalTo(self.snp.width).multipliedBy(0.9).inset(5) 66 | make.height.equalTo(circleCheckView.snp.width).priority(.required) 67 | } 68 | 69 | dayLabel.snp.makeConstraints { make in 70 | make.top.equalTo(circleCheckView.snp.bottom).offset(4) 71 | make.horizontalEdges.equalToSuperview() 72 | make.bottom.lessThanOrEqualToSuperview().priority(.high) 73 | } 74 | } 75 | 76 | // MARK: - Methods 77 | 78 | /// CalendarCell의 UI를 변경하는 메소드 79 | /// - Parameter completion: DailyQuestCompletion 80 | func configure(_ completion: DailyQuestCompletion) { 81 | self.isHidden = false 82 | 83 | switch completion.state { 84 | case .hidden: 85 | self.isHidden = true 86 | case .normal: 87 | self.circleCheckView.setNormal() 88 | case .notDone(let number): 89 | self.circleCheckView.setNumber(to: number) 90 | case .done: 91 | self.circleCheckView.setDone() 92 | } 93 | dayLabel.text = "\(completion.day.day)" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Common/Cells/FollowingCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FollowingCell.swift 3 | // DailyQuest 4 | // 5 | // Created by 이다연 on 2022/11/30. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | final class FollowingCell: UICollectionViewCell { 12 | 13 | static let reuseIdentifier = "FollowingCell" 14 | 15 | private lazy var userImage: UIImageView = { 16 | let userImage = UIImageView() 17 | userImage.image = UIImage(systemName: "person.circle") 18 | userImage.clipsToBounds = true 19 | userImage.contentMode = .scaleAspectFill 20 | userImage.layer.cornerRadius = 35.0 / 2 21 | return userImage 22 | }() 23 | 24 | override init(frame:CGRect) { 25 | super.init(frame: frame) 26 | contentView.addSubview(userImage) 27 | userImage.snp.makeConstraints { make in 28 | make.edges.equalToSuperview() 29 | } 30 | } 31 | 32 | required init?(coder: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | 36 | override func layoutSubviews() { 37 | super.layoutSubviews() 38 | } 39 | 40 | public func configure(with name: String?) { 41 | if name == nil { 42 | userImage.image = UIImage(systemName: "person.circle") 43 | } else{ 44 | userImage.image = UIImage(named: name ?? "") 45 | } 46 | } 47 | 48 | override func prepareForReuse() { 49 | super.prepareForReuse() 50 | userImage.image = nil 51 | } 52 | } 53 | 54 | 55 | #if canImport(SwiftUI) && DEBUG 56 | import SwiftUI 57 | 58 | struct FollowingCellPreview: PreviewProvider{ 59 | static var previews: some View { 60 | UIViewPreview { 61 | let cell = FollowingCell(frame: .zero) 62 | return cell 63 | }.previewLayout(.fixed(width: 40, height: 40)) 64 | } 65 | } 66 | #endif 67 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Common/Cells/LastFollowingCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LastFollowingCell.swift 3 | // DailyQuest 4 | // 5 | // Created by 이다연 on 2022/12/01. 6 | // 7 | 8 | import UIKit 9 | 10 | final class LastFollowingCell: UICollectionViewCell { 11 | 12 | static let reuseIdentifier = "LastFollowingCell" 13 | 14 | private lazy var plusButton: UIButton = { 15 | var config = UIButton.Configuration.maxStyle() 16 | let plusButton = UIButton(configuration: config) 17 | 18 | return plusButton 19 | }() 20 | 21 | override init(frame:CGRect) { 22 | super.init(frame: frame) 23 | contentView.addSubview(plusButton) 24 | plusButton.snp.makeConstraints { make in 25 | make.edges.equalToSuperview() 26 | } 27 | } 28 | 29 | required init?(coder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | 33 | override func layoutSubviews() { 34 | super.layoutSubviews() 35 | } 36 | 37 | override func prepareForReuse() { 38 | super.prepareForReuse() 39 | } 40 | } 41 | 42 | 43 | #if canImport(SwiftUI) && DEBUG 44 | import SwiftUI 45 | 46 | struct LastFollowingCellPreview: PreviewProvider{ 47 | static var previews: some View { 48 | UIViewPreview { 49 | let cell = LastFollowingCell(frame: .zero) 50 | return cell 51 | }.previewLayout(.fixed(width: 40, height: 40)) 52 | } 53 | } 54 | #endif 55 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Common/Cells/QuestCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuestCell.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/15. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | final class QuestCell: UITableViewCell { 13 | /// dequeuResusable을 위한 아이덴티파이어입니다. 14 | static let reuseIdentifier = "QuestCell" 15 | 16 | // MARK: - Components 17 | /** 18 | 프로그래스 바 입니다. 기존의 `UIProgressBar`를 상속받는 19 | `CustomProgressBar`를 타입으로 합니다. 20 | 이 클래스는 코너의 radius 값을 주기 위해 만들어졌습니다. 21 | */ 22 | private lazy var progressView: CustomProgressBar = { 23 | let progressView = CustomProgressBar() 24 | progressView.trackTintColor = .maxLightYellow 25 | progressView.progressTintColor = .maxYellow 26 | progressView.progress = 0.2 27 | 28 | return progressView 29 | }() 30 | 31 | /** 32 | 좌측에 quest의 제목이 들어갈 레이블입니다. 33 | */ 34 | private lazy var questLabel: UILabel = { 35 | let questLabel = UILabel() 36 | questLabel.text = "0" 37 | questLabel.font = UIFont.boldSystemFont(ofSize: 16) 38 | questLabel.textColor = .maxViolet 39 | 40 | return questLabel 41 | }() 42 | 43 | /** 44 | 우측에 현재 달성량과 목표량이 들어갈 레이블입니다. 45 | */ 46 | private lazy var countLabel: UILabel = { 47 | let countLabel = UILabel() 48 | countLabel.text = "0" 49 | countLabel.font = UIFont.boldSystemFont(ofSize: 16) 50 | countLabel.textColor = .maxViolet 51 | 52 | return countLabel 53 | }() 54 | 55 | 56 | // MARK: - Methods 57 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 58 | super.init(style: style, reuseIdentifier: reuseIdentifier) 59 | 60 | configureUI() 61 | } 62 | 63 | required init?(coder: NSCoder) { 64 | fatalError("init(coder:) has not been implemented") 65 | } 66 | 67 | /** 68 | UI의 constraints를 설정하기 위한 메서드입니다. 69 | constraints를 설정하기 전에, 해당 뷰를 먼저 add해야함을 유념하세요. 70 | */ 71 | private func configureUI() { 72 | addSubview(progressView) 73 | addSubview(questLabel) 74 | addSubview(countLabel) 75 | 76 | backgroundColor = UIColor(white: 1, alpha: 0) 77 | 78 | progressView.snp.makeConstraints { make in 79 | make.edges.equalToSuperview().inset(10) 80 | } 81 | 82 | questLabel.snp.makeConstraints { make in 83 | make.leading.equalToSuperview().inset(20) 84 | make.centerY.equalToSuperview() 85 | } 86 | 87 | countLabel.snp.makeConstraints { make in 88 | make.trailing.equalTo(progressView).inset(20) 89 | make.centerY.equalToSuperview() 90 | } 91 | } 92 | 93 | /** 94 | 인자로 받은 Entity Quest타입을 통해 그 정보를 기반으로 cell에 아이템을 넣습니다. 95 | 96 | 애니메이션 효과가 필요없다고 판단하여, `setProgress(_:animated)`에서 두번째 인자를 97 | `false`로 설정하였습니다. 98 | 99 | - Parameters: 100 | - quest: Quest타입의 엔티티입니다. 101 | */ 102 | func setup(with quest: Quest) { 103 | let value = Float(quest.currentCount) / Float(quest.totalCount) 104 | questLabel.text = "\(quest.title)" 105 | countLabel.text = "\(quest.currentCount) / \(quest.totalCount)" 106 | 107 | progressView.setProgress(value, animated: false) 108 | } 109 | } 110 | 111 | #if canImport(SwiftUI) && DEBUG 112 | import SwiftUI 113 | 114 | struct QuestCellPreview: PreviewProvider{ 115 | static var previews: some View { 116 | UIViewPreview { 117 | let cell = QuestCell(frame: .zero) 118 | let quest = Quest(groupId: UUID(), uuid: UUID(),date: Date(), title: "my quest", currentCount: 2, totalCount: 5) 119 | 120 | cell.setup(with: quest) 121 | return cell 122 | }.previewLayout(.fixed(width: 300, height: 80)) 123 | } 124 | } 125 | #endif 126 | 127 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Common/Cells/UserInfoCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserInfoCell.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/14. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | final class UserInfoCell: UITableViewCell { 13 | /// dequeuResusable을 위한 아이덴티파이어입니다. 14 | static let reuseIdentifier = "UserInfoCell" 15 | 16 | // MARK: - Components 17 | private lazy var container: UIStackView = { 18 | let container = UIStackView() 19 | container.axis = .horizontal 20 | container.alignment = .center 21 | container.backgroundColor = .maxLightGrey 22 | container.spacing = 10 23 | container.isLayoutMarginsRelativeArrangement = true 24 | container.layoutMargins = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 0) 25 | container.layer.cornerRadius = 15 26 | 27 | return container 28 | }() 29 | 30 | private lazy var userImage: UIImageView = { 31 | let userImage = UIImageView() 32 | userImage.image = UIImage(systemName: "heart.fill") 33 | userImage.clipsToBounds = true 34 | userImage.backgroundColor = .white 35 | 36 | return userImage 37 | }() 38 | 39 | private lazy var userName: UILabel = { 40 | let userName = UILabel() 41 | userName.text = " " 42 | 43 | return userName 44 | }() 45 | 46 | // MARK: - Methods 47 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 48 | super.init(style: style, reuseIdentifier: reuseIdentifier) 49 | 50 | configureUI() 51 | } 52 | 53 | required init?(coder: NSCoder) { 54 | fatalError("init(coder:) has not been implemented") 55 | } 56 | 57 | override func layoutSubviews() { 58 | super.layoutSubviews() 59 | userImage.layer.cornerRadius = userImage.frame.height / 2 60 | } 61 | 62 | private func configureUI() { 63 | container.addArrangedSubview(userImage) 64 | container.addArrangedSubview(userName) 65 | addSubview(container) 66 | 67 | container.snp.makeConstraints { make in 68 | make.edges.equalToSuperview() 69 | } 70 | 71 | userImage.snp.makeConstraints { make in 72 | make.height.equalToSuperview().offset(-40) 73 | make.width.equalTo(userImage.snp.height) 74 | } 75 | } 76 | 77 | func setup(with user: User) { 78 | userName.text = user.nickName 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Common/CircleCheckView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircleCheckView.swift 3 | // DailyQuest 4 | // 5 | // Created by wickedRun on 2022/11/14. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | final class CircleCheckView: UIView { 12 | 13 | // MARK: - Sub Views 14 | 15 | private lazy var circleBackground: UIView = { 16 | let view = UIView() 17 | return view 18 | }() 19 | 20 | private lazy var displayLabel: UILabel = { 21 | let label = UILabel() 22 | label.textAlignment = .center 23 | label.adjustsFontSizeToFitWidth = true 24 | label.textColor = .white 25 | return label 26 | }() 27 | 28 | override init(frame: CGRect = .zero) { 29 | super.init(frame: frame) 30 | 31 | self.clipsToBounds = true 32 | 33 | addSubviews() 34 | setupConstraints() 35 | } 36 | 37 | required init?(coder: NSCoder) { 38 | fatalError("init(coder:) has not been implemented") 39 | } 40 | 41 | override func layoutSubviews() { 42 | super.layoutSubviews() 43 | self.layer.cornerRadius = self.bounds.height / 2 44 | } 45 | 46 | // MARK: - Configuration View 47 | 48 | private func addSubviews() { 49 | self.addSubview(circleBackground) 50 | circleBackground.addSubview(displayLabel) 51 | } 52 | 53 | private func setupConstraints() { 54 | circleBackground.snp.makeConstraints { make in 55 | make.edges.equalToSuperview() 56 | } 57 | 58 | displayLabel.snp.makeConstraints { make in 59 | make.edges.equalToSuperview() 60 | } 61 | } 62 | 63 | // MARK: - Methods 64 | 65 | func setDone() { 66 | displayLabel.text = "✓" 67 | displayLabel.font = .systemFont(ofSize: self.displayLabel.font.pointSize, weight: .bold) 68 | displayLabel.textColor = .white 69 | circleBackground.backgroundColor = .maxYellow 70 | } 71 | 72 | func setNumber(to number: Int) { 73 | let range = (0...9) 74 | 75 | if range ~= number { 76 | self.displayLabel.text = "\(number)" 77 | } else { 78 | self.displayLabel.text = "9+" 79 | } 80 | 81 | displayLabel.font = .boldSystemFont(ofSize: self.displayLabel.font.pointSize) 82 | displayLabel.textColor = .maxViolet 83 | circleBackground.backgroundColor = .maxLightYellow 84 | } 85 | 86 | func setNormal() { 87 | displayLabel.text = "" 88 | circleBackground.backgroundColor = .maxLightYellow 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Common/CustomProgressBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomProgressBar.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/15. 6 | // 7 | 8 | import UIKit 9 | 10 | final class CustomProgressBar: UIProgressView { 11 | 12 | override func layoutSubviews() { 13 | super.layoutSubviews() 14 | subviews.forEach { subview in 15 | subview.layer.masksToBounds = true 16 | subview.layer.cornerRadius = 15 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Common/TextFieldForm.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextFieldForm.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/28. 6 | // 7 | 8 | import UIKit 9 | 10 | final class TextFieldForm: UITextField { 11 | var textPadding = UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 15) 12 | 13 | override init(frame: CGRect) { 14 | super.init(frame: frame) 15 | layer.borderWidth = 1 16 | layer.borderColor = UIColor.maxYellow.cgColor 17 | layer.cornerRadius = 5 18 | backgroundColor = UIColor(named: "White") 19 | textColor = .maxViolet 20 | } 21 | 22 | required init?(coder: NSCoder) { 23 | fatalError("init(coder:) has not been implemented") 24 | } 25 | 26 | override func textRect(forBounds bounds: CGRect) -> CGRect { 27 | let rect = super.textRect(forBounds: bounds) 28 | return rect.inset(by: textPadding) 29 | } 30 | 31 | override func editingRect(forBounds bounds: CGRect) -> CGRect { 32 | let rect = super.editingRect(forBounds: bounds) 33 | return rect.inset(by: textPadding) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Common/View/FriendStatusView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FriendStatusView.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/12/05. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | final class FriendStatusView: UIView { 13 | 14 | // MARK: - Components 15 | private(set) lazy var profileImageView: UIImageView = { 16 | let profileImageView = UIImageView() 17 | profileImageView.image = UIImage(named: "StatusMax") 18 | profileImageView.layer.cornerRadius = 35 19 | profileImageView.clipsToBounds = true 20 | 21 | return profileImageView 22 | }() 23 | 24 | private lazy var labelContainer: UIStackView = { 25 | let labelContainer = UIStackView() 26 | labelContainer.axis = .vertical 27 | 28 | return labelContainer 29 | }() 30 | 31 | private(set) lazy var userNameLabel: UILabel = { 32 | let userNameLabel = UILabel() 33 | userNameLabel.text = "User name label님의 Quest" 34 | userNameLabel.font = .systemFont(ofSize: 22,weight: .bold) 35 | 36 | return userNameLabel 37 | }() 38 | 39 | private(set) lazy var introduceLabel: UILabel = { 40 | let introduceLabel = UILabel() 41 | introduceLabel.text = "introduceLabel" 42 | introduceLabel.textColor = .darkGray 43 | 44 | return introduceLabel 45 | }() 46 | 47 | // MARK: - Methods 48 | override init(frame: CGRect) { 49 | super.init(frame: frame) 50 | 51 | configureUI() 52 | } 53 | 54 | required init?(coder: NSCoder) { 55 | fatalError("init(coder:) has not been implemented") 56 | } 57 | 58 | private func configureUI() { 59 | labelContainer.addArrangedSubview(userNameLabel) 60 | labelContainer.addArrangedSubview(introduceLabel) 61 | 62 | addSubview(profileImageView) 63 | profileImageView.snp.makeConstraints { make in 64 | make.height.width.equalTo(70) 65 | make.centerY.equalToSuperview() 66 | make.leading.equalToSuperview().inset(15) 67 | } 68 | 69 | addSubview(labelContainer) 70 | labelContainer.snp.makeConstraints { make in 71 | make.leading.equalTo(profileImageView.snp.trailing).offset(15) 72 | make.centerY.equalToSuperview() 73 | } 74 | } 75 | } 76 | 77 | #if canImport(SwiftUI) && DEBUG 78 | import SwiftUI 79 | 80 | struct FriendStatusViewPreview: PreviewProvider{ 81 | static var previews: some View { 82 | UIViewPreview { 83 | let view = FriendStatusView(frame: .zero) 84 | 85 | return view 86 | } 87 | .previewLayout(.fixed(width: 500, height: 100)) 88 | } 89 | } 90 | #endif 91 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Common/View/QuestView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuestView.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | import RxSwift 11 | import RxCocoa 12 | 13 | enum QuestViewType { 14 | case home 15 | case friend 16 | } 17 | 18 | final class QuestView: UITableView { 19 | private var disposableBag = DisposeBag() 20 | 21 | override var contentSize: CGSize { 22 | didSet { 23 | invalidateIntrinsicContentSize() 24 | } 25 | } 26 | 27 | override var intrinsicContentSize: CGSize { 28 | let number = numberOfRows(inSection: 0) 29 | return CGSize(width: contentSize.width, height: CGFloat(75*number + 75)) 30 | } 31 | 32 | override init(frame: CGRect, style: UITableView.Style) { 33 | super.init(frame: frame, style: style) 34 | 35 | configureTableView() 36 | } 37 | 38 | required init?(coder: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | 42 | private func configureTableView() { 43 | rowHeight = 75 44 | sectionHeaderTopPadding = 0 45 | isScrollEnabled = false 46 | 47 | register(QuestCell.self, forCellReuseIdentifier: QuestCell.reuseIdentifier) 48 | } 49 | } 50 | 51 | final class QuestViewDelegate: NSObject, UITableViewDelegate { 52 | private let header: QuestViewHeader 53 | private var type: QuestViewType! 54 | var itemDidDeleteClicked = PublishSubject() 55 | 56 | init(header: QuestViewHeader, type: QuestViewType) { 57 | self.header = header 58 | self.type = type 59 | } 60 | 61 | func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 62 | return 75 63 | } 64 | 65 | func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 66 | return header 67 | } 68 | 69 | func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { 70 | if type == .home { 71 | let deleteAction = UIContextualAction(style: .normal, title: nil) { action, view, success in 72 | if let quest = tableView.cellForRow(at: indexPath)?.layer.value(forKey: "item") as? Quest { 73 | self.itemDidDeleteClicked.onNext(quest) 74 | success(true) 75 | } 76 | } 77 | deleteAction.image = UIImage(systemName: "x.circle")? 78 | .withTintColor(.red, renderingMode: .alwaysOriginal) 79 | deleteAction.backgroundColor = UIColor.white 80 | 81 | let action = UISwipeActionsConfiguration(actions: [deleteAction]) 82 | action.performsFirstActionWithFullSwipe = false 83 | 84 | return action 85 | } else { 86 | return nil 87 | } 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Common/View/QuestViewHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuestViewHeader.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | import RxSwift 11 | import RxCocoa 12 | import SnapKit 13 | 14 | final class QuestViewHeader: UIStackView { 15 | private var disposableBag = DisposeBag() 16 | var buttonDidClick = PublishSubject() 17 | 18 | // MARK: - Components 19 | private(set) lazy var titleLabel: UILabel = { 20 | let titleLabel = UILabel() 21 | titleLabel.text = "오늘의 퀘스트" 22 | titleLabel.textColor = .maxViolet 23 | titleLabel.font = UIFont.boldSystemFont(ofSize: 32) 24 | 25 | return titleLabel 26 | }() 27 | 28 | private(set) lazy var plusButton: UIButton = { 29 | var config = UIButton.Configuration.maxStyle() 30 | let plusButton = UIButton(configuration: config) 31 | 32 | return plusButton 33 | }() 34 | 35 | // MARK: - Methods 36 | convenience init() { 37 | self.init(frame: .zero) 38 | 39 | axis = .horizontal 40 | alignment = .center 41 | distribution = .equalSpacing 42 | isLayoutMarginsRelativeArrangement = true 43 | layoutMargins = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 10) 44 | 45 | configureUI() 46 | bind() 47 | } 48 | 49 | private func configureUI() { 50 | addArrangedSubview(titleLabel) 51 | addArrangedSubview(plusButton) 52 | } 53 | 54 | private func bind() { 55 | plusButton.rx.tap 56 | .subscribe(onNext: { [weak self] _ in 57 | self?.buttonDidClick.onNext(()) 58 | }) 59 | .disposed(by: disposableBag) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Common/ViewController/LaunchViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LaunchViewController.swift 3 | // DailyQuest 4 | // 5 | // Created by 이다연 on 2022/12/12. 6 | // 7 | 8 | import UIKit 9 | import Lottie 10 | 11 | class LaunchViewController: UIViewController { 12 | 13 | let animationView: LottieAnimationView = { 14 | let animationView = LottieAnimationView(name: "max.lottie") 15 | animationView.frame = CGRect(x:0,y:0,width: 300, height: 400) 16 | animationView.contentMode = .scaleAspectFill 17 | return animationView 18 | }() 19 | 20 | override func viewDidLoad() { 21 | super.viewDidLoad() 22 | 23 | view.backgroundColor = .white 24 | view.addSubview(animationView) 25 | animationView.center = view.center 26 | 27 | animationView.play{ (finish) in 28 | if let scene = UIApplication.shared.connectedScenes.first?.delegate as? SceneDelegate { 29 | scene.switchRoot() 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Common/ViewModel/FriendViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FriendViewModel.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/12/08. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | import RxCocoa 12 | 13 | final class FriendViewModel { 14 | private(set) var user: User 15 | private let friendQuestUseCase: FriendQuestUseCase 16 | private let friendCalendarUseCase: CalendarUseCase 17 | 18 | init(user: User, 19 | friendQuestUseCase: FriendQuestUseCase, 20 | friendCalendarUseCase: CalendarUseCase) 21 | { 22 | self.user = user 23 | self.friendQuestUseCase = friendQuestUseCase 24 | self.friendCalendarUseCase = friendCalendarUseCase 25 | } 26 | 27 | struct Input { 28 | let viewDidLoad: Observable 29 | let daySelected: Observable 30 | let dragEventInCalendar: Observable 31 | } 32 | 33 | struct Output { 34 | let questHeaderLabel: Observable 35 | let userData: Driver 36 | let data: Driver<[Quest]> 37 | let currentMonth: Observable 38 | let displayDays: Driver<[[DailyQuestCompletion]]> 39 | } 40 | 41 | func transform(input: Input, disposableBag: DisposeBag) -> Output { 42 | 43 | let data = Observable 44 | .merge( 45 | input.viewDidLoad, 46 | input.daySelected 47 | ) 48 | .flatMap(fetch(by:)) 49 | .asDriver(onErrorJustReturn: []) 50 | 51 | let userData = input.viewDidLoad 52 | .compactMap({ [weak self] _ in self?.user }) 53 | .asDriver(onErrorJustReturn: User()) 54 | 55 | input 56 | .viewDidLoad 57 | .subscribe(onNext: { [weak self] _ in 58 | self?.friendCalendarUseCase.setupMonths() 59 | }) 60 | .disposed(by: disposableBag) 61 | 62 | input 63 | .dragEventInCalendar 64 | .subscribe(onNext: { [weak self] direction in 65 | switch direction { 66 | case .prev: 67 | self?.friendCalendarUseCase.fetchLastMontlyCompletion() 68 | case .none: 69 | break 70 | case .next: 71 | self?.friendCalendarUseCase.fetchNextMontlyCompletion() 72 | } 73 | }) 74 | .disposed(by: disposableBag) 75 | 76 | input.daySelected 77 | .bind { [weak self] date in 78 | self?.friendCalendarUseCase.selectDate(date) 79 | } 80 | .disposed(by: disposableBag) 81 | 82 | let questHeaderLabel = input 83 | .daySelected 84 | .map(calculateRelative(_:)) 85 | .asObservable() 86 | .share() 87 | 88 | let currentMonth = friendCalendarUseCase 89 | .currentMonth 90 | .asObserver() 91 | 92 | let displayDays = friendCalendarUseCase 93 | .completionOfMonths 94 | .asDriver(onErrorJustReturn: [[], [], []]) 95 | 96 | return Output(questHeaderLabel: questHeaderLabel, userData: userData, data: data, currentMonth: currentMonth, displayDays: displayDays) 97 | } 98 | } 99 | 100 | private extension FriendViewModel { 101 | func fetch(by date: Date) -> Observable<[Quest]> { 102 | return friendQuestUseCase.fetch(with: user.uuid, by: date) 103 | .asObservable() 104 | } 105 | } 106 | 107 | extension FriendViewModel { 108 | func calculateRelative(_ date: Date) -> String { 109 | let today = Date() 110 | if today.startOfDay == date.startOfDay { 111 | return "오늘의 퀘스트" 112 | } else { 113 | return "\(date.toFormatMonthDay)의 퀘스트 " 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Home/Flow/HomeCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeCoordinator.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | import RxSwift 11 | 12 | protocol HomeCoordinator: Coordinator { 13 | func showProfileFlow() 14 | func showAddQuestFlow() 15 | } 16 | 17 | final class DefaultHomeCoordinator: HomeCoordinator { 18 | private var disposableBag = DisposeBag() 19 | 20 | var finishDelegate: CoordinatorFinishDelegate? 21 | var childCoordinators: [Coordinator] = [] 22 | var navigationController: UINavigationController 23 | let homeSceneDIContainer: HomeSceneDIContainer 24 | 25 | init(navigationController: UINavigationController, 26 | homeSceneDIContainer: HomeSceneDIContainer) { 27 | self.navigationController = navigationController 28 | self.homeSceneDIContainer = homeSceneDIContainer 29 | } 30 | 31 | func start() { 32 | let homeViewController = homeSceneDIContainer.makeHomeViewController() 33 | navigationController.pushViewController(homeViewController, animated: false) 34 | navigationController.isNavigationBarHidden = true 35 | 36 | homeViewController 37 | .coordinatorPublisher 38 | .bind(onNext: { [weak self] event in 39 | switch event { 40 | case .showAddQuestsFlow: 41 | self?.showAddQuestFlow() 42 | case .showProfileFlow: 43 | self?.showProfileFlow() 44 | } 45 | }) 46 | .disposed(by: disposableBag) 47 | } 48 | 49 | func showProfileFlow() { 50 | let profileViewController = homeSceneDIContainer.makeProfileViewController() 51 | navigationController.present(profileViewController, animated: true) 52 | } 53 | 54 | func showAddQuestFlow() { 55 | let enrollViewController = homeSceneDIContainer.makeEnrollViewController() 56 | navigationController.present(enrollViewController, animated: true) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Home/View/DayNamePickerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DayNamePickerView.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/30. 6 | // 7 | 8 | import UIKit 9 | 10 | import RxSwift 11 | import RxCocoa 12 | 13 | final class DayNamePickerView: UIStackView { 14 | private(set) lazy var buttons: [UIButton] = { 15 | let days: [String] = { 16 | var calendar = Calendar.current 17 | calendar.locale = .init(identifier: "ko_KR") 18 | return calendar.veryShortWeekdaySymbols 19 | }() 20 | 21 | return days.map { day in 22 | var config = UIButton.Configuration.filled() 23 | config.title = day 24 | config.baseBackgroundColor = .maxLightYellow 25 | config.baseForegroundColor = .maxViolet 26 | 27 | let button = UIButton(configuration: config) 28 | 29 | return button 30 | } 31 | }() 32 | 33 | override init(frame: CGRect) { 34 | super.init(frame: frame) 35 | 36 | configureUI() 37 | } 38 | 39 | required init(coder: NSCoder) { 40 | fatalError("init(coder:) has not been implemented") 41 | } 42 | 43 | private func configureUI() { 44 | axis = .horizontal 45 | spacing = 2 46 | distribution = .fillEqually 47 | 48 | buttons.forEach { button in 49 | self.addArrangedSubview(button) 50 | } 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Home/View/MessageBubble.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessageBubble.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/22. 6 | // 7 | 8 | import UIKit 9 | 10 | final class MessageBubbleLabel: UILabel { 11 | private var topInset: CGFloat = 10.0 12 | private var bottomInset: CGFloat = 10.0 13 | private var leftInset: CGFloat = 15.0 14 | private var rightInset: CGFloat = 10.0 15 | 16 | override init(frame: CGRect) { 17 | super.init(frame: frame) 18 | } 19 | 20 | required init?(coder: NSCoder) { 21 | fatalError("init(coder:) has not been implemented") 22 | } 23 | 24 | convenience init(text: String) { 25 | self.init(frame: .zero) 26 | self.text = text 27 | } 28 | 29 | override func drawText(in rect: CGRect) { 30 | let insets = UIEdgeInsets(top: topInset, left: leftInset, bottom: bottomInset, right: rightInset) 31 | 32 | let bezierPath = UIBezierPath() 33 | let width = rect.width 34 | let height = rect.height 35 | 36 | bezierPath.move(to: CGPoint(x: 4, y: 25)) 37 | bezierPath.addLine(to: CGPoint(x: 4, y: 20)) 38 | bezierPath.addCurve(to: CGPoint(x: 20, y: 0), 39 | controlPoint1: CGPoint(x: 4, y: 8), 40 | controlPoint2: CGPoint(x: 12, y: 0)) 41 | 42 | bezierPath.addLine(to: CGPoint(x: width - 20, y: 0)) 43 | bezierPath.addCurve(to: CGPoint(x: width, y: 20), 44 | controlPoint1: CGPoint(x: width - 8, y: 0), 45 | controlPoint2: CGPoint(x: width, y: 8)) 46 | 47 | bezierPath.addLine(to: CGPoint(x: width, y: height - 20)) 48 | bezierPath.addCurve(to: CGPoint(x: width - 20, y: height), 49 | controlPoint1: CGPoint(x: width, y: height - 8), 50 | controlPoint2: CGPoint(x: width - 8, y: height)) 51 | 52 | bezierPath.addLine(to: CGPoint(x: 20, y: height)) 53 | bezierPath.addCurve(to: CGPoint(x: 4, y: height - 4), 54 | controlPoint1: CGPoint(x: 7, y: height), 55 | controlPoint2: CGPoint(x: 5, y: height - 3)) 56 | bezierPath.addCurve(to: CGPoint(x: 0, y: height), 57 | controlPoint1: CGPoint(x: 8, y: height - 3), 58 | controlPoint2: CGPoint(x: 4, y: height - 2)) 59 | 60 | 61 | UIColor.maxYellow.setFill() 62 | bezierPath.fill() 63 | bezierPath.close() 64 | 65 | super.drawText(in: rect.inset(by: insets)) 66 | } 67 | 68 | override var intrinsicContentSize: CGSize { 69 | let size = super.intrinsicContentSize 70 | return CGSize(width: size.width + leftInset + rightInset, 71 | height: size.height + topInset + bottomInset) 72 | } 73 | } 74 | 75 | extension MessageBubbleLabel { 76 | func setText(text: String) { 77 | self.text = text 78 | } 79 | } 80 | 81 | #if canImport(SwiftUI) && DEBUG 82 | import SwiftUI 83 | 84 | struct MessageBubblePreview: PreviewProvider { 85 | static var previews: some View { 86 | UIViewPreview { 87 | let bubble = MessageBubbleLabel(frame: .zero) 88 | 89 | return bubble 90 | } 91 | .previewLayout(.fixed(width: 350, height: 80)) 92 | } 93 | } 94 | #endif 95 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Home/View/PlanDatePickerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanDatePickerView.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/30. 6 | // 7 | 8 | import UIKit 9 | 10 | import RxSwift 11 | import SnapKit 12 | 13 | final class PlanDatePickerView: UIView { 14 | private var disposableBag = DisposeBag() 15 | 16 | private lazy var titleLabel: UILabel = { 17 | let titleLabel = UILabel() 18 | titleLabel.textColor = .maxViolet 19 | titleLabel.text = "description" 20 | 21 | return titleLabel 22 | }() 23 | 24 | private(set) lazy var datePicker: UIDatePicker = { 25 | let datePicker = UIDatePicker() 26 | datePicker.datePickerMode = .date 27 | datePicker.locale = .init(identifier: "ko_KR") 28 | 29 | return datePicker 30 | }() 31 | 32 | override init(frame: CGRect) { 33 | super.init(frame: frame) 34 | 35 | configureUI() 36 | } 37 | 38 | required init?(coder: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | 42 | private func configureUI() { 43 | backgroundColor = .maxLightYellow 44 | layer.cornerRadius = 15 45 | 46 | addSubview(titleLabel) 47 | addSubview(datePicker) 48 | 49 | titleLabel.snp.makeConstraints { make in 50 | make.top.leading.bottom.equalToSuperview().inset(15) 51 | } 52 | 53 | datePicker.snp.makeConstraints { make in 54 | make.edges.equalToSuperview().inset(15) 55 | } 56 | } 57 | 58 | func title(_ title: String) { 59 | titleLabel.text = title 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Home/View/QuantityView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuantityView.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/30. 6 | // 7 | 8 | import UIKit 9 | 10 | import RxSwift 11 | import RxCocoa 12 | 13 | final class QuantityView: UIView { 14 | 15 | private lazy var titleLabel: UILabel = { 16 | let titleLabel = UILabel() 17 | titleLabel.textColor = .maxViolet 18 | titleLabel.text = "목표량" 19 | 20 | return titleLabel 21 | }() 22 | 23 | private(set) lazy var quantityField: UITextField = { 24 | let quantityField = UITextField() 25 | quantityField.textAlignment = .right 26 | quantityField.placeholder = "0" 27 | quantityField.keyboardType = .numberPad 28 | 29 | return quantityField 30 | }() 31 | 32 | override init(frame: CGRect) { 33 | super.init(frame: frame) 34 | 35 | configureUI() 36 | } 37 | 38 | required init?(coder: NSCoder) { 39 | fatalError("init(coder:) has not been implemented") 40 | } 41 | 42 | private func configureUI() { 43 | backgroundColor = .maxLightYellow 44 | layer.cornerRadius = 15 45 | 46 | addSubview(titleLabel) 47 | addSubview(quantityField) 48 | 49 | titleLabel.snp.makeConstraints { make in 50 | make.top.leading.bottom.equalToSuperview().inset(15) 51 | } 52 | 53 | quantityField.snp.makeConstraints { make in 54 | make.top.trailing.bottom.equalToSuperview().inset(15) 55 | make.width.equalToSuperview().multipliedBy(0.3) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Home/View/UserImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserImageView.swift 3 | // DailyQuest 4 | // 5 | // Created by 이다연 on 2022/12/08. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | 11 | final class UserImageView: UIView { 12 | 13 | private(set) lazy var userImage: UIImageView = { 14 | let userImage = UIImageView() 15 | userImage.image = UIImage(named: "AppIcon") 16 | userImage.clipsToBounds = true 17 | userImage.layer.cornerRadius = 100.0 / 2 18 | userImage.contentMode = .scaleAspectFill 19 | return userImage 20 | }() 21 | 22 | 23 | override init(frame: CGRect) { 24 | super.init(frame: frame) 25 | addSubviews() 26 | setupConstraints() 27 | } 28 | 29 | required init?(coder: NSCoder) { 30 | fatalError("init(coder:) has not been implemented") 31 | } 32 | 33 | private func addSubviews() { 34 | addSubview(userImage) 35 | } 36 | 37 | private func setupConstraints() { 38 | userImage.snp.makeConstraints { make in 39 | make.centerX.centerY.equalToSuperview() 40 | make.width.height.equalTo(100) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Home/ViewModel/ProfileViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileViewModel.swift 3 | // DailyQuest 4 | // 5 | // Created by 이다연 on 2022/12/06. 6 | // 7 | 8 | import UIKit 9 | import RxSwift 10 | import RxCocoa 11 | 12 | final class ProfileViewModel { 13 | 14 | private let userUseCase: UserUseCase 15 | 16 | private var disposableBag = DisposeBag() 17 | 18 | init(userUseCase: UserUseCase) { 19 | self.userUseCase = userUseCase 20 | } 21 | 22 | struct Input { 23 | let viewDidLoad: Observable 24 | let changeProfileImage: Observable 25 | let changeIntroduceLabel : Observable 26 | } 27 | 28 | struct Output { 29 | let data: Driver 30 | let changeProfileImageResult: Driver 31 | let changeIntroduceLabelResult: Driver 32 | } 33 | 34 | func transform(input: Input) -> Output { 35 | let data = input 36 | .viewDidLoad 37 | .flatMap { _ in 38 | self.userUseCase.fetch() 39 | } 40 | .asDriver(onErrorJustReturn: User()) 41 | 42 | let changeProfileImageResult = input.changeProfileImage.flatMap { image in 43 | 44 | guard let image = image else { 45 | return Observable.just(nil) 46 | } 47 | 48 | guard let data = image.jpegData(compressionQuality: 0.9) else { 49 | return Observable.just(nil) 50 | } 51 | return self.userUseCase.saveProfileImage(data: data) 52 | .map{ _ in image } 53 | .catchAndReturn(nil) 54 | .asObservable() 55 | }.map{ _ in 56 | Void() 57 | }.flatMap(userUseCase.fetch) 58 | .asDriver(onErrorJustReturn: User()) 59 | 60 | let changeIntroduceLabelResult = input.changeIntroduceLabel.flatMap { introduce in 61 | return self.userUseCase.updateIntroduce(introduce: introduce) 62 | .map{ _ in introduce } 63 | .catchAndReturn(nil) 64 | .asObservable() 65 | }.map{ _ in 66 | Void() 67 | }.flatMap(userUseCase.fetch) 68 | .asDriver(onErrorJustReturn: User()) 69 | 70 | return Output(data: data, changeProfileImageResult: changeProfileImageResult, changeIntroduceLabelResult: changeIntroduceLabelResult) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Settings/Flow/SettingsCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsCoordinator.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/23. 6 | // 7 | 8 | import UIKit 9 | 10 | import RxSwift 11 | 12 | protocol SettingsCoordinator: Coordinator { 13 | func showLoginFlow() 14 | } 15 | 16 | final class DefaultSettingsCoordinator: SettingsCoordinator { 17 | private var disposableBag = DisposeBag() 18 | 19 | var finishDelegate: CoordinatorFinishDelegate? 20 | var childCoordinators: [Coordinator] = [] 21 | var navigationController: UINavigationController 22 | let settingsSceneDIContainer: SettingsSceneDIContainer 23 | 24 | init(navigationController: UINavigationController, 25 | settingsSceneDIContainer: SettingsSceneDIContainer) { 26 | self.navigationController = navigationController 27 | self.settingsSceneDIContainer = settingsSceneDIContainer 28 | } 29 | 30 | func start() { 31 | let settingsViewController = settingsSceneDIContainer.makeSettingsViewController() 32 | navigationController.pushViewController(settingsViewController, animated: false) 33 | 34 | settingsViewController 35 | .itemDidClick 36 | .bind(onNext: { [weak self] event in 37 | switch event { 38 | case .showLoginFlow: 39 | self?.showLoginFlow() 40 | } 41 | }) 42 | .disposed(by: disposableBag) 43 | } 44 | 45 | func showLoginFlow() { 46 | let loginViewController = settingsSceneDIContainer.makeLoginViewController() 47 | navigationController.pushViewController(loginViewController, animated: true) 48 | 49 | loginViewController 50 | .itemDidClick 51 | .bind(onNext: { [weak self] event in 52 | switch event { 53 | case .showSignUpFlow: 54 | self?.showSignUpFlow() 55 | case .back: 56 | self?.navigationController.popViewController(animated: true) 57 | } 58 | }) 59 | .disposed(by: disposableBag) 60 | } 61 | 62 | func showSignUpFlow() { 63 | let signUpViewController = settingsSceneDIContainer.makeSignUpViewController() 64 | navigationController.pushViewController(signUpViewController, animated: true) 65 | 66 | signUpViewController 67 | .itemDidClick 68 | .bind(onNext: { [weak self] event in 69 | switch event { 70 | case .back: 71 | self?.navigationController.popToRootViewController(animated: true) 72 | } 73 | }) 74 | .disposed(by: disposableBag) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Settings/View/CommonField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommonField.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/23. 6 | // 7 | 8 | import UIKit 9 | 10 | enum ViewType: String { 11 | case login = "로그인" 12 | case logout = "로그아웃" 13 | case version = "앱 버전" 14 | case delete = "탈퇴하기" 15 | } 16 | 17 | protocol CommonField { 18 | func register(for tableView: UITableView) 19 | func dequeue(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell 20 | func didSelect() -> ViewType? 21 | } 22 | 23 | extension CommonField { 24 | func didSelect() -> ViewType? { 25 | return nil 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Settings/View/NavigateField/NavigateCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigateCell.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/23. 6 | // 7 | 8 | import UIKit 9 | 10 | final class NavigateCell: UITableViewCell { 11 | static let reuseIdentifier = "NavigateCell" 12 | 13 | private let padding = 20 14 | 15 | private lazy var icon: UIImageView = { 16 | let icon = UIImageView() 17 | icon.image = UIImage(systemName: "pencil") 18 | icon.tintColor = .black 19 | 20 | return icon 21 | }() 22 | 23 | private lazy var title: UILabel = { 24 | let title = UILabel() 25 | title.text = "some setting title" 26 | title.textColor = .black 27 | 28 | return title 29 | }() 30 | 31 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 32 | super.init(style: style, reuseIdentifier: reuseIdentifier) 33 | 34 | configureUI() 35 | } 36 | 37 | required init?(coder: NSCoder) { 38 | fatalError("init(coder:) has not been implemented") 39 | } 40 | 41 | private func configureUI() { 42 | addSubview(icon) 43 | addSubview(title) 44 | 45 | icon.snp.makeConstraints { make in 46 | make.leading.top.bottom.equalToSuperview().inset(padding) 47 | } 48 | 49 | title.snp.makeConstraints { make in 50 | make.top.bottom.equalToSuperview().inset(padding) 51 | make.leading.equalTo(icon.snp.trailing).offset(10) 52 | } 53 | } 54 | 55 | func setup(with viewModel: NavigateItemViewModel) { 56 | icon.image = UIImage(systemName: viewModel.imageName) 57 | title.text = viewModel.viewType.rawValue 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Settings/View/NavigateField/NavigateField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigateField.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/23. 6 | // 7 | 8 | import UIKit 9 | 10 | final class NavigateField { 11 | private var viewModel: NavigateItemViewModel 12 | 13 | init(viewModel: NavigateItemViewModel) { 14 | self.viewModel = viewModel 15 | } 16 | 17 | func toggle(with status: Bool) { 18 | if status { 19 | viewModel.viewType = .logout 20 | } else { 21 | viewModel.viewType = .login 22 | } 23 | } 24 | } 25 | 26 | extension NavigateField: CommonField { 27 | func register(for tableView: UITableView) { 28 | tableView.register(NavigateCell.self, forCellReuseIdentifier: NavigateCell.reuseIdentifier) 29 | } 30 | 31 | func dequeue(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { 32 | guard let cell = tableView.dequeueReusableCell(withIdentifier: NavigateCell.reuseIdentifier, for: indexPath) as? NavigateCell else { 33 | return UITableViewCell() 34 | } 35 | cell.setup(with: viewModel) 36 | cell.accessoryType = .disclosureIndicator 37 | 38 | return cell 39 | } 40 | 41 | func didSelect() -> ViewType? { 42 | return viewModel.viewType 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Settings/View/NavigateField/NavigateItemViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NavigateItemViewModel.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct NavigateItemViewModel { 11 | let title: String 12 | let imageName: String 13 | var viewType: ViewType 14 | } 15 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Settings/View/PlainField/PlainCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlainCell.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/23. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | final class PlainCell: UITableViewCell { 13 | static let reuseIdentifier = "PlainCell" 14 | 15 | private let padding = 20 16 | 17 | private lazy var icon: UIImageView = { 18 | let icon = UIImageView() 19 | icon.image = UIImage(systemName: "pencil") 20 | icon.tintColor = .black 21 | 22 | return icon 23 | }() 24 | 25 | private lazy var title: UILabel = { 26 | let title = UILabel() 27 | title.text = "some setting title" 28 | title.textColor = .black 29 | 30 | return title 31 | }() 32 | 33 | private lazy var info: UILabel = { 34 | let info = UILabel() 35 | info.text = "some text" 36 | info.textColor = .maxViolet 37 | 38 | return info 39 | }() 40 | 41 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 42 | super.init(style: style, reuseIdentifier: reuseIdentifier) 43 | 44 | configureUI() 45 | } 46 | 47 | required init?(coder: NSCoder) { 48 | fatalError("init(coder:) has not been implemented") 49 | } 50 | 51 | private func configureUI() { 52 | addSubview(icon) 53 | addSubview(title) 54 | addSubview(info) 55 | 56 | icon.snp.makeConstraints { make in 57 | make.leading.top.bottom.equalToSuperview().inset(padding) 58 | } 59 | 60 | title.snp.makeConstraints { make in 61 | make.top.bottom.equalToSuperview().inset(padding) 62 | make.leading.equalTo(icon.snp.trailing).offset(10) 63 | } 64 | 65 | info.snp.makeConstraints { make in 66 | make.trailing.top.bottom.equalToSuperview().inset(padding) 67 | } 68 | } 69 | 70 | func setup(with viewModel: PlainItemViewModel) { 71 | icon.image = UIImage(systemName: viewModel.imageName) 72 | title.text = viewModel.title 73 | info.text = viewModel.info 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Settings/View/PlainField/PlainField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlainField.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/23. 6 | // 7 | 8 | import UIKit 9 | 10 | final class PlainField { 11 | private var viewModel: PlainItemViewModel 12 | 13 | init(viewModel: PlainItemViewModel) { 14 | self.viewModel = viewModel 15 | } 16 | } 17 | 18 | extension PlainField: CommonField { 19 | func register(for tableView: UITableView) { 20 | tableView.register(PlainCell.self, forCellReuseIdentifier: PlainCell.reuseIdentifier) 21 | } 22 | 23 | func dequeue(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { 24 | guard let cell = tableView.dequeueReusableCell(withIdentifier: PlainCell.reuseIdentifier, for: indexPath) as? PlainCell else { 25 | return UITableViewCell() 26 | } 27 | cell.setup(with: viewModel) 28 | 29 | return cell 30 | } 31 | 32 | func didSelect() -> ViewType? { 33 | return viewModel.viewType 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Settings/View/PlainField/PlainItemViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlainItemViewModel.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct PlainItemViewModel { 11 | let title: String 12 | let info: String 13 | let imageName: String 14 | let viewType: ViewType 15 | } 16 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Settings/View/ToggleField/ToggleCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToggleCell.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/23. 6 | // 7 | 8 | import UIKit 9 | import RxSwift 10 | import RxCocoa 11 | 12 | final class ToggleCell: UITableViewCell { 13 | static let reuseIdentifier = "ToggleCell" 14 | private var viewModel: ToggleItemViewModel! 15 | 16 | var toggleItemDidClicked = PublishSubject() 17 | private var disposableBag = DisposeBag() 18 | 19 | private let padding = 20 20 | 21 | private lazy var icon: UIImageView = { 22 | let icon = UIImageView() 23 | icon.image = UIImage(systemName: "pencil") 24 | icon.tintColor = .black 25 | 26 | return icon 27 | }() 28 | 29 | private lazy var title: UILabel = { 30 | let title = UILabel() 31 | title.text = "some setting title" 32 | title.textColor = .black 33 | 34 | return title 35 | }() 36 | 37 | private lazy var toggle: UISwitch = { 38 | let toggle = UISwitch() 39 | 40 | return toggle 41 | }() 42 | 43 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 44 | super.init(style: style, reuseIdentifier: reuseIdentifier) 45 | 46 | configureUI() 47 | } 48 | 49 | required init?(coder: NSCoder) { 50 | fatalError("init(coder:) has not been implemented") 51 | } 52 | 53 | private func configureUI() { 54 | contentView.addSubview(icon) 55 | contentView.addSubview(title) 56 | contentView.addSubview(toggle) 57 | 58 | icon.snp.makeConstraints { make in 59 | make.leading.top.bottom.equalToSuperview().inset(padding) 60 | } 61 | 62 | title.snp.makeConstraints { make in 63 | make.top.bottom.equalToSuperview().inset(padding) 64 | make.leading.equalTo(icon.snp.trailing).offset(10) 65 | } 66 | 67 | toggle.snp.makeConstraints { make in 68 | make.centerY.equalToSuperview() 69 | make.trailing.equalToSuperview().inset(padding) 70 | } 71 | } 72 | 73 | func setup(with viewModel: ToggleItemViewModel) { 74 | icon.image = UIImage(systemName: viewModel.imageName) 75 | title.text = viewModel.title 76 | self.viewModel = viewModel 77 | bind() 78 | } 79 | 80 | func bind() { 81 | toggle.rx.tapGesture() 82 | .when(.ended) 83 | .bind(onNext: {_ in 84 | self.toggleItemDidClicked.onNext(!self.toggle.isOn) 85 | }) 86 | .disposed(by: disposableBag) 87 | 88 | let output = viewModel.transform(input: ToggleItemViewModel.Input( 89 | toggleItemDidClicked: toggleItemDidClicked 90 | )) 91 | 92 | output.toggleItemResult 93 | .subscribe(onNext: { isOn in 94 | guard let isOn = isOn else { 95 | self.toggle.isOn = false 96 | self.toggle.isEnabled = false 97 | return 98 | } 99 | 100 | if !self.toggle.isEnabled { 101 | self.toggle.isEnabled = true 102 | } 103 | DispatchQueue.main.async { 104 | self.toggle.isOn = isOn 105 | } 106 | }) 107 | .disposed(by: disposableBag) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Settings/View/ToggleField/ToggleField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToggleField.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/23. 6 | // 7 | 8 | import UIKit 9 | 10 | final class ToggleField { 11 | private var viewModel: ToggleItemViewModel 12 | 13 | init(viewModel: ToggleItemViewModel) { 14 | self.viewModel = viewModel 15 | } 16 | } 17 | 18 | extension ToggleField: CommonField { 19 | func register(for tableView: UITableView) { 20 | tableView.register(ToggleCell.self, forCellReuseIdentifier: ToggleCell.reuseIdentifier) 21 | } 22 | 23 | func dequeue(for tableView: UITableView, at indexPath: IndexPath) -> UITableViewCell { 24 | guard let cell = tableView.dequeueReusableCell(withIdentifier: ToggleCell.reuseIdentifier, for: indexPath) as? ToggleCell else { 25 | return UITableViewCell() 26 | } 27 | cell.setup(with: viewModel) 28 | 29 | return cell 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Settings/View/ToggleField/ToggleItemViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ToggleItemViewModel.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/23. 6 | // 7 | 8 | import Foundation 9 | import RxSwift 10 | 11 | struct ToggleItemViewModel { 12 | let title: String 13 | let imageName: String 14 | let settingsUseCase: SettingsUseCase! 15 | 16 | struct Input { 17 | let toggleItemDidClicked: Observable 18 | } 19 | 20 | struct Output { 21 | let toggleItemResult: Observable 22 | } 23 | 24 | func transform(input: Input) -> Output { 25 | let fetchAllow = settingsUseCase.isLoggedIn() 26 | .flatMap { _ in settingsUseCase.fetchAllow() } 27 | .asObservable() 28 | 29 | let changeAllow = input.toggleItemDidClicked 30 | .flatMap { isOn in 31 | settingsUseCase.updateAllow(allow: isOn).asObservable() 32 | .map { result in result ? isOn : nil } 33 | } 34 | 35 | let toggleItemResult = Observable.merge(fetchAllow, changeAllow) 36 | 37 | return Output(toggleItemResult: toggleItemResult) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Settings/ViewModel/LoginViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewModel.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/28. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | import RxCocoa 12 | 13 | final class LoginViewModel { 14 | private let authUseCase: AuthUseCase 15 | 16 | init(authUseCase: AuthUseCase) { 17 | self.authUseCase = authUseCase 18 | } 19 | 20 | struct Input { 21 | let emailFieldDidEditEvent: Observable 22 | let passwordFieldDidEditEvent: Observable 23 | let submitButtonDidTapEvent: Observable 24 | } 25 | 26 | struct Output { 27 | let buttonEnabled: Driver 28 | let loginResult: Observable 29 | } 30 | 31 | func transform(input: Input, disposeBag: DisposeBag) -> Output { 32 | let buttonEnabled = Observable 33 | .combineLatest(input.emailFieldDidEditEvent, 34 | input.passwordFieldDidEditEvent) { !$0.isEmpty && !$1.isEmpty } 35 | .asDriver(onErrorJustReturn: false) 36 | 37 | let loginResult = input 38 | .submitButtonDidTapEvent 39 | .withLatestFrom( 40 | Observable 41 | .combineLatest(input.emailFieldDidEditEvent, 42 | input.passwordFieldDidEditEvent) 43 | ) 44 | .flatMap(authUseCase.signIn(email:password:)) 45 | 46 | return Output(buttonEnabled: buttonEnabled, loginResult: loginResult) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Settings/ViewModel/SettingsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsViewModel.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/12/06. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class SettingsViewModel { 13 | private(set) var fields: [CommonField] 14 | private var toggleField: ToggleField 15 | private var plainField: PlainField 16 | private var navigateField: NavigateField 17 | 18 | private let settingsUseCase: SettingsUseCase 19 | private var disposableBag = DisposeBag() 20 | 21 | init(settingsUseCase: SettingsUseCase) { 22 | let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String 23 | 24 | self.settingsUseCase = settingsUseCase 25 | 26 | self.toggleField = ToggleField(viewModel: .init(title: "둘러보기 허용", 27 | imageName: "person.crop.circle.badge.checkmark", 28 | settingsUseCase: settingsUseCase)) 29 | self.plainField = PlainField(viewModel: .init(title: "앱 버전", info: version, imageName: "exclamationmark.transmission", viewType: .version)) 30 | self.navigateField = NavigateField(viewModel: .init(title: "로그인", imageName: "person.circle.fill", viewType: .login)) 31 | 32 | self.fields = [ 33 | toggleField, 34 | plainField, 35 | navigateField 36 | ] 37 | } 38 | 39 | struct Input {} 40 | 41 | struct Output { 42 | let loginStatusDidChange: Observable 43 | } 44 | 45 | func transform() -> Output { 46 | let loginStatusDidChange = settingsUseCase 47 | .isLoggedIn() 48 | .do(onNext: { result in 49 | if result { 50 | let deleteUserField = PlainField(viewModel: .init(title: "탈퇴하기", info: "", imageName: "person.fill.xmark", viewType: .delete)) 51 | self.fields = [ 52 | self.toggleField, 53 | self.plainField, 54 | self.navigateField, 55 | deleteUserField 56 | ] 57 | } else { 58 | self.fields = [ 59 | self.toggleField, 60 | self.plainField, 61 | self.navigateField 62 | ] 63 | } 64 | self.navigateField.toggle(with: result) 65 | }) 66 | .map { _ in Void() } 67 | 68 | return Output(loginStatusDidChange: loginStatusDidChange) 69 | } 70 | 71 | func signOut() -> Observable { 72 | return settingsUseCase.signOut() 73 | } 74 | 75 | func deleteUser() -> Single { 76 | return settingsUseCase.delete() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Presentation/Settings/ViewModel/SignUpViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpViewModel.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/12/06. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | import RxCocoa 12 | 13 | final class SignUpViewModel { 14 | private let authUseCase: AuthUseCase 15 | 16 | init(authUseCase: AuthUseCase) { 17 | self.authUseCase = authUseCase 18 | } 19 | 20 | struct Input { 21 | let emailFieldDidEditEvent: Observable 22 | let passwordFieldDidEditEvent: Observable 23 | let passwordConfirmFieldDidEditEvent: Observable 24 | let nickNameFieldDidEditEvent: Observable 25 | let submitButtonDidTapEvent: Observable 26 | } 27 | 28 | struct Output { 29 | let buttonEnabled: Driver 30 | let signUpResult: Observable 31 | } 32 | 33 | func transform(input: Input, disposeBag: DisposeBag) -> Output { 34 | let buttonEnabled = Observable 35 | .combineLatest(input.emailFieldDidEditEvent, 36 | input.passwordFieldDidEditEvent, 37 | input.passwordConfirmFieldDidEditEvent, 38 | input.nickNameFieldDidEditEvent, 39 | resultSelector: checkButtonEnble(emailText: passwordText: passwordConfirmText: nickName:)) 40 | .asDriver(onErrorJustReturn: false) 41 | 42 | let signUpResult = input 43 | .submitButtonDidTapEvent 44 | .withLatestFrom(Observable 45 | .combineLatest(input.emailFieldDidEditEvent, 46 | input.passwordFieldDidEditEvent, 47 | input.nickNameFieldDidEditEvent, 48 | resultSelector: { ($0, $1, User(nickName: $2)) })) 49 | .flatMap (authUseCase.signUp(email: password: user:)) 50 | 51 | return Output(buttonEnabled: buttonEnabled, signUpResult: signUpResult) 52 | } 53 | } 54 | 55 | private extension SignUpViewModel { 56 | func checkEmpty(_ strArray: [String]) -> Bool { 57 | return !strArray.reduce(false) { $0 || $1.isEmpty } 58 | } 59 | 60 | func checkSame(str1: String, str2: String) -> Bool { 61 | return str1 == str2 62 | } 63 | 64 | func checkEmail(str: String) -> Bool { 65 | 66 | guard let index = str.firstIndex(of: "@") else { return false } 67 | let pos = str.distance(from: str.startIndex, to: index) 68 | return 0 < pos && pos < str.count - 1 69 | } 70 | 71 | func checkPasswordCount(str: String) -> Bool { 72 | return str.count >= 6 73 | } 74 | 75 | func checkButtonEnble(emailText: String, 76 | passwordText: String, 77 | passwordConfirmText: String, 78 | nickName: String) -> Bool { 79 | return checkEmail(str: emailText) && 80 | checkEmpty([emailText, passwordText, passwordConfirmText, nickName]) && 81 | checkSame(str1: passwordText, str2: passwordConfirmText) && 82 | checkPasswordCount(str: passwordText) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.490", 9 | "green" : "0.871", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Assets.xcassets/AppIcon.appiconset/AppIcon (1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS03-DailyQuest/61306fb766800eb23fa8231254a6d53185c79723/DailyQuest/DailyQuest/Resource/Assets.xcassets/AppIcon.appiconset/AppIcon (1).png -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "AppIcon (1).png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Assets.xcassets/ColorSet/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Assets.xcassets/ColorSet/MaxDarkYellow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.357", 9 | "green" : "0.733", 10 | "red" : "0.863" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Assets.xcassets/ColorSet/MaxGreen.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.663", 9 | "green" : "0.722", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Assets.xcassets/ColorSet/MaxLightBlue.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.792", 9 | "green" : "0.741", 10 | "red" : "0.682" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Assets.xcassets/ColorSet/MaxLightGrey.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.859", 9 | "green" : "0.859", 10 | "red" : "0.859" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Assets.xcassets/ColorSet/MaxLightYellow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.831", 9 | "green" : "0.953", 10 | "red" : "0.973" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Assets.xcassets/ColorSet/MaxRed.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.184", 9 | "green" : "0.145", 10 | "red" : "0.922" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Assets.xcassets/ColorSet/MaxViolet.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.235", 9 | "green" : "0.114", 10 | "red" : "0.365" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Assets.xcassets/ColorSet/MaxYellow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.490", 9 | "green" : "0.871", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Assets.xcassets/NoMoreQuests.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "NoMoreQuests.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Assets.xcassets/NoMoreQuests.imageset/NoMoreQuests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS03-DailyQuest/61306fb766800eb23fa8231254a6d53185c79723/DailyQuest/DailyQuest/Resource/Assets.xcassets/NoMoreQuests.imageset/NoMoreQuests.png -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Assets.xcassets/StatusMax.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Max 7.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Assets.xcassets/StatusMax.imageset/Max 7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS03-DailyQuest/61306fb766800eb23fa8231254a6d53185c79723/DailyQuest/DailyQuest/Resource/Assets.xcassets/StatusMax.imageset/Max 7.png -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Assets.xcassets/defaultBackground.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "blur.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Assets.xcassets/defaultBackground.imageset/blur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS03-DailyQuest/61306fb766800eb23fa8231254a6d53185c79723/DailyQuest/DailyQuest/Resource/Assets.xcassets/defaultBackground.imageset/blur.png -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIUserInterfaceStyle 6 | Light 7 | UIApplicationSceneManifest 8 | 9 | UIApplicationSupportsMultipleScenes 10 | 11 | UISceneConfigurations 12 | 13 | UIWindowSceneSessionRoleApplication 14 | 15 | 16 | UISceneConfigurationName 17 | Default Configuration 18 | UISceneDelegateClassName 19 | $(PRODUCT_MODULE_NAME).SceneDelegate 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Resource/ko.lproj/LaunchScreen.strings: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Utils/Alertable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Alertable.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/12/07. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol Alertable {} 11 | 12 | extension Alertable where Self: UIViewController { 13 | func showAlert(title: String, 14 | message: String, 15 | preferredStyle: UIAlertController.Style = .alert, 16 | completion: (() -> Void)? = nil) 17 | { 18 | let title = title 19 | let message = message 20 | let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) 21 | 22 | let okAction = UIAlertAction(title: "Ok", style: .default) 23 | 24 | alert.addAction(okAction) 25 | 26 | self.present(alert, animated: true, completion: completion) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Utils/Notification+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notification+.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/12/06. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Notification.Name { 11 | static let updated = Notification.Name("updated") 12 | static let userUpdated = Notification.Name("userUpdated") 13 | static let questStateChanged = Notification.Name("questStateChanged") 14 | } 15 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Utils/StatusView+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusView+.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/12/08. 6 | // 7 | 8 | import Foundation 9 | 10 | extension StatusView { 11 | private var messages: [String] { 12 | ["화이팅", "잘 할 수 있어", "오늘은 공부를 해보자!", "Hello, World!", "🎹🎵🎶🎵🎶"] 13 | } 14 | 15 | func getRandomMessage() -> String { 16 | return messages.randomElement() ?? "Hello,World!" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Utils/String+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+.swift 3 | // DailyQuest 4 | // 5 | // Created by 이전희 on 2022/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | func toDate() -> Date? { 12 | let dateFormatter = DateFormatter() 13 | dateFormatter.dateFormat = "yyyy-MM-dd" 14 | dateFormatter.timeZone = TimeZone(identifier: "UTC") 15 | return dateFormatter.date(from: self) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Utils/SwiftUIPreview.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftUIPreview.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/15. 6 | // 7 | 8 | import Foundation 9 | 10 | #if canImport(SwiftUI) && DEBUG 11 | import SwiftUI 12 | struct UIViewPreview: UIViewRepresentable { 13 | let view: View 14 | 15 | init(_ builder: @escaping () -> View) { 16 | view = builder() 17 | } 18 | 19 | // MARK: - UIViewRepresentable 20 | 21 | func makeUIView(context: Context) -> UIView { 22 | return view 23 | } 24 | 25 | func updateUIView(_ view: UIView, context: Context) { 26 | view.setContentHuggingPriority(.defaultHigh, for: .horizontal) 27 | view.setContentHuggingPriority(.defaultHigh, for: .vertical) 28 | } 29 | } 30 | #endif 31 | 32 | enum DeviceType { 33 | case iPhoneSE2 34 | case iPhone8 35 | case iPhone12Pro 36 | case iPhone12ProMax 37 | case iPhone14 38 | case iPhone14Pro 39 | 40 | func name() -> String { 41 | switch self { 42 | case .iPhoneSE2: 43 | return "iPhone SE" 44 | case .iPhone8: 45 | return "iPhone 8" 46 | case .iPhone12Pro: 47 | return "iPhone 12 Pro" 48 | case .iPhone12ProMax: 49 | return "iPhone 12 Pro Max" 50 | case .iPhone14: 51 | return "iPhone 14" 52 | case .iPhone14Pro: 53 | return "iPhone 14 Pro" 54 | } 55 | } 56 | } 57 | 58 | #if canImport(SwiftUI) && DEBUG 59 | import SwiftUI 60 | extension UIViewController { 61 | 62 | private struct Preview: UIViewControllerRepresentable { 63 | let viewController: UIViewController 64 | 65 | func makeUIViewController(context: Context) -> UIViewController { 66 | return viewController 67 | } 68 | 69 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) { 70 | } 71 | } 72 | 73 | func showPreview(_ deviceType: DeviceType = .iPhone12Pro) -> some View { 74 | Preview(viewController: self).previewDevice(PreviewDevice(rawValue: deviceType.name())) 75 | } 76 | } 77 | #endif 78 | 79 | // MARK: - Cell Preview 80 | /* 81 | #if canImport(SwiftUI) && DEBUG 82 | import SwiftUI 83 | 84 | struct QuestCellPreview: PreviewProvider{ 85 | static var previews: some View { 86 | UIViewPreview { 87 | let cell = QuestCell(frame: .zero) 88 | /** Cell setup code */ 89 | return cell 90 | } 91 | .previewLayout(.sizeThatFits) 92 | } 93 | } 94 | 95 | #endif 96 | */ 97 | 98 | // MARK: - View Controller Preview 99 | /* 100 | #if canImport(SwiftUI) && DEBUG 101 | import SwiftUI 102 | 103 | struct ViewController_Preview: PreviewProvider { 104 | static var previews: some View { 105 | ViewController().showPreview(.iPhone8) 106 | } 107 | } 108 | #endif 109 | */ 110 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Utils/UIButton+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIButton+.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | import Kingfisher 11 | 12 | extension UIButton.Configuration { 13 | public static func maxStyle() -> UIButton.Configuration { 14 | var style = UIButton.Configuration.plain() 15 | style.image = UIImage(systemName: "plus") 16 | style.baseForegroundColor = .maxViolet 17 | style.background.backgroundColor = .maxLightYellow 18 | style.cornerStyle = .capsule 19 | 20 | return style 21 | } 22 | } 23 | 24 | extension UIButton { 25 | func setImage(with urlString: String) { 26 | ImageCache.default.retrieveImage(forKey: urlString, options: nil) { result in 27 | switch result { 28 | case .success(let value): 29 | if let image = value.image { 30 | self.setImage(image, for: .normal) 31 | } else { 32 | guard let url = URL(string: urlString) else { return } 33 | let resource = ImageResource(downloadURL: url, cacheKey: urlString) 34 | self.kf.setImage(with: resource, for: .normal) 35 | } 36 | case .failure(let error): 37 | print(error) 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Utils/UIColor+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+.swift 3 | // DailyQuest 4 | // 5 | // Created by jinwoong Kim on 2022/11/15. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIColor { 11 | static let maxDarkYellow = UIColor(named: "MaxDarkYellow") ?? .white 12 | static let maxGreen = UIColor(named: "MaxGreen") ?? .white 13 | static let maxLightGrey = UIColor(named: "MaxLightGrey") ?? .white 14 | static let maxLightYellow = UIColor(named: "MaxLightYellow") ?? .white 15 | static let maxRed = UIColor(named: "MaxRed") ?? .white 16 | static let maxViolet = UIColor(named: "MaxViolet") ?? .white 17 | static let maxYellow = UIColor(named: "MaxYellow") ?? .white 18 | static let maxLightBlue = UIColor(named: "MaxLightBlue") ?? .white 19 | } 20 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuest/Utils/UIImageView+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageView+.swift 3 | // DailyQuest 4 | // 5 | // Created by 이다연 on 2022/12/06. 6 | // 7 | 8 | import UIKit 9 | import Kingfisher 10 | 11 | extension UIImageView { 12 | func setImage(with urlString: String) { 13 | ImageCache.default.retrieveImage(forKey: urlString, options: nil) { result in 14 | switch result { 15 | case .success(let value): 16 | if let image = value.image { 17 | self.image = image 18 | } else { 19 | guard let url = URL(string: urlString) else { return } 20 | let resource = ImageResource(downloadURL: url, cacheKey: urlString) 21 | self.kf.setImage(with: resource) 22 | } 23 | case .failure(let error): 24 | print(error) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuestTests/Data/QuestsRepositoryTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuestsRepositoryTests.swift 3 | // DailyQuestTests 4 | // 5 | // Created by 이전희 on 2022/12/05. 6 | // 7 | 8 | import XCTest 9 | import RxSwift 10 | 11 | final class QuestsRepositoryTests: XCTestCase { 12 | private var questsStorage: QuestsStorage! 13 | private var questsRepository: QuestsRepository! 14 | private var disposeBag = DisposeBag() 15 | 16 | override func setUpWithError() throws { 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | } 19 | 20 | override func tearDownWithError() throws { 21 | // Put teardown code here. This method is called after the invocation of each test method in the class. 22 | } 23 | 24 | func test_noAuthAndSave() throws { 25 | let dummyDate = ["2022-12-05".toDate()!, 26 | "2022-12-05".toDate()!, 27 | "2022-12-05".toDate()!, 28 | "2022-12-05".toDate()!, 29 | "2022-12-06".toDate()!, 30 | "2022-12-06".toDate()!, 31 | "2022-12-06".toDate()!, 32 | "2022-12-07".toDate()!, 33 | "2022-12-07".toDate()!, 34 | "2022-12-07".toDate()!] 35 | let groupId = UUID() 36 | let dummyData = (0..<10).map { i in 37 | Quest(groupId: groupId, uuid: UUID(), date: dummyDate[i % dummyDate.count], title: "title \(i)", currentCount: (0...((i % 10) + 1)).randomElement()!, totalCount: (i % 10) + 1) 38 | } 39 | 40 | questsStorage = RealmQuestsStorage() 41 | questsRepository = DefaultQuestsRepository(persistentStorage: questsStorage) 42 | questsRepository.save(with: dummyData) 43 | .subscribe { quests in 44 | // XCTAssertEqual(dummyData, quests) 45 | } onFailure: { error in 46 | XCTFail(error.localizedDescription) 47 | } onDisposed: { 48 | print("❎ Disposed") 49 | XCTSkip() 50 | }.disposed(by: disposeBag) 51 | } 52 | 53 | func test_noAuthAnd() throws { 54 | 55 | } 56 | 57 | func testPerformanceExample() throws { 58 | // This is an example of a performance test case. 59 | self.measure { 60 | // Put the code you want to measure the time of here. 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuestTests/Domain/Mocks/BrowseRepositoryMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowseRepositoryMock.swift 3 | // DailyQuestTests 4 | // 5 | // Created by jinwoong Kim on 2022/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class BrowseRepositoryMock: BrowseRepository { 13 | let browseQuests: [BrowseQuest] 14 | 15 | init() { 16 | let user1 = User.stub(nickName: "jinwoong") 17 | let user2 = User.stub(nickName: "Jose") 18 | 19 | let quests = [ 20 | Quest.stub(groupId: UUID(), 21 | uuid: UUID(), 22 | date: Date(), 23 | title: "물마시기", 24 | currentCount: 0, 25 | totalCount: 10), 26 | Quest.stub(groupId: UUID(), 27 | uuid: UUID(), 28 | date: Date(), 29 | title: "물마시기", 30 | currentCount: 0, 31 | totalCount: 10), 32 | ] 33 | 34 | self.browseQuests = [BrowseQuest.stub(user: user1, quests: quests), 35 | BrowseQuest.stub(user: user2, quests: quests)] 36 | } 37 | 38 | func fetch() -> Observable<[BrowseQuest]> { 39 | return .just(browseQuests) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuestTests/Domain/Mocks/QuestRepositoryMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuestRepositoryMock.swift 3 | // DailyQuestTests 4 | // 5 | // Created by jinwoong Kim on 2022/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class QuestRepositoryMock: QuestsRepository { 13 | func fetch(by uuid: String, date: Date) -> RxSwift.Observable<[Quest]> { 14 | .just([]) 15 | } 16 | 17 | let quests = [ 18 | Quest.stub(groupId: UUID(), 19 | uuid: UUID(), 20 | date: Date(), 21 | title: "물마시기", 22 | currentCount: 0, 23 | totalCount: 10), 24 | Quest.stub(groupId: UUID(), 25 | uuid: UUID(), 26 | date: Date(), 27 | title: "물마시기", 28 | currentCount: 0, 29 | totalCount: 10), 30 | ] 31 | 32 | func save(with quest: [Quest]) -> Single<[Quest]> { 33 | 34 | return Single.just([]) 35 | } 36 | 37 | func update(with quest: Quest) -> Single { 38 | return Single.just(quests[0]) 39 | } 40 | 41 | func delete(with questId: UUID) -> Single { 42 | return Single.just(quests[0]) 43 | } 44 | 45 | func deleteAll(with groupId: UUID) -> Single<[Quest]> { 46 | return Single.just(quests) 47 | } 48 | 49 | func fetch(by date: Date) -> Observable<[Quest]> { 50 | return Observable.just(quests) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuestTests/Domain/UseCases/BrowseUseCaseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowseUseCaseTests.swift 3 | // DailyQuestTests 4 | // 5 | // Created by jinwoong Kim on 2022/11/29. 6 | // 7 | 8 | import XCTest 9 | 10 | import RxSwift 11 | 12 | final class BrowseUseCaseTests: XCTestCase { 13 | private var browseUseCase: BrowseUseCase! 14 | private var browseRepo: BrowseRepository! 15 | private var disposeBag = DisposeBag() 16 | 17 | override func setUpWithError() throws { 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | browseRepo = nil 24 | browseUseCase = nil 25 | disposeBag = .init() 26 | } 27 | 28 | func testBrowseUseCase_WhenRepoSendCorrectQuests_ThenExpectationWillFulfillWithSuccess() { 29 | // given 30 | browseRepo = BrowseRepositoryMock() 31 | 32 | browseUseCase = DefaultBrowseUseCase(browseRepository: browseRepo) 33 | 34 | let expectation = XCTestExpectation(description: "test success") 35 | 36 | // when 37 | browseUseCase 38 | .excute() 39 | // then 40 | .subscribe(onNext: { browseQuests in 41 | print(browseQuests) 42 | expectation.fulfill() 43 | }, onError: { error in 44 | print(error) 45 | }) 46 | .disposed(by: disposeBag) 47 | 48 | wait(for: [expectation], timeout: 1) 49 | } 50 | 51 | func testPerformanceExample() throws { 52 | // This is an example of a performance test case. 53 | self.measure { 54 | // Put the code you want to measure the time of here. 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuestTests/Domain/UseCases/QuestUseCaseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuestUseCaseTests.swift 3 | // DailyQuestTests 4 | // 5 | // Created by jinwoong Kim on 2022/11/21. 6 | // 7 | 8 | import XCTest 9 | 10 | import RxSwift 11 | 12 | final class QuestUseCaseTests: XCTestCase { 13 | private var questUseCase: QuestUseCase! 14 | private var questRepo: QuestsRepository! 15 | private var disposeBag = DisposeBag() 16 | 17 | override func setUpWithError() throws { 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | 20 | } 21 | 22 | override func tearDownWithError() throws { 23 | // Put teardown code here. This method is called after the invocation of each test method in the class. 24 | questRepo = nil 25 | questUseCase = nil 26 | disposeBag = .init() 27 | } 28 | 29 | func testQuestUseCase_WhenRepoSendCorrectQuests_ThenExpectationWillBeFulfilledWithSuccess() { 30 | // given 31 | questRepo = QuestRepositoryMock() 32 | 33 | questUseCase = DefaultQuestUseCase(questsRepository: questRepo) 34 | 35 | let expectation = XCTestExpectation(description: "test success") 36 | 37 | // when 38 | questUseCase 39 | .fetch(by: Date()) 40 | // then 41 | .subscribe(onNext: { data in 42 | expectation.fulfill() 43 | }, onError: { _ in 44 | XCTFail("test failed") 45 | }) 46 | .disposed(by: disposeBag) 47 | 48 | wait(for: [expectation], timeout: 1) 49 | } 50 | 51 | func testPerformanceExample() throws { 52 | // This is an example of a performance test case. 53 | self.measure { 54 | // Put the code you want to measure the time of here. 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuestTests/Mocks/BrowseQuest+Stub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowseQuest+Stub.swift 3 | // DailyQuestTests 4 | // 5 | // Created by jinwoong Kim on 2022/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | extension BrowseQuest { 11 | static func stub(user: User, quests: [Quest]) -> Self { 12 | return .init(user: user, quests: quests) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuestTests/Mocks/Quest+Stub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Quest+Stub.swift 3 | // DailyQuestTests 4 | // 5 | // Created by jinwoong Kim on 2022/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Quest { 11 | static func stub(groupId: UUID, 12 | uuid: UUID, 13 | date: Date, 14 | title: String, 15 | currentCount: Int, 16 | totalCount: Int) -> Self { 17 | return .init(groupId: groupId, 18 | uuid: uuid, 19 | date: date, 20 | title: title, 21 | currentCount: currentCount, 22 | totalCount: totalCount) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuestTests/Mocks/User+Stub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User+Stub.swift 3 | // DailyQuestTests 4 | // 5 | // Created by jinwoong Kim on 2022/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | extension User { 11 | static func stub(uuid: String = "", 12 | nickName: String, 13 | profileURL: String = "", 14 | backgroundImageURL: String = "", 15 | introduce: String = "", 16 | allow: Bool = true) -> Self { 17 | return .init(uuid: uuid, 18 | nickName: nickName, 19 | profileURL: profileURL, 20 | backgroundImageURL: backgroundImageURL, 21 | introduce: introduce, 22 | allow: allow) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuestTests/Presentation/Mocks/BrowseUseCaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowseUseCaseMock.swift 3 | // DailyQuestTests 4 | // 5 | // Created by jinwoong Kim on 2022/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class BrowseUseCaseMock: BrowseUseCase { 13 | let browseQuests: [BrowseQuest] 14 | 15 | init() { 16 | let user1 = User.stub(nickName: "jinwoong") 17 | let user2 = User.stub(nickName: "Jose") 18 | 19 | let quests = [ 20 | Quest.stub(groupId: UUID(), 21 | uuid: UUID(), 22 | date: Date(), 23 | title: "물마시기", 24 | currentCount: 0, 25 | totalCount: 10), 26 | Quest.stub(groupId: UUID(), 27 | uuid: UUID(), 28 | date: Date(), 29 | title: "물마시기", 30 | currentCount: 0, 31 | totalCount: 10), 32 | ] 33 | 34 | self.browseQuests = [BrowseQuest.stub(user: user1, quests: quests), 35 | BrowseQuest.stub(user: user2, quests: quests)] 36 | } 37 | 38 | func excute() -> Observable<[BrowseQuest]> { 39 | return .just(browseQuests) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuestTests/Presentation/Mocks/QuestUseCaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuestUseCaseMock.swift 3 | // DailyQuestTests 4 | // 5 | // Created by jinwoong Kim on 2022/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class QuestUseCaseMock: QuestUseCase { 13 | func update(with quest: Quest) -> RxSwift.Observable { 14 | .just(true) 15 | } 16 | 17 | let quests = [ 18 | Quest.stub(groupId: UUID(), 19 | uuid: UUID(), 20 | date: Date(), 21 | title: "물마시기", 22 | currentCount: 0, 23 | totalCount: 10), 24 | Quest.stub(groupId: UUID(), 25 | uuid: UUID(), 26 | date: Date(), 27 | title: "물마시기", 28 | currentCount: 0, 29 | totalCount: 10), 30 | ] 31 | 32 | func fetch(by date: Date) -> Observable<[Quest]> { 33 | return Observable.just(quests) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuestTests/Presentation/ViewModel/BrowseViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrowseViewModelTests.swift 3 | // DailyQuestTests 4 | // 5 | // Created by jinwoong Kim on 2022/11/29. 6 | // 7 | 8 | import XCTest 9 | 10 | import RxSwift 11 | 12 | final class BrowseViewModelTests: XCTestCase { 13 | private var browseViewModel: BrowseViewModel! 14 | private var browseUseCase: BrowseUseCase! 15 | private var disposeBag = DisposeBag() 16 | 17 | override func setUpWithError() throws { 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | browseViewModel = nil 24 | browseUseCase = nil 25 | disposeBag = .init() 26 | } 27 | 28 | func testBrowseViewModel_WhenUseCaseSendTwoUsersQuests_ThenViewModelWillGenerateTwoBrowseItemViewModel() { 29 | // given 30 | browseUseCase = BrowseUseCaseMock() 31 | 32 | browseViewModel = BrowseViewModel(browseUseCase: browseUseCase) 33 | 34 | // when 35 | let output = browseViewModel 36 | .transform( 37 | input: BrowseViewModel 38 | .Input(viewDidLoad: .just(()).asObservable()) 39 | ) 40 | 41 | // then 42 | output 43 | .data 44 | .drive(onNext: { items in 45 | XCTAssertEqual(2, items.count) 46 | }) 47 | .disposed(by: disposeBag) 48 | } 49 | 50 | func testPerformanceExample() throws { 51 | // This is an example of a performance test case. 52 | self.measure { 53 | // Put the code you want to measure the time of here. 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuestTests/Presentation/ViewModel/QuestViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuestViewModelTests.swift 3 | // DailyQuestTests 4 | // 5 | // Created by jinwoong Kim on 2022/11/21. 6 | // 7 | 8 | import XCTest 9 | 10 | import RxSwift 11 | 12 | final class QuestViewModelTests: XCTestCase { 13 | private var questViewModel: QuestViewModel! 14 | private var questUseCase: QuestUseCase! 15 | private var disposeBag = DisposeBag() 16 | 17 | override func setUpWithError() throws { 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | questViewModel = nil 24 | questUseCase = nil 25 | disposeBag = .init() 26 | } 27 | 28 | func testQuestViewModel_WhenUseCaseSendCorrectQuests_ThenExpectationWillBeFulfilledWithSuccess() { 29 | // given 30 | questUseCase = QuestUseCaseMock() 31 | 32 | questViewModel = QuestViewModel(questUseCase: questUseCase) 33 | 34 | let expectation = XCTestExpectation(description: "test success") 35 | 36 | // // when 37 | // let output = questViewModel.transform(input: QuestViewModel.Input(viewDidLoad: .just(Date()).asObservable(), itemDidClicked: .just(<#T##element: Quest##Quest#>)), disposeBag: disposeBag) 38 | // 39 | // // then 40 | // output 41 | // .data 42 | // .drive(onNext: { quests in 43 | // print(quests) 44 | // expectation.fulfill() 45 | // }) 46 | // .disposed(by: disposeBag) 47 | // 48 | // wait(for: [expectation], timeout: 1) 49 | } 50 | 51 | func testPerformanceExample() throws { 52 | // This is an example of a performance test case. 53 | self.measure { 54 | // Put the code you want to measure the time of here. 55 | } 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuestUITests/DailyQuestUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DailyQuestUITests.swift 3 | // DailyQuestUITests 4 | // 5 | // Created by jinwoong Kim on 2022/11/11. 6 | // 7 | 8 | import XCTest 9 | 10 | final class DailyQuestUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | } 32 | 33 | func testLaunchPerformance() throws { 34 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 35 | // This measures how long it takes to launch your application. 36 | measure(metrics: [XCTApplicationLaunchMetric()]) { 37 | XCUIApplication().launch() 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /DailyQuest/DailyQuestUITests/DailyQuestUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DailyQuestUITestsLaunchTests.swift 3 | // DailyQuestUITests 4 | // 5 | // Created by jinwoong Kim on 2022/11/11. 6 | // 7 | 8 | import XCTest 9 | 10 | final class DailyQuestUITestsLaunchTests: XCTestCase { 11 | 12 | override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | true 14 | } 15 | 16 | override func setUpWithError() throws { 17 | continueAfterFailure = false 18 | } 19 | 20 | func testLaunch() throws { 21 | let app = XCUIApplication() 22 | app.launch() 23 | 24 | // Insert steps here to perform after app launch but before taking a screenshot, 25 | // such as logging into a test account or navigating somewhere in the app 26 | 27 | let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | attachment.name = "Launch Screen" 29 | attachment.lifetime = .keepAlways 30 | add(attachment) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /DailyQuest/SubFrameworks/DailyContainer/DailyContainer.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DailyQuest/SubFrameworks/DailyContainer/DailyContainer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DailyQuest/SubFrameworks/DailyContainer/DailyContainer/Container.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Container.swift 3 | // DailyContainer 4 | // 5 | // Created by jinwoong Kim on 2022/12/12. 6 | // 7 | 8 | import Foundation 9 | 10 | public class Container { 11 | public static var shared = Container() 12 | 13 | private var modules: [String: Module] = [:] 14 | 15 | public init() {} 16 | deinit { modules.removeAll() } 17 | } 18 | 19 | extension Container { 20 | func register(with module: Module) { 21 | modules[module.name] = module 22 | } 23 | 24 | func resolve(for type: Any.Type?) -> T { 25 | let name = type.map { String(describing: $0) } ?? String(describing: T.self) 26 | 27 | guard let dependancy: T = modules[name]?.resolve() as? T else { 28 | fatalError("dependancy \(T.self) not resolved.") 29 | } 30 | 31 | return dependancy 32 | } 33 | } 34 | 35 | public extension Container { 36 | func register(@ModuleBuilder _ modules: () -> [Module]) { 37 | modules().forEach { register(with: $0) } 38 | } 39 | 40 | func register(@ModuleBuilder _ module: () -> Module) { 41 | register(with: module()) 42 | } 43 | 44 | @resultBuilder 45 | struct ModuleBuilder { 46 | public static func buildBlock(_ modules: Module...) -> [Module] { 47 | modules 48 | } 49 | 50 | public static func buildBlock(_ module: Module) -> Module { 51 | module 52 | } 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /DailyQuest/SubFrameworks/DailyContainer/DailyContainer/DailyContainer.docc/DailyContainer.md: -------------------------------------------------------------------------------- 1 | # ``DailyContainer`` 2 | 3 | Summary 4 | 5 | ## Overview 6 | 7 | Text 8 | 9 | ## Topics 10 | 11 | ### Group 12 | 13 | - ``Symbol`` -------------------------------------------------------------------------------- /DailyQuest/SubFrameworks/DailyContainer/DailyContainer/DailyContainer.h: -------------------------------------------------------------------------------- 1 | // 2 | // DailyContainer.h 3 | // DailyContainer 4 | // 5 | // Created by jinwoong Kim on 2022/12/12. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for DailyContainer. 11 | FOUNDATION_EXPORT double DailyContainerVersionNumber; 12 | 13 | //! Project version string for DailyContainer. 14 | FOUNDATION_EXPORT const unsigned char DailyContainerVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | -------------------------------------------------------------------------------- /DailyQuest/SubFrameworks/DailyContainer/DailyContainer/Injected.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Injected.swift 3 | // DailyContainer 4 | // 5 | // Created by jinwoong Kim on 2022/12/12. 6 | // 7 | 8 | import Foundation 9 | 10 | @propertyWrapper 11 | public class Injected { 12 | private let lazyValue: (() -> Value) 13 | private var storage: Value? 14 | 15 | public var wrappedValue: Value { 16 | storage ?? { 17 | let value: Value = lazyValue() 18 | storage = value 19 | return value 20 | }() 21 | } 22 | 23 | public init(_ key: K.Type) where K: InjectionKey, Value == K.Value { 24 | lazyValue = { key.currentValue } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /DailyQuest/SubFrameworks/DailyContainer/DailyContainer/InjectionKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectionKey.swift 3 | // DailyContainer 4 | // 5 | // Created by jinwoong Kim on 2022/12/12. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol InjectionKey { 11 | associatedtype Value 12 | static var currentValue: Self.Value { get } 13 | } 14 | 15 | public extension InjectionKey { 16 | static var currentValue: Value { 17 | return Container.shared.resolve(for: Self.self) 18 | } 19 | } 20 | 21 | public struct Module { 22 | let name: String 23 | let resolve: () -> Any 24 | 25 | public init(_ name: T.Type, _ resolve: @escaping () -> Any) { 26 | self.name = String(describing: name) 27 | self.resolve = resolve 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 🗣 설명 2 | - 이슈 내용 작성 3 | 4 | 5 | ## 📋 체크리스트 6 | 7 | > 구현해야하는 이슈 체크리스트 8 | 9 | - [ ] 완료하지 못한 체크리스트 10 | - [x] 완료한 체크리스트 (완료한 내용들을 하나씩 작성하여 주세요. ex. 프로그래스바 로직 수정) 11 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### 📕 Issue Number 2 | 3 | Close # 4 | 5 | 6 | ### 📙 작업 내역 7 | 8 | > 구현 내용 및 작업 했던 내역 9 | 10 | - [x] 작업 내역 작성 11 | 12 | 13 | ### 📘 작업 유형 14 | 15 | - [ ] 신규 기능 추가 16 | - [ ] 버그 수정 17 | - [ ] 리펙토링 18 | - [ ] 문서 업데이트 19 | 20 | 21 | ### 📋 체크리스트 22 | 23 | - [ ] Merge 하는 브랜치가 올바른가? 24 | - [ ] 코딩컨벤션을 준수하는가? 25 | - [ ] PR과 관련없는 변경사항이 없는가? 26 | - [ ] 내 코드에 대한 자기 검토가 되었는가? 27 | - [ ] 변경사항이 효과적이거나 동작이 작동한다는 것을 보증하는 테스트를 추가하였는가? 28 | - [ ] 새로운 테스트와 기존의 테스트가 변경사항에 대해 만족하는가? 29 |
30 | 31 | ### 📝 PR 특이 사항 32 | 33 | > PR을 볼 때 주의깊게 봐야하거나 말하고 싶은 점 34 | 35 | - 특이 사항 1 36 | - 특이 사항 2 37 | 38 |

39 | -------------------------------------------------------------------------------- /keynotes/week01/DailyQuest01_기획공유.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS03-DailyQuest/61306fb766800eb23fa8231254a6d53185c79723/keynotes/week01/DailyQuest01_기획공유.key -------------------------------------------------------------------------------- /keynotes/week01/DailyQuest01_기획공유.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS03-DailyQuest/61306fb766800eb23fa8231254a6d53185c79723/keynotes/week01/DailyQuest01_기획공유.pdf -------------------------------------------------------------------------------- /keynotes/week02/DailyQuest02_데모발표.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2022/iOS03-DailyQuest/61306fb766800eb23fa8231254a6d53185c79723/keynotes/week02/DailyQuest02_데모발표.pdf --------------------------------------------------------------------------------