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