├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── README.md └── Routinus ├── .swiftlint.yml ├── Podfile ├── Routinus.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Routinus.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Routinus ├── Application │ ├── AppDelegate.swift │ ├── Coordinator │ │ ├── AppCoordinator.swift │ │ ├── AuthCoordinator.swift │ │ ├── AuthImagesCoordinator.swift │ │ ├── ChallengeCoordinator.swift │ │ ├── DetailCoordinator.swift │ │ ├── FormCoordinator.swift │ │ ├── HomeCoordinator.swift │ │ ├── ManageCoordinator.swift │ │ ├── MyPageCoordinator.swift │ │ ├── RoutinusCoordinator.swift │ │ ├── SearchCoordinator.swift │ │ └── TabBarCoordinator.swift │ └── SceneDelegate.swift ├── Data │ ├── Factory │ │ └── UsernameFactory.swift │ └── Repository │ │ ├── AchievementRepository.swift │ │ ├── AuthRepository.swift │ │ ├── ChallengeRepository.swift │ │ ├── ImageRepository.swift │ │ ├── ParticipationRepository.swift │ │ ├── RoutinusRepository.swift │ │ ├── TodayRoutineRepository.swift │ │ └── UserRepository.swift ├── Domain │ ├── Entity │ │ ├── Achievement.swift │ │ ├── ArchievementInfo.swift │ │ ├── Auth.swift │ │ ├── Calendar.swift │ │ ├── Challenge.swift │ │ ├── Participation.swift │ │ ├── PopularKeyword.swift │ │ ├── TodayRoutine.swift │ │ └── User.swift │ └── Usecase │ │ ├── AchievementFetchUsecase.swift │ │ ├── AchievementUpdateUsecase.swift │ │ ├── AuthCreateUsecase.swift │ │ ├── AuthFetchUsecase.swift │ │ ├── ChallengeCreateUsecase.swift │ │ ├── ChallengeFetchUsecase.swift │ │ ├── ChallengeUpdateUsecase.swift │ │ ├── ImageFetchUsecase.swift │ │ ├── ImageSaveUsecase.swift │ │ ├── ImageUpdateUsecase.swift │ │ ├── ParticipationCreateUsecase.swift │ │ ├── ParticipationFetchUsecase.swift │ │ ├── ParticipationUpdateUsecase.swift │ │ ├── TodayRoutineFetchUsecase.swift │ │ ├── UserCreateUsecase.swift │ │ ├── UserFetchUsecase.swift │ │ └── UserUpdateUsecase.swift ├── Extensions │ ├── Date+Extensions.swift │ ├── String+Extensions.swift │ ├── UICollectionView+Extensions.swift │ ├── UIImage+Extensions.swift │ ├── UIScrollView+Extensions.swift │ └── UIView+Extensions.swift ├── Network │ ├── DTO │ │ ├── AchievementDTO.swift │ │ ├── AuthDTO.swift │ │ ├── ChallengeDTO.swift │ │ ├── Fields.swift │ │ ├── ParticipationDTO.swift │ │ ├── TodayRoutineDTO.swift │ │ └── UserDTO.swift │ ├── FirebaseService.swift │ └── Query │ │ ├── AchievementQuery.swift │ │ ├── AuthQuery.swift │ │ ├── ChallengeQuery.swift │ │ ├── ParticipationQuery.swift │ │ └── UserQuery.swift ├── Presentation │ ├── Auth │ │ ├── AuthViewController.swift │ │ ├── AuthViewModel.swift │ │ └── Subviews │ │ │ ├── AuthButton.swift │ │ │ └── AuthPreviewView.swift │ ├── AuthImages │ │ ├── AuthImagesViewController.swift │ │ ├── AuthImagesViewModel.swift │ │ └── Cells │ │ │ └── AuthImagesCollectionViewCell.swift │ ├── Challenge │ │ ├── Cells │ │ │ ├── ChallengeCategoryCollectionViewCell.swift │ │ │ ├── ChallengeCategoryCollectionViewHeader.swift │ │ │ ├── ChallengeRecommendCollectionViewCell.swift │ │ │ └── ChallengeRecommendCollectionViewHeader.swift │ │ ├── ChallengeViewController.swift │ │ ├── ChallengeViewModel.swift │ │ ├── Delegates │ │ │ ├── ChallengeCategoryHeaderDelegate.swift │ │ │ └── ChallengeRecommendHeaderDelegate.swift │ │ ├── Layouts │ │ │ └── ChallengeCollectionViewLayouts.swift │ │ └── Subviews │ │ │ └── ChallengeCategoryIconView.swift │ ├── Common │ │ ├── Cells │ │ │ └── ChallengeCollectionViewCell.swift │ │ ├── ImagePanViewController.swift │ │ └── Subviews │ │ │ ├── AuthMethodView.swift │ │ │ └── ChallengePromotionView.swift │ ├── Detail │ │ ├── DetailViewController.swift │ │ ├── DetailViewModel.swift │ │ └── Subviews │ │ │ ├── DetailAuthDisplayListView.swift │ │ │ ├── DetailAuthDisplayView.swift │ │ │ ├── DetailInformationView.swift │ │ │ └── DetailParticipantButton.swift │ ├── Form │ │ ├── Delegates │ │ │ ├── FormImagePickerDelegate.swift │ │ │ └── FormSubviewDelegate.swift │ │ ├── FormViewController.swift │ │ ├── FormViewModel.swift │ │ └── Subviews │ │ │ ├── FormAuthImageRegisterView.swift │ │ │ ├── FormAuthMethodView.swift │ │ │ ├── FormCategoryView.swift │ │ │ ├── FormImageRegisterView.swift │ │ │ ├── FormIntroductionView.swift │ │ │ ├── FormTitleView.swift │ │ │ └── FormWeekView.swift │ ├── Home │ │ ├── Cells │ │ │ ├── HomeCalendarDetailTableViewCell.swift │ │ │ ├── HomeDateCollelctionViewCell.swift │ │ │ └── HomeRoutineTableViewCell.swift │ │ ├── HomeCalendarDetailViewController.swift │ │ ├── HomeCalendarExplanationViewController.swift │ │ ├── HomeViewController.swift │ │ ├── HomeViewModel.swift │ │ └── Subviews │ │ │ ├── HomeCalendarHeaderView.swift │ │ │ ├── HomeCalendarView.swift │ │ │ ├── HomeContinuityView.swift │ │ │ └── HomeTodayRoutineView.swift │ ├── Launch │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ └── Subviews │ │ │ └── LaunchView.swift │ ├── Manage │ │ ├── Cells │ │ │ ├── ManageAddCollectionViewCell.swift │ │ │ └── ManageCollectionViewHeader.swift │ │ ├── Layouts │ │ │ └── ManageCollectionViewLayouts.swift │ │ ├── ManageViewController.swift │ │ └── ManageViewModel.swift │ ├── MyPage │ │ ├── Delegates │ │ │ └── MyPageUserNameUpdatableDelegate.swift │ │ ├── MyPageViewController.swift │ │ ├── MyPageViewModel.swift │ │ └── Subviews │ │ │ └── MyPageProfileView.swift │ └── Search │ │ ├── Cells │ │ ├── SearchCollectionViewHeader.swift │ │ └── SearchPopularKeywordCollectionViewCell.swift │ │ ├── Delegates │ │ └── SearchPopularKeywordDelegate.swift │ │ ├── Layouts │ │ └── SearchCollectionViewLayouts.swift │ │ ├── SearchViewController.swift │ │ ├── SearchViewModel.swift │ │ └── Subviews │ │ ├── SearchBarContainerView.swift │ │ └── SearchPopularKeywordButton.swift ├── Resources │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── icon-40.png │ │ │ ├── icon-40@2x.png │ │ │ ├── icon-40@3x.png │ │ │ ├── icon-60@2x.png │ │ │ ├── icon-60@3x.png │ │ │ ├── icon-72.png │ │ │ ├── icon-72@2x.png │ │ │ ├── icon-76.png │ │ │ ├── icon-76@2x.png │ │ │ ├── icon-83.5@2x.png │ │ │ ├── icon-small-50.png │ │ │ ├── icon-small-50@2x.png │ │ │ ├── icon-small.png │ │ │ ├── icon-small@2x.png │ │ │ ├── icon-small@3x.png │ │ │ ├── icon.png │ │ │ ├── icon@2x.png │ │ │ ├── ios-marketing.png │ │ │ ├── notification-icon@2x.png │ │ │ ├── notification-icon@3x.png │ │ │ ├── notification-icon~ipad.png │ │ │ └── notification-icon~ipad@2x.png │ │ ├── Contents.json │ │ ├── Icons │ │ │ ├── Contents.json │ │ │ ├── challenge.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── challenge@x1.png │ │ │ │ ├── challenge@x2.png │ │ │ │ └── challenge@x3.png │ │ │ ├── check.calendar.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── calendar.svg │ │ │ └── dumbbell.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── dumbbell.svg │ │ ├── Logo.imageset │ │ │ ├── Contents.json │ │ │ ├── logo@x1.png │ │ │ ├── logo@x2.png │ │ │ └── logo@x3.png │ │ ├── achievement │ │ │ ├── 1-19.imageset │ │ │ │ ├── 0-20.png │ │ │ │ └── Contents.json │ │ │ ├── 100.imageset │ │ │ │ ├── 100.png │ │ │ │ └── Contents.json │ │ │ ├── 20-39.imageset │ │ │ │ ├── 20-40.png │ │ │ │ └── Contents.json │ │ │ ├── 40-65.imageset │ │ │ │ ├── 40-70.png │ │ │ │ └── Contents.json │ │ │ ├── 66-99.imageset │ │ │ │ ├── 70-99.png │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── colors │ │ │ ├── Black.colorset │ │ │ │ └── Contents.json │ │ │ ├── Calendar │ │ │ │ ├── Contents.json │ │ │ │ ├── DayColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── SaturdayColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ └── SundayColor.colorset │ │ │ │ │ └── Contents.json │ │ │ ├── Category │ │ │ │ ├── Contents.json │ │ │ │ ├── ETCColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── EmotionManageColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── ExerciseColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── FinanceColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── HobbyColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── LifeStyleColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── ReadColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ └── StudyColor.colorset │ │ │ │ │ └── Contents.json │ │ │ ├── Contents.json │ │ │ ├── EndDateColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── LightGray.colorset │ │ │ │ └── Contents.json │ │ │ ├── MainColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── MainColor0.5.colorset │ │ │ │ └── Contents.json │ │ │ ├── MainColorReverse.colorset │ │ │ │ └── Contents.json │ │ │ ├── SystemBackground.colorset │ │ │ │ └── Contents.json │ │ │ ├── SystemForeground.colorset │ │ │ │ └── Contents.json │ │ │ ├── WeekColor.colorset │ │ │ │ └── Contents.json │ │ │ └── White.colorset │ │ │ │ └── Contents.json │ │ └── seed │ │ │ ├── Contents.json │ │ │ ├── seed1.imageset │ │ │ ├── Contents.json │ │ │ └── seed1.svg │ │ │ ├── seed2.imageset │ │ │ ├── Contents.json │ │ │ └── seed2.svg │ │ │ ├── seed3.imageset │ │ │ ├── Contents.json │ │ │ └── seed3.svg │ │ │ ├── seed4.imageset │ │ │ ├── Contents.json │ │ │ └── seed4.svg │ │ │ ├── seed5.imageset │ │ │ ├── Contents.json │ │ │ └── seed5.svg │ │ │ ├── seed6.imageset │ │ │ ├── Contents.json │ │ │ └── seed6.svg │ │ │ └── seed7.imageset │ │ │ ├── Contents.json │ │ │ └── seed7.svg │ ├── Info.plist │ ├── en.lproj │ │ └── Localizable.strings │ └── ko.lproj │ │ └── Localizable.strings └── Storage │ └── ImageManager.swift └── RoutinusTests ├── MockDatas ├── AchievementFetchableUsecaseMock.swift ├── AchievementUpdatableUsecaseMock.swift ├── AuthFetchableUsecaseMock.swift ├── ChallengeFetchableUsecaseMock.swift ├── ChallengeUpdatableUsecaseMock.swift ├── ImageFetchableUsecaseMock.swift ├── ParticipationCreatableUsecaseMock.swift ├── ParticipationFetchableUsecaseMock.swift ├── ParticipationFetchableUsecaseNilMock.swift ├── TodayRoutineFetchableUsecaseMock.swift ├── UserCreatableUsecaseMock.swift ├── UserFetchableUsecaseMock.swift └── UserUpdatableUsecaseMock.swift └── ViewModelTests ├── ChallengeViewModelTests.swift ├── DetailViewModelTests.swift ├── HomeViewModelTests.swift ├── ManageViewModelTests.swift ├── MyPageViewModelTests.swift └── SearchViewModelTests.swift /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj merge=union 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: "\U0001F41Bbug" 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### 상황 11 | 12 | 1. 홈화면으로 이동한다. 13 | 2. 새로고침 버튼을 누른다. 14 | 3. 앱종료되는 버그 발생. 15 | 16 | ### 스크린샷 17 | 18 | ![]() 19 | 20 | ### 스마트폰 정보 21 | 22 | - 디바이스 : iPhone12 23 | - OS : 15.0.2 24 | - 네트워크 상태 : wifi 25 | 26 | ### 기타 사항 27 | 28 | 의견 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: "✨feature" 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### 내용 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 관련 issue 2 | 3 | #1 4 | 5 | ## 작업 내용 6 | 7 | - [x] task 1 8 | - [x] task 2 9 | - [ ] task 3 10 | 11 | ## 기타 사항 12 | 13 | - contents 14 | -------------------------------------------------------------------------------- /Routinus/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | line_length: 140 2 | 3 | disabled_rules: 4 | - type_body_length 5 | - function_parameter_count 6 | - file_length 7 | - identifier_name 8 | - function_body_length 9 | - inclusive_language 10 | 11 | excluded: 12 | - Pods/ 13 | -------------------------------------------------------------------------------- /Routinus/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :ios, '15.0' 3 | 4 | target 'Routinus' do 5 | # Comment the next line if you don't want to use dynamic frameworks 6 | use_frameworks! 7 | 8 | # Pods for Routinus 9 | pod 'SwiftLint', '0.45.0' 10 | end 11 | -------------------------------------------------------------------------------- /Routinus/Routinus.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Routinus/Routinus.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Routinus/Routinus.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Routinus/Routinus.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Routinus/Routinus/Application/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/01. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | final class AppDelegate: UIResponder, UIApplicationDelegate { 12 | func application( 13 | _ application: UIApplication, 14 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 15 | ) -> Bool { 16 | preloadImagePickerController() 17 | return true 18 | } 19 | 20 | func application(_ application: UIApplication, 21 | configurationForConnecting connectingSceneSession: UISceneSession, 22 | options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | return UISceneConfiguration(name: "Default Configuration", 24 | sessionRole: connectingSceneSession.role) 25 | } 26 | 27 | func application( 28 | _ application: UIApplication, 29 | supportedInterfaceOrientationsFor window: UIWindow? 30 | ) -> UIInterfaceOrientationMask { 31 | return .portrait 32 | } 33 | 34 | private func preloadImagePickerController() { 35 | _ = UIImagePickerController() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Routinus/Routinus/Application/Coordinator/AppCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppCoordinator.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/02. 6 | // 7 | 8 | import UIKit 9 | 10 | final class AppCoordinator: RoutinusCoordinator { 11 | var childCoordinator: [RoutinusCoordinator] = [] 12 | var navigationController: UINavigationController 13 | 14 | init(navigationController: UINavigationController) { 15 | self.navigationController = navigationController 16 | configureNavigationController() 17 | } 18 | 19 | func start() { 20 | configureTabBar() 21 | } 22 | 23 | private func configureNavigationController() { 24 | navigationController.navigationBar.isHidden = true 25 | } 26 | 27 | private func configureTabBar() { 28 | let tabBarCoordinator = TabBarCoordinator(navigationController: navigationController) 29 | tabBarCoordinator.start() 30 | childCoordinator.append(tabBarCoordinator) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Routinus/Routinus/Application/Coordinator/AuthCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthCoordinator.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/07. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | final class AuthCoordinator: RoutinusCoordinator { 12 | let challengeID: String 13 | var childCoordinator: [RoutinusCoordinator] = [] 14 | var navigationController: UINavigationController 15 | var cancellables = Set() 16 | 17 | init(navigationController: UINavigationController, challengeID: String) { 18 | self.navigationController = navigationController 19 | self.challengeID = challengeID 20 | } 21 | 22 | func start() { 23 | let repository = RoutinusRepository() 24 | let challengeFetchUsecase = ChallengeFetchUsecase(repository: repository) 25 | let imageFetchUsecase = ImageFetchUsecase(repository: repository) 26 | let imageSaveUsecase = ImageSaveUsecase(repository: repository) 27 | let authCreateUsecase = AuthCreateUsecase(repository: repository) 28 | let participationUpdateUsecase = ParticipationUpdateUsecase(repository: repository) 29 | let achievementUpdateUsecase = AchievementUpdateUsecase(repository: repository) 30 | let userUpdateUsecase = UserUpdateUsecase(repository: repository) 31 | let userFetchUsecase = UserFetchUsecase(repository: repository) 32 | let authViewModel = AuthViewModel(challengeID: challengeID, 33 | challengeFetchUsecase: challengeFetchUsecase, 34 | imageFetchUsecase: imageFetchUsecase, 35 | imageSaveUsecase: imageSaveUsecase, 36 | authCreateUsecase: authCreateUsecase, 37 | participationUpdateUsecase: participationUpdateUsecase, 38 | achievementUpdateUsecase: achievementUpdateUsecase, 39 | userUpdateUsecase: userUpdateUsecase, 40 | userFetchUsecase: userFetchUsecase) 41 | let authViewController = AuthViewController(viewModel: authViewModel) 42 | 43 | authViewModel.authMethodImageTap 44 | .receive(on: RunLoop.main) 45 | .sink { [weak self] imageData in 46 | guard let self = self else { return } 47 | let imageViewController = ImagePanViewController() 48 | imageViewController.updateImage(data: imageData) 49 | imageViewController.modalPresentationStyle = .overCurrentContext 50 | imageViewController.modalTransitionStyle = .crossDissolve 51 | self.navigationController.present(imageViewController, animated: true) 52 | } 53 | .store(in: &cancellables) 54 | 55 | authViewModel.authMethodImageLoad 56 | .receive(on: RunLoop.main) 57 | .sink { imageData in 58 | let imageViewController = authViewController.presentedViewController as? ImagePanViewController 59 | imageViewController?.updateImage(data: imageData) 60 | } 61 | .store(in: &cancellables) 62 | 63 | authViewController.hidesBottomBarWhenPushed = true 64 | navigationController.pushViewController(authViewController, animated: true) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Routinus/Routinus/Application/Coordinator/AuthImagesCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthImagesCoordinator.swift 3 | // Routinus 4 | // 5 | // Created by 백지현 on 2021/11/23. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | final class AuthImagesCoordinator: RoutinusCoordinator { 12 | let challengeID: String 13 | let authDisplayState: AuthDisplayState 14 | var childCoordinator: [RoutinusCoordinator] = [] 15 | var navigationController: UINavigationController 16 | var cancellables = Set() 17 | 18 | init(navigationController: UINavigationController, 19 | challengeID: String, 20 | authDisplayState: AuthDisplayState) { 21 | self.navigationController = navigationController 22 | self.challengeID = challengeID 23 | self.authDisplayState = authDisplayState 24 | } 25 | 26 | func start() { 27 | let repository = RoutinusRepository() 28 | let authFetchUsecase = AuthFetchUsecase(repository: repository) 29 | let imageFetchUsecase = ImageFetchUsecase(repository: repository) 30 | let authImagesViewModel = AuthImagesViewModel( 31 | challengeID: challengeID, 32 | authDisplayState: authDisplayState, 33 | authFetchUsecase: authFetchUsecase, 34 | imageFetchUsecase: imageFetchUsecase 35 | ) 36 | let authImagesViewController = AuthImagesViewController(viewModel: authImagesViewModel) 37 | 38 | authImagesViewModel.authImageTap 39 | .receive(on: RunLoop.main) 40 | .sink { [weak self] imageData in 41 | guard let self = self else { return } 42 | let imageViewController = ImagePanViewController() 43 | imageViewController.updateImage(data: imageData) 44 | imageViewController.modalPresentationStyle = .overCurrentContext 45 | imageViewController.modalTransitionStyle = .crossDissolve 46 | self.navigationController.present(imageViewController, animated: true) 47 | } 48 | .store(in: &cancellables) 49 | 50 | authImagesViewModel.authImageLoad 51 | .receive(on: RunLoop.main) 52 | .sink { imageData in 53 | let imageViewController = authImagesViewController.presentedViewController as? ImagePanViewController 54 | imageViewController?.updateImage(data: imageData) 55 | } 56 | .store(in: &cancellables) 57 | 58 | navigationController.pushViewController(authImagesViewController, animated: true) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Routinus/Routinus/Application/Coordinator/ChallengeCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChallengeCoordinator.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/02. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | final class ChallengeCoordinator: RoutinusCoordinator { 12 | var childCoordinator: [RoutinusCoordinator] = [] 13 | var navigationController: UINavigationController 14 | var cancellables = Set() 15 | 16 | init(navigationController: UINavigationController) { 17 | self.navigationController = navigationController 18 | } 19 | 20 | func start() { 21 | let repository = RoutinusRepository() 22 | let challengeFetchUsecase = ChallengeFetchUsecase(repository: repository) 23 | let challengeViewModel = ChallengeViewModel(challengeFetchUsecase: challengeFetchUsecase) 24 | let challengeViewController = ChallengeViewController(with: challengeViewModel) 25 | 26 | challengeViewModel.searchButtonTap 27 | .sink { [weak self] _ in 28 | guard let self = self else { return } 29 | let searchCoordinator = SearchCoordinator(navigationController: self.navigationController) 30 | searchCoordinator.start() 31 | self.childCoordinator.append(searchCoordinator) 32 | } 33 | .store(in: &cancellables) 34 | 35 | challengeViewModel.seeAllButtonTap 36 | .sink { [weak self] _ in 37 | guard let self = self else { return } 38 | let searchCoordinator = SearchCoordinator(navigationController: self.navigationController) 39 | searchCoordinator.start() 40 | self.childCoordinator.append(searchCoordinator) 41 | } 42 | .store(in: &cancellables) 43 | 44 | challengeViewModel.recommendChallengeTap 45 | .sink { [weak self] challengeID in 46 | guard let self = self else { return } 47 | let detailCoordinator = DetailCoordinator( 48 | navigationController: self.navigationController, 49 | challengeID: challengeID 50 | ) 51 | detailCoordinator.start() 52 | self.childCoordinator.append(detailCoordinator) 53 | } 54 | .store(in: &cancellables) 55 | 56 | challengeViewModel.categoryButtonTap 57 | .sink { [weak self] category in 58 | guard let self = self else { return } 59 | let searchCoordinator = SearchCoordinator( 60 | navigationController: self.navigationController, 61 | category: category 62 | ) 63 | searchCoordinator.start() 64 | self.childCoordinator.append(searchCoordinator) 65 | } 66 | .store(in: &cancellables) 67 | 68 | self.navigationController.pushViewController(challengeViewController, animated: false) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Routinus/Routinus/Application/Coordinator/FormCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormCoordinator.swift 3 | // Routinus 4 | // 5 | // Created by 백지현 on 2021/11/10. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | final class FormCoordinator: RoutinusCoordinator { 12 | var challengeID: String? 13 | var childCoordinator: [RoutinusCoordinator] = [] 14 | var navigationController: UINavigationController 15 | var cancellables = Set() 16 | 17 | init(navigationController: UINavigationController) { 18 | self.navigationController = navigationController 19 | } 20 | 21 | init(navigationController: UINavigationController, challengeID: String? = nil) { 22 | self.navigationController = navigationController 23 | self.challengeID = challengeID 24 | } 25 | 26 | func start() { 27 | let repository = RoutinusRepository() 28 | let challengeCreateUsecase = ChallengeCreateUsecase(repository: repository) 29 | let challengeUpdateUsecase = ChallengeUpdateUsecase(repository: repository) 30 | let challengeFetchUsecase = ChallengeFetchUsecase(repository: repository) 31 | let imageFetchUsecase = ImageFetchUsecase(repository: repository) 32 | let imageSaveUsecase = ImageSaveUsecase(repository: repository) 33 | let imageUpdateUsecase = ImageUpdateUsecase(repository: repository) 34 | let formViewModel = FormViewModel(challengeID: challengeID, 35 | challengeCreateUsecase: challengeCreateUsecase, 36 | challengeUpdateUsecase: challengeUpdateUsecase, 37 | challengeFetchUsecase: challengeFetchUsecase, 38 | imageFetchUsecase: imageFetchUsecase, 39 | imageSaveUsecase: imageSaveUsecase, 40 | imageUpdateUsecase: imageUpdateUsecase) 41 | let formViewController = FormViewController(with: formViewModel) 42 | 43 | formViewController.hidesBottomBarWhenPushed = true 44 | navigationController.pushViewController(formViewController, animated: true) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Routinus/Routinus/Application/Coordinator/ManageCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ManageCoordinator.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/02. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | final class ManageCoordinator: RoutinusCoordinator { 12 | var childCoordinator: [RoutinusCoordinator] = [] 13 | var navigationController: UINavigationController 14 | var cancellables = Set() 15 | 16 | init(navigationController: UINavigationController) { 17 | self.navigationController = navigationController 18 | } 19 | 20 | func start() { 21 | let repository = RoutinusRepository() 22 | let imageFetchUsecase = ImageFetchUsecase(repository: repository) 23 | let challengeFetchUsecase = ChallengeFetchUsecase(repository: repository) 24 | let manageViewModel = ManageViewModel(imageFetchUsecase: imageFetchUsecase, 25 | challengeFetchUsecase: challengeFetchUsecase) 26 | let manageViewController = ManageViewController(with: manageViewModel) 27 | 28 | manageViewModel.challengeTap 29 | .sink { [weak self] challengeID in 30 | guard let self = self else { return } 31 | let detailCoordinator = DetailCoordinator( 32 | navigationController: self.navigationController, 33 | challengeID: challengeID 34 | ) 35 | detailCoordinator.start() 36 | self.childCoordinator.append(detailCoordinator) 37 | } 38 | .store(in: &cancellables) 39 | 40 | manageViewModel.challengeAddButtonTap 41 | .sink { [weak self] _ in 42 | guard let self = self else { return } 43 | self.navigationController.navigationBar.prefersLargeTitles = false 44 | let formCoordinator = FormCoordinator(navigationController: self.navigationController) 45 | formCoordinator.start() 46 | self.childCoordinator.append(formCoordinator) 47 | } 48 | .store(in: &cancellables) 49 | 50 | navigationController.pushViewController(manageViewController, animated: false) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Routinus/Routinus/Application/Coordinator/MyPageCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyPageCoordinator.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/02. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | final class MyPageCoordinator: RoutinusCoordinator { 12 | var childCoordinator: [RoutinusCoordinator] = [] 13 | var navigationController: UINavigationController 14 | var cancellables = Set() 15 | 16 | init(navigationController: UINavigationController) { 17 | self.navigationController = navigationController 18 | } 19 | 20 | func start() { 21 | let repository = RoutinusRepository() 22 | let userFetchUsecase = UserFetchUsecase(repository: repository) 23 | let userUpdateUsecase = UserUpdateUsecase(repository: repository) 24 | let myPageViewModel = MyPageViewModel(userFetchUsecase: userFetchUsecase, 25 | userUpdateUsecase: userUpdateUsecase) 26 | let myPageViewController = MyPageViewController(with: myPageViewModel) 27 | 28 | navigationController.pushViewController(myPageViewController, animated: false) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Routinus/Routinus/Application/Coordinator/RoutinusCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoutinusCoordinator.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/02. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol RoutinusCoordinator: AnyObject { 11 | var childCoordinator: [RoutinusCoordinator] { get set } 12 | var navigationController: UINavigationController { get } 13 | 14 | func start() 15 | } 16 | -------------------------------------------------------------------------------- /Routinus/Routinus/Application/Coordinator/SearchCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchCoordinator.swift 3 | // Routinus 4 | // 5 | // Created by 김민서 on 2021/11/09. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | final class SearchCoordinator: RoutinusCoordinator { 12 | var category: Challenge.Category? 13 | var childCoordinator: [RoutinusCoordinator] = [] 14 | var navigationController: UINavigationController 15 | var cancellables = Set() 16 | 17 | init(navigationController: UINavigationController, category: Challenge.Category? = nil) { 18 | self.navigationController = navigationController 19 | self.category = category 20 | } 21 | 22 | func start() { 23 | let repository = RoutinusRepository() 24 | let imageFetchUsecase = ImageFetchUsecase(repository: repository) 25 | let challengeFetchUsecase = ChallengeFetchUsecase(repository: repository) 26 | let searchViewModel = SearchViewModel(category: category, 27 | imageFetchUsecase: imageFetchUsecase, 28 | challengeFetchUsecase: challengeFetchUsecase) 29 | let searchViewController = SearchViewController(with: searchViewModel) 30 | 31 | searchViewModel.challengeTap 32 | .sink { [weak self] challengeID in 33 | guard let self = self else { return } 34 | let detailCoordinator = DetailCoordinator( 35 | navigationController: self.navigationController, 36 | challengeID: challengeID 37 | ) 38 | detailCoordinator.start() 39 | self.childCoordinator.append(detailCoordinator) 40 | } 41 | .store(in: &cancellables) 42 | 43 | navigationController.pushViewController(searchViewController, animated: true) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Routinus/Routinus/Application/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/01. 6 | // 7 | 8 | import UIKit 9 | 10 | final class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | var window: UIWindow? 12 | var appCoordinator: AppCoordinator? 13 | 14 | func scene(_ scene: UIScene, 15 | willConnectTo session: UISceneSession, 16 | options connectionOptions: UIScene.ConnectionOptions) { 17 | guard let windowScene = (scene as? UIWindowScene) else { return } 18 | window = UIWindow(windowScene: windowScene) 19 | let naviationController = UINavigationController() 20 | window?.rootViewController = naviationController 21 | window?.makeKeyAndVisible() 22 | 23 | let appCoordinator = AppCoordinator(navigationController: naviationController) 24 | appCoordinator.start() 25 | self.appCoordinator = appCoordinator 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Routinus/Routinus/Data/Factory/UsernameFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UsernameFactory.swift 3 | // Routinus 4 | // 5 | // Created by 백지현 on 2021/11/08. 6 | // 7 | 8 | import Foundation 9 | 10 | enum UsernameFactory { 11 | static func randomName() -> String { 12 | var result = "" 13 | let adjective = ["귀여운", "멋진", "정직한", "재미있는", "느긋한", "친절한", "다정한"] 14 | let noun = ["코끼리", "감자", "맥북", "강아지", "고양이", "토끼", "송아지", "사자", "호랑이", "병아리"] 15 | 16 | result += adjective.randomElement() ?? "" 17 | result += noun.randomElement() ?? "" 18 | 19 | return result 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Routinus/Routinus/Data/Repository/AchievementRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AchievementRepository.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol AchievementRepository { 11 | func fetchAchievements(by id: String, 12 | in yearMonth: String, 13 | completion: (([Achievement]) -> Void)?) 14 | func updateAchievementCount(userID: String, 15 | yearMonth: String, 16 | day: String, 17 | completion: (() -> Void)?) 18 | func updateTotalCount(userID: String, 19 | yearMonth: String, 20 | day: String, 21 | completion: (() -> Void)?) 22 | } 23 | 24 | extension RoutinusRepository: AchievementRepository { 25 | func fetchAchievements(by id: String, 26 | in yearMonth: String, 27 | completion: (([Achievement]) -> Void)?) { 28 | FirebaseService.achievements(of: id, in: yearMonth) { list in 29 | completion?(list.map { Achievement(achievementDTO: $0) }) 30 | } 31 | } 32 | 33 | func updateAchievementCount(userID: String, 34 | yearMonth: String, 35 | day: String, 36 | completion: (() -> Void)?) { 37 | FirebaseService.updateAchievementCount(userID: userID, 38 | yearMonth: yearMonth, 39 | day: day) { 40 | completion?() 41 | } 42 | } 43 | 44 | func updateTotalCount(userID: String, 45 | yearMonth: String, 46 | day: String, 47 | completion: (() -> Void)?) { 48 | FirebaseService.updateTotalCount(userID: userID, yearMonth: yearMonth, day: day) { 49 | completion?() 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Routinus/Routinus/Data/Repository/ParticipationRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParticipationRepository.swift 3 | // Routinus 4 | // 5 | // Created by 백지현 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ParticipationRepository { 11 | func fetchParticipation(userID: String, 12 | challengeID: String, 13 | completion: @escaping (Participation?) -> Void) 14 | func save(challengeID: String, joinDate: String, completion: (() -> Void)?) 15 | func updateAuthCount(challengeID: String, completion: (() -> Void)?) 16 | } 17 | 18 | extension RoutinusRepository: ParticipationRepository { 19 | func fetchParticipation(userID: String, 20 | challengeID: String, 21 | completion: @escaping (Participation?) -> Void) { 22 | FirebaseService.participation(userID: userID, challengeID: challengeID) { dto in 23 | guard let dto = dto, dto.document != nil else { 24 | completion(nil) 25 | return 26 | } 27 | completion(Participation(participationDTO: dto)) 28 | } 29 | } 30 | 31 | func save(challengeID: String, joinDate: String, completion: (() -> Void)?) { 32 | guard let userID = RoutinusRepository.userID() else { return } 33 | let dto = ParticipationDTO(authCount: 0, 34 | challengeID: challengeID, 35 | joinDate: joinDate, 36 | userID: userID) 37 | FirebaseService.insertParticipation(dto: dto) { 38 | completion?() 39 | } 40 | } 41 | 42 | func updateAuthCount(challengeID: String, completion: (() -> Void)?) { 43 | guard let userID = RoutinusRepository.userID() else { return } 44 | FirebaseService.updateParticipationAuthCount(challengeID: challengeID, userID: userID) { 45 | completion?() 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Routinus/Routinus/Data/Repository/RoutinusRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoutinusRepository.swift 3 | // Routinus 4 | // 5 | // Created by 백지현 on 2021/11/08. 6 | // 7 | 8 | import Foundation 9 | 10 | final class RoutinusRepository { 11 | static let userIDKey = "id" 12 | static let themeStyleKey = "themeStyle" 13 | 14 | static func userID() -> String? { 15 | return UserDefaults.standard.string(forKey: RoutinusRepository.userIDKey) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Routinus/Routinus/Data/Repository/TodayRoutineRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodayRoutineRepository.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol TodayRoutineRepository { 11 | func fetchTodayRoutine(by id: String, completion: (([TodayRoutine]) -> Void)?) 12 | } 13 | 14 | extension RoutinusRepository: TodayRoutineRepository { 15 | func fetchTodayRoutine(by id: String, completion: (([TodayRoutine]) -> Void)?) { 16 | FirebaseService.routines(of: id) { list in 17 | completion?(list.map { TodayRoutine(todayRoutineDTO: $0) }) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Routinus/Routinus/Data/Repository/UserRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserRepository.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol UserRepository { 11 | func isEmptyUserID() -> Bool 12 | func save(id: String, name: String, completion: (() -> Void)?) 13 | func fetchUser(by id: String, completion: ((User) -> Void)?) 14 | func updateContinuityDay(by id: String, completion: ((UserDTO) -> Void)?) 15 | func updateContinuityDayByAuth(by id: String, completion: (() -> Void)?) 16 | func fetchThemeStyle() -> Int 17 | func updateUsername(of id: String, name: String, completion: (() -> Void)?) 18 | func updateThemeStyle(_ style: Int) 19 | } 20 | 21 | extension RoutinusRepository: UserRepository { 22 | func isEmptyUserID() -> Bool { 23 | return UserDefaults.standard.string(forKey: RoutinusRepository.userIDKey) == nil 24 | } 25 | 26 | func save(id: String, name: String, completion: (() -> Void)?) { 27 | UserDefaults.standard.set(id, forKey: RoutinusRepository.userIDKey) 28 | FirebaseService.insertUser(id: id, name: name) { 29 | completion?() 30 | } 31 | } 32 | 33 | func fetchUser(by id: String, completion: ((User) -> Void)?) { 34 | FirebaseService.user(of: id) { dto in 35 | completion?(User(userDTO: dto)) 36 | } 37 | } 38 | 39 | func updateContinuityDay(by id: String, completion: ((UserDTO) -> Void)?) { 40 | FirebaseService.updateContinuityDay(of: id) { dto in 41 | completion?(dto) 42 | } 43 | } 44 | 45 | func updateContinuityDayByAuth(by id: String, completion: (() -> Void)?) { 46 | FirebaseService.updateContinuityDayByAuth(of: id) { 47 | completion?() 48 | } 49 | } 50 | 51 | func fetchThemeStyle() -> Int { 52 | return UserDefaults.standard.integer(forKey: RoutinusRepository.themeStyleKey) 53 | } 54 | 55 | func updateUsername(of id: String, name: String, completion: (() -> Void)?) { 56 | FirebaseService.updateUsername(of: id, name: name) { 57 | completion?() 58 | } 59 | } 60 | 61 | func updateThemeStyle(_ style: Int) { 62 | UserDefaults.standard.set(style, forKey: RoutinusRepository.themeStyleKey) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Entity/Achievement.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Achievement.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/03. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Achievement { 11 | let yearMonth: String 12 | let day: String 13 | let achievementCount: Int 14 | let totalCount: Int 15 | 16 | init(yearMonth: String, day: String, achievementCount: Int, totalCount: Int) { 17 | self.yearMonth = yearMonth 18 | self.day = day 19 | self.achievementCount = achievementCount 20 | self.totalCount = totalCount 21 | } 22 | 23 | init(achievementDTO: AchievementDTO) { 24 | let document = achievementDTO.document?.fields 25 | 26 | self.yearMonth = document?.yearMonth.stringValue ?? "" 27 | self.day = document?.day.stringValue ?? "" 28 | self.achievementCount = Int(document?.achievementCount.integerValue ?? "") ?? 0 29 | self.totalCount = Int(document?.totalCount.integerValue ?? "") ?? 0 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Entity/ArchievementInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArchievementInfo.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/03. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ArchievementInfo { 11 | let day: Int 12 | let achievementCount: Int 13 | let totalCount: Int 14 | } 15 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Entity/Auth.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Auth.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Auth { 11 | var challengeID: String 12 | var userID: String 13 | var date: Date? 14 | var time: Date? 15 | 16 | init(challengeID: String, userID: String, date: Date?, time: Date?) { 17 | self.challengeID = challengeID 18 | self.userID = userID 19 | self.date = date 20 | self.time = time 21 | } 22 | 23 | init(authDTO: AuthDTO) { 24 | let document = authDTO.document?.fields 25 | 26 | self.challengeID = document?.challengeID.stringValue ?? "" 27 | self.userID = document?.userID.stringValue ?? "" 28 | self.date = Date(dateString: document?.date.stringValue ?? "") 29 | self.time = Date(timeString: document?.time.stringValue ?? "") 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Entity/Calendar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Calendar.swift 3 | // Routinus 4 | // 5 | // Created by 김민서 on 2021/11/16. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Day { 11 | let date: Date 12 | let number: String 13 | let isSelected: Bool 14 | var achievementRate: Double 15 | let isWithinDisplayedMonth: Bool 16 | } 17 | 18 | struct MonthMetadata { 19 | let numberOfDays: Int 20 | let firstDay: Date 21 | let firstDayWeekday: Int 22 | } 23 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Entity/Participation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Participation.swift 3 | // Routinus 4 | // 5 | // Created by 백지현 on 2021/11/18. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Participation { 11 | let authCount: Int 12 | let challengeID: String 13 | let joinDate: String 14 | let userID: String 15 | 16 | init(participationDTO: ParticipationDTO) { 17 | let document = participationDTO.document?.fields 18 | self.authCount = Int(document?.authCount.integerValue ?? "") ?? 0 19 | self.challengeID = document?.challengeID.stringValue ?? "" 20 | self.joinDate = document?.joinDate.stringValue ?? "" 21 | self.userID = document?.userID.stringValue ?? "" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Entity/PopularKeyword.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PopularKeyword.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/11. 6 | // 7 | 8 | import Foundation 9 | 10 | enum PopularKeyword: String, CaseIterable { 11 | case exercise = "운동" 12 | case reading = "독서" 13 | case commit = "커밋" 14 | case study = "공부" 15 | case english = "영어" 16 | } 17 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Entity/TodayRoutine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodayRoutine.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/04. 6 | // 7 | 8 | import Foundation 9 | 10 | struct TodayRoutine: Hashable { 11 | let challengeID: String 12 | let category: Challenge.Category 13 | let title: String 14 | let authCount: Int 15 | let totalCount: Int 16 | 17 | init(challengeID: String, 18 | category: Challenge.Category, 19 | title: String, 20 | authCount: Int, 21 | totalCount: Int) { 22 | self.challengeID = challengeID 23 | self.category = category 24 | self.title = title 25 | self.authCount = authCount 26 | self.totalCount = totalCount 27 | } 28 | 29 | init(todayRoutineDTO: TodayRoutineDTO) { 30 | self.challengeID = todayRoutineDTO.challengeID 31 | self.category = Challenge.Category.category(by: todayRoutineDTO.categoryID) 32 | self.title = todayRoutineDTO.title 33 | self.authCount = todayRoutineDTO.authCount 34 | self.totalCount = Date.days(from: todayRoutineDTO.endDate, to: todayRoutineDTO.joinDate) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Entity/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/03. 6 | // 7 | 8 | import Foundation 9 | 10 | struct User { 11 | var id: String 12 | var name: String 13 | var continuityDay: Int 14 | var userImageCategoryID: String 15 | var grade: Int 16 | var lastAuthDay: String 17 | 18 | init() { 19 | self.id = "" 20 | self.name = "" 21 | self.continuityDay = 0 22 | self.userImageCategoryID = "" 23 | self.grade = 0 24 | self.lastAuthDay = "" 25 | } 26 | 27 | init(id: String, 28 | name: String, 29 | continuityDay: Int, 30 | userImageCategoryID: String, 31 | grade: Int, 32 | lastAuthDay: String) { 33 | self.id = id 34 | self.name = name 35 | self.continuityDay = continuityDay 36 | self.userImageCategoryID = userImageCategoryID 37 | self.grade = grade 38 | self.lastAuthDay = lastAuthDay 39 | } 40 | 41 | init(userDTO: UserDTO) { 42 | let document = userDTO.document?.fields 43 | 44 | self.id = document?.id.stringValue ?? "" 45 | self.name = document?.name.stringValue ?? "" 46 | self.continuityDay = Int(document?.continuityDay.integerValue ?? "") ?? 0 47 | self.userImageCategoryID = document?.userImageCategoryID.stringValue ?? "" 48 | self.grade = Int(document?.grade.integerValue ?? "") ?? 0 49 | self.lastAuthDay = document?.lastAuthDay.stringValue ?? "" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Usecase/AchievementFetchUsecase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AchievementFetchUsecase.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol AchievementFetchableUsecase { 11 | func fetchAchievements(yearMonth: String, completion: @escaping ([Achievement]) -> Void) 12 | } 13 | 14 | struct AchievementFetchUsecase: AchievementFetchableUsecase { 15 | var repository: AchievementRepository 16 | 17 | init(repository: AchievementRepository) { 18 | self.repository = repository 19 | } 20 | 21 | func fetchAchievements(yearMonth: String, completion: @escaping ([Achievement]) -> Void) { 22 | guard let id = RoutinusRepository.userID() else { return } 23 | 24 | repository.fetchAchievements(by: id, in: yearMonth) { achievements in 25 | completion(achievements) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Usecase/AchievementUpdateUsecase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AchievementUpdateUsecase.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/18. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol AchievementUpdatableUsecase { 11 | func updateAchievementCount() 12 | func updateTotalCount() 13 | } 14 | 15 | struct AchievementUpdateUsecase: AchievementUpdatableUsecase { 16 | static let didUpdateAchievement = Notification.Name("didUpdateAchievement") 17 | 18 | var repository: AchievementRepository 19 | 20 | init(repository: AchievementRepository) { 21 | self.repository = repository 22 | } 23 | 24 | func updateAchievementCount() { 25 | guard let userID = RoutinusRepository.userID() else { return } 26 | let yearMonth = Date().toYearMonthString() 27 | let day = Date().toDayString() 28 | repository.updateAchievementCount(userID: userID, yearMonth: yearMonth, day: day) { 29 | NotificationCenter.default.post(name: AchievementUpdateUsecase.didUpdateAchievement, 30 | object: nil) 31 | } 32 | } 33 | 34 | func updateTotalCount() { 35 | guard let userID = RoutinusRepository.userID() else { return } 36 | let yearMonth = Date().toYearMonthString() 37 | let day = Date().toDayString() 38 | repository.updateTotalCount(userID: userID, yearMonth: yearMonth, day: day) { 39 | NotificationCenter.default.post(name: AchievementUpdateUsecase.didUpdateAchievement, 40 | object: nil) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Usecase/AuthCreateUsecase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthCreateUsecase.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol AuthCreatableUsecase { 11 | func createAuth(challengeID: String, 12 | userAuthImageURL: String, 13 | userAuthThumbnailImageURL: String) 14 | } 15 | 16 | struct AuthCreateUsecase: AuthCreatableUsecase { 17 | static let didCreateAuth = Notification.Name("didCreateAuth") 18 | 19 | var repository: AuthRepository 20 | 21 | init(repository: AuthRepository) { 22 | self.repository = repository 23 | } 24 | 25 | func createAuth(challengeID: String, 26 | userAuthImageURL: String, 27 | userAuthThumbnailImageURL: String) { 28 | guard let userID = RoutinusRepository.userID() else { return } 29 | let auth = Auth(challengeID: challengeID, userID: userID, date: Date(), time: Date()) 30 | 31 | repository.insert(auth: auth, 32 | userAuthImageURL: userAuthImageURL, 33 | userAuthThumbnailImageURL: userAuthThumbnailImageURL) { 34 | NotificationCenter.default.post(name: AuthCreateUsecase.didCreateAuth, 35 | object: nil) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Usecase/AuthFetchUsecase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthFetchUsecase.swift 3 | // Routinus 4 | // 5 | // Created by 백지현 on 2021/11/18. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol AuthFetchableUsecase { 11 | func fetchAuth(challengeID: String, completion: @escaping (Auth?) -> Void) 12 | func fetchAuths(challengeID: String, completion: @escaping ([Auth]) -> Void) 13 | func fetchMyAuths(challengeID: String, completion: @escaping ([Auth]) -> Void) 14 | func fetchAuthedChallenges(date: Date, completion: @escaping ([Challenge]) -> Void) 15 | } 16 | 17 | struct AuthFetchUsecase: AuthFetchableUsecase { 18 | var repository: AuthRepository 19 | 20 | init(repository: AuthRepository) { 21 | self.repository = repository 22 | } 23 | 24 | func fetchAuth(challengeID: String, completion: @escaping (Auth?) -> Void) { 25 | guard let userID = RoutinusRepository.userID() else { return } 26 | repository.fetchAuth(todayDate: Date().toDateString(), 27 | userID: userID, 28 | challengeID: challengeID) { auth in 29 | completion(auth) 30 | } 31 | } 32 | 33 | func fetchAuths(challengeID: String, completion: @escaping ([Auth]) -> Void) { 34 | repository.fetchAuths(challengeID: challengeID) { auths in 35 | completion(auths) 36 | } 37 | } 38 | 39 | func fetchMyAuths(challengeID: String, completion: @escaping ([Auth]) -> Void) { 40 | guard let userID = RoutinusRepository.userID() else { return } 41 | repository.fetchMyAuths(userID: userID, challengeID: challengeID) { auths in 42 | completion(auths) 43 | } 44 | } 45 | 46 | func fetchAuthedChallenges(date: Date, completion: @escaping ([Challenge]) -> Void) { 47 | guard let userID = RoutinusRepository.userID() else { return } 48 | repository.fetchMyAuthedChallenges(date: date.toDateString(), userID: userID) { challenges in 49 | completion(challenges) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Usecase/ChallengeUpdateUsecase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChallengeUpdateUsecase.swift 3 | // Routinus 4 | // 5 | // Created by 백지현 on 2021/11/13. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ChallengeUpdatableUsecase { 11 | func endDate(startDate: Date, week: Int) -> Date? 12 | func update(challenge: Challenge) 13 | func updateParticipantCount(challengeID: String) 14 | } 15 | 16 | struct ChallengeUpdateUsecase: ChallengeUpdatableUsecase { 17 | static let didUpdateChallenge = Notification.Name("didUpdateChallenge") 18 | 19 | var repository: ChallengeRepository 20 | 21 | init(repository: ChallengeRepository) { 22 | self.repository = repository 23 | } 24 | 25 | func endDate(startDate: Date, week: Int) -> Date? { 26 | let calendar = Calendar.current 27 | let day = DateComponents(day: week * 7) 28 | guard let endDate = calendar.date(byAdding: day, to: startDate) else { return nil } 29 | return endDate 30 | } 31 | 32 | func update(challenge: Challenge) { 33 | repository.update(challenge: challenge) { 34 | NotificationCenter.default.post(name: ChallengeUpdateUsecase.didUpdateChallenge, 35 | object: nil) 36 | } 37 | } 38 | 39 | func updateParticipantCount(challengeID: String) { 40 | repository.updateParticipantCount(challengeID: challengeID) { 41 | NotificationCenter.default.post(name: ChallengeUpdateUsecase.didUpdateChallenge, 42 | object: nil) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Usecase/ImageFetchUsecase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageFetchUsecase.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ImageFetchableUsecase { 11 | func fetchImageData(from directory: String, 12 | filename: String, 13 | completion: ((Data?) -> Void)?) 14 | } 15 | 16 | struct ImageFetchUsecase: ImageFetchableUsecase { 17 | var repository: ImageRepository 18 | 19 | init(repository: ImageRepository) { 20 | self.repository = repository 21 | } 22 | 23 | func fetchImageData(from directory: String, 24 | filename: String, 25 | completion: ((Data?) -> Void)? = nil) { 26 | repository.fetchImageData(from: directory, filename: filename) { data in 27 | completion?(data) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Usecase/ImageSaveUsecase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageSaveUsecase.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ImageSavableUsecase { 11 | func saveImage(to directory: String, filename: String, data: Data?) -> String? 12 | } 13 | 14 | struct ImageSaveUsecase: ImageSavableUsecase { 15 | var repository: ImageRepository 16 | 17 | init(repository: ImageRepository) { 18 | self.repository = repository 19 | } 20 | 21 | func saveImage(to directory: String, filename: String, data: Data?) -> String? { 22 | return repository.saveImage(to: directory, filename: filename, data: data) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Usecase/ImageUpdateUsecase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageUpdateUsecase.swift 3 | // Routinus 4 | // 5 | // Created by 백지현 on 2021/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ImageUpdatableUsecase { 11 | func updateImage(challenge: Challenge, isChangedImage: Bool, isChangedAuthImage: Bool) 12 | } 13 | 14 | struct ImageUpdateUsecase: ImageUpdatableUsecase { 15 | static let didUpdateChallengeImage = Notification.Name("didUpdateChallengeImage") 16 | 17 | var repository: ImageRepository 18 | 19 | init(repository: ImageRepository) { 20 | self.repository = repository 21 | } 22 | 23 | func updateImage(challenge: Challenge, isChangedImage: Bool, isChangedAuthImage: Bool) { 24 | if isChangedImage { 25 | repository.updateImage(challengeID: challenge.challengeID, 26 | imageURL: challenge.imageURL, 27 | thumbnailImageURL: challenge.thumbnailImageURL) { 28 | NotificationCenter.default.post(name: ImageUpdateUsecase.didUpdateChallengeImage, 29 | object: nil) 30 | } 31 | } 32 | if isChangedAuthImage { 33 | repository.updateImage( 34 | challengeID: challenge.challengeID, 35 | authExampleImageURL: challenge.authExampleImageURL, 36 | authExampleThumbnailImageURL: challenge.authExampleThumbnailImageURL 37 | ) { 38 | NotificationCenter.default.post(name: ImageUpdateUsecase.didUpdateChallengeImage, 39 | object: nil) 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Usecase/ParticipationCreateUsecase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParticipationCreateUsecase.swift 3 | // Routinus 4 | // 5 | // Created by 백지현 on 2021/11/18. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ParticipationCreatableUsecase { 11 | func createParticipation(challengeID: String) 12 | } 13 | 14 | struct ParticipationCreateUsecase: ParticipationCreatableUsecase { 15 | static let didCreateParticipation = Notification.Name("didCreateParticipation") 16 | 17 | var repository: ParticipationRepository 18 | 19 | init(repository: ParticipationRepository) { 20 | self.repository = repository 21 | } 22 | 23 | func createParticipation(challengeID: String) { 24 | let date = Date().toDateString() 25 | repository.save(challengeID: challengeID, joinDate: date) { 26 | NotificationCenter.default.post(name: ParticipationCreateUsecase.didCreateParticipation, 27 | object: nil) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Usecase/ParticipationFetchUsecase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParticipationFetchUsecase.swift 3 | // Routinus 4 | // 5 | // Created by 백지현 on 2021/11/18. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ParticipationFetchableUsecase { 11 | func fetchParticipation(challengeID: String, completion: @escaping (Participation?) -> Void) 12 | } 13 | 14 | struct ParticipationFetchUsecase: ParticipationFetchableUsecase { 15 | var repository: ParticipationRepository 16 | 17 | init(repository: ParticipationRepository) { 18 | self.repository = repository 19 | } 20 | 21 | func fetchParticipation(challengeID: String, completion: @escaping (Participation?) -> Void) { 22 | guard let userID = RoutinusRepository.userID() else { 23 | completion(nil) 24 | return 25 | } 26 | repository.fetchParticipation(userID: userID, challengeID: challengeID) { participation in 27 | completion(participation) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Usecase/ParticipationUpdateUsecase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParticipationUpdateUsecase.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/18. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ParticipationUpdatableUsecase { 11 | func updateParticipationAuthCount(challengeID: String) 12 | } 13 | 14 | struct ParticipationUpdateUsecase: ParticipationUpdatableUsecase { 15 | static let didUpdateParticipation = Notification.Name("didUpdateParticipation") 16 | 17 | var repository: ParticipationRepository 18 | 19 | init(repository: ParticipationRepository) { 20 | self.repository = repository 21 | } 22 | 23 | func updateParticipationAuthCount(challengeID: String) { 24 | repository.updateAuthCount(challengeID: challengeID) { 25 | NotificationCenter.default.post(name: ParticipationUpdateUsecase.didUpdateParticipation, 26 | object: nil) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Usecase/TodayRoutineFetchUsecase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodayRoutineFetchUsecase.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol TodayRoutineFetchableUsecase { 11 | func fetchTodayRoutines(completion: @escaping ([TodayRoutine]) -> Void) 12 | } 13 | 14 | struct TodayRoutineFetchUsecase: TodayRoutineFetchableUsecase { 15 | var repository: TodayRoutineRepository 16 | 17 | init(repository: TodayRoutineRepository) { 18 | self.repository = repository 19 | } 20 | 21 | func fetchTodayRoutines(completion: @escaping ([TodayRoutine]) -> Void) { 22 | guard let id = RoutinusRepository.userID() else { return } 23 | 24 | repository.fetchTodayRoutine(by: id) { todayRoutines in 25 | completion(todayRoutines) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Usecase/UserCreateUsecase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeCreateUsecase.swift 3 | // Routinus 4 | // 5 | // Created by 백지현 on 2021/11/08. 6 | // 7 | 8 | import CryptoKit 9 | import Foundation 10 | 11 | protocol UserCreatableUsecase { 12 | func createUser() 13 | } 14 | 15 | struct UserCreateUsecase: UserCreatableUsecase { 16 | static let didCreateUser = Notification.Name("didCreateUser") 17 | 18 | var repository: UserRepository 19 | 20 | init(repository: UserRepository) { 21 | self.repository = repository 22 | } 23 | 24 | private func createID() -> String { 25 | let uuid = UUID().uuidString 26 | guard let data = uuid.data(using: .utf8) else { return "" } 27 | let sha256 = SHA256.hash(data: data) 28 | return sha256.compactMap { String(format: "%02x", $0) }.joined() 29 | } 30 | 31 | func createUser() { 32 | guard repository.isEmptyUserID() else { return } 33 | let id = createID() 34 | let name = UsernameFactory.randomName() 35 | 36 | repository.save(id: id, name: name) { 37 | NotificationCenter.default.post(name: UserCreateUsecase.didCreateUser, 38 | object: nil) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Usecase/UserFetchUsecase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserFetchUsecase.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol UserFetchableUsecase { 11 | func fetchUser(id: String, completion: @escaping (User) -> Void) 12 | func fetchUserID() -> String? 13 | func fetchThemeStyle() -> Int 14 | } 15 | 16 | struct UserFetchUsecase: UserFetchableUsecase { 17 | var repository: UserRepository 18 | 19 | init(repository: UserRepository) { 20 | self.repository = repository 21 | } 22 | 23 | func fetchUser(id: String, completion: @escaping (User) -> Void) { 24 | repository.fetchUser(by: id) { user in 25 | completion(user) 26 | } 27 | } 28 | 29 | func fetchUserID() -> String? { 30 | return RoutinusRepository.userID() 31 | } 32 | 33 | func fetchThemeStyle() -> Int { 34 | return repository.fetchThemeStyle() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Routinus/Routinus/Domain/Usecase/UserUpdateUsecase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserUpdateUsecase.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol UserUpdatableUsecase { 11 | func updateContinuityDay(completion: ((User) -> Void)?) 12 | func updateContinuityDayByAuth() 13 | func updateUsername(of id: String, name: String) 14 | func updateThemeStyle(_ style: Int) 15 | } 16 | 17 | struct UserUpdateUsecase: UserUpdatableUsecase { 18 | static let didUpdateUser = Notification.Name("didUpdateUser") 19 | 20 | var repository: UserRepository 21 | 22 | init(repository: UserRepository) { 23 | self.repository = repository 24 | } 25 | 26 | func updateContinuityDay(completion: ((User) -> Void)?) { 27 | guard let userID = RoutinusRepository.userID() else { return } 28 | repository.updateContinuityDay(by: userID) { userDTO in 29 | let user = User(userDTO: userDTO) 30 | completion?(user) 31 | } 32 | } 33 | 34 | func updateContinuityDayByAuth() { 35 | guard let userID = RoutinusRepository.userID() else { return } 36 | repository.updateContinuityDayByAuth(by: userID) { 37 | NotificationCenter.default.post(name: UserUpdateUsecase.didUpdateUser, 38 | object: nil) 39 | } 40 | } 41 | 42 | func updateUsername(of id: String, name: String) { 43 | repository.updateUsername(of: id, name: name) { 44 | NotificationCenter.default.post(name: UserUpdateUsecase.didUpdateUser, 45 | object: nil) 46 | } 47 | } 48 | 49 | func updateThemeStyle(_ style: Int) { 50 | repository.updateThemeStyle(style) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Routinus/Routinus/Extensions/Date+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date+Extensions.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/04. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Date { 11 | init(dateString: String) { 12 | let formatter = DateFormatter() 13 | formatter.dateFormat = "yyyyMMdd" 14 | guard let date = formatter.date(from: dateString) else { 15 | self.init(timeInterval: 0, since: Date()) 16 | return 17 | } 18 | self.init(timeInterval: 0, since: date) 19 | } 20 | 21 | init(timeString: String) { 22 | let formatter = DateFormatter() 23 | formatter.dateFormat = "HHmm" 24 | guard let date = formatter.date(from: timeString) else { 25 | self.init(timeInterval: 0, since: Date()) 26 | return 27 | } 28 | self.init(timeInterval: 0, since: date) 29 | } 30 | 31 | public var year: Int { 32 | return Calendar.current.component(.year, from: self) 33 | } 34 | 35 | public var month: Int { 36 | return Calendar.current.component(.month, from: self) 37 | } 38 | 39 | public var day: Int { 40 | return Calendar.current.component(.day, from: self) 41 | } 42 | 43 | static func days(from: String, to: String) -> Int { 44 | let formatter = DateFormatter() 45 | formatter.dateFormat = "yyyyMMdd" 46 | 47 | guard let fromDate = formatter.date(from: from), 48 | let toDate = formatter.date(from: to) else { return -1 } 49 | 50 | let dateComponents = Calendar.current.dateComponents([.day], from: fromDate, to: toDate) 51 | guard let days = dateComponents.day else { return -1 } 52 | 53 | return abs(days) + 1 54 | } 55 | 56 | static func currentYearMonth() -> String { 57 | let dateComponents = Calendar.current.dateComponents([.year, .month], from: Date()) 58 | 59 | guard let year = dateComponents.year, 60 | let month = dateComponents.month else { return "" } 61 | 62 | return "\(year)\(month)" 63 | } 64 | 65 | func toDateString() -> String { 66 | let dateFormatter = DateFormatter() 67 | dateFormatter.dateFormat = "yyyyMMdd" 68 | return dateFormatter.string(from: self) 69 | } 70 | 71 | func toYearMonthString() -> String { 72 | let dateFormatter = DateFormatter() 73 | dateFormatter.dateFormat = "yyyyMM" 74 | return dateFormatter.string(from: self) 75 | } 76 | 77 | func toDayString() -> String { 78 | let dateFormatter = DateFormatter() 79 | dateFormatter.dateFormat = "dd" 80 | return dateFormatter.string(from: self) 81 | } 82 | 83 | func toTimeString() -> String { 84 | let dateFormatter = DateFormatter() 85 | dateFormatter.dateFormat = "HHmm" 86 | return dateFormatter.string(from: self) 87 | } 88 | 89 | func toTimeColonString() -> String { 90 | let dateFormatter = DateFormatter() 91 | dateFormatter.dateFormat = "HH:mm" 92 | return dateFormatter.string(from: self) 93 | } 94 | 95 | func toDateWithWeekdayString() -> String { 96 | let dateFormatter = DateFormatter() 97 | dateFormatter.dateFormat = "yyyy.MM.dd(E)" 98 | return dateFormatter.string(from: self) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Routinus/Routinus/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extensions.swift 3 | // Routinus 4 | // 5 | // Created by 김민서 on 2021/11/16. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | var localized: String { 12 | return NSLocalizedString(self, value: self, comment: "") 13 | } 14 | 15 | func localized(with argument: CVarArg = []) -> String { 16 | return String(format: localized, argument) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Routinus/Routinus/Extensions/UICollectionView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICollectionView+Extensions.swift 3 | // Routinus 4 | // 5 | // Created by 백지현 on 2021/11/24. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UICollectionView { 11 | func notifyEmptyData() { 12 | let informationView: UIView = { 13 | let view = UIView(frame: CGRect(x: center.x, 14 | y: center.y, 15 | width: bounds.width, 16 | height: bounds.height)) 17 | return view 18 | }() 19 | 20 | let titleLabel: UILabel = { 21 | let label = UILabel() 22 | label.text = "auth empty".localized 23 | label.textColor = UIColor(named: "SystemForeground") 24 | label.font = UIFont.boldSystemFont(ofSize: 18) 25 | return label 26 | }() 27 | 28 | let messageLabel: UILabel = { 29 | let label = UILabel() 30 | label.text = "lets auth".localized 31 | label.textColor = UIColor(named: "DayColor") 32 | label.font = UIFont.systemFont(ofSize: 15, weight: .medium) 33 | label.numberOfLines = 0 34 | label.textAlignment = .center 35 | label.numberOfLines = 2 36 | return label 37 | }() 38 | 39 | informationView.addSubview(titleLabel) 40 | informationView.addSubview(messageLabel) 41 | 42 | titleLabel.anchor(centerX: informationView.centerXAnchor, 43 | centerY: informationView.centerYAnchor) 44 | 45 | messageLabel.anchor(centerX: informationView.centerXAnchor, 46 | top: titleLabel.bottomAnchor, paddingTop: 20) 47 | 48 | backgroundView = informationView 49 | } 50 | 51 | func restore() { 52 | backgroundView = nil 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Routinus/Routinus/Extensions/UIImage+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+Extension.swift 3 | // Routinus 4 | // 5 | // Created by 김민서 on 2021/11/10. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIImage { 11 | enum RoutinusImageType { 12 | case original 13 | case thumbnail 14 | 15 | var length: CGFloat { 16 | switch self { 17 | case .original: 18 | return CGFloat(200.0) 19 | case .thumbnail: 20 | return CGFloat(80.0) 21 | } 22 | } 23 | } 24 | 25 | func resizedImage(_ type: RoutinusImageType) -> UIImage { 26 | let isWidthLongerThanHeight = size.width > size.height 27 | guard (isWidthLongerThanHeight ? size.width : size.height) >= type.length else { return self } 28 | 29 | let longerSide = type.length 30 | let scale = longerSide / (isWidthLongerThanHeight ? size.width : size.height) 31 | let shorterSide = (isWidthLongerThanHeight ? size.height : size.width) * scale 32 | let size = CGSize(width: (isWidthLongerThanHeight ? longerSide : shorterSide), 33 | height: (isWidthLongerThanHeight ? shorterSide : longerSide)) 34 | let render = UIGraphicsImageRenderer(size: size) 35 | 36 | return render.image { _ in 37 | draw(in: CGRect(origin: .zero, size: size)) 38 | } 39 | } 40 | 41 | func insertText(name: String, date: String, time: String) -> UIImage { 42 | let scale = UIScreen.main.scale 43 | UIGraphicsBeginImageContextWithOptions(size, false, scale) 44 | 45 | let label = UILabel(frame: CGRect(origin: .zero, size: size)) 46 | label.backgroundColor = UIColor(named: "SystemBackground")?.withAlphaComponent(0) 47 | label.textAlignment = .center 48 | label.numberOfLines = 2 49 | label.textColor = UIColor(named: "White")?.withAlphaComponent(0.5) 50 | label.font = UIFont.boldSystemFont(ofSize: 12) 51 | label.text = "\(name)\n\(date) \(time)" 52 | 53 | let imageView = UIImageView(image: self) 54 | UIGraphicsBeginImageContextWithOptions(label.bounds.size, false, 0) 55 | guard let context = UIGraphicsGetCurrentContext() else { return UIImage() } 56 | imageView.layer.render(in: context) 57 | label.layer.render(in: context) 58 | guard let imageWithText = UIGraphicsGetImageFromCurrentImageContext() else { return UIImage() } 59 | UIGraphicsEndImageContext() 60 | 61 | return imageWithText 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Routinus/Routinus/Extensions/UIScrollView+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+Extensions.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/27. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIScrollView { 11 | func removeAfterimage() { 12 | refreshControl?.beginRefreshing() 13 | refreshControl?.endRefreshing() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Routinus/Routinus/Network/DTO/AchievementDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AchievementDTO.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | struct AchievementDTO: Codable { 11 | let document: Fields? 12 | 13 | var documentID: String? { 14 | return document?.name?.components(separatedBy: "/").last 15 | } 16 | 17 | init() { 18 | self.document = nil 19 | } 20 | 21 | init(totalCount: Int, day: String, userID: String, achievementCount: Int, yearMonth: String) { 22 | let field = AchievementFields( 23 | totalCount: IntegerField(integerValue: "\(totalCount)"), 24 | day: StringField(stringValue: day), 25 | userID: StringField(stringValue: userID), 26 | achievementCount: IntegerField(integerValue: "\(achievementCount)"), 27 | yearMonth: StringField(stringValue: yearMonth) 28 | ) 29 | self.document = Fields(name: nil, fields: field) 30 | } 31 | } 32 | 33 | struct AchievementFields: Codable { 34 | let totalCount: IntegerField 35 | let day: StringField 36 | let userID: StringField 37 | let achievementCount: IntegerField 38 | let yearMonth: StringField 39 | 40 | enum CodingKeys: String, CodingKey { 41 | case day 42 | case totalCount = "total_count" 43 | case userID = "user_id" 44 | case achievementCount = "achievement_count" 45 | case yearMonth = "year_month" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Routinus/Routinus/Network/DTO/AuthDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthDTO.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | struct AuthDTO: Codable { 11 | let document: Fields? 12 | 13 | init() { 14 | self.document = nil 15 | } 16 | 17 | init(challengeID: String, userID: String, date: String, time: String) { 18 | let field = AuthFields(challengeID: StringField(stringValue: challengeID), 19 | userID: StringField(stringValue: userID), 20 | date: StringField(stringValue: date), 21 | time: StringField(stringValue: time)) 22 | self.document = Fields(name: nil, fields: field) 23 | } 24 | } 25 | 26 | struct AuthFields: Codable { 27 | let challengeID: StringField 28 | let userID: StringField 29 | let date: StringField 30 | let time: StringField 31 | 32 | enum CodingKeys: String, CodingKey { 33 | case date, time 34 | case challengeID = "challenge_id" 35 | case userID = "user_id" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Routinus/Routinus/Network/DTO/ChallengeDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChallengeDTO.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ChallengeDTO: Codable { 11 | var document: Fields? 12 | 13 | var documentID: String? { 14 | return document?.name?.components(separatedBy: "/").last 15 | } 16 | 17 | init() { 18 | self.document = nil 19 | } 20 | 21 | init(id: String, 22 | title: String, 23 | authMethod: String, 24 | categoryID: String, 25 | week: Int, 26 | desc: String, 27 | startDate: String, 28 | endDate: String, 29 | participantCount: Int, 30 | ownerID: String) { 31 | let field = ChallengeFields( 32 | authMethod: StringField(stringValue: authMethod), 33 | categoryID: StringField(stringValue: categoryID), 34 | desc: StringField(stringValue: desc), 35 | endDate: StringField(stringValue: endDate), 36 | id: StringField(stringValue: id), 37 | ownerID: StringField(stringValue: ownerID), 38 | participantCount: IntegerField(integerValue: "\(participantCount)"), 39 | startDate: StringField(stringValue: startDate), 40 | title: StringField(stringValue: title), 41 | week: IntegerField(integerValue: "\(week)") 42 | ) 43 | self.document = Fields(name: nil, fields: field) 44 | } 45 | } 46 | 47 | struct ChallengeFields: Codable { 48 | var authMethod: StringField 49 | var categoryID: StringField 50 | var desc: StringField 51 | var endDate: StringField 52 | var id: StringField 53 | var ownerID: StringField 54 | var participantCount: IntegerField 55 | var startDate: StringField 56 | var title: StringField 57 | var week: IntegerField 58 | 59 | enum CodingKeys: String, CodingKey { 60 | case desc, id, title, week 61 | case authMethod = "auth_method" 62 | case categoryID = "category_id" 63 | case endDate = "end_date" 64 | case ownerID = "owner_id" 65 | case participantCount = "participant_count" 66 | case startDate = "start_date" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Routinus/Routinus/Network/DTO/Fields.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Fields.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Fields: Codable { 11 | var name: String? 12 | var fields: T 13 | var createTime: String? 14 | } 15 | 16 | struct StringField: Codable { 17 | var stringValue: String 18 | } 19 | 20 | struct IntegerField: Codable { 21 | var integerValue: String 22 | } 23 | -------------------------------------------------------------------------------- /Routinus/Routinus/Network/DTO/ParticipationDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParticipationDTO.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ParticipationDTO: Codable { 11 | let document: Fields? 12 | 13 | var documentID: String? { 14 | return document?.name?.components(separatedBy: "/").last 15 | } 16 | 17 | init() { 18 | self.document = nil 19 | } 20 | 21 | init(authCount: Int, challengeID: String, joinDate: String, userID: String) { 22 | let field = ParticipationFields(authCount: IntegerField(integerValue: "\(authCount)"), 23 | challengeID: StringField(stringValue: challengeID), 24 | joinDate: StringField(stringValue: joinDate), 25 | userID: StringField(stringValue: userID)) 26 | self.document = Fields(name: nil, fields: field) 27 | } 28 | } 29 | 30 | struct ParticipationFields: Codable { 31 | let authCount: IntegerField 32 | let challengeID: StringField 33 | let joinDate: StringField 34 | let userID: StringField 35 | 36 | enum CodingKeys: String, CodingKey { 37 | case authCount = "auth_count" 38 | case challengeID = "challenge_id" 39 | case joinDate = "join_date" 40 | case userID = "user_id" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Routinus/Routinus/Network/DTO/TodayRoutineDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodayRoutineDTO.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | struct TodayRoutineDTO { 11 | var id: String 12 | var challengeID: String 13 | var categoryID: String 14 | var title: String 15 | var authCount: Int 16 | var joinDate: String 17 | var endDate: String 18 | 19 | init(participation: ParticipationDTO, challenge: ChallengeDTO) { 20 | self.id = participation.document?.fields.userID.stringValue ?? "" 21 | self.challengeID = participation.document?.fields.challengeID.stringValue ?? "" 22 | self.categoryID = challenge.document?.fields.categoryID.stringValue ?? "" 23 | self.title = challenge.document?.fields.title.stringValue ?? "" 24 | self.authCount = Int(participation.document?.fields.authCount.integerValue ?? "") ?? 0 25 | self.joinDate = participation.document?.fields.joinDate.stringValue ?? "" 26 | self.endDate = challenge.document?.fields.endDate.stringValue ?? "" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Routinus/Routinus/Network/DTO/UserDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDTO.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | struct UserDTO: Codable { 11 | let document: Fields? 12 | 13 | var documentID: String? { 14 | return document?.name?.components(separatedBy: "/").last 15 | } 16 | 17 | init() { 18 | self.document = nil 19 | } 20 | 21 | init(id: String, 22 | name: String, 23 | grade: Int, 24 | continuityDay: Int, 25 | userImageCategoryID: String, 26 | lastAuthDay: String) { 27 | let field = UserFields(id: StringField(stringValue: id), 28 | name: StringField(stringValue: name), 29 | grade: IntegerField(integerValue: "\(grade)"), 30 | continuityDay: IntegerField(integerValue: "\(continuityDay)"), 31 | userImageCategoryID: StringField(stringValue: userImageCategoryID), 32 | lastAuthDay: StringField(stringValue: lastAuthDay)) 33 | self.document = Fields(name: nil, fields: field) 34 | } 35 | } 36 | 37 | struct UserFields: Codable { 38 | let id: StringField 39 | let name: StringField 40 | let grade: IntegerField 41 | let continuityDay: IntegerField 42 | let userImageCategoryID: StringField 43 | let lastAuthDay: StringField 44 | 45 | enum CodingKeys: String, CodingKey { 46 | case id, name, grade 47 | case continuityDay = "continuity_day" 48 | case userImageCategoryID = "user_image_category_id" 49 | case lastAuthDay = "last_auth_day" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Routinus/Routinus/Network/Query/ParticipationQuery.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParticipationQuery.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ParticipationQuery { 11 | static func select(userID: String) -> Data? { 12 | return """ 13 | { 14 | "structuredQuery": { 15 | "from": { "collectionId": "challenge_participation" }, 16 | "where": { 17 | "fieldFilter": { 18 | "field": { "fieldPath": "user_id" }, 19 | "op": "EQUAL", 20 | "value": { "stringValue": "\(userID)" } 21 | }, 22 | }, 23 | "limit": 50 24 | } 25 | } 26 | """.data(using: .utf8) 27 | } 28 | 29 | static func select(userID: String, challengeID: String) -> Data? { 30 | return """ 31 | { 32 | "structuredQuery": { 33 | "from": { "collectionId": "challenge_participation" }, 34 | "where": { 35 | "compositeFilter": { 36 | "filters": [ 37 | { 38 | "fieldFilter": { 39 | "field": { "fieldPath": "user_id" }, 40 | "op": "EQUAL", 41 | "value": { "stringValue": "\(userID)" } 42 | } 43 | }, 44 | { 45 | "fieldFilter": { 46 | "field": { "fieldPath": "challenge_id" }, 47 | "op": "EQUAL", 48 | "value": { "stringValue": "\(challengeID)" } 49 | }, 50 | } 51 | ], 52 | "op": "AND" 53 | } 54 | }, 55 | "limit": 50 56 | } 57 | } 58 | """.data(using: .utf8) 59 | } 60 | 61 | static func insert(document: ParticipationFields) -> Data? { 62 | return """ 63 | { 64 | "fields": { 65 | "auth_count": { "integerValue": "0" }, 66 | "challenge_id": { "stringValue": "\(document.challengeID.stringValue)" }, 67 | "join_date": { "stringValue": "\(document.joinDate.stringValue)" }, 68 | "user_id": { "stringValue": "\(document.userID.stringValue)" } 69 | } 70 | } 71 | """.data(using: .utf8) 72 | } 73 | 74 | static func update(document: ParticipationFields) -> Data? { 75 | return """ 76 | { 77 | "fields": { 78 | "auth_count": { "integerValue": "\(document.authCount.integerValue)" } 79 | } 80 | } 81 | """.data(using: .utf8) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Routinus/Routinus/Network/Query/UserQuery.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserQuery.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | enum UserQuery { 11 | static func select(id: String) -> Data? { 12 | return """ 13 | { 14 | "structuredQuery": { 15 | "from": { "collectionId": "user" }, 16 | "where": { 17 | "fieldFilter": { 18 | "field": { "fieldPath": "id" }, 19 | "op": "EQUAL", 20 | "value": { "stringValue": "\(id)" } 21 | }, 22 | }, 23 | "limit": 1 24 | } 25 | } 26 | """.data(using: .utf8) 27 | } 28 | 29 | static func insert(id: String, name: String) -> Data? { 30 | return """ 31 | { 32 | "fields": { 33 | "id": { "stringValue": "\(id)" }, 34 | "name": { "stringValue": "\(name)" }, 35 | "grade": { "integerValue": "0" }, 36 | "continuity_day": { "integerValue": "0" }, 37 | "user_image_category_id": { "stringValue": "0" }, 38 | "last_auth_day": { "stringValue": "0" } 39 | } 40 | } 41 | """.data(using: .utf8) 42 | } 43 | 44 | static func update(document: UserFields) -> Data? { 45 | return """ 46 | { 47 | "fields": { 48 | "continuity_day": { "integerValue": "\(document.continuityDay.integerValue)" }, 49 | "last_auth_day": { "stringValue": "\(document.lastAuthDay.stringValue)" } 50 | } 51 | } 52 | """.data(using: .utf8) 53 | } 54 | 55 | static func updateUsername(document: UserFields) -> Data? { 56 | return """ 57 | { 58 | "fields": { 59 | "name": { "stringValue": "\(document.name.stringValue)" }, 60 | } 61 | } 62 | """.data(using: .utf8) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Auth/Subviews/AuthButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthButton.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | final class AuthButton: UIButton { 11 | weak var delegate: AuthButtonDelegate? 12 | 13 | init() { 14 | super.init(frame: .zero) 15 | self.setTitle("certify".localized, for: .normal) 16 | self.titleLabel?.font = UIFont.boldSystemFont(ofSize: 26) 17 | self.setTitleColor(UIColor(named: "Black"), for: .normal) 18 | self.backgroundColor = UIColor(named: "MainColor") 19 | self.layer.cornerRadius = 15 20 | self.addTarget(self, action: #selector(didTappedAuthButton), for: .touchUpInside) 21 | } 22 | 23 | override init(frame: CGRect) { 24 | super.init(frame: frame) 25 | } 26 | 27 | required init?(coder: NSCoder) { 28 | super.init(coder: coder) 29 | } 30 | 31 | func updateEnabled(isEnabled: Bool) { 32 | self.isEnabled = isEnabled 33 | let color = isEnabled ? "MainColor" : "MainColor0.5" 34 | self.backgroundColor = UIColor(named: color) 35 | } 36 | 37 | @objc func didTappedAuthButton() { 38 | delegate?.didTappedAuthButton() 39 | } 40 | } 41 | 42 | protocol AuthButtonDelegate: AnyObject { 43 | func didTappedAuthButton() 44 | } 45 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Auth/Subviews/AuthPreviewView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthPreviewView.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | final class AuthPreviewView: UIView { 11 | weak var delegate: PreviewViewDelegate? 12 | 13 | private lazy var previewView: UIView = { 14 | let view = UIView() 15 | view.backgroundColor = UIColor(named: "LightGray") 16 | view.layer.cornerRadius = 15 17 | view.layer.borderColor = UIColor(named: "MainColor")?.cgColor 18 | view.layer.borderWidth = 2 19 | view.clipsToBounds = true 20 | return view 21 | }() 22 | private lazy var authButton: UIButton = { 23 | let button = UIButton() 24 | button.setImage(UIImage(systemName: "camera.fill"), for: .normal) 25 | button.tintColor = UIColor(named: "SystemForeground") 26 | button.imageView?.contentMode = .scaleAspectFit 27 | button.contentVerticalAlignment = .fill 28 | button.contentHorizontalAlignment = .fill 29 | button.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) 30 | button.layer.borderWidth = 1 31 | button.layer.cornerRadius = 15 32 | button.isUserInteractionEnabled = false 33 | return button 34 | }() 35 | 36 | override init(frame: CGRect) { 37 | super.init(frame: frame) 38 | configure() 39 | } 40 | 41 | required init?(coder: NSCoder) { 42 | super.init(coder: coder) 43 | configure() 44 | } 45 | 46 | convenience init() { 47 | self.init(frame: CGRect.zero) 48 | } 49 | 50 | func updateImage(_ image: UIImage) { 51 | let backgroundImage = UIImageView(frame: previewView.bounds) 52 | backgroundImage.image = image 53 | backgroundImage.contentMode = .scaleAspectFill 54 | backgroundImage.clipsToBounds = true 55 | previewView.addSubview(backgroundImage) 56 | } 57 | } 58 | 59 | extension AuthPreviewView { 60 | private func configure() { 61 | configureSubviews() 62 | configureGesture() 63 | } 64 | 65 | private func configureSubviews() { 66 | addSubview(previewView) 67 | previewView.anchor(horizontal: self, paddingHorizontal: 20, vertical: self) 68 | 69 | previewView.addSubview(authButton) 70 | authButton.anchor(centerX: self.centerXAnchor, 71 | centerY: self.centerYAnchor, 72 | width: 80, 73 | height: 80) 74 | } 75 | 76 | private func configureGesture() { 77 | let recognizer = UITapGestureRecognizer(target: self, 78 | action: #selector(didTappedPreviewView)) 79 | previewView.isUserInteractionEnabled = true 80 | previewView.addGestureRecognizer(recognizer) 81 | } 82 | 83 | @objc private func didTappedPreviewView(_ sender: UITapGestureRecognizer) { 84 | guard sender.state == .ended else { return } 85 | delegate?.didTappedPreviewView() 86 | } 87 | } 88 | 89 | protocol PreviewViewDelegate: AnyObject { 90 | func didTappedPreviewView() 91 | } 92 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/AuthImages/AuthImagesViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthImagesViewModel.swift 3 | // Routinus 4 | // 5 | // Created by 백지현 on 2021/11/24. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | protocol AuthImagesViewModelInput { 12 | func fetchAuthData(authDisplayState: AuthDisplayState) 13 | func imageData(from directory: String, filename: String, completion: ((Data?) -> Void)?) 14 | } 15 | 16 | protocol AuthImagesViewModelOutput { 17 | var auths: CurrentValueSubject<[Auth], Never> { get } 18 | var authDisplayState: CurrentValueSubject { get } 19 | 20 | var authImageTap: PassthroughSubject { get } 21 | var authImageLoad: PassthroughSubject { get } 22 | } 23 | 24 | protocol AuthImagesViewModelIO: AuthImagesViewModelInput, AuthImagesViewModelOutput { } 25 | 26 | final class AuthImagesViewModel: AuthImagesViewModelIO { 27 | var auths = CurrentValueSubject<[Auth], Never>([]) 28 | var authDisplayState = CurrentValueSubject(.all) 29 | 30 | var authImageTap = PassthroughSubject() 31 | var authImageLoad = PassthroughSubject() 32 | 33 | private var challengeID: String? 34 | 35 | let authFetchUsecase: AuthFetchableUsecase 36 | let imageFetchUsecase: ImageFetchableUsecase 37 | 38 | init(challengeID: String, 39 | authDisplayState: AuthDisplayState, 40 | authFetchUsecase: AuthFetchableUsecase, 41 | imageFetchUsecase: ImageFetchableUsecase) { 42 | self.challengeID = challengeID 43 | self.authFetchUsecase = authFetchUsecase 44 | self.imageFetchUsecase = imageFetchUsecase 45 | fetchAuthData(authDisplayState: authDisplayState) 46 | } 47 | } 48 | 49 | extension AuthImagesViewModel { 50 | private func fetchAuths() { 51 | guard let challengeID = challengeID else { return } 52 | authFetchUsecase.fetchAuths(challengeID: challengeID) { auths in 53 | self.auths.value = auths 54 | } 55 | } 56 | 57 | private func fetchMyAuths() { 58 | guard let challengeID = challengeID else { return } 59 | authFetchUsecase.fetchMyAuths(challengeID: challengeID) { auths in 60 | self.auths.value = auths 61 | } 62 | } 63 | } 64 | 65 | extension AuthImagesViewModel: AuthImagesViewModelInput { 66 | func fetchAuthData(authDisplayState: AuthDisplayState) { 67 | switch authDisplayState { 68 | case .all: 69 | self.authDisplayState.value = .all 70 | self.fetchAuths() 71 | case .my: 72 | self.authDisplayState.value = .my 73 | self.fetchMyAuths() 74 | } 75 | } 76 | 77 | func imageData(from directory: String, filename: String, completion: ((Data?) -> Void)? = nil) { 78 | imageFetchUsecase.fetchImageData(from: directory, filename: filename) { data in 79 | completion?(data) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/AuthImages/Cells/AuthImagesCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthImagesCollectionViewCell.swift 3 | // Routinus 4 | // 5 | // Created by 백지현 on 2021/11/24. 6 | // 7 | 8 | import UIKit 9 | 10 | final class AuthImagesCollectionViewCell: UICollectionViewCell { 11 | static let identifier = "AuthImagesCollectionViewCell" 12 | 13 | private lazy var imageView: UIImageView = { 14 | let imageView = UIImageView() 15 | imageView.layer.cornerRadius = 10 16 | imageView.contentMode = .scaleAspectFill 17 | imageView.clipsToBounds = true 18 | return imageView 19 | }() 20 | 21 | override init(frame: CGRect) { 22 | super.init(frame: frame) 23 | configure() 24 | } 25 | 26 | required init?(coder: NSCoder) { 27 | super.init(coder: coder) 28 | configure() 29 | } 30 | 31 | func update(image: UIImage) { 32 | imageView.image = image 33 | } 34 | 35 | func imageData() -> Data { 36 | guard let imageData = imageView.image?.jpegData(compressionQuality: 1) else { return Data() } 37 | return imageData 38 | } 39 | } 40 | 41 | extension AuthImagesCollectionViewCell { 42 | private func configure() { 43 | configureCell() 44 | } 45 | 46 | private func configureCell() { 47 | addSubview(imageView) 48 | imageView.anchor(leading: leadingAnchor, 49 | trailing: trailingAnchor, 50 | top: topAnchor, 51 | bottom: bottomAnchor) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Challenge/Cells/ChallengeCategoryCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChallengeCategoryCollectionViewCell.swift 3 | // Routinus 4 | // 5 | // Created by 김민서 on 2021/11/04. 6 | // 7 | 8 | import UIKit 9 | 10 | final class ChallengeCategoryCollectionViewCell: UICollectionViewCell { 11 | static let identifier = "ChallengeCategoryCollectionViewCell" 12 | 13 | weak var delegate: ChallengeCategoryCellDelegate? 14 | 15 | private lazy var yStackView: UIStackView = { 16 | let stack = UIStackView() 17 | stack.axis = .vertical 18 | stack.spacing = 25 19 | return stack 20 | }() 21 | 22 | private lazy var xStackView1: UIStackView = { 23 | let stack = UIStackView() 24 | stack.axis = .horizontal 25 | stack.distribution = .equalSpacing 26 | return stack 27 | }() 28 | 29 | private lazy var xStackView2: UIStackView = { 30 | let stack = UIStackView() 31 | stack.axis = .horizontal 32 | stack.distribution = .equalSpacing 33 | return stack 34 | }() 35 | 36 | func configureViews() { 37 | addSubview(yStackView) 38 | yStackView.anchor(horizontal: yStackView.superview, top: yStackView.superview?.topAnchor) 39 | 40 | yStackView.addArrangedSubview(xStackView1) 41 | yStackView.addArrangedSubview(xStackView2) 42 | 43 | for (index, category) in Challenge.Category.allCases.enumerated() { 44 | let button = ChallengeCategoryIconView() 45 | if UIImage(named: category.symbol) != nil { 46 | button.updateImage(UIImage(named: category.symbol)) 47 | } else { 48 | button.updateImage(UIImage(systemName: category.symbol)) 49 | } 50 | button.updateTitle(category.title) 51 | button.updateTintColor(UIColor(named: category.color)) 52 | 53 | let gesture = ChallengeCategoryIconViewTapGesture( 54 | target: self, 55 | action: #selector(didTappedCategoryButton) 56 | ) 57 | gesture.numberOfTapsRequired = 1 58 | gesture.configureCategory(category: category) 59 | isUserInteractionEnabled = true 60 | button.addGestureRecognizer(gesture) 61 | 62 | if index < 4 { 63 | xStackView1.addArrangedSubview(button) 64 | } else { 65 | xStackView2.addArrangedSubview(button) 66 | } 67 | } 68 | } 69 | } 70 | 71 | extension ChallengeCategoryCollectionViewCell { 72 | @objc private func didTappedCategoryButton(_ gesture: ChallengeCategoryIconViewTapGesture) { 73 | guard let category = gesture.category else { return } 74 | delegate?.didTappedCategoryButton(category: category) 75 | } 76 | } 77 | 78 | protocol ChallengeCategoryCellDelegate: AnyObject { 79 | func didTappedCategoryButton(category: Challenge.Category) 80 | } 81 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Challenge/Cells/ChallengeCategoryCollectionViewHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChallengeCollectionViewHeader.swift 3 | // Routinus 4 | // 5 | // Created by 김민서 on 2021/11/04. 6 | // 7 | 8 | import UIKit 9 | 10 | final class ChallengeCategoryCollectionViewHeader: UICollectionReusableView { 11 | static let identifier = "ChallengeCategoryCollectionViewHeader" 12 | 13 | weak var delegate: ChallengeCategoryHeaderDelegate? 14 | 15 | private let stackView = UIStackView() 16 | 17 | private lazy var label: UILabel = { 18 | let label = UILabel() 19 | label.textColor = UIColor(named: "SystemForeground") 20 | label.font = UIFont.boldSystemFont(ofSize: 18) 21 | 22 | return label 23 | }() 24 | 25 | private lazy var seeAllButton: UIButton = { 26 | let button = UIButton(type: .system) 27 | button.setTitle("See all", for: .normal) 28 | button.setTitleColor(UIColor(named: "SystemForeground"), for: .normal) 29 | button.setContentHuggingPriority(.defaultHigh, for: .horizontal) 30 | button.addTarget(self, action: #selector(didTappedSeeAllButton), for: .touchUpInside) 31 | return button 32 | }() 33 | 34 | var title: String = "" { 35 | didSet { 36 | label.text = title 37 | } 38 | } 39 | 40 | override init(frame: CGRect) { 41 | super.init(frame: frame) 42 | configure() 43 | } 44 | 45 | required init?(coder: NSCoder) { 46 | super.init(coder: coder) 47 | configure() 48 | } 49 | 50 | override func layoutSubviews() { 51 | super.layoutSubviews() 52 | stackView.frame = bounds 53 | } 54 | 55 | func addSeeAllButton() { 56 | stackView.addArrangedSubview(seeAllButton) 57 | } 58 | } 59 | 60 | extension ChallengeCategoryCollectionViewHeader { 61 | private func configure() { 62 | configureView() 63 | } 64 | 65 | private func configureView() { 66 | addSubview(stackView) 67 | stackView.addArrangedSubview(label) 68 | } 69 | 70 | @objc private func didTappedSeeAllButton() { 71 | delegate?.didTappedSeeAllButton() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Challenge/Cells/ChallengeRecommendCollectionViewHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChallengeRecommendCollectionViewHeader.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/24. 6 | // 7 | 8 | import UIKit 9 | 10 | final class ChallengeRecommendCollectionViewHeader: UICollectionReusableView { 11 | static let identifier = "ChallengeRecommendCollectionViewHeader" 12 | 13 | weak var delegate: ChallengeRecommendHeaderDelegate? 14 | 15 | private let stackView = UIStackView() 16 | 17 | private lazy var titleLabel: UILabel = { 18 | let label = UILabel() 19 | label.text = "challenges".localized 20 | label.font = UIFont.systemFont(ofSize: UIScreen.main.bounds.width <= 350 ? 30 : 34, 21 | weight: .bold) 22 | return label 23 | }() 24 | 25 | private lazy var searchButton: UIButton = { 26 | let button = UIButton() 27 | button.setImage(UIImage(systemName: "magnifyingglass"), for: .normal) 28 | button.tintColor = UIColor(named: "SystemForeground") 29 | button.addTarget(self, action: #selector(didTappedSearchButton), for: .touchUpInside) 30 | return button 31 | }() 32 | 33 | var title: String = "" { 34 | didSet { 35 | titleLabel.text = title 36 | } 37 | } 38 | 39 | override init(frame: CGRect) { 40 | super.init(frame: frame) 41 | configure() 42 | } 43 | 44 | required init?(coder: NSCoder) { 45 | super.init(coder: coder) 46 | configure() 47 | } 48 | 49 | override func layoutSubviews() { 50 | super.layoutSubviews() 51 | stackView.frame = bounds 52 | } 53 | 54 | func addSearchButton() { 55 | stackView.addArrangedSubview(searchButton) 56 | } 57 | } 58 | 59 | extension ChallengeRecommendCollectionViewHeader { 60 | private func configure() { 61 | configureView() 62 | } 63 | 64 | private func configureView() { 65 | addSubview(stackView) 66 | stackView.addArrangedSubview(titleLabel) 67 | } 68 | 69 | @objc private func didTappedSearchButton() { 70 | delegate?.didTappedSearchButton() 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Challenge/Delegates/ChallengeCategoryHeaderDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChallengeCategoryHeaderDelegate.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ChallengeCategoryHeaderDelegate: AnyObject { 11 | func didTappedSeeAllButton() 12 | } 13 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Challenge/Delegates/ChallengeRecommendHeaderDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChallengeRecommendHeaderDelegate.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol ChallengeRecommendHeaderDelegate: AnyObject { 11 | func didTappedSearchButton() 12 | } 13 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Challenge/Layouts/ChallengeCollectionViewLayouts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChallengeCollectionViewLayouts.swift 3 | // Routinus 4 | // 5 | // Created by 김민서 on 2021/11/04. 6 | // 7 | 8 | import UIKit 9 | 10 | final class ChallengeCollectionViewLayouts { 11 | private var recommendLayout: NSCollectionLayoutSection { 12 | let smallWidth = UIScreen.main.bounds.width <= 350 13 | let offset = smallWidth ? 15.0 : 25.0 14 | 15 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), 16 | heightDimension: .fractionalHeight(1)) 17 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 18 | 19 | let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.8), 20 | heightDimension: .estimated(170)) 21 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) 22 | 23 | let section = NSCollectionLayoutSection(group: group) 24 | section.boundarySupplementaryItems = [.init( 25 | layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .estimated(80)), 26 | elementKind: UICollectionView.elementKindSectionHeader, 27 | alignment: .topLeading 28 | )] 29 | 30 | section.orthogonalScrollingBehavior = .groupPaging 31 | section.contentInsets = .init(top: 10, leading: offset, bottom: 20, trailing: offset) 32 | section.interGroupSpacing = 20 33 | return section 34 | } 35 | 36 | private var mainLayout: NSCollectionLayoutSection { 37 | let smallWidth = UIScreen.main.bounds.width <= 350 38 | let offset = smallWidth ? 15.0 : 25.0 39 | 40 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), 41 | heightDimension: .estimated(200)) 42 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 43 | 44 | let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), 45 | heightDimension: .estimated(200)) 46 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item]) 47 | 48 | let section = NSCollectionLayoutSection(group: group) 49 | section.boundarySupplementaryItems = [.init( 50 | layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(70)), 51 | elementKind: UICollectionView.elementKindSectionHeader, 52 | alignment: .topLeading 53 | )] 54 | 55 | section.orthogonalScrollingBehavior = .none 56 | section.contentInsets = .init(top: 0, leading: offset, bottom: 0, trailing: offset) 57 | return section 58 | } 59 | 60 | func section(at sectionNumber: Int) -> NSCollectionLayoutSection { 61 | if sectionNumber == 0 { 62 | return recommendLayout 63 | } else { 64 | return mainLayout 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Challenge/Subviews/ChallengeCategoryIconView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChallengeCategoryIconView.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/08. 6 | // 7 | 8 | import UIKit 9 | 10 | final class ChallengeCategoryIconView: UIView { 11 | private var imageView = UIImageView() 12 | private var title = UILabel() 13 | 14 | override init(frame: CGRect) { 15 | super.init(frame: frame) 16 | imageView = UIImageView() 17 | title = UILabel() 18 | configure() 19 | } 20 | 21 | required init?(coder aDecoder: NSCoder) { 22 | super.init(coder: aDecoder) 23 | imageView = UIImageView() 24 | title = UILabel() 25 | configure() 26 | } 27 | 28 | func updateImage(_ image: UIImage?) { 29 | imageView.image = image 30 | imageView.contentMode = .scaleAspectFit 31 | } 32 | 33 | func updateTitle(_ text: String) { 34 | title.text = text 35 | title.tintColor = UIColor(named: "SystemForeground") 36 | } 37 | 38 | func updateTintColor(_ color: UIColor?) { 39 | imageView.tintColor = color 40 | } 41 | } 42 | 43 | extension ChallengeCategoryIconView { 44 | private func configure() { 45 | configureViews() 46 | } 47 | 48 | private func configureViews() { 49 | anchor(width: 60, height: 60) 50 | 51 | addSubview(imageView) 52 | imageView.anchor(centerX: imageView.superview?.centerXAnchor, 53 | top: imageView.superview?.topAnchor, 54 | width: 35, 55 | height: 35) 56 | 57 | addSubview(title) 58 | title.anchor(centerX: title.superview?.centerXAnchor, 59 | bottom: title.superview?.bottomAnchor) 60 | } 61 | } 62 | 63 | final class ChallengeCategoryIconViewTapGesture: UITapGestureRecognizer { 64 | private(set) var category: Challenge.Category? 65 | 66 | func configureCategory(category: Challenge.Category) { 67 | self.category = category 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Common/Cells/ChallengeCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChallengeCollectionViewCell.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/09. 6 | // 7 | 8 | import UIKit 9 | 10 | final class ChallengeCollectionViewCell: UICollectionViewCell { 11 | static let identifier = "ChallengeCollectionViewCell" 12 | 13 | private lazy var imageView: UIImageView = { 14 | let imageView = UIImageView() 15 | imageView.contentMode = .scaleAspectFill 16 | imageView.clipsToBounds = true 17 | imageView.layer.cornerRadius = 5 18 | return imageView 19 | }() 20 | 21 | private lazy var titleLabel: UILabel = { 22 | let label = UILabel() 23 | label.textColor = UIColor(named: "SystemForeground") 24 | label.font = UIFont.boldSystemFont(ofSize: 15) 25 | label.lineBreakMode = .byTruncatingTail 26 | return label 27 | }() 28 | 29 | override init(frame: CGRect) { 30 | super.init(frame: frame) 31 | configure() 32 | } 33 | 34 | required init?(coder: NSCoder) { 35 | super.init(coder: coder) 36 | configure() 37 | } 38 | 39 | override func prepareForReuse() { 40 | super.prepareForReuse() 41 | titleLabel.text = "" 42 | imageView.image = nil 43 | } 44 | 45 | func updateTitle(_ title: String) { 46 | titleLabel.text = title 47 | } 48 | 49 | func updateImage(_ image: UIImage) { 50 | imageView.image = image 51 | } 52 | } 53 | 54 | extension ChallengeCollectionViewCell { 55 | private func configure() { 56 | configureViews() 57 | } 58 | 59 | private func configureViews() { 60 | addSubview(imageView) 61 | imageView.anchor(horizontal: imageView.superview, 62 | top: imageView.superview?.topAnchor, 63 | height: 110) 64 | 65 | addSubview(titleLabel) 66 | titleLabel.anchor(horizontal: titleLabel.superview, 67 | top: imageView.bottomAnchor, 68 | paddingTop: 10, 69 | bottom: titleLabel.superview?.bottomAnchor) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Common/ImagePanViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePanViewController.swift 3 | // Routinus 4 | // 5 | // Created by 김민서 on 2021/11/23. 6 | // 7 | 8 | import UIKit 9 | 10 | final class ImagePanViewController: UIViewController { 11 | private lazy var dimmedBackgroundView: UIView = { 12 | let view = UIView() 13 | view.backgroundColor = UIColor(named: "Black")?.withAlphaComponent(0.8) 14 | return view 15 | }() 16 | 17 | private lazy var imageView: UIImageView = { 18 | let imageView = UIImageView() 19 | imageView.contentMode = .scaleAspectFit 20 | return imageView 21 | }() 22 | 23 | private var imageData: Data? { 24 | didSet { 25 | fetchImage() 26 | } 27 | } 28 | 29 | override func viewDidLoad() { 30 | super.viewDidLoad() 31 | configureViews() 32 | configureGesture() 33 | } 34 | 35 | func updateImage(data: Data) { 36 | imageData = data 37 | } 38 | } 39 | 40 | extension ImagePanViewController { 41 | private func configureViews() { 42 | view.addSubview(dimmedBackgroundView) 43 | dimmedBackgroundView.anchor(centerX: view.centerXAnchor, 44 | centerY: view.centerYAnchor, 45 | width: view.frame.width * 2, 46 | height: view.frame.height * 2) 47 | view.addSubview(imageView) 48 | imageView.anchor(centerX: view.centerXAnchor, 49 | centerY: view.centerYAnchor, 50 | width: UIScreen.main.bounds.width, 51 | height: UIScreen.main.bounds.height) 52 | } 53 | 54 | private func configureGesture() { 55 | let pan = UIPanGestureRecognizer(target: self, action: #selector(doPan(_:))) 56 | pan.delegate = self 57 | view.addGestureRecognizer(pan) 58 | } 59 | 60 | @objc func doPan(_ pan: UIPanGestureRecognizer) { 61 | let translation = pan.translation(in: imageView) 62 | let centerX = imageView.center.x 63 | let centerY = imageView.center.y 64 | 65 | if let imageView = pan.view { 66 | imageView.center = CGPoint(x: imageView.center.x + translation.x, 67 | y: imageView.center.y + translation.y) 68 | let xDistance = imageView.center.x - centerX 69 | let yDistacne = imageView.center.y - centerY 70 | let distanceFromCenter = sqrt(xDistance * xDistance + yDistacne * yDistacne) 71 | dimmedBackgroundView.alpha = 0.9 - (distanceFromCenter / 250) 72 | } 73 | pan.setTranslation(.zero, in: imageView) 74 | 75 | if pan.state == .ended { 76 | dismiss(animated: false) 77 | } 78 | } 79 | 80 | private func fetchImage() { 81 | guard let image = imageData else { return } 82 | DispatchQueue.main.async { [weak self] in 83 | guard let self = self else { return } 84 | self.imageView.image = UIImage(data: image) 85 | } 86 | } 87 | } 88 | 89 | extension ImagePanViewController: UIGestureRecognizerDelegate { 90 | func gestureRecognizer( 91 | _ gestureRecognizer: UIGestureRecognizer, 92 | shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer 93 | ) -> Bool { 94 | return true 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Common/Subviews/ChallengePromotionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddChallengeView.swift 3 | // Routinus 4 | // 5 | // Created by 김민서 on 2021/11/20. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol ChallengePromotionViewDelegate: AnyObject { 11 | func didTappedPromotionButton() 12 | } 13 | 14 | final class ChallengePromotionView: UIView { 15 | weak var delegate: ChallengePromotionViewDelegate? 16 | 17 | private lazy var stackView: UIStackView = { 18 | let stackView = UIStackView() 19 | stackView.axis = .vertical 20 | stackView.spacing = 20 21 | return stackView 22 | }() 23 | 24 | private lazy var titleLabel: UILabel = { 25 | let label = UILabel() 26 | label.text = "make your challenge".localized 27 | label.numberOfLines = 2 28 | label.textAlignment = .center 29 | label.font = .boldSystemFont(ofSize: 18) 30 | return label 31 | }() 32 | 33 | private lazy var promotionButton: UIButton = { 34 | let button = UIButton() 35 | button.setTitle("make challenge".localized, for: .normal) 36 | button.setTitleColor(UIColor(named: "SystemBackground"), for: .normal) 37 | button.backgroundColor = UIColor(named: "MainColor") 38 | button.layer.cornerRadius = 5 39 | button.addTarget(self, action: #selector(didTappedPromotionButton), for: .touchUpInside) 40 | return button 41 | }() 42 | 43 | override init(frame: CGRect) { 44 | super.init(frame: frame) 45 | configure() 46 | } 47 | 48 | required init?(coder: NSCoder) { 49 | super.init(coder: coder) 50 | configure() 51 | } 52 | 53 | convenience init() { 54 | self.init(frame: CGRect.zero) 55 | } 56 | 57 | func configureChallengePromotionView(titleLabel: String, buttonLabel: String) { 58 | self.titleLabel.text = titleLabel 59 | promotionButton.setTitle(buttonLabel, for: .normal) 60 | } 61 | } 62 | 63 | extension ChallengePromotionView { 64 | private func configure() { 65 | configureView() 66 | } 67 | 68 | private func configureView() { 69 | backgroundColor = UIColor(named: "MainColor")?.withAlphaComponent(0.5) 70 | layer.cornerRadius = 10 71 | anchor(height: 160) 72 | 73 | addSubview(stackView) 74 | stackView.addArrangedSubview(titleLabel) 75 | stackView.addArrangedSubview(promotionButton) 76 | stackView.anchor(centerX: centerXAnchor, centerY: centerYAnchor) 77 | } 78 | 79 | @objc func didTappedPromotionButton() { 80 | delegate?.didTappedPromotionButton() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Detail/Subviews/DetailParticipantButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailParticipantButton.swift 3 | // Routinus 4 | // 5 | // Created by 백지현 on 2021/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | final class DetailParticipantButton: UIButton { 11 | weak var delegate: ParticipantButtonDelegate? 12 | 13 | init() { 14 | super.init(frame: .zero) 15 | self.setTitle(ParticipationAuthState.notParticipating.rawValue, for: .normal) 16 | self.isEnabled = true 17 | self.titleLabel?.font = UIFont.boldSystemFont(ofSize: 26) 18 | self.setTitleColor(UIColor(named: "Black"), for: .normal) 19 | self.backgroundColor = UIColor(named: "MainColor") 20 | self.layer.cornerRadius = 15 21 | self.addTarget(self, action: #selector(didTappedAuthButton), for: .touchUpInside) 22 | } 23 | 24 | override init(frame: CGRect) { 25 | super.init(frame: frame) 26 | } 27 | 28 | required init?(coder: NSCoder) { 29 | super.init(coder: coder) 30 | } 31 | 32 | @objc func didTappedAuthButton() { 33 | delegate?.didTappedParticipantButton() 34 | } 35 | 36 | func update(to state: ParticipationAuthState) { 37 | isEnabled = state == .authenticated ? false : true 38 | let backgroundColor = isEnabled ? "MainColor" : "MainColor0.5" 39 | let titleColor = isEnabled ? "SystemForeground" : "DayColor" 40 | setTitle(state.rawValue.localized, for: .normal) 41 | setTitleColor(UIColor(named: titleColor), for: .normal) 42 | self.backgroundColor = UIColor(named: backgroundColor) 43 | } 44 | } 45 | 46 | protocol ParticipantButtonDelegate: AnyObject { 47 | func didTappedParticipantButton() 48 | } 49 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Form/Delegates/FormImagePickerDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormImagePickerDelegate.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/10. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol FormImagePickerDelegate: AnyObject { 11 | func didTappedImageView(_ tag: Int) 12 | } 13 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Form/Delegates/FormSubviewDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormSubviewDelegate.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/10. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol FormSubviewDelegate: AnyObject { 11 | func didChange(category: Challenge.Category) 12 | func didChange(imageURL: String) 13 | func didChange(authExampleImageURL: String) 14 | func didTappedCategoryButton() 15 | } 16 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Form/Subviews/FormAuthImageRegisterView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormAuthImageRegisterView.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/09. 6 | // 7 | 8 | import UIKit 9 | 10 | final class FormAuthImageRegisterView: UIView { 11 | weak var delegate: FormImagePickerDelegate? 12 | 13 | private lazy var titleLabel: UILabel = { 14 | let label = UILabel() 15 | label.text = "upload auth exmaple photo".localized 16 | label.font = .boldSystemFont(ofSize: 20) 17 | label.numberOfLines = 2 18 | return label 19 | }() 20 | private lazy var descriptionLabel: UILabel = { 21 | let label = UILabel() 22 | label.text = "add auth example photo".localized 23 | label.font = .systemFont(ofSize: 16) 24 | label.textColor = UIColor(named: "DayColor") 25 | label.numberOfLines = 3 26 | return label 27 | }() 28 | private lazy var imageView: UIImageView = { 29 | let imageView = UIImageView() 30 | imageView.image = UIImage(systemName: "camera") 31 | imageView.tintColor = UIColor(named: "SystemForeground") 32 | imageView.contentMode = .scaleAspectFit 33 | imageView.clipsToBounds = true 34 | imageView.layer.cornerRadius = 10 35 | imageView.layer.borderWidth = 1 36 | imageView.layer.borderColor = UIColor(named: "DayColor")?.cgColor 37 | return imageView 38 | }() 39 | 40 | override init(frame: CGRect) { 41 | super.init(frame: frame) 42 | configure() 43 | } 44 | 45 | required init?(coder: NSCoder) { 46 | super.init(coder: coder) 47 | configure() 48 | } 49 | 50 | convenience init() { 51 | self.init(frame: CGRect.zero) 52 | } 53 | 54 | func updateImage(_ image: UIImage) { 55 | imageView.image = image 56 | imageView.contentMode = .scaleAspectFill 57 | } 58 | } 59 | 60 | extension FormAuthImageRegisterView { 61 | private func configure() { 62 | configureSubviews() 63 | configureGesture() 64 | } 65 | 66 | private func configureSubviews() { 67 | addSubview(titleLabel) 68 | titleLabel.anchor(top: titleLabel.superview?.topAnchor, 69 | width: UIScreen.main.bounds.width - 40) 70 | 71 | addSubview(descriptionLabel) 72 | descriptionLabel.anchor(top: titleLabel.bottomAnchor, paddingTop: 10, 73 | width: UIScreen.main.bounds.width - 40) 74 | 75 | addSubview(imageView) 76 | imageView.anchor(centerX: imageView.superview?.centerXAnchor, 77 | top: descriptionLabel.bottomAnchor, paddingTop: 30, 78 | width: 150, height: 150) 79 | 80 | anchor(bottom: imageView.bottomAnchor) 81 | } 82 | 83 | private func configureGesture() { 84 | let recognizer = UITapGestureRecognizer(target: self, 85 | action: #selector(didTappedImageView(_:))) 86 | imageView.isUserInteractionEnabled = true 87 | imageView.addGestureRecognizer(recognizer) 88 | } 89 | 90 | @objc private func didTappedImageView(_ sender: UITapGestureRecognizer) { 91 | guard sender.state == .ended else { return } 92 | delegate?.didTappedImageView(FormViewController.InputTag.authImage.rawValue) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Form/Subviews/FormAuthMethodView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormAuthMethodView.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/09. 6 | // 7 | 8 | import UIKit 9 | 10 | final class FormAuthMethodView: UIView { 11 | typealias Tag = FormViewController.InputTag 12 | 13 | weak var delegate: UITextViewDelegate? { 14 | didSet { 15 | textView.delegate = delegate 16 | } 17 | } 18 | 19 | private lazy var titleLabel: UILabel = { 20 | let label = UILabel() 21 | label.text = "write auth method".localized 22 | label.font = .boldSystemFont(ofSize: 20) 23 | label.numberOfLines = 2 24 | return label 25 | }() 26 | private lazy var descriptionLabel: UILabel = { 27 | let label = UILabel() 28 | label.text = "auth method 150".localized 29 | label.font = .systemFont(ofSize: 16) 30 | label.textColor = UIColor(named: "DayColor") 31 | label.numberOfLines = 3 32 | return label 33 | }() 34 | private lazy var textView: UITextView = { 35 | let textView = UITextView() 36 | textView.layer.borderColor = UIColor(named: "SystemForeground")?.cgColor 37 | textView.layer.borderWidth = 1 38 | textView.layer.cornerRadius = 10 39 | textView.font = .systemFont(ofSize: 16) 40 | textView.tag = Tag.authMethod.rawValue 41 | return textView 42 | }() 43 | 44 | override init(frame: CGRect) { 45 | super.init(frame: frame) 46 | configure() 47 | } 48 | 49 | required init?(coder: NSCoder) { 50 | super.init(coder: coder) 51 | configure() 52 | } 53 | 54 | convenience init() { 55 | self.init(frame: CGRect.zero) 56 | } 57 | 58 | func hideKeyboard() { 59 | textView.endEditing(true) 60 | } 61 | 62 | func update(authMethod: String) { 63 | textView.text = authMethod 64 | } 65 | } 66 | 67 | extension FormAuthMethodView { 68 | private func configure() { 69 | configureSubviews() 70 | } 71 | 72 | private func configureSubviews() { 73 | addSubview(titleLabel) 74 | titleLabel.anchor(top: titleLabel.superview?.topAnchor, 75 | width: UIScreen.main.bounds.width - 40) 76 | 77 | addSubview(descriptionLabel) 78 | descriptionLabel.anchor(top: titleLabel.bottomAnchor, paddingTop: 10, 79 | width: UIScreen.main.bounds.width - 40) 80 | 81 | addSubview(textView) 82 | textView.anchor(top: descriptionLabel.bottomAnchor, paddingTop: 20, 83 | width: UIScreen.main.bounds.width - 40, 84 | height: 150) 85 | 86 | anchor(bottom: textView.bottomAnchor) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Form/Subviews/FormImageRegisterView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormImageRegisterView.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/09. 6 | // 7 | 8 | import UIKit 9 | 10 | final class FormImageRegisterView: UIView { 11 | weak var delegate: FormImagePickerDelegate? 12 | 13 | private lazy var titleLabel: UILabel = { 14 | let label = UILabel() 15 | label.text = "upload challenge main image".localized 16 | label.font = .boldSystemFont(ofSize: 20) 17 | label.numberOfLines = 2 18 | return label 19 | }() 20 | private lazy var imageView: UIImageView = { 21 | let imageView = UIImageView() 22 | imageView.image = UIImage(systemName: "camera") 23 | imageView.tintColor = UIColor(named: "SystemForeground") 24 | imageView.contentMode = .scaleAspectFit 25 | imageView.clipsToBounds = true 26 | imageView.layer.cornerRadius = 10 27 | imageView.layer.borderWidth = 1 28 | imageView.layer.borderColor = UIColor(named: "DayColor")?.cgColor 29 | return imageView 30 | }() 31 | 32 | override init(frame: CGRect) { 33 | super.init(frame: frame) 34 | configure() 35 | } 36 | 37 | required init?(coder: NSCoder) { 38 | super.init(coder: coder) 39 | configure() 40 | } 41 | 42 | convenience init() { 43 | self.init(frame: CGRect.zero) 44 | } 45 | 46 | func updateImage(_ image: UIImage) { 47 | imageView.image = image 48 | imageView.contentMode = .scaleAspectFill 49 | } 50 | } 51 | 52 | extension FormImageRegisterView { 53 | private func configure() { 54 | configureSubviews() 55 | configureGesture() 56 | } 57 | 58 | private func configureSubviews() { 59 | addSubview(titleLabel) 60 | titleLabel.anchor(top: titleLabel.superview?.topAnchor, 61 | width: UIScreen.main.bounds.width - 40) 62 | 63 | addSubview(imageView) 64 | imageView.anchor(centerX: imageView.superview?.centerXAnchor, 65 | top: titleLabel.bottomAnchor, paddingTop: 30, 66 | width: 150, height: 150) 67 | 68 | anchor(bottom: imageView.bottomAnchor) 69 | } 70 | 71 | private func configureGesture() { 72 | let recognizer = UITapGestureRecognizer( 73 | target: self, 74 | action: #selector(didTappedImageView(_:)) 75 | ) 76 | imageView.isUserInteractionEnabled = true 77 | imageView.addGestureRecognizer(recognizer) 78 | } 79 | 80 | @objc private func didTappedImageView(_ sender: UITapGestureRecognizer) { 81 | guard sender.state == .ended else { return } 82 | delegate?.didTappedImageView(FormViewController.InputTag.image.rawValue) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Form/Subviews/FormIntroductionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormIntroductionView.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/09. 6 | // 7 | 8 | import UIKit 9 | 10 | final class FormIntroductionView: UIView { 11 | typealias Tag = FormViewController.InputTag 12 | 13 | weak var delegate: UITextViewDelegate? { 14 | didSet { 15 | textView.delegate = delegate 16 | } 17 | } 18 | 19 | private lazy var titleLabel: UILabel = { 20 | let label = UILabel() 21 | label.text = "introduce challenge".localized 22 | label.font = .boldSystemFont(ofSize: 20) 23 | return label 24 | }() 25 | private lazy var descriptionLabel: UILabel = { 26 | let label = UILabel() 27 | label.text = "introduce challenge 150".localized 28 | label.font = .systemFont(ofSize: 16) 29 | label.textColor = UIColor(named: "DayColor") 30 | label.numberOfLines = 2 31 | return label 32 | }() 33 | private lazy var textView: UITextView = { 34 | let textView = UITextView() 35 | textView.layer.borderColor = UIColor(named: "SystemForeground")?.cgColor 36 | textView.layer.borderWidth = 1 37 | textView.layer.cornerRadius = 10 38 | textView.font = .systemFont(ofSize: 16) 39 | textView.tag = Tag.introduction.rawValue 40 | return textView 41 | }() 42 | 43 | override init(frame: CGRect) { 44 | super.init(frame: frame) 45 | configure() 46 | } 47 | 48 | required init?(coder: NSCoder) { 49 | super.init(coder: coder) 50 | configure() 51 | } 52 | 53 | convenience init() { 54 | self.init(frame: CGRect.zero) 55 | } 56 | 57 | func hideKeyboard() { 58 | textView.endEditing(true) 59 | } 60 | 61 | func update(introduction: String) { 62 | textView.text = introduction 63 | } 64 | } 65 | 66 | extension FormIntroductionView { 67 | private func configure() { 68 | configureSubviews() 69 | } 70 | 71 | private func configureSubviews() { 72 | addSubview(titleLabel) 73 | titleLabel.anchor(top: titleLabel.superview?.topAnchor, 74 | width: UIScreen.main.bounds.width - 40) 75 | 76 | addSubview(descriptionLabel) 77 | descriptionLabel.anchor(top: titleLabel.bottomAnchor, paddingTop: 10, 78 | width: UIScreen.main.bounds.width - 40) 79 | 80 | addSubview(textView) 81 | textView.anchor(top: descriptionLabel.bottomAnchor, paddingTop: 20, 82 | width: UIScreen.main.bounds.width - 40, 83 | height: 150) 84 | 85 | anchor(bottom: textView.bottomAnchor) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Form/Subviews/FormTitleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormTitleView.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/09. 6 | // 7 | 8 | import UIKit 9 | 10 | final class FormTitleView: UIView { 11 | typealias Tag = FormViewController.InputTag 12 | 13 | weak var delegate: UITextFieldDelegate? { 14 | didSet { 15 | textField.delegate = delegate 16 | } 17 | } 18 | 19 | private lazy var titleLabel: UILabel = { 20 | let label = UILabel() 21 | label.text = "write challenge name".localized 22 | label.font = .boldSystemFont(ofSize: 20) 23 | label.numberOfLines = 2 24 | return label 25 | }() 26 | private lazy var descriptionLabel: UILabel = { 27 | let label = UILabel() 28 | label.text = "don't use bad words".localized 29 | label.font = .systemFont(ofSize: 16) 30 | label.numberOfLines = 2 31 | label.textColor = UIColor(named: "DayColor") 32 | return label 33 | }() 34 | private lazy var textField: UITextField = { 35 | let textField = UITextField() 36 | textField.placeholder = "ex) wake up at 6am".localized 37 | textField.borderStyle = .roundedRect 38 | textField.tag = Tag.title.rawValue 39 | return textField 40 | }() 41 | 42 | override init(frame: CGRect) { 43 | super.init(frame: frame) 44 | configure() 45 | } 46 | 47 | required init?(coder: NSCoder) { 48 | super.init(coder: coder) 49 | configure() 50 | } 51 | 52 | convenience init() { 53 | self.init(frame: CGRect.zero) 54 | } 55 | 56 | func hideKeyboard() { 57 | textField.endEditing(true) 58 | } 59 | 60 | func update(title: String) { 61 | textField.text = title 62 | } 63 | } 64 | 65 | extension FormTitleView { 66 | private func configure() { 67 | configureSubviews() 68 | } 69 | 70 | private func configureSubviews() { 71 | addSubview(titleLabel) 72 | titleLabel.anchor(top: titleLabel.superview?.topAnchor, 73 | width: UIScreen.main.bounds.width - 40) 74 | 75 | addSubview(descriptionLabel) 76 | descriptionLabel.anchor(top: titleLabel.bottomAnchor, paddingTop: 10, 77 | width: UIScreen.main.bounds.width - 40) 78 | 79 | addSubview(textField) 80 | textField.anchor(top: descriptionLabel.bottomAnchor, paddingTop: 20, 81 | width: UIScreen.main.bounds.width - 40) 82 | 83 | anchor(bottom: textField.bottomAnchor) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Home/Cells/HomeCalendarDetailTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeCalendarDetailTableViewCell.swift 3 | // Routinus 4 | // 5 | // Created by 김민서 on 2021/12/01. 6 | // 7 | 8 | import UIKit 9 | 10 | final class HomeCalendarDetailTableViewCell: UITableViewCell { 11 | static let identifier: String = "HomeCalendarDetailTableViewCell" 12 | 13 | private lazy var categoryImageView: UIImageView = { 14 | let imageView = UIImageView() 15 | imageView.contentMode = .scaleAspectFit 16 | imageView.tintColor = UIColor(named: "SystemForeground") 17 | return imageView 18 | }() 19 | 20 | private lazy var challengeNameLabel: UILabel = { 21 | let label = UILabel() 22 | label.font = UIFont.systemFont(ofSize: 18, weight: .medium) 23 | return label 24 | }() 25 | 26 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 27 | super.init(style: style, reuseIdentifier: reuseIdentifier) 28 | self.configureViews() 29 | } 30 | 31 | required init?(coder: NSCoder) { 32 | super.init(coder: coder) 33 | self.configureViews() 34 | } 35 | 36 | func updateCell(challenge: Challenge) { 37 | if UIImage(systemName: challenge.category.symbol) == nil { 38 | categoryImageView.image = UIImage(named: challenge.category.symbol) 39 | } else { 40 | categoryImageView.image = UIImage(systemName: challenge.category.symbol) 41 | } 42 | categoryImageView.tintColor = UIColor(named: challenge.category.color) 43 | challengeNameLabel.text = challenge.title 44 | } 45 | 46 | override func awakeFromNib() { 47 | super.awakeFromNib() 48 | } 49 | } 50 | 51 | extension HomeCalendarDetailTableViewCell { 52 | private func configureViews() { 53 | let smallWidth = UIScreen.main.bounds.width <= 350 54 | let offset = smallWidth ? 15.0 : 20.0 55 | 56 | backgroundColor = UIColor(named: "SystemBackground") 57 | 58 | contentView.addSubview(categoryImageView) 59 | categoryImageView.anchor(leading: leadingAnchor, 60 | paddingLeading: offset, 61 | centerY: centerYAnchor, 62 | width: 30, 63 | height: 30) 64 | 65 | contentView.addSubview(challengeNameLabel) 66 | challengeNameLabel.anchor(leading: categoryImageView.trailingAnchor, 67 | paddingLeading: 20, 68 | trailing: contentView.trailingAnchor, 69 | centerY: centerYAnchor) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Launch/Subviews/LaunchView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LaunchView.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/22. 6 | // 7 | 8 | import UIKit 9 | 10 | final class LaunchView: UIView { 11 | private lazy var launchImageView: UIImageView = { 12 | let imageView = UIImageView(image: UIImage(named: "Logo")) 13 | imageView.contentMode = .scaleAspectFit 14 | return imageView 15 | }() 16 | 17 | override init(frame: CGRect) { 18 | super.init(frame: frame) 19 | self.configureViews() 20 | self.animation() 21 | } 22 | 23 | required init?(coder: NSCoder) { 24 | super.init(coder: coder) 25 | self.configureViews() 26 | self.animation() 27 | } 28 | 29 | convenience init() { 30 | self.init(frame: CGRect.zero) 31 | } 32 | } 33 | 34 | extension LaunchView { 35 | private func configureViews() { 36 | backgroundColor = UIColor(named: "SystemBackground") 37 | addSubview(launchImageView) 38 | 39 | launchImageView.anchor(leading: leadingAnchor, 40 | paddingLeading: 30, 41 | trailing: trailingAnchor, 42 | paddingTrailing: 30, 43 | centerX: centerXAnchor, 44 | centerY: centerYAnchor) 45 | } 46 | 47 | private func animation() { 48 | UIView.animate(withDuration: 0.5) { 49 | self.launchImageView.alpha = 0 50 | } completion: { _ in 51 | self.removeFromSuperview() 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Manage/Cells/ManageAddCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ManageAddCollectionViewCell.swift 3 | // Routinus 4 | // 5 | // Created by 김민서 on 2021/11/21. 6 | // 7 | 8 | import UIKit 9 | 10 | final class ManageAddCollectionViewCell: UICollectionViewCell { 11 | static let identifier = "ManageAddCollectionViewCell" 12 | 13 | private lazy var titleLabel: UILabel = { 14 | let label = UILabel() 15 | label.text = "my challenges".localized 16 | label.font = UIFont.systemFont(ofSize: UIScreen.main.bounds.width <= 350 ? 30 : 34, 17 | weight: .bold) 18 | return label 19 | }() 20 | 21 | private lazy var addChallengeView = ChallengePromotionView() 22 | 23 | weak var delegate: ChallengePromotionViewDelegate? { 24 | didSet { 25 | addChallengeView.delegate = delegate 26 | } 27 | } 28 | 29 | override init(frame: CGRect) { 30 | super.init(frame: frame) 31 | configureView() 32 | } 33 | 34 | required init?(coder: NSCoder) { 35 | super.init(coder: coder) 36 | } 37 | } 38 | 39 | extension ManageAddCollectionViewCell { 40 | private func configureView() { 41 | let smallWidth = UIScreen.main.bounds.width <= 350 42 | let offset = smallWidth ? 28.0 : 32.0 43 | 44 | anchor(height: 250 + offset) 45 | 46 | addSubview(titleLabel) 47 | titleLabel.anchor(horizontal: self, top: topAnchor, paddingTop: offset, height: 80) 48 | 49 | addSubview(addChallengeView) 50 | addChallengeView.anchor(horizontal: self, top: titleLabel.bottomAnchor, paddingTop: 10) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Manage/Cells/ManageCollectionViewHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ManageCollectionViewHeader.swift 3 | // Routinus 4 | // 5 | // Created by 김민서 on 2021/11/20. 6 | // 7 | 8 | import UIKit 9 | 10 | final class ManageCollectionViewHeader: UICollectionReusableView { 11 | enum Section: Int { 12 | case participating = 1, created, ended 13 | 14 | var title: String { 15 | switch self { 16 | case .participating: 17 | return "participatingChallenges".localized 18 | case .created: 19 | return "createdChallenges".localized 20 | case .ended: 21 | return "endedChallenges".localized 22 | } 23 | } 24 | } 25 | 26 | static let identifier = "ManageCollectionViewHeader" 27 | 28 | private lazy var label: UILabel = { 29 | let label = UILabel() 30 | label.textColor = UIColor(named: "SystemForeground") 31 | label.font = UIFont.boldSystemFont(ofSize: 18) 32 | return label 33 | }() 34 | 35 | private lazy var toggleImageView: UIImageView = { 36 | let imageView = UIImageView() 37 | imageView.image = UIImage(systemName: "chevron.down") 38 | imageView.tintColor = UIColor(named: "SystemForeground") 39 | return imageView 40 | }() 41 | 42 | private(set) var section: Section? 43 | public var isExpanded: Bool = true { 44 | didSet { 45 | let angle = isExpanded ? 0 : -Double.pi / 2 46 | DispatchQueue.main.async { [weak self] in 47 | guard let self = self else { return } 48 | UIView.animate(withDuration: 0.3) { 49 | self.toggleImageView.transform = CGAffineTransform(rotationAngle: angle) 50 | } 51 | } 52 | } 53 | } 54 | 55 | func updateViews(section: Section) { 56 | self.section = section 57 | label.text = section.title 58 | 59 | addSubview(label) 60 | label.anchor(leading: leadingAnchor, centerY: centerYAnchor) 61 | 62 | addSubview(toggleImageView) 63 | toggleImageView.anchor(trailing: trailingAnchor, centerY: centerYAnchor) 64 | } 65 | 66 | func didTouchedHeader() { 67 | isExpanded.toggle() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Manage/Layouts/ManageCollectionViewLayouts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ManageCollectionViewLayouts.swift 3 | // Routinus 4 | // 5 | // Created by 김민서 on 2021/11/12. 6 | // 7 | 8 | import UIKit 9 | 10 | final class ManageCollectionViewLayouts { 11 | private var addLayout: NSCollectionLayoutSection { 12 | let smallWidth = UIScreen.main.bounds.width <= 350 13 | let offset = smallWidth ? 15.0 : 20.0 14 | 15 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), 16 | heightDimension: .fractionalHeight(1)) 17 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 18 | 19 | let groupSize = NSCollectionLayoutSize( 20 | widthDimension: .fractionalWidth(1), 21 | heightDimension: .estimated(250 + (smallWidth ? 28 : 32)) 22 | ) 23 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, 24 | subitem: item, 25 | count: 1) 26 | 27 | let section = NSCollectionLayoutSection(group: group) 28 | section.orthogonalScrollingBehavior = .none 29 | section.contentInsets = .init(top: 0, leading: offset, bottom: 10, trailing: offset) 30 | return section 31 | } 32 | 33 | private var challengeLayout: NSCollectionLayoutSection { 34 | let smallWidth = UIScreen.main.bounds.width <= 350 35 | let offset = smallWidth ? 15.0 : 25.0 36 | 37 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), 38 | heightDimension: .estimated(140)) 39 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 40 | 41 | let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), 42 | heightDimension: .estimated(140)) 43 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, 44 | subitem: item, 45 | count: 2) 46 | group.interItemSpacing = .fixed(20) 47 | 48 | let section = NSCollectionLayoutSection(group: group) 49 | section.boundarySupplementaryItems = [.init( 50 | layoutSize: .init(widthDimension: .fractionalWidth(1), 51 | heightDimension: .absolute(70)), 52 | elementKind: UICollectionView.elementKindSectionHeader, 53 | alignment: .topLeading 54 | )] 55 | section.orthogonalScrollingBehavior = .none 56 | section.contentInsets = .init(top: 10, leading: offset, bottom: 10, trailing: offset) 57 | section.interGroupSpacing = 30 58 | return section 59 | } 60 | 61 | func section(at sectionNumber: Int) -> NSCollectionLayoutSection { 62 | if sectionNumber == 0 { 63 | return addLayout 64 | } else { 65 | return challengeLayout 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/MyPage/Delegates/MyPageUserNameUpdatableDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyPageUserNameUpdatableDelegate.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/22. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol MyPageUserNameUpdatableDelegate: AnyObject { 11 | func didTappedNameStackView() 12 | } 13 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/MyPage/MyPageViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyPageViewModel.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/22. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | protocol MyPageViewModelInput { 12 | func fetchUser() 13 | func fetchThemeStyle() 14 | func updateUsername(_ name: String) 15 | func updateThemeStyle(_ style: Int) 16 | } 17 | 18 | protocol MyPageViewModelOutput { 19 | var user: CurrentValueSubject { get } 20 | var themeStyle: CurrentValueSubject { get } 21 | } 22 | 23 | protocol MyPageViewModelIO: MyPageViewModelInput, MyPageViewModelOutput { } 24 | 25 | final class MyPageViewModel: MyPageViewModelIO { 26 | var user = CurrentValueSubject(User()) 27 | var themeStyle = CurrentValueSubject(0) 28 | 29 | var cancellables = Set() 30 | 31 | var userFetchUsecase: UserFetchableUsecase 32 | var userUpdateUsecase: UserUpdatableUsecase 33 | 34 | let userCreatePublisher = NotificationCenter.default.publisher( 35 | for: UserCreateUsecase.didCreateUser, 36 | object: nil 37 | ) 38 | let userUpdatePublisher = NotificationCenter.default.publisher( 39 | for: UserUpdateUsecase.didUpdateUser, 40 | object: nil 41 | ) 42 | 43 | init(userFetchUsecase: UserFetchableUsecase, userUpdateUsecase: UserUpdatableUsecase) { 44 | self.userFetchUsecase = userFetchUsecase 45 | self.userUpdateUsecase = userUpdateUsecase 46 | configure() 47 | fetchUser() 48 | fetchThemeStyle() 49 | } 50 | } 51 | 52 | extension MyPageViewModel { 53 | private func configure() { 54 | configurePublishers() 55 | } 56 | 57 | private func configurePublishers() { 58 | userCreatePublisher 59 | .receive(on: RunLoop.main) 60 | .sink { [weak self] _ in 61 | guard let self = self else { return } 62 | self.fetchUser() 63 | } 64 | .store(in: &cancellables) 65 | 66 | userUpdatePublisher 67 | .receive(on: RunLoop.main) 68 | .sink { [weak self] _ in 69 | guard let self = self else { return } 70 | self.fetchUser() 71 | } 72 | .store(in: &cancellables) 73 | } 74 | } 75 | 76 | extension MyPageViewModel: MyPageViewModelInput { 77 | func fetchUser() { 78 | guard let id = userFetchUsecase.fetchUserID() else { return } 79 | userFetchUsecase.fetchUser(id: id) { [weak self] user in 80 | guard let self = self else { return } 81 | self.user.value = user 82 | } 83 | } 84 | 85 | func fetchThemeStyle() { 86 | themeStyle.value = userFetchUsecase.fetchThemeStyle() 87 | } 88 | 89 | func updateUsername(_ name: String) { 90 | userUpdateUsecase.updateUsername(of: user.value.id, name: name) 91 | } 92 | 93 | func updateThemeStyle(_ style: Int) { 94 | userUpdateUsecase.updateThemeStyle(style) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/MyPage/Subviews/MyPageProfileView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyPageProfileView.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/22. 6 | // 7 | 8 | import UIKit 9 | 10 | final class MyPageProfileView: UIView { 11 | weak var delegate: MyPageUserNameUpdatableDelegate? 12 | 13 | private lazy var imageView: UIImageView = { 14 | let imageView = UIImageView() 15 | imageView.contentMode = .scaleAspectFit 16 | return imageView 17 | }() 18 | private lazy var recognizer = UITapGestureRecognizer(target: self, 19 | action: #selector(didTapNameStackView)) 20 | private lazy var nameStackView: UIStackView = { 21 | let stackView = UIStackView() 22 | stackView.axis = .horizontal 23 | stackView.spacing = 10 24 | stackView.addGestureRecognizer(recognizer) 25 | return stackView 26 | }() 27 | private lazy var nameLabel: UILabel = { 28 | let label = UILabel() 29 | label.font = UIFont.systemFont(ofSize: 20) 30 | label.text = "username" 31 | return label 32 | }() 33 | private lazy var updateIconImageView: UIImageView = { 34 | let imageView = UIImageView() 35 | imageView.image = UIImage(systemName: "pencil") 36 | imageView.tintColor = UIColor(named: "SystemForeground") 37 | return imageView 38 | }() 39 | 40 | var name: String? { 41 | return nameLabel.text 42 | } 43 | 44 | override init(frame: CGRect) { 45 | super.init(frame: frame) 46 | configure() 47 | } 48 | 49 | required init?(coder: NSCoder) { 50 | super.init(coder: coder) 51 | configure() 52 | } 53 | 54 | func updateName(_ name: String) { 55 | nameLabel.text = name 56 | } 57 | 58 | func updateImage(with user: User) { 59 | imageView.image = UIImage(named: ContinuityState.image(for: user.continuityDay)) 60 | } 61 | } 62 | 63 | extension MyPageProfileView { 64 | private func configure() { 65 | configureViews() 66 | } 67 | 68 | private func configureViews() { 69 | addSubview(imageView) 70 | imageView.anchor(centerX: centerXAnchor, 71 | top: topAnchor, 72 | paddingTop: 10, 73 | width: 120, 74 | height: 120) 75 | 76 | addSubview(nameStackView) 77 | nameStackView.anchor(centerX: centerXAnchor, 78 | top: imageView.bottomAnchor, 79 | paddingTop: 20) 80 | 81 | nameStackView.addArrangedSubview(nameLabel) 82 | nameStackView.addArrangedSubview(updateIconImageView) 83 | } 84 | 85 | @objc private func didTapNameStackView() { 86 | delegate?.didTappedNameStackView() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Search/Cells/SearchCollectionViewHeader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchCollectionViewHeader.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/09. 6 | // 7 | 8 | import UIKit 9 | 10 | final class SearchCollectionViewHeader: UICollectionReusableView { 11 | static let identifier = "SearchCollectionViewHeader" 12 | 13 | private lazy var label: UILabel = { 14 | let label = UILabel() 15 | label.textColor = UIColor(named: "SystemForeground") 16 | label.font = UIFont.boldSystemFont(ofSize: 18) 17 | return label 18 | }() 19 | 20 | override init(frame: CGRect) { 21 | super.init(frame: frame) 22 | configure() 23 | } 24 | 25 | required init?(coder: NSCoder) { 26 | super.init(coder: coder) 27 | configure() 28 | } 29 | 30 | func updateTitle(_ title: String) { 31 | label.text = title 32 | } 33 | } 34 | 35 | extension SearchCollectionViewHeader { 36 | private func configure() { 37 | configureViews() 38 | } 39 | 40 | private func configureViews() { 41 | addSubview(label) 42 | label.anchor(leading: label.superview?.leadingAnchor, 43 | centerY: label.superview?.centerYAnchor) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Search/Cells/SearchPopularKeywordCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchPopularKeywordCollectionViewCell.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/09. 6 | // 7 | 8 | import UIKit 9 | 10 | final class SearchPopularKeywordCollectionViewCell: UICollectionViewCell { 11 | static let identifier = "SearchPopularKeywordCollectionViewCell" 12 | 13 | weak var delegate: SearchPopularKeywordDelegate? 14 | 15 | private lazy var popularKeywordButton: SearchPopularKeywordButton = { 16 | let button = SearchPopularKeywordButton() 17 | button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16) 18 | button.backgroundColor = UIColor(named: "SundayColor") 19 | button.layer.borderWidth = 1 20 | button.layer.borderColor = UIColor(named: "SundayColor")?.cgColor 21 | button.layer.cornerRadius = 15 22 | button.addTarget(self, action: #selector(didTappedPopularKeyword), for: .touchUpInside) 23 | return button 24 | }() 25 | 26 | override init(frame: CGRect) { 27 | super.init(frame: frame) 28 | configure() 29 | } 30 | 31 | required init?(coder: NSCoder) { 32 | super.init(coder: coder) 33 | configure() 34 | } 35 | 36 | func updateKeyword(_ keyword: String) { 37 | popularKeywordButton.keyword = keyword 38 | popularKeywordButton.setTitle(keyword, for: .normal) 39 | } 40 | } 41 | 42 | extension SearchPopularKeywordCollectionViewCell { 43 | private func configure() { 44 | configureViews() 45 | } 46 | 47 | private func configureViews() { 48 | addSubview(popularKeywordButton) 49 | popularKeywordButton.anchor(horizontal: popularKeywordButton.superview, 50 | vertical: popularKeywordButton.superview) 51 | } 52 | 53 | @objc private func didTappedPopularKeyword(_ sender: SearchPopularKeywordButton) { 54 | delegate?.didTappedKeywordButton(keyword: sender.keyword) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Search/Delegates/SearchPopularKeywordDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchPopularKeywordDelegate.swift 3 | // Routinus 4 | // 5 | // Created by 유석환 on 2021/11/30. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol SearchPopularKeywordDelegate: AnyObject { 11 | func didTappedKeywordButton(keyword: String?) 12 | } 13 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Search/Layouts/SearchCollectionViewLayouts.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchCollectionViewLayouts.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/09. 6 | // 7 | 8 | import UIKit 9 | 10 | final class SearchCollectionViewLayouts { 11 | private var pupularSearchKeywordLayout: NSCollectionLayoutSection { 12 | let smallWidth = UIScreen.main.bounds.width <= 350 13 | let offset = smallWidth ? 15.0 : 20.0 14 | 15 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), 16 | heightDimension: .fractionalHeight(1)) 17 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 18 | 19 | let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.17), 20 | heightDimension: .estimated(30)) 21 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, 22 | subitems: [item]) 23 | 24 | let section = NSCollectionLayoutSection(group: group) 25 | section.boundarySupplementaryItems = [.init( 26 | layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50)), 27 | elementKind: UICollectionView.elementKindSectionHeader, 28 | alignment: .topLeading 29 | )] 30 | section.orthogonalScrollingBehavior = .groupPaging 31 | section.contentInsets = .init(top: 5, leading: offset, bottom: 20, trailing: offset) 32 | section.interGroupSpacing = 20 33 | return section 34 | } 35 | 36 | private var challengeLayout: NSCollectionLayoutSection { 37 | let smallWidth = UIScreen.main.bounds.width <= 350 38 | let offset = smallWidth ? 15.0 : 20.0 39 | 40 | let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), 41 | heightDimension: .estimated(140)) 42 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 43 | 44 | let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), 45 | heightDimension: .estimated(140)) 46 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, 47 | subitem: item, 48 | count: 2) 49 | group.interItemSpacing = .fixed(15) 50 | 51 | let section = NSCollectionLayoutSection(group: group) 52 | section.boundarySupplementaryItems = [.init( 53 | layoutSize: .init(widthDimension: .fractionalWidth(1), heightDimension: .absolute(50)), 54 | elementKind: UICollectionView.elementKindSectionHeader, alignment: .topLeading 55 | )] 56 | section.orthogonalScrollingBehavior = .none 57 | section.contentInsets = .init(top: 10, leading: offset, bottom: 10, trailing: offset) 58 | section.interGroupSpacing = 30 59 | return section 60 | } 61 | 62 | func section(at sectionNumber: Int) -> NSCollectionLayoutSection { 63 | if sectionNumber == 0 { 64 | return pupularSearchKeywordLayout 65 | } else { 66 | return challengeLayout 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Search/Subviews/SearchBarContainerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchBarContainerView.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/11. 6 | // 7 | 8 | import UIKit 9 | 10 | final class SearchBarContainerView: UIView { 11 | private lazy var searchBar: UISearchBar = { 12 | let searchBar = UISearchBar() 13 | return searchBar 14 | }() 15 | 16 | weak var delegate: UISearchBarDelegate? { 17 | didSet { 18 | searchBar.delegate = delegate 19 | } 20 | } 21 | 22 | init() { 23 | super.init(frame: CGRect.zero) 24 | configure() 25 | } 26 | 27 | required init?(coder aDecoder: NSCoder) { 28 | super.init(coder: aDecoder) 29 | configure() 30 | } 31 | 32 | override func layoutSubviews() { 33 | super.layoutSubviews() 34 | searchBar.frame = bounds 35 | } 36 | 37 | func hideKeyboard() { 38 | searchBar.endEditing(true) 39 | } 40 | 41 | func updateSearchBar(keyword: String) { 42 | searchBar.text = keyword 43 | } 44 | } 45 | 46 | extension SearchBarContainerView { 47 | private func configure() { 48 | self.addSubview(searchBar) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Routinus/Routinus/Presentation/Search/Subviews/SearchPopularKeywordButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SearchPopularKeywordButton.swift 3 | // Routinus 4 | // 5 | // Created by 박상우 on 2021/11/10. 6 | // 7 | 8 | import UIKit 9 | 10 | final class SearchPopularKeywordButton: UIButton { 11 | var keyword: String? 12 | 13 | override init(frame: CGRect) { 14 | super.init(frame: frame) 15 | } 16 | 17 | required init?(coder: NSCoder) { 18 | super.init(coder: coder) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-40.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-72.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-72@2x.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-76.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-small-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-small-50.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-small-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-small-50@2x.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-small.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-small@2x.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon-small@3x.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/icon@2x.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/ios-marketing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/ios-marketing.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/notification-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/notification-icon@2x.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/notification-icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/notification-icon@3x.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/notification-icon~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/notification-icon~ipad.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/notification-icon~ipad@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/AppIcon.appiconset/notification-icon~ipad@2x.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/Icons/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/Icons/challenge.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "challenge@x1.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "challenge@x2.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "challenge@x3.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/Icons/challenge.imageset/challenge@x1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/Icons/challenge.imageset/challenge@x1.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/Icons/challenge.imageset/challenge@x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/Icons/challenge.imageset/challenge@x2.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/Icons/challenge.imageset/challenge@x3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/Icons/challenge.imageset/challenge@x3.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/Icons/check.calendar.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "calendar.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "auto-scaling" : "auto", 14 | "preserves-vector-representation" : true, 15 | "template-rendering-intent" : "template" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/Icons/check.calendar.imageset/calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/Icons/dumbbell.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "dumbbell.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/Icons/dumbbell.imageset/dumbbell.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 11 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/Logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "logo@x1.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "logo@x2.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "logo@x3.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/Logo.imageset/logo@x1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/Logo.imageset/logo@x1.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/Logo.imageset/logo@x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/Logo.imageset/logo@x2.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/Logo.imageset/logo@x3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/Logo.imageset/logo@x3.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/achievement/1-19.imageset/0-20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/achievement/1-19.imageset/0-20.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/achievement/1-19.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "0-20.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 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/achievement/100.imageset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/achievement/100.imageset/100.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/achievement/100.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "100.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 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/achievement/20-39.imageset/20-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/achievement/20-39.imageset/20-40.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/achievement/20-39.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "20-40.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 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/achievement/40-65.imageset/40-70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/achievement/40-65.imageset/40-70.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/achievement/40-65.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "40-70.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 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/achievement/66-99.imageset/70-99.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS03-Routinus/75f52a87ac74fd8e3f40a9814e8f363c2237fece/Routinus/Routinus/Resources/Assets.xcassets/achievement/66-99.imageset/70-99.png -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/achievement/66-99.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "70-99.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 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/achievement/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/Black.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.100", 9 | "green" : "0.100", 10 | "red" : "0.100" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.100", 27 | "green" : "0.100", 28 | "red" : "0.100" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/Calendar/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/Calendar/DayColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.400", 9 | "green" : "0.400", 10 | "red" : "0.400" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.700", 27 | "green" : "0.700", 28 | "red" : "0.700" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/Calendar/SaturdayColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "0.620", 10 | "red" : "0.420" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "0.620", 28 | "red" : "0.420" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/Calendar/SundayColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.467", 9 | "green" : "0.467", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.467", 27 | "green" : "0.467", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/Category/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/Category/ETCColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.627", 9 | "green" : "0.686", 10 | "red" : "0.902" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.627", 27 | "green" : "0.686", 28 | "red" : "0.902" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/Category/EmotionManageColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.706", 9 | "green" : "0.627", 10 | "red" : "0.902" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.706", 27 | "green" : "0.627", 28 | "red" : "0.902" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/Category/ExerciseColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.902", 9 | "green" : "0.843", 10 | "red" : "0.745" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.902", 27 | "green" : "0.843", 28 | "red" : "0.745" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/Category/FinanceColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.627", 9 | "green" : "0.824", 10 | "red" : "0.902" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.627", 27 | "green" : "0.824", 28 | "red" : "0.902" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/Category/HobbyColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.741", 9 | "green" : "0.635", 10 | "red" : "0.796" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.741", 27 | "green" : "0.635", 28 | "red" : "0.796" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/Category/LifeStyleColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.902", 9 | "green" : "0.702", 10 | "red" : "0.627" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.902", 27 | "green" : "0.702", 28 | "red" : "0.627" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/Category/ReadColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.855", 9 | "green" : "0.745", 10 | "red" : "0.800" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.855", 27 | "green" : "0.745", 28 | "red" : "0.800" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/Category/StudyColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "0.871", 10 | "red" : "0.890" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "0.871", 28 | "red" : "0.890" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/EndDateColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.467", 9 | "green" : "0.467", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.467", 27 | "green" : "0.467", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/LightGray.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.969", 9 | "green" : "0.949", 10 | "red" : "0.949" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.180", 27 | "green" : "0.173", 28 | "red" : "0.173" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/MainColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.627", 9 | "green" : "0.906", 10 | "red" : "0.706" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.384", 27 | "green" : "0.843", 28 | "red" : "0.514" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/MainColor0.5.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.816", 9 | "green" : "0.953", 10 | "red" : "0.855" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0xD0", 27 | "green" : "0xF3", 28 | "red" : "0xDA" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/MainColorReverse.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.384", 9 | "green" : "0.843", 10 | "red" : "0.514" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.627", 27 | "green" : "0.906", 28 | "red" : "0.706" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/SystemBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.000", 27 | "green" : "0.000", 28 | "red" : "0.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/SystemForeground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.120", 9 | "green" : "0.120", 10 | "red" : "0.120" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.950", 27 | "green" : "0.950", 28 | "red" : "0.950" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/WeekColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "209", 9 | "green" : "209", 10 | "red" : "252" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "152", 27 | "green" : "152", 28 | "red" : "255" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/colors/White.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/seed/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/seed/seed1.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "seed1.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/seed/seed1.imageset/seed1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/seed/seed2.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "seed2.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/seed/seed3.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "seed3.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/seed/seed4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "seed4.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/seed/seed5.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "seed5.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/seed/seed6.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "seed6.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Assets.xcassets/seed/seed7.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "seed7.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Routinus/Routinus/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Routinus/RoutinusTests/MockDatas/AchievementFetchableUsecaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AchievementFetchableUsecaseMock.swift 3 | // RoutinusTests 4 | // 5 | // Created by 박상우 on 2021/11/30. 6 | // 7 | 8 | import XCTest 9 | @testable import Routinus 10 | 11 | class AchievementFetchableUsecaseMock: AchievementFetchableUsecase { 12 | func fetchAchievements(yearMonth: String, completion: @escaping ([Achievement]) -> Void) { 13 | let achievements: [Achievement] = [Achievement(yearMonth: "202111", 14 | day: "29", 15 | achievementCount: 2, 16 | totalCount: 4), 17 | Achievement(yearMonth: "202111", 18 | day: "30", 19 | achievementCount: 4, 20 | totalCount: 5)] 21 | completion(achievements) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Routinus/RoutinusTests/MockDatas/AchievementUpdatableUsecaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AchievementUpdatableUsecaseMock.swift 3 | // RoutinusTests 4 | // 5 | // Created by 박상우 on 2021/11/30. 6 | // 7 | 8 | import XCTest 9 | @testable import Routinus 10 | 11 | class AchievementUpdatableUsecaseMock: AchievementUpdatableUsecase { 12 | func updateAchievementCount() { 13 | print("update AchievementCount") 14 | } 15 | 16 | func updateTotalCount() { 17 | print("update TotalCount") 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Routinus/RoutinusTests/MockDatas/AuthFetchableUsecaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthFetchableUsecaseMock.swift 3 | // RoutinusTests 4 | // 5 | // Created by 박상우 on 2021/11/30. 6 | // 7 | 8 | import XCTest 9 | @testable import Routinus 10 | 11 | class AuthFetchableUsecaseMock: AuthFetchableUsecase { 12 | func fetchAuth(challengeID: String, completion: @escaping (Auth?) -> Void) { 13 | completion(nil) 14 | } 15 | 16 | func fetchAuths(challengeID: String, completion: @escaping ([Auth]) -> Void) { 17 | let auths: [Auth] = [Auth(challengeID: "testChallenge2", 18 | userID: "testUserID", 19 | date: Date(dateString: "20211129"), 20 | time: Date(timeString: "1230")), 21 | Auth(challengeID: "testChallenge3", 22 | userID: "testUserID", 23 | date: Date(dateString: "20211130"), 24 | time: Date(timeString: "1330"))] 25 | completion(auths) 26 | } 27 | 28 | func fetchMyAuths(challengeID: String, completion: @escaping ([Auth]) -> Void) { 29 | let auths: [Auth] = [Auth(challengeID: "testChallenge4", 30 | userID: "testUserID", 31 | date: Date(dateString: "20211129"), 32 | time: Date(timeString: "1430")), 33 | Auth(challengeID: "testChallenge5", 34 | userID: "testUserID", 35 | date: Date(dateString: "20211130"), 36 | time: Date(timeString: "1530"))] 37 | completion(auths) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Routinus/RoutinusTests/MockDatas/ChallengeUpdatableUsecaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ChallengeUpdatableUsecaseMock.swift 3 | // RoutinusTests 4 | // 5 | // Created by 박상우 on 2021/11/30. 6 | // 7 | 8 | import XCTest 9 | @testable import Routinus 10 | 11 | class ChallengeUpdatableUsecaseMock: ChallengeUpdatableUsecase { 12 | func endDate(startDate: Date, week: Int) -> Date? { 13 | let calendar = Calendar.current 14 | let day = DateComponents(day: week * 7) 15 | guard let endDate = calendar.date(byAdding: day, to: startDate) else { return nil } 16 | return endDate 17 | } 18 | 19 | func update(challenge: Challenge) { 20 | print("update Challenge") 21 | } 22 | 23 | func updateParticipantCount(challengeID: String) { 24 | print("update ParticipantCount") 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Routinus/RoutinusTests/MockDatas/ImageFetchableUsecaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageFetchableUsecaseMock.swift 3 | // RoutinusTests 4 | // 5 | // Created by 박상우 on 2021/11/30. 6 | // 7 | 8 | import XCTest 9 | @testable import Routinus 10 | 11 | class ImageFetchableUsecaseMock: ImageFetchableUsecase { 12 | func fetchImageData(from directory: String, filename: String, completion: ((Data?) -> Void)?) { 13 | completion?(Data()) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Routinus/RoutinusTests/MockDatas/ParticipationCreatableUsecaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParticipationCreatableUsecaseMock.swift 3 | // RoutinusTests 4 | // 5 | // Created by 박상우 on 2021/11/30. 6 | // 7 | 8 | import XCTest 9 | @testable import Routinus 10 | 11 | class ParticipationCreatableUsecaseMock: ParticipationCreatableUsecase { 12 | func createParticipation(challengeID: String) { 13 | print("create Participation") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Routinus/RoutinusTests/MockDatas/ParticipationFetchableUsecaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParticipantionFetchableUsecaseMock.swift 3 | // RoutinusTests 4 | // 5 | // Created by 박상우 on 2021/11/30. 6 | // 7 | 8 | import XCTest 9 | @testable import Routinus 10 | 11 | class ParticipationFetchableUsecaseMock: ParticipationFetchableUsecase { 12 | func fetchParticipation(challengeID: String, completion: @escaping (Participation?) -> Void) { 13 | let participationDTO = ParticipationDTO(authCount: 2, 14 | challengeID: "testChallengeID", 15 | joinDate: "20211124", 16 | userID: "testUserID") 17 | let participation = Participation(participationDTO: participationDTO) 18 | completion(participation) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Routinus/RoutinusTests/MockDatas/ParticipationFetchableUsecaseNilMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParticipationFetchableUsecaseNilMock.swift 3 | // RoutinusTests 4 | // 5 | // Created by 박상우 on 2021/12/01. 6 | // 7 | 8 | import XCTest 9 | @testable import Routinus 10 | 11 | class ParticipationFetchableUsecaseNilMock: ParticipationFetchableUsecase { 12 | func fetchParticipation(challengeID: String, completion: @escaping (Participation?) -> Void) { 13 | completion(nil) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Routinus/RoutinusTests/MockDatas/TodayRoutineFetchableUsecaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TodayRoutineFetchableUsecaseMock.swift 3 | // RoutinusTests 4 | // 5 | // Created by 박상우 on 2021/11/30. 6 | // 7 | 8 | import XCTest 9 | @testable import Routinus 10 | 11 | class TodayRoutineFetchableUsecaseMock: TodayRoutineFetchableUsecase { 12 | func fetchTodayRoutines(completion: @escaping ([TodayRoutine]) -> Void) { 13 | let todayRoutines: [TodayRoutine] = [TodayRoutine(challengeID: "testChallenge1", 14 | category: .exercise, 15 | title: "테스트 챌린지1", 16 | authCount: 0, 17 | totalCount: 7), 18 | TodayRoutine(challengeID: "testChallenge2", 19 | category: .etc, 20 | title: "테스트 챌린지2", 21 | authCount: 1, 22 | totalCount: 14)] 23 | completion(todayRoutines) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Routinus/RoutinusTests/MockDatas/UserCreatableUsecaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserCreatableUsecaseMock.swift 3 | // RoutinusTests 4 | // 5 | // Created by 박상우 on 2021/11/30. 6 | // 7 | 8 | import XCTest 9 | @testable import Routinus 10 | 11 | class UserCreatableUsecaseMock: UserCreatableUsecase { 12 | func createUser() { 13 | print("User Create") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Routinus/RoutinusTests/MockDatas/UserFetchableUsecaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserFetchableUsecaseMock.swift 3 | // RoutinusTests 4 | // 5 | // Created by 박상우 on 2021/11/30. 6 | // 7 | 8 | import XCTest 9 | @testable import Routinus 10 | 11 | class UserFetchableUsecaseMock: UserFetchableUsecase { 12 | func fetchUser(id: String, completion: @escaping (User) -> Void) { 13 | let user = User(id: "MockUserID", 14 | name: "차분한 고양이", 15 | continuityDay: 0, 16 | userImageCategoryID: "0", 17 | grade: 0, 18 | lastAuthDay: "20201130") 19 | completion(user) 20 | } 21 | 22 | func fetchUserID() -> String? { 23 | return "MockUserID" 24 | } 25 | 26 | func fetchThemeStyle() -> Int { 27 | return 1 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Routinus/RoutinusTests/MockDatas/UserUpdatableUsecaseMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserUpdatableUsecaseMock.swift 3 | // RoutinusTests 4 | // 5 | // Created by 박상우 on 2021/11/30. 6 | // 7 | 8 | import XCTest 9 | @testable import Routinus 10 | 11 | class UserUpdatableUsecaseMock: UserUpdatableUsecase { 12 | func updateContinuityDay(completion: ((User) -> Void)?) { 13 | let user = User(id: "MockUserID", 14 | name: "차분한 고양이", 15 | continuityDay: 0, 16 | userImageCategoryID: "0", 17 | grade: 0, 18 | lastAuthDay: "20201130") 19 | completion?(user) 20 | } 21 | 22 | func updateContinuityDayByAuth() { 23 | print("update ContinuityDay!!") 24 | } 25 | 26 | func updateUsername(of id: String, name: String) { 27 | print("update id: \(id), userName: \(name)") 28 | } 29 | 30 | func updateThemeStyle(_ style: Int) { 31 | print("update ThemeStyle \(style)") 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Routinus/RoutinusTests/ViewModelTests/MyPageViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyPageViewModelTests.swift 3 | // RoutinusTests 4 | // 5 | // Created by 박상우 on 2021/12/01. 6 | // 7 | 8 | import Combine 9 | import XCTest 10 | @testable import Routinus 11 | 12 | class MyPageViewModelTests: XCTestCase { 13 | var myPageViewModel: MyPageViewModel! 14 | var cancellables: Set! 15 | 16 | override func setUp() { 17 | myPageViewModel = MyPageViewModel(userFetchUsecase: UserFetchableUsecaseMock(), 18 | userUpdateUsecase: UserUpdatableUsecaseMock()) 19 | cancellables = [] 20 | } 21 | 22 | override func tearDown() { 23 | cancellables.removeAll() 24 | } 25 | 26 | func testFetchUser() { 27 | myPageViewModel.fetchUser() 28 | 29 | let id = myPageViewModel.user.value.id 30 | let name = myPageViewModel.user.value.name 31 | let continuityDay = myPageViewModel.user.value.continuityDay 32 | let userImageCategoryID = myPageViewModel.user.value.userImageCategoryID 33 | let grade = myPageViewModel.user.value.grade 34 | let lastAuthDay = myPageViewModel.user.value.lastAuthDay 35 | 36 | XCTAssertEqual(id, "MockUserID") 37 | XCTAssertEqual(name, "차분한 고양이") 38 | XCTAssertEqual(continuityDay, 0) 39 | XCTAssertEqual(userImageCategoryID, "0") 40 | XCTAssertEqual(grade, 0) 41 | XCTAssertEqual(lastAuthDay, "20201130") 42 | } 43 | 44 | func testFetchThemeStyle() { 45 | myPageViewModel.fetchThemeStyle() 46 | let themeStyle = myPageViewModel.themeStyle.value 47 | XCTAssertEqual(themeStyle, 1) 48 | } 49 | } 50 | --------------------------------------------------------------------------------