├── .github └── workflows │ └── swift.yml ├── .gitignore ├── ISSUE_TEMPLATE.md ├── MateRunner ├── .swiftlint.yml ├── MateRunner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ ├── MateRunner.xcscheme │ │ └── RunningPreparationViewModelTests.xcscheme ├── MateRunner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── MateRunner │ ├── Application │ │ ├── AppDelegate.swift │ │ └── SceneDelegate.swift │ ├── Data │ │ ├── Network │ │ │ └── DataMapping │ │ │ │ ├── EmojiFirestoreDTO.swift │ │ │ │ ├── MateListFirestoreDTO.swift │ │ │ │ ├── MessagingRequestDTO.swift │ │ │ │ ├── NoticeDTO+Mapping.swift │ │ │ │ ├── PersonalTotalRecordDTO.swift │ │ │ │ ├── RunningResultFirestoreDTO.swift │ │ │ │ ├── UserDataFirestoreDTO.swift │ │ │ │ ├── UserProfileDTO.swift │ │ │ │ └── UserProfileFirestoreDTO.swift │ │ └── Repository │ │ │ ├── Common │ │ │ ├── DefaultFirestoreRepository.swift │ │ │ ├── DefaultUserRepository.swift │ │ │ └── Protocol │ │ │ │ ├── FirestoreRepository.swift │ │ │ │ └── UserRepository.swift │ │ │ ├── Home │ │ │ ├── DefaultInvitationRepository.swift │ │ │ ├── DefaultInviteMateRepository.swift │ │ │ ├── DefaultRunningRepository.swift │ │ │ └── Protocol │ │ │ │ ├── InvitationRepository.swift │ │ │ │ ├── InviteMateRepository.swift │ │ │ │ └── RunningRepository.swift │ │ │ └── Mate │ │ │ ├── DefaultMateRepository.swift │ │ │ └── Protocol │ │ │ └── MateRepository.swift │ ├── Domain │ │ ├── Model │ │ │ ├── CacheableImage.swift │ │ │ ├── CalendarModel.swift │ │ │ ├── ComplimentEmoji.swift │ │ │ ├── FirestoreValues.swift │ │ │ ├── Invitation.swift │ │ │ ├── MateRequest.swift │ │ │ ├── Notice.swift │ │ │ ├── PersonalTotalRecord.swift │ │ │ ├── Point.swift │ │ │ ├── RaceRunningResult.swift │ │ │ ├── Region.swift │ │ │ ├── RunningData.swift │ │ │ ├── RunningRealTimeData.swift │ │ │ ├── RunningResult.swift │ │ │ ├── RunningSetting.swift │ │ │ ├── TeamRunningResult.swift │ │ │ ├── UserData.swift │ │ │ └── UserProfile.swift │ │ └── UseCase │ │ │ ├── Account │ │ │ ├── DefaultLoginUseCase.swift │ │ │ ├── DefaultSignUpUseCase.swift │ │ │ └── Protocol │ │ │ │ ├── LoginUseCase.swift │ │ │ │ └── SignUpUseCase.swift │ │ │ ├── Common │ │ │ ├── DefaultEmojiUseCase.swift │ │ │ ├── DefaultInvitationUseCase.swift │ │ │ ├── Delegate │ │ │ │ └── EmojiDidSelectDelegate.swift │ │ │ └── Protocol │ │ │ │ ├── EmojiUseCase.swift │ │ │ │ └── InvitationUseCase.swift │ │ │ ├── Home │ │ │ ├── DefaultHomeUseCase.swift │ │ │ ├── Protocol │ │ │ │ ├── DistanceSettingUseCase.swift │ │ │ │ ├── HomeUseCase.swift │ │ │ │ ├── InvitationWaitingUseCase.swift │ │ │ │ ├── LocationDidUpdateDelegate.swift │ │ │ │ ├── MapUseCase.swift │ │ │ │ ├── RunningPreparationUseCase.swift │ │ │ │ ├── RunningResultUseCase.swift │ │ │ │ ├── RunningSettingUseCase.swift │ │ │ │ └── RunningUseCase.swift │ │ │ ├── ResultUseCase │ │ │ │ └── DefaultRunningResultUseCase.swift │ │ │ ├── RunningUseCase │ │ │ │ ├── DefaultMapUseCase.swift │ │ │ │ └── DefaultRunningUseCase.swift │ │ │ └── SettingUseCase │ │ │ │ ├── DefaultDistanceSettingUseCase.swift │ │ │ │ ├── DefaultInvitationWaitingUseCase.swift │ │ │ │ ├── DefaultRunningPreparationUseCase.swift │ │ │ │ └── DefaultRunningSettingUseCase.swift │ │ │ ├── Mate │ │ │ ├── DefaultMateUseCase.swift │ │ │ ├── DefaultProfileUseCase.swift │ │ │ └── Protocol │ │ │ │ ├── MateUseCase.swift │ │ │ │ └── ProfileUseCase.swift │ │ │ ├── MyPage │ │ │ ├── DefaultMyPageUseCase.swift │ │ │ ├── DefaultNotificationUseCase.swift │ │ │ ├── DefaultProfileEditUseCase.swift │ │ │ └── Protocol │ │ │ │ ├── MyPageUseCase.swift │ │ │ │ ├── NotificationUseCase.swift │ │ │ │ └── ProfileEditUseCase.swift │ │ │ └── Record │ │ │ ├── DefaultRecordDetailUseCase.swift │ │ │ ├── DefaultRecordUseCase.swift │ │ │ └── Protocol │ │ │ ├── RecordDetailUseCase.swift │ │ │ └── RecordUseCase.swift │ ├── MateRunner.entitlements │ ├── Network │ │ ├── DefaultRealtimeDatabaseNetworkService.swift │ │ ├── DefaultURLSessionNetworkService.swift │ │ └── Protocol │ │ │ ├── RealtimeDatabaseNetworkService.swift │ │ │ └── URLSessionNetworkService.swift │ ├── Presentation │ │ ├── Common │ │ │ ├── Coordinator │ │ │ │ ├── DefaultAppCoordinator.swift │ │ │ │ ├── DefaultTabBarCoordinator.swift │ │ │ │ ├── Delegate │ │ │ │ │ └── CoordinatorFinishDelegate.swift │ │ │ │ └── Protocol │ │ │ │ │ ├── AppCoordinator.swift │ │ │ │ │ ├── Coordinator.swift │ │ │ │ │ ├── InvitationReceivable.swift │ │ │ │ │ └── TabBarCoordinator.swift │ │ │ ├── View │ │ │ │ ├── CumulativeRecordView.swift │ │ │ │ ├── EmojiListView.swift │ │ │ │ ├── EmojiView.swift │ │ │ │ ├── MateRunnerActivityIndicatorView.swift │ │ │ │ ├── PickerTextField.swift │ │ │ │ └── RecordCell.swift │ │ │ ├── ViewController │ │ │ │ ├── EmojiViewController.swift │ │ │ │ └── InvitationViewController.swift │ │ │ └── ViewModel │ │ │ │ ├── EmojiViewModel.swift │ │ │ │ ├── InvitationViewModel.swift │ │ │ │ └── Protocol │ │ │ │ └── CoreLocationConvertable.swift │ │ ├── HomeScene │ │ │ ├── Coordinator │ │ │ │ ├── DefaultHomeCoordinator.swift │ │ │ │ ├── DefaultRunningCoordinator.swift │ │ │ │ ├── DefaultRunningSettingCoordinator.swift │ │ │ │ └── Protocol │ │ │ │ │ ├── HomeCoordinator.swift │ │ │ │ │ ├── RunningCoordinator.swift │ │ │ │ │ ├── RunningSettingCoordinator.swift │ │ │ │ │ └── SettingCoordinatorDidFinishDelegate.swift │ │ │ ├── View │ │ │ │ ├── CursorDisabledTextField.swift │ │ │ │ ├── InvitationView.swift │ │ │ │ ├── MyResultView.swift │ │ │ │ ├── RaceResultView.swift │ │ │ │ ├── RoundedButton.swift │ │ │ │ ├── RunningCardView.swift │ │ │ │ ├── RunningInfoView.swift │ │ │ │ ├── RunningProgressView.swift │ │ │ │ └── TeamResultView.swift │ │ │ ├── ViewController │ │ │ │ ├── Delegate │ │ │ │ │ └── BackButtonDelegate.swift │ │ │ │ ├── HomeViewController.swift │ │ │ │ ├── ResultViewController │ │ │ │ │ ├── RaceRunningResultViewController.swift │ │ │ │ │ ├── RunningResultViewController.swift │ │ │ │ │ ├── SingleRunningResultViewController.swift │ │ │ │ │ └── TeamRunningResultViewController.swift │ │ │ │ ├── RunningViewController │ │ │ │ │ ├── MapViewController.swift │ │ │ │ │ ├── RaceRunningViewController.swift │ │ │ │ │ ├── RunningViewController.swift │ │ │ │ │ ├── SingleRunningViewController.swift │ │ │ │ │ └── TeamRunningViewController.swift │ │ │ │ └── SettingViewController │ │ │ │ │ ├── DistanceSettingViewController.swift │ │ │ │ │ ├── InvitationWaitingViewController.swift │ │ │ │ │ ├── MateRunningModeSettingViewController.swift │ │ │ │ │ ├── MateSettingViewController.swift │ │ │ │ │ ├── RunningModeSettingViewController.swift │ │ │ │ │ └── RunningPreparationViewController.swift │ │ │ └── ViewModel │ │ │ │ ├── HomeViewModel.swift │ │ │ │ ├── ResultViewModel │ │ │ │ ├── RaceRunningResultViewModel.swift │ │ │ │ ├── SingleRunningResultViewModel.swift │ │ │ │ └── TeamRunningResultViewModel.swift │ │ │ │ ├── RunningViewModel │ │ │ │ ├── MapViewModel.swift │ │ │ │ ├── RaceRunningViewModel.swift │ │ │ │ ├── SingleRunningViewModel.swift │ │ │ │ └── TeamRunningViewModel.swift │ │ │ │ └── SettingViewModel │ │ │ │ ├── DistanceSettingViewModel.swift │ │ │ │ ├── InvitationWaitingViewModel.swift │ │ │ │ ├── MateRunningModeSettingViewModel.swift │ │ │ │ ├── MateSettingViewModel.swift │ │ │ │ ├── RunningModeSettingViewModel.swift │ │ │ │ └── RunningPreparationViewModel.swift │ │ ├── LoginScene │ │ │ ├── Coordinator │ │ │ │ ├── DefaultLoginCoordinator.swift │ │ │ │ ├── DefaultSignUpCoordinator.swift │ │ │ │ └── Protocol │ │ │ │ │ ├── LoginCoordinator.swift │ │ │ │ │ └── SignUpCoordinator.swift │ │ │ ├── ViewController │ │ │ │ ├── LoginViewController.swift │ │ │ │ ├── SignUpViewController.swift │ │ │ │ └── TermsViewController.swift │ │ │ └── ViewModel │ │ │ │ ├── LoginViewModel.swift │ │ │ │ └── SignUpViewModel.swift │ │ ├── MateScene │ │ │ ├── Cooditnator │ │ │ │ ├── DefaultAddMateCoordinator.swift │ │ │ │ ├── DefaultMateCoordinator.swift │ │ │ │ ├── DefaultMateProfileCoordinator.swift │ │ │ │ └── Protocol │ │ │ │ │ ├── AddMateCoordinator.swift │ │ │ │ │ ├── MateCoordinator.swift │ │ │ │ │ └── MateProfileCoordinator.swift │ │ │ ├── View │ │ │ │ ├── AddMateTableViewCell.swift │ │ │ │ ├── MateEmptyView.swift │ │ │ │ ├── MateHeaderView.swift │ │ │ │ ├── MateProfileTableViewCell.swift │ │ │ │ ├── MateRecordTableViewCell.swift │ │ │ │ ├── MateTableViewCell.swift │ │ │ │ └── MateViewModel.swift │ │ │ ├── ViewController │ │ │ │ ├── AddMateViewController.swift │ │ │ │ ├── MateProfileViewController.swift │ │ │ │ └── MateViewController.swift │ │ │ └── ViewModel │ │ │ │ ├── AddMateViewModel.swift │ │ │ │ ├── MateProfileViewModel.swift │ │ │ │ └── MateViewModel.swift │ │ ├── MyPageScene │ │ │ ├── Coordinator │ │ │ │ ├── DefaultMyPageCoordinator.swift │ │ │ │ └── Protocol │ │ │ │ │ └── MyPageCoordinator.swift │ │ │ ├── View │ │ │ │ ├── ImageEditButton.swift │ │ │ │ └── NotificationTableViewCell.swift │ │ │ ├── ViewController │ │ │ │ ├── LicenseViewController.swift │ │ │ │ ├── MyPageViewController.swift │ │ │ │ ├── NotificationViewController.swift │ │ │ │ └── ProfileEditViewController.swift │ │ │ └── ViewModel │ │ │ │ ├── MyPageViewModel.swift │ │ │ │ ├── NotificationViewModel.swift │ │ │ │ └── ProfileEditViewModel.swift │ │ └── RecordScene │ │ │ ├── Coordinator │ │ │ ├── DefaultRecordCoordinator.swift │ │ │ └── Protocol │ │ │ │ └── RecordCoordinator.swift │ │ │ ├── View │ │ │ ├── CalendarCell.swift │ │ │ ├── CalendarHeaderView.swift │ │ │ ├── CalendarView.swift │ │ │ └── WeekdayView.swift │ │ │ ├── ViewController │ │ │ ├── RecordDetailViewController.swift │ │ │ └── RecordViewController.swift │ │ │ └── ViewModel │ │ │ ├── RecordDetailViewModel.swift │ │ │ └── RecordViewModel.swift │ ├── Resource │ │ ├── Asset │ │ │ ├── Assets.xcassets │ │ │ │ ├── AccentColor.colorset │ │ │ │ │ └── Contents.json │ │ │ │ ├── AppIcon.appiconset │ │ │ │ │ ├── 1024.png │ │ │ │ │ ├── 114.png │ │ │ │ │ ├── 120.png │ │ │ │ │ ├── 180.png │ │ │ │ │ ├── 29.png │ │ │ │ │ ├── 40.png │ │ │ │ │ ├── 57.png │ │ │ │ │ ├── 58.png │ │ │ │ │ ├── 60.png │ │ │ │ │ ├── 80.png │ │ │ │ │ ├── 87.png │ │ │ │ │ └── Contents.json │ │ │ │ ├── Contents.json │ │ │ │ ├── bell.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── bell.png │ │ │ │ │ ├── bell@2x.png │ │ │ │ │ └── bell@3x.png │ │ │ │ ├── home.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── home.png │ │ │ │ │ ├── home@2x.png │ │ │ │ │ └── home@3x.png │ │ │ │ ├── launchScreen.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── launchScreen-1.png │ │ │ │ │ ├── launchScreen-2.png │ │ │ │ │ └── launchScreen.png │ │ │ │ ├── mate.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── mate.png │ │ │ │ │ ├── mate@2x.png │ │ │ │ │ └── mate@3x.png │ │ │ │ ├── mypage.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── mypage.png │ │ │ │ │ ├── mypage@2x.png │ │ │ │ │ └── mypage@3x.png │ │ │ │ ├── person-add.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── person-add.png │ │ │ │ │ ├── person-add@2x.png │ │ │ │ │ └── person-add@3x.png │ │ │ │ └── record.imageset │ │ │ │ │ ├── Contents.json │ │ │ │ │ ├── record.png │ │ │ │ │ ├── record@2x.png │ │ │ │ │ └── record@3x.png │ │ │ └── Font │ │ │ │ ├── NotoSans-boldItalic.ttf │ │ │ │ ├── NotoSansKR-black.otf │ │ │ │ ├── NotoSansKR-bold.otf │ │ │ │ ├── NotoSansKR-light.otf │ │ │ │ ├── NotoSansKR-medium.otf │ │ │ │ ├── NotoSansKR-regular.otf │ │ │ │ ├── NotoSansKR-thin.otf │ │ │ │ └── RacingSansOne.ttf │ │ ├── CoreData │ │ │ └── MateRunner.xcdatamodeld │ │ │ │ ├── .xccurrentversion │ │ │ │ └── MateRunner.xcdatamodel │ │ │ │ └── contents │ │ ├── GoogleService-Info.plist │ │ ├── Info.plist │ │ ├── Storyboard │ │ │ └── Base.lproj │ │ │ │ ├── LaunchScreen.storyboard │ │ │ │ └── Main.storyboard │ │ ├── license.txt │ │ ├── termsOfLocationService.txt │ │ ├── termsOfPrivacy.txt │ │ └── termsOfService.txt │ └── Util │ │ ├── Constant │ │ ├── CacheSizeConstants.swift │ │ ├── Configuration.swift │ │ ├── CoordinatorType.swift │ │ ├── Emoji.swift │ │ ├── FilePath.swift │ │ ├── FireStoreConstants.swift │ │ ├── FirebaseCollection.swift │ │ ├── FirestoreQuery.swift │ │ ├── Height.swift │ │ ├── LocationAuthorizationStatus.swift │ │ ├── Mets.swift │ │ ├── NoticeMode.swift │ │ ├── NotificationCenterKey.swift │ │ ├── RealtimeDatabaseKey.swift │ │ ├── RunningMode.swift │ │ ├── SignUpValidationState.swift │ │ ├── TabBarPage.swift │ │ ├── User.swift │ │ ├── UserDefaultKey.swift │ │ └── Weight.swift │ │ ├── Error │ │ ├── FirebaseServiceError.swift │ │ ├── ImageCacheError.swift │ │ └── SignUpValidationError.swift │ │ ├── Extension │ │ ├── Date+Formatter.swift │ │ ├── Double+Formatter.swift │ │ ├── Int+Formatter.swift │ │ ├── Point+CLLocationCoordinate2D.swift │ │ ├── String+UIImageConverter.swift │ │ ├── UIColor+CustomColor.swift │ │ ├── UIFont+CustomFont.swift │ │ ├── UIImageView+ImageCache.swift │ │ ├── UIScrollView+ObserveScroll.swift │ │ └── UIView+ShadowEffect.swift │ │ ├── Factory │ │ └── RunningResultFactory.swift │ │ ├── PropertyWrapper │ │ └── BehaviorRelayProperty.swift │ │ └── Service │ │ ├── DefaultCoreMotionService.swift │ │ ├── DefaultImageCacheService.swift │ │ ├── DefaultLocationService.swift │ │ ├── DefaultRxTimerService.swift │ │ └── Protocol │ │ ├── CoreMotionService.swift │ │ ├── LocationService.swift │ │ └── RxTimerService.swift ├── MateRunnerUseCaseTests │ ├── Common │ │ ├── EmojiUseCaseTests.swift │ │ └── Mock │ │ │ ├── MockEmojiDidSelectDelegate.swift │ │ │ ├── MockFirestoreRepository.swift │ │ │ └── MockUserRepository.swift │ ├── HomeScene │ │ ├── DistanceSettingUseCaseTests.swift │ │ ├── HomeUseCaseTests.swift │ │ ├── InvitationUseCaseTests.swift │ │ ├── MapUseCaseTests.swift │ │ ├── Mock │ │ │ ├── MockCoreMotionService.swift │ │ │ ├── MockLocationService.swift │ │ │ ├── MockRunningRepository.swift │ │ │ └── MockTimerService.swift │ │ ├── MockInvitationRepository.swift │ │ ├── RunningPreparationUseCaseTests.swift │ │ ├── RunningResultUseCaseTests.swift │ │ ├── RunningSettingUseCaseTests.swift │ │ └── RunningUseCaseTests.swift │ ├── LoginScene │ │ ├── LoginUseCaseTests.swift │ │ └── SignUpUseCaseTests.swift │ ├── MateScene │ │ ├── MateUseCaseTests.swift │ │ ├── Mock │ │ │ └── MockMateRepository.swift │ │ └── ProfileUseCaseTests.swift │ ├── MyPageScene │ │ ├── MypageUseCaseTests.swift │ │ └── NotificationUseCaseTests.swift │ └── RecordScene │ │ ├── ProfileEditUseCaseTests.swift │ │ └── RecordUseCaseTests.swift ├── MateRunnerViewModelTests │ ├── Common │ │ ├── InvitationViewModelTests.swift │ │ └── Mock │ │ │ └── MockInvitationUseCase.swift │ ├── HomeScene │ │ ├── DistanceSettingViewModelTests.swift │ │ ├── InvitationWaitingViewModelTests.swift │ │ ├── MapViewModelTests.swift │ │ ├── MateRunningModeSettingViewModelTests.swift │ │ ├── MateSettingViewModelTests.swift │ │ ├── Mock │ │ │ ├── MockDistanceSettingUseCase.swift │ │ │ ├── MockInvitationWaitingUseCase.swift │ │ │ ├── MockMapUseCase.swift │ │ │ ├── MockMateRunningUseCase.swift │ │ │ ├── MockRunningPreparationUseCase.swift │ │ │ ├── MockRunningResultUseCase.swift │ │ │ ├── MockRunningSettingUseCase.swift │ │ │ └── MockSingleRunningUseCase.swift │ │ ├── RaceRunningResultViewModelTests.swift │ │ ├── RaceRunningViewModelTests.swift │ │ ├── RunningModeSettingViewModelTests.swift │ │ ├── RunningPreparationViewModelTests.swift │ │ ├── SingleRunningResultViewModelTests.swift │ │ ├── SingleRunningViewModelTests.swift │ │ ├── TeamRunningResultViewModelTests.swift │ │ └── TeamRunningViewModelTests.swift │ ├── LoginScene │ │ ├── LoginViewModelTests.swift │ │ ├── Mock │ │ │ ├── MockLoginUseCase.swift │ │ │ └── MockSignUpUseCase.swift │ │ └── SignUpViewModelTests.swift │ ├── MateScene │ │ ├── AddMateViewModelTests.swift │ │ ├── EmojiViewModelTests.swift │ │ ├── MateProfileViewModelTests.swift │ │ ├── MateViewModelTests.swift │ │ └── Mock │ │ │ ├── MockEmojiUseCase.swift │ │ │ ├── MockMateUseCase.swift │ │ │ └── MockProfileUseCase.swift │ ├── MyPageScene │ │ ├── Mock │ │ │ ├── MockMyPageUseCase.swift │ │ │ ├── MockNotificationUseCase.swift │ │ │ └── MockProfileEditUseCase.swift │ │ ├── MyPageViewModelTests.swift │ │ ├── NotificationViewModelTests.swift │ │ └── ProfileEditViewModelTests.swift │ └── RecordScene │ │ ├── Mock │ │ ├── MockRecordDetailUseCase.swift │ │ └── MockRecordUseCase.swift │ │ ├── RecordDetailViewModelTests.swift │ │ └── RecordViewModelTests.swift ├── Podfile └── Podfile.lock ├── PULL_REQUEST_TEMPLATE.md └── README.md /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: 6 | - dev 7 | - feature/* 8 | - fix/* 9 | - refactor/* 10 | - test/* 11 | pull_request: 12 | branches: 13 | - dev 14 | - feature/* 15 | - fix/* 16 | - refactor/* 17 | - test/* 18 | 19 | jobs: 20 | build: 21 | 22 | runs-on: macos-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Run Tests 27 | run: | 28 | pod install --repo-update --clean-install --project-directory=MateRunner/ 29 | xcodebuild test -workspace MateRunner.xcworkspace -scheme MateRunner -destination 'platform=iOS Simulator,name=iPhone 11 Pro,OS=14.4' 30 | -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 🗣 설명 2 | - 이슈 내용 작성 3 | 4 | 5 | ## 📋 체크리스트 6 | 7 | > 구현해야하는 이슈 체크리스트 8 | 9 | - [ ] 완료하지 못한 체크리스트 10 | - [x] 완료한 체크리스트 11 | -------------------------------------------------------------------------------- /MateRunner/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | excluded: # 린트 과정에서 무시할 파일 경로. `included`보다 우선순위 높음 2 | - Carthage 3 | - Pods 4 | - MateRunner/Application/AppDelegate.swift 5 | - MateRunner/Application/SceneDelegate.swift 6 | - MateRunner/Data/Network/DataMapping/MessagingRequestDTO.swift 7 | 8 | disabled_rules: 9 | - trailing_whitespace 10 | -------------------------------------------------------------------------------- /MateRunner/MateRunner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MateRunner/MateRunner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MateRunner/MateRunner.xcodeproj/xcshareddata/xcschemes/RunningPreparationViewModelTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 37 | 38 | 44 | 45 | 47 | 48 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /MateRunner/MateRunner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /MateRunner/MateRunner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Data/Network/DataMapping/EmojiFirestoreDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiFirestoreDTO.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct EmojiFirestoreDTO: Codable { 11 | private let emoji: StringValue 12 | private let userNickname: StringValue 13 | 14 | private enum RootKey: String, CodingKey { 15 | case fields 16 | } 17 | 18 | private enum FieldKeys: String, CodingKey { 19 | case emoji, userNickname 20 | } 21 | 22 | init(from decoder: Decoder) throws { 23 | let container = try decoder.container(keyedBy: RootKey.self) 24 | let fieldContainer = try container.nestedContainer(keyedBy: FieldKeys.self, forKey: .fields) 25 | self.emoji = try fieldContainer.decode(StringValue.self, forKey: .emoji) 26 | self.userNickname = try fieldContainer.decode(StringValue.self, forKey: .userNickname) 27 | } 28 | 29 | init(emoji: String, userNickname: String) { 30 | self.emoji = StringValue(value: emoji) 31 | self.userNickname = StringValue(value: userNickname) 32 | } 33 | 34 | func toDomain() -> [String: String] { 35 | return [userNickname.value: emoji.value] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Data/Network/DataMapping/MateListFirestoreDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MateListFirestoreDTO.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct MateListFirestoreDTO: Decodable { 11 | private let mate: ArrayValue 12 | 13 | private enum RootKey: String, CodingKey { 14 | case fields 15 | } 16 | 17 | private enum FieldKeys: String, CodingKey { 18 | case mate 19 | } 20 | 21 | init(from decoder: Decoder) throws { 22 | let container = try decoder.container(keyedBy: RootKey.self) 23 | let fieldContainer = try container.nestedContainer(keyedBy: FieldKeys.self, forKey: .fields) 24 | self.mate = try fieldContainer.decode(ArrayValue.self, forKey: .mate) 25 | } 26 | 27 | init(mates: [String]) { 28 | self.mate = ArrayValue(values: mates.map({ StringValue(value: $0) })) 29 | } 30 | 31 | func toDomain() -> [String] { 32 | return self.mate.arrayValue["values"]?.compactMap({ $0.value }) ?? [] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Data/Network/DataMapping/MessagingRequestDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MessagingRequestDTO.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/18. 6 | // 7 | 8 | import Foundation 9 | 10 | struct FCMNotificationInfo: Codable { 11 | private var title: String 12 | private var body: String 13 | 14 | init(title: String, body: String) { 15 | self.title = title 16 | self.body = body 17 | } 18 | } 19 | 20 | struct MessagingRequestDTO: Codable { 21 | private var notification: FCMNotificationInfo 22 | private var data: T 23 | private var to: String 24 | private var priority: String 25 | private var contentAvailable: Bool 26 | private var mutableContent: Bool 27 | 28 | init(title: String, body: String, data: T, to: String) { 29 | self.notification = FCMNotificationInfo(title: title, body: body) 30 | self.data = data 31 | self.to = to 32 | self.priority = "high" 33 | self.contentAvailable = true 34 | self.mutableContent = true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Data/Network/DataMapping/NoticeDTO+Mapping.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoticeDTO+Mapping.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | struct NoticeDTO: Codable { 11 | private let id: String? 12 | private let sender: StringValue 13 | private let receiver: StringValue 14 | private let mode: StringValue 15 | private let isReceived: BooleanValue 16 | 17 | private enum RootKey: String, CodingKey { 18 | case id = "name", fields 19 | } 20 | 21 | private enum FieldKeys: String, CodingKey { 22 | case sender, receiver, mode, isReceived 23 | } 24 | 25 | init(from domain: Notice) { 26 | self.id = nil 27 | self.sender = StringValue(value: domain.sender) 28 | self.receiver = StringValue(value: domain.receiver) 29 | self.isReceived = BooleanValue(value: domain.isReceived) 30 | 31 | switch domain.mode { 32 | case .invite: 33 | self.mode = StringValue(value: NoticeMode.invite.text()) 34 | case .requestMate: 35 | self.mode = StringValue(value: NoticeMode.requestMate.text()) 36 | case .receiveEmoji: 37 | self.mode = StringValue(value: NoticeMode.receiveEmoji.text()) 38 | } 39 | } 40 | 41 | init(from decoder: Decoder) throws { 42 | let container = try decoder.container(keyedBy: RootKey.self) 43 | let fieldContainer = try container.nestedContainer(keyedBy: FieldKeys.self, forKey: .fields) 44 | 45 | self.id = try container.decode(String.self, forKey: .id) 46 | self.sender = try fieldContainer.decode(StringValue.self, forKey: .sender) 47 | self.receiver = try fieldContainer.decode(StringValue.self, forKey: .receiver) 48 | self.isReceived = try fieldContainer.decode(BooleanValue.self, forKey: .isReceived) 49 | self.mode = try fieldContainer.decode(StringValue.self, forKey: .mode) 50 | } 51 | 52 | func toDomain() -> Notice { 53 | return Notice( 54 | id: self.id, 55 | sender: self.sender.value, 56 | receiver: self.receiver.value, 57 | mode: NoticeMode(rawValue: self.mode.value) ?? .invite, 58 | isReceived: self.isReceived.value 59 | ) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Data/Network/DataMapping/PersonalTotalRecordDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PersonalTotalRecordDTO.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct PersonalTotalRecordDTO: Codable { 11 | private let distance: DoubleValue 12 | private let time: IntegerValue 13 | private let calorie: DoubleValue 14 | 15 | private enum RootKey: String, CodingKey { 16 | case fields 17 | } 18 | 19 | private enum FieldKeys: String, CodingKey { 20 | case time, distance, calorie 21 | } 22 | 23 | init(from decoder: Decoder) throws { 24 | let container = try decoder.container(keyedBy: RootKey.self) 25 | let fieldContainer = try container.nestedContainer(keyedBy: FieldKeys.self, forKey: .fields) 26 | self.time = try fieldContainer.decode(IntegerValue.self, forKey: .time) 27 | self.distance = try fieldContainer.decode(DoubleValue.self, forKey: .distance) 28 | self.calorie = try fieldContainer.decode(DoubleValue.self, forKey: .calorie) 29 | } 30 | 31 | init(totalRecord: PersonalTotalRecord) { 32 | self.time = IntegerValue(value: String(totalRecord.time)) 33 | self.distance = DoubleValue(value: totalRecord.distance) 34 | self.calorie = DoubleValue(value: totalRecord.calorie) 35 | } 36 | 37 | func toDomain() -> PersonalTotalRecord { 38 | return PersonalTotalRecord( 39 | distance: self.distance.value, 40 | time: Int(self.time.value) ?? 0, 41 | calorie: self.calorie.value 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Data/Network/DataMapping/UserProfileDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserProfileDTO.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | struct UserProfileDTO: Codable { 11 | let nickname: String 12 | let image: String 13 | let time: Int 14 | let distance: Double 15 | let calorie: Double 16 | let height: Double 17 | let weight: Double 18 | let mate: [String] 19 | 20 | init() { 21 | self.nickname = "" 22 | self.image = "" 23 | self.time = 0 24 | self.distance = 0.0 25 | self.calorie = 0.0 26 | self.height = 0.0 27 | self.weight = 0.0 28 | self.mate = [] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Data/Network/DataMapping/UserProfileFirestoreDTO.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserProfileFirestoreDTO.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct UserProfileFirestoreDTO: Codable { 11 | private let height: DoubleValue 12 | private let weight: DoubleValue 13 | private let image: StringValue 14 | 15 | private enum RootKey: String, CodingKey { 16 | case fields 17 | } 18 | 19 | private enum FieldKeys: String, CodingKey { 20 | case height, weight, image 21 | } 22 | 23 | init(from decoder: Decoder) throws { 24 | let container = try decoder.container(keyedBy: RootKey.self) 25 | let fieldContainer = try container.nestedContainer(keyedBy: FieldKeys.self, forKey: .fields) 26 | self.height = try fieldContainer.decode(DoubleValue.self, forKey: .height) 27 | self.weight = try fieldContainer.decode(DoubleValue.self, forKey: .weight) 28 | self.image = try fieldContainer.decode(StringValue.self, forKey: .image) 29 | } 30 | 31 | init(userProfile: UserProfile) { 32 | self.image = StringValue(value: userProfile.image) 33 | self.height = DoubleValue(value: userProfile.height) 34 | self.weight = DoubleValue(value: userProfile.weight) 35 | } 36 | 37 | func toDomain() -> UserProfile { 38 | return UserProfile( 39 | image: self.image.value, 40 | height: self.height.value, 41 | weight: self.weight.value 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Data/Repository/Common/DefaultUserRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultUserRepository.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/18. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class DefaultUserRepository: UserRepository { 13 | private let realtimeDatabaseNetworkService: RealtimeDatabaseNetworkService 14 | 15 | init(realtimeDatabaseNetworkService: RealtimeDatabaseNetworkService) { 16 | self.realtimeDatabaseNetworkService = realtimeDatabaseNetworkService 17 | } 18 | 19 | func fetchFCMToken() -> String? { 20 | return UserDefaults.standard.string(forKey: UserDefaultKey.fcmToken) 21 | } 22 | 23 | func fetchFCMTokenFromServer(of nickname: String) -> Observable { 24 | return self.realtimeDatabaseNetworkService.fetchFCMToken(of: nickname) 25 | } 26 | 27 | func deleteFCMToken() { 28 | UserDefaults.standard.removeObject(forKey: UserDefaultKey.fcmToken) 29 | } 30 | 31 | func saveFCMToken(_ fcmToken: String, of nickname: String) -> Observable { 32 | return self.realtimeDatabaseNetworkService.update( 33 | with: fcmToken, 34 | path: [RealtimeDatabaseKey.fcmToken, nickname] 35 | ) 36 | } 37 | 38 | func fetchUserNickname() -> String? { 39 | return UserDefaults.standard.string(forKey: UserDefaultKey.nickname) 40 | } 41 | 42 | func saveLoginInfo(nickname: String) { 43 | UserDefaults.standard.set(nickname, forKey: UserDefaultKey.nickname) 44 | UserDefaults.standard.set(true, forKey: UserDefaultKey.isLoggedIn) 45 | } 46 | 47 | func saveLogoutInfo() { 48 | UserDefaults.standard.set(false, forKey: UserDefaultKey.isLoggedIn) 49 | } 50 | 51 | func deleteUserInfo() { 52 | UserDefaults.standard.removeObject(forKey: UserDefaultKey.isLoggedIn) 53 | UserDefaults.standard.removeObject(forKey: UserDefaultKey.nickname) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Data/Repository/Common/Protocol/UserRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserRepository.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/18. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol UserRepository { 13 | func fetchFCMToken() -> String? 14 | func fetchFCMTokenFromServer(of nickname: String) -> Observable 15 | func deleteFCMToken() 16 | func saveFCMToken(_ fcmToken: String, of nickname: String) -> Observable 17 | func fetchUserNickname() -> String? 18 | func saveLoginInfo(nickname: String) 19 | func saveLogoutInfo() 20 | func deleteUserInfo() 21 | } 22 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Data/Repository/Home/DefaultInvitationRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultInvitationRepository.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/15. 6 | // 7 | import Foundation 8 | 9 | import RxSwift 10 | 11 | final class DefaultInvitationRepository: InvitationRepository { 12 | private let realtimeDatabaseNetworkService: RealtimeDatabaseNetworkService 13 | 14 | init(realtimeDatabaseNetworkService: RealtimeDatabaseNetworkService) { 15 | self.realtimeDatabaseNetworkService = realtimeDatabaseNetworkService 16 | } 17 | 18 | func fetchCancellationStatus(of invitation: Invitation) -> Observable { 19 | let sessionId = invitation.sessionId 20 | 21 | return self.realtimeDatabaseNetworkService.fetch(of: [RealtimeDatabaseKey.session, sessionId]) 22 | .map { data in 23 | guard let isCancelled = data[RealtimeDatabaseKey.isCancelled] as? Bool else { 24 | return false 25 | } 26 | return isCancelled 27 | } 28 | } 29 | 30 | func saveInvitationResponse(accept: Bool, invitation: Invitation) -> Observable { 31 | let sessionId = invitation.sessionId 32 | 33 | return self.realtimeDatabaseNetworkService.updateChildValues( 34 | with: [ 35 | RealtimeDatabaseKey.isAccepted: accept, 36 | RealtimeDatabaseKey.isReceived: true 37 | ], 38 | path: [RealtimeDatabaseKey.session, sessionId] 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Data/Repository/Home/Protocol/InvitationRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InvitationRepository.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/15. 6 | // 7 | import Foundation 8 | 9 | import RxSwift 10 | 11 | protocol InvitationRepository { 12 | func fetchCancellationStatus(of invitation: Invitation) -> Observable 13 | func saveInvitationResponse(accept: Bool, invitation: Invitation) -> Observable 14 | } 15 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Data/Repository/Home/Protocol/InviteMateRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InviteMateRepository.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/18. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol InviteMateRepository { 13 | func createSession(invitation: Invitation, mate: String) -> Observable 14 | func cancelSession(invitation: Invitation) -> Observable 15 | func listenInvitationResponse(of invitation: Invitation) -> Observable<(Bool, Bool)> 16 | func fetchFCMToken(of mate: String) -> Observable 17 | func sendInvitation(_ invitation: Invitation, fcmToken: String) -> Observable 18 | func stopListen(invitation: Invitation) 19 | } 20 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Data/Repository/Home/Protocol/RunningRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningRepository.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/09. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol RunningRepository { 13 | func listen(sessionId: String, mate: String) -> Observable 14 | func listenIsCancelled(of sessionId: String) -> Observable 15 | func saveRunningRealTimeData(_ domain: RunningRealTimeData, sessionId: String, user: String) -> Observable 16 | func cancelSession(of runningSetting: RunningSetting) -> Observable 17 | func stopListen(sessionId: String, mate: String) 18 | func saveRunningStatus(of user: String, isRunning: Bool) -> Observable 19 | func fetchRunningStatus(of mate: String) -> Observable 20 | } 21 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Data/Repository/Mate/Protocol/MateRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MateRepository.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol MateRepository { 13 | func sendRequestMate(from sender: String, fcmToken: String) -> Observable 14 | func fetchFCMToken(of mate: String)-> Observable 15 | func sendEmoji(from sender: String, fcmToken: String) -> Observable 16 | } 17 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/Model/CacheableImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CachableImage.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | final class CacheableImage { 11 | let imageData: Data 12 | let cacheInfo: CacheInfo 13 | 14 | init(imageData: Data, etag: String) { 15 | self.cacheInfo = CacheInfo(etag: etag, lastRead: Date()) 16 | self.imageData = imageData 17 | } 18 | } 19 | 20 | struct CacheInfo: Codable { 21 | let etag: String 22 | let lastRead: Date 23 | } 24 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/Model/CalendarModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CalendarModel.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | struct CalendarModel { 11 | let day: String 12 | let hasRecord: Bool 13 | let isSelected: Bool 14 | } 15 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/Model/ComplimentEmoji.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComplimentEmoji.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/27. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ComplimentEmoji: Codable { 11 | let sender: String 12 | 13 | init(sender: String) { 14 | self.sender = sender 15 | } 16 | 17 | init?(from dictionary: [AnyHashable: Any]) { 18 | guard let sender = dictionary["sender"] as? String else { 19 | return nil 20 | } 21 | self.init(sender: sender) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/Model/MateRequest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MateRequest.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/18. 6 | // 7 | 8 | import Foundation 9 | 10 | struct MateRequest: Codable { 11 | let sender: String 12 | 13 | init(sender: String) { 14 | self.sender = sender 15 | } 16 | 17 | init?(from dictionary: [AnyHashable: Any]) { 18 | guard let sender = dictionary[NotificationCenterKey.sender] as? String else { 19 | return nil 20 | } 21 | self.init(sender: sender) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/Model/Notice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notice.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Notice: Equatable { 11 | private(set) var id: String? 12 | private(set) var sender: String 13 | private(set) var receiver: String 14 | private(set) var mode: NoticeMode 15 | private(set) var isReceived: Bool 16 | 17 | init( 18 | id: String?, 19 | sender: String, 20 | receiver: String, 21 | mode: NoticeMode, 22 | isReceived: Bool 23 | ) { 24 | self.id = id 25 | self.sender = sender 26 | self.receiver = receiver 27 | self.mode = mode 28 | self.isReceived = isReceived 29 | } 30 | 31 | func copyUpdatedReceived() -> Notice { 32 | return .init( 33 | id: self.id, 34 | sender: self.sender, 35 | receiver: self.receiver, 36 | mode: self.mode, 37 | isReceived: true 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/Model/PersonalTotalRecord.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PresonalTotalRecord.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct PersonalTotalRecord: Equatable { 11 | let distance: Double 12 | let time: Int 13 | let calorie: Double 14 | 15 | func createCumulativeRecord(distance: Double, time: Int, calorie: Double) -> Self { 16 | return PersonalTotalRecord( 17 | distance: self.distance + distance, 18 | time: self.time + time, 19 | calorie: self.calorie + calorie 20 | ) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/Model/Point.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Point.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/03. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Point: Codable, Equatable { 11 | let latitude: Double 12 | let longitude: Double 13 | 14 | enum CodingKeys: String, CodingKey { 15 | case latitude 16 | case longitude 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/Model/RaceRunningResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RaceRunningResult.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/03. 6 | // 7 | 8 | import Foundation 9 | 10 | final class RaceRunningResult: RunningResult { 11 | private(set) var mateElapsedDistance: Double = 0 12 | private(set) var mateElapsedTime: Int = 0 13 | 14 | var isUserWinner: Bool { 15 | return self.userElapsedDistance >= self.mateElapsedDistance 16 | } 17 | 18 | init( 19 | userNickname: String, 20 | runningSetting: RunningSetting, 21 | userElapsedDistance: Double, 22 | userElapsedTime: Int, 23 | calorie: Double, 24 | points: [Point], 25 | emojis: [String: Emoji]? = nil, 26 | isCanceled: Bool, 27 | mateElapsedDistance: Double, 28 | mateElapsedTime: Int 29 | ) { 30 | self.mateElapsedTime = mateElapsedTime 31 | self.mateElapsedDistance = mateElapsedDistance 32 | super.init( 33 | userNickname: userNickname, 34 | runningSetting: runningSetting, 35 | userElapsedDistance: userElapsedDistance, 36 | userElapsedTime: userElapsedTime, 37 | calorie: calorie, 38 | points: points, 39 | emojis: emojis, 40 | isCanceled: isCanceled 41 | ) 42 | } 43 | 44 | func updateMateElaspedTime(to newTime: Int) { 45 | self.mateElapsedTime += newTime 46 | } 47 | 48 | func updateMateDistance(to newDistance: Double) { 49 | self.mateElapsedDistance += newDistance 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/Model/Region.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Region.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/17. 6 | // 7 | 8 | import CoreLocation 9 | import Foundation 10 | 11 | struct Region { 12 | private(set) var center: CLLocationCoordinate2D 13 | private(set) var span: (Double, Double) 14 | 15 | init() { 16 | self.center = CLLocationCoordinate2DMake(0, 0) 17 | self.span = (0, 0) 18 | } 19 | 20 | init(center: CLLocationCoordinate2D, span: (Double, Double)) { 21 | self.center = center 22 | self.span = span 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/Model/RunningRealTimeData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningRealTimeData.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/08. 6 | // 7 | 8 | import Foundation 9 | 10 | struct RunningRealTimeData: Codable { 11 | private(set) var elapsedDistance: Double 12 | private(set) var elapsedTime: Int 13 | } 14 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/Model/RunningSetting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningSetting.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/03. 6 | // 7 | 8 | import Foundation 9 | 10 | struct RunningSetting: Equatable { 11 | var sessionId: String? 12 | var mode: RunningMode? 13 | var targetDistance: Double? 14 | var hostNickname: String? 15 | var mateNickname: String? 16 | var dateTime: Date? 17 | } 18 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/Model/TeamRunningResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TeamRunningResult.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/03. 6 | // 7 | 8 | import Foundation 9 | 10 | final class TeamRunningResult: RunningResult { 11 | private(set) var mateElapsedDistance: Double = 0 12 | private(set) var mateElapsedTime: Int = 0 13 | 14 | var totalDistance: Double { 15 | return self.userElapsedDistance + self.mateElapsedDistance 16 | } 17 | var contribution: Double { 18 | return self.userElapsedDistance / self.totalDistance 19 | } 20 | 21 | init( 22 | userNickname: String, 23 | runningSetting: RunningSetting, 24 | userElapsedDistance: Double, 25 | userElapsedTime: Int, 26 | calorie: Double, 27 | points: [Point], 28 | emojis: [String: Emoji]? = nil, 29 | isCanceled: Bool, 30 | mateElapsedDistance: Double, 31 | mateElapsedTime: Int 32 | ) { 33 | self.mateElapsedTime = mateElapsedTime 34 | self.mateElapsedDistance = mateElapsedDistance 35 | super.init( 36 | userNickname: userNickname, 37 | runningSetting: runningSetting, 38 | userElapsedDistance: userElapsedDistance, 39 | userElapsedTime: userElapsedTime, 40 | calorie: calorie, 41 | points: points, 42 | emojis: emojis, 43 | isCanceled: isCanceled 44 | ) 45 | } 46 | 47 | func updateMateElaspedTime(to newTime: Int) { 48 | self.mateElapsedTime += newTime 49 | } 50 | 51 | func updateMateElapsedDistance(to newDistance: Double) { 52 | self.mateElapsedDistance += newDistance 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/Model/UserData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserData.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct UserData { 11 | let nickname: String 12 | let image: String 13 | let time: Int 14 | let distance: Double 15 | let calorie: Double 16 | let height: Double 17 | let weight: Double 18 | let mate: [String] 19 | 20 | init ( 21 | nickname: String = "", 22 | image: String = "", 23 | time: Int = 0, 24 | distance: Double = 0.0, 25 | calorie: Double = 0.0, 26 | height: Double = 0.0, 27 | weight: Double = 0.0, 28 | mate: [String] = [] 29 | ) { 30 | self.nickname = nickname 31 | self.image = image 32 | self.time = time 33 | self.distance = distance 34 | self.calorie = calorie 35 | self.height = height 36 | self.weight = weight 37 | self.mate = mate 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/Model/UserProfile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserProfile.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct UserProfile { 11 | let image: String 12 | let height: Double 13 | let weight: Double 14 | } 15 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Account/DefaultLoginUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultLoginUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/22. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class DefaultLoginUseCase: LoginUseCase { 13 | private let repository: UserRepository 14 | private let firestoreRepository: FirestoreRepository 15 | var isRegistered = PublishSubject() 16 | var isSaved = PublishSubject() 17 | private let disposeBag = DisposeBag() 18 | 19 | init(repository: UserRepository, firestoreRepository: FirestoreRepository) { 20 | self.repository = repository 21 | self.firestoreRepository = firestoreRepository 22 | } 23 | 24 | func checkRegistration(uid: String) { 25 | self.firestoreRepository.fetchUserNickname(of: uid) 26 | .subscribe(onNext: { [weak self] _ in 27 | self?.isRegistered.onNext(true) 28 | }, onError: { [weak self] _ in 29 | self?.isRegistered.onNext(false) 30 | }) 31 | .disposed(by: self.disposeBag) 32 | } 33 | 34 | func saveLoginInfo(uid: String) { 35 | self.firestoreRepository.fetchUserNickname(of: uid) 36 | .subscribe(onNext: { [weak self] nickname in 37 | self?.repository.saveLoginInfo(nickname: nickname) 38 | self?.saveFCMToken(of: nickname) 39 | self?.isSaved.onNext(true) 40 | }) 41 | .disposed(by: self.disposeBag) 42 | } 43 | 44 | private func saveFCMToken(of nickname: String) { 45 | guard let fcmToken = self.repository.fetchFCMToken() else { return } 46 | 47 | self.repository.saveFCMToken(fcmToken, of: nickname) 48 | .subscribe(onNext: { [weak self] in 49 | self?.repository.deleteFCMToken() 50 | }) 51 | .disposed(by: self.disposeBag) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Account/Protocol/LoginUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/22. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol LoginUseCase { 13 | var isRegistered: PublishSubject { get set } 14 | var isSaved: PublishSubject { get set } 15 | func checkRegistration(uid: String) 16 | func saveLoginInfo(uid: String) 17 | } 18 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Account/Protocol/SignUpUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/15. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol SignUpUseCase { 13 | var selectedProfileEmoji: BehaviorSubject { get } 14 | var nickname: String { get set } 15 | var height: BehaviorSubject { get } 16 | var weight: BehaviorSubject { get } 17 | var nicknameValidationState: BehaviorSubject { get } 18 | func validate(text: String) 19 | func signUp() -> Observable 20 | func saveLoginInfo() 21 | func shuffleProfileEmoji() 22 | } 23 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Common/DefaultInvitationUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultInvitationUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/15. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxRelay 11 | import RxSwift 12 | 13 | final class DefaultInvitationUseCase: InvitationUseCase { 14 | var invitation: Invitation 15 | var isCancelled: PublishSubject = PublishSubject() 16 | private let disposeBag = DisposeBag() 17 | private let invitationRepository: InvitationRepository 18 | 19 | init(invitation: Invitation, invitationRepository: InvitationRepository) { 20 | self.invitation = invitation 21 | self.invitationRepository = invitationRepository 22 | } 23 | 24 | func checkIsCancelled() -> Observable { 25 | self.invitationRepository.fetchCancellationStatus(of: self.invitation) 26 | .subscribe(self.isCancelled) 27 | .disposed(by: self.disposeBag) 28 | 29 | return self.invitationRepository.fetchCancellationStatus(of: self.invitation) 30 | } 31 | 32 | func acceptInvitation() -> Observable { 33 | return self.invitationRepository.saveInvitationResponse(accept: true, invitation: self.invitation) 34 | } 35 | 36 | func rejectInvitation() -> Observable { 37 | return self.invitationRepository.saveInvitationResponse(accept: false, invitation: self.invitation) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Common/Delegate/EmojiDidSelectDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiDidSelectDelegate.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/21. 6 | // 7 | 8 | protocol EmojiDidSelectDelegate: AnyObject { 9 | func emojiDidSelect(selectedEmoji: Emoji) 10 | } 11 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Common/Protocol/EmojiUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/12. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol EmojiUseCase { 13 | var selectedEmoji: PublishSubject { get set } 14 | func saveSentEmoji(_ emoji: Emoji) 15 | func selectEmoji(_ emoji: Emoji) 16 | func sendComplimentEmoji() 17 | var runningID: String? { get set } 18 | var mateNickname: String? { get set } 19 | } 20 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Common/Protocol/InvitationUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InvitationUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/15. 6 | // 7 | import Foundation 8 | 9 | import RxRelay 10 | import RxSwift 11 | 12 | protocol InvitationUseCase { 13 | var invitation: Invitation { get set } 14 | var isCancelled: PublishSubject { get set } 15 | func checkIsCancelled() -> Observable 16 | func acceptInvitation() -> Observable 17 | func rejectInvitation() -> Observable 18 | } 19 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Home/DefaultHomeUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/09. 6 | // 7 | 8 | import CoreLocation 9 | import Foundation 10 | 11 | import RxSwift 12 | 13 | final class DefaultHomeUseCase: HomeUseCase { 14 | var authorizationStatus = BehaviorSubject(value: nil) 15 | var userLocation = PublishSubject() 16 | private let locationService: LocationService 17 | private let disposeBag = DisposeBag() 18 | 19 | init(locationService: LocationService) { 20 | self.locationService = locationService 21 | } 22 | 23 | func checkAuthorization() { 24 | self.locationService.observeUpdatedAuthorization() 25 | .subscribe(onNext: { [weak self] status in 26 | switch status { 27 | case .authorizedAlways, .authorizedWhenInUse: 28 | self?.authorizationStatus.onNext(.allowed) 29 | self?.locationService.start() 30 | case .notDetermined: 31 | self?.authorizationStatus.onNext(.notDetermined) 32 | self?.locationService.requestAuthorization() 33 | case .denied, .restricted: 34 | self?.authorizationStatus.onNext(.disallowed) 35 | @unknown default: 36 | self?.authorizationStatus.onNext(nil) 37 | } 38 | }) 39 | .disposed(by: self.disposeBag) 40 | } 41 | 42 | func observeUserLocation() { 43 | return self.locationService.observeUpdatedLocation() 44 | .compactMap({ $0.last }) 45 | .subscribe(onNext: { [weak self] location in 46 | self?.userLocation.onNext(location) 47 | }) 48 | .disposed(by: self.disposeBag) 49 | } 50 | 51 | func stopUpdatingLocation() { 52 | self.locationService.stop() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Home/Protocol/DistanceSettingUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DistanceSettingUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/04. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol DistanceSettingUseCase { 13 | var validatedText: BehaviorSubject {get set} 14 | func validate(text: String) 15 | } 16 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Home/Protocol/HomeUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/17. 6 | // 7 | 8 | import CoreLocation 9 | import Foundation 10 | 11 | import RxSwift 12 | 13 | protocol HomeUseCase { 14 | var authorizationStatus: BehaviorSubject { get set } 15 | var userLocation: PublishSubject { get set } 16 | init(locationService: LocationService) 17 | func checkAuthorization() 18 | func observeUserLocation() 19 | func stopUpdatingLocation() 20 | } 21 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Home/Protocol/InvitationWaitingUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InvitationWaitingUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/11. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxRelay 11 | import RxSwift 12 | 13 | protocol InvitationWaitingUseCase { 14 | var runningSetting: RunningSetting { get set } 15 | var requestSuccess: PublishRelay { get set } 16 | var requestStatus: PublishSubject<(Bool, Bool)> { get set } 17 | var isAccepted: PublishSubject { get set } 18 | var isRejected: PublishSubject { get set } 19 | var isCanceled: PublishSubject { get set } 20 | func inviteMate() 21 | } 22 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Home/Protocol/LocationDidUpdateDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationDidUpdateDelegate.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/15. 6 | // 7 | 8 | import CoreLocation 9 | import Foundation 10 | 11 | protocol LocationDidUpdateDelegate: AnyObject { 12 | func locationDidUpdate(_ location: CLLocation) 13 | } 14 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Home/Protocol/MapUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/12. 6 | // 7 | 8 | import CoreLocation 9 | import Foundation 10 | 11 | import RxRelay 12 | 13 | protocol MapUseCase { 14 | var updatedLocation: PublishRelay { get set } 15 | init(locationService: LocationService, delegate: LocationDidUpdateDelegate?) 16 | func executeLocationTracker() 17 | func terminateLocationTracker() 18 | func requestLocation() 19 | } 20 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Home/Protocol/RunningPreparationUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningPreparationUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/04. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol RunningPreparationUseCase { 13 | var timeLeft: BehaviorSubject { get set } 14 | var isTimeOver: BehaviorSubject { get set } 15 | func executeTimer() 16 | } 17 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Home/Protocol/RunningResultUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningResultUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/06. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxRelay 11 | import RxSwift 12 | 13 | protocol RunningResultUseCase: EmojiDidSelectDelegate { 14 | var runningResult: RunningResult { get set } 15 | var selectedEmoji: PublishRelay { get set } 16 | func saveRunningResult() -> Observable 17 | } 18 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Home/Protocol/RunningSettingUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningSettingUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/04. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol RunningSettingUseCase { 13 | var runningSetting: BehaviorSubject { get set } 14 | var mateIsRunning: PublishSubject { get set } 15 | func updateHostNickname() 16 | func updateSessionId() 17 | func updateMode(mode: RunningMode) 18 | func updateTargetDistance(distance: Double) 19 | func deleteMateNickname() 20 | func updateMateNickname(nickname: String) 21 | func updateDateTime(date: Date) 22 | } 23 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Home/Protocol/RunningUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/05. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol RunningUseCase { 13 | var points: [Point] { get set } 14 | var currentMETs: Double { get set } 15 | var runningSetting: RunningSetting { get set } 16 | var runningData: BehaviorSubject { get set } 17 | var isCanceled: BehaviorSubject { get set } 18 | var isCanceledByMate: BehaviorSubject { get set } 19 | var isFinished: BehaviorSubject { get set } 20 | var shouldShowPopUp: BehaviorSubject { get set } 21 | var myProgress: BehaviorSubject { get set } 22 | var mateProgress: BehaviorSubject { get set } 23 | var totalProgress: BehaviorSubject { get set } 24 | var cancelTimeLeft: PublishSubject { get set } 25 | var popUpTimeLeft: PublishSubject { get set } 26 | var selfImageURL: PublishSubject { get set } 27 | var selfWeight: BehaviorSubject { get set } 28 | var mateImageURL: PublishSubject { get set } 29 | func loadUserInfo() 30 | func loadMateInfo() 31 | func updateRunningStatus() 32 | func cancelRunningStatus() 33 | func executePedometer() 34 | func executeActivity() 35 | func executeTimer() 36 | func executeCancelTimer() 37 | func executePopUpTimer() 38 | func invalidateCancelTimer() 39 | func listenRunningSession() 40 | func createRunningResult(isCanceled: Bool) -> RunningResult 41 | } 42 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Home/RunningUseCase/DefaultMapUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultMapUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/12. 6 | // 7 | 8 | import CoreLocation 9 | import Foundation 10 | 11 | import RxRelay 12 | import RxSwift 13 | 14 | final class DefaultMapUseCase: MapUseCase { 15 | weak var delegate: LocationDidUpdateDelegate? 16 | private let locationService: LocationService 17 | var updatedLocation: PublishRelay 18 | var disposeBag: DisposeBag 19 | 20 | required init(locationService: LocationService, delegate: LocationDidUpdateDelegate?) { 21 | self.locationService = locationService 22 | self.updatedLocation = PublishRelay() 23 | self.delegate = delegate 24 | self.disposeBag = DisposeBag() 25 | } 26 | 27 | func executeLocationTracker() { 28 | self.locationService.start() 29 | } 30 | 31 | func terminateLocationTracker() { 32 | self.locationService.stop() 33 | } 34 | 35 | func requestLocation() { 36 | self.locationService.observeUpdatedLocation() 37 | .compactMap({ $0.last }) 38 | .subscribe(onNext: { [weak self] location in 39 | self?.updatedLocation.accept(location) 40 | self?.delegate?.locationDidUpdate(location) 41 | }) 42 | .disposed(by: self.disposeBag) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Home/SettingUseCase/DefaultDistanceSettingUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultDistanceSettingUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/01. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class DefaultDistanceSettingUseCase: DistanceSettingUseCase { 13 | var validatedText: BehaviorSubject = BehaviorSubject(value: "5.00") 14 | 15 | func validate(text: String) { 16 | self.validatedText.onNext(self.checkValidty(of: text)) 17 | } 18 | private func checkValidty(of distanceText: String) -> String? { 19 | // "." 문자는 최대 1개 20 | guard distanceText.filter({ $0 == "." }).count <= 1 else { return nil } 21 | 22 | // "." 이 없을 때는 문자 최대 2개 23 | if !distanceText.contains(".") && distanceText.count > 2 { return nil } 24 | 25 | // "." 이 있을 때는 앞뒤 모두 문자 최대 2개 26 | // 이미 .이 없을 때는 문자를 2개까지 밖에 입력하지 못하므로 앞은 확인 안해도 됨 27 | if distanceText.contains(".") && distanceText.components(separatedBy: ".")[1].count > 2 { return nil } 28 | 29 | return distanceText 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Home/SettingUseCase/DefaultRunningPreparationUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningPreparationUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/03. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class DefaultRunningPreparationUseCase: RunningPreparationUseCase { 13 | var timeLeft = BehaviorSubject(value: 3) 14 | var isTimeOver = BehaviorSubject(value: false) 15 | var timerDisposeBag = DisposeBag() 16 | private let maxTime = 3 17 | 18 | func executeTimer() { 19 | Observable 20 | .interval( 21 | RxTimeInterval.seconds(1), 22 | scheduler: MainScheduler.instance 23 | ) 24 | .map { $0 + 1 } 25 | .subscribe(onNext: { [weak self] newTime in 26 | self?.updateTime(with: newTime) 27 | }) 28 | .disposed(by: self.timerDisposeBag) 29 | } 30 | 31 | private func updateTime(with time: Int) { 32 | if time == self.maxTime { 33 | self.timerDisposeBag = DisposeBag() 34 | self.timeLeft.onCompleted() 35 | self.isTimeOver.onNext(true) 36 | } 37 | self.timeLeft.onNext((self.maxTime) - time) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Mate/Protocol/MateUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MateUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/09. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol MateUseCase { 13 | typealias MateList = [(key: String, value: String)] 14 | var mateList: PublishSubject { get set } 15 | var didLoadMate: PublishSubject { get set } 16 | var didRequestMate: PublishSubject { get set } 17 | func fetchMateList() 18 | func fetchMateImage(from mate: [String]) 19 | func fetchSearchedUser(with nickname: String) 20 | func sendRequestMate(to mate: String) 21 | func filterMate(base mate: MateList, from text: String) 22 | } 23 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Mate/Protocol/ProfileUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol ProfileUseCase: EmojiDidSelectDelegate { 13 | var userInfo: PublishSubject { get set } 14 | var recordInfo: PublishSubject<[RunningResult]> { get set } 15 | var selectEmoji: PublishSubject { get set } 16 | func fetchUserInfo(_ nickname: String) 17 | func fetchRecordList(nickname: String, from index: Int, by count: Int) 18 | func fetchUserNickname() -> String? 19 | func emojiDidSelect(selectedEmoji: Emoji) 20 | func deleteEmoji(from runningID: String, of mate: String) -> Observable 21 | } 22 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/MyPage/DefaultMyPageUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultMyPageUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class DefaultMyPageUseCase: MyPageUseCase { 13 | private let userRepository: UserRepository 14 | private let firestoreRepository: FirestoreRepository 15 | private let disposeBag = DisposeBag() 16 | 17 | var nickname: String? 18 | var imageURL = PublishSubject() 19 | 20 | init(userRepository: UserRepository, firestoreRepository: FirestoreRepository) { 21 | self.userRepository = userRepository 22 | self.firestoreRepository = firestoreRepository 23 | self.nickname = userRepository.fetchUserNickname() 24 | } 25 | 26 | func loadUserInfo() { 27 | guard let nickname = self.nickname else { return } 28 | self.firestoreRepository.fetchUserData(of: nickname) 29 | .compactMap { $0 } 30 | .subscribe(onNext: { [weak self] userData in 31 | self?.imageURL.onNext(userData.image) 32 | }) 33 | .disposed(by: self.disposeBag) 34 | } 35 | 36 | func logout() { 37 | self.userRepository.saveLogoutInfo() 38 | } 39 | 40 | func deleteUserData() -> Observable { 41 | self.userRepository.deleteUserInfo() 42 | guard let nickname = self.nickname else { return Observable.just(false) } 43 | 44 | let removeUserInfoResult = self.firestoreRepository.remove(user: nickname) 45 | let removeUIDResult = self.firestoreRepository.fetchUID(of: nickname) 46 | .compactMap { $0 } 47 | .flatMap { [weak self] uid in 48 | self?.firestoreRepository.removeUID(uid: uid) ?? Observable.just(()) 49 | } 50 | 51 | return Observable.zip( 52 | removeUserInfoResult, 53 | removeUIDResult 54 | ) { (_, _) in 55 | return true 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/MyPage/DefaultProfileEditUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultProfileEditUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class DefaultProfileEditUseCase: ProfileEditUseCase { 13 | private let firestoreRepository: FirestoreRepository 14 | private let disposeBag = DisposeBag() 15 | 16 | var nickname: String? 17 | var height = BehaviorSubject(value: nil) 18 | var weight = BehaviorSubject(value: nil) 19 | var imageURL = BehaviorSubject(value: nil) 20 | var saveResult = PublishSubject() 21 | 22 | init(firestoreRepository: FirestoreRepository, with nickname: String?) { 23 | self.firestoreRepository = firestoreRepository 24 | self.nickname = nickname 25 | } 26 | 27 | func loadUserInfo() { 28 | guard let nickname = self.nickname else { return } 29 | 30 | self.firestoreRepository.fetchUserProfile(of: nickname) 31 | .subscribe(onNext: { [weak self] userProfile in 32 | self?.height.onNext(userProfile.height) 33 | self?.weight.onNext(userProfile.weight) 34 | self?.imageURL.onNext(userProfile.image) 35 | }) 36 | .disposed(by: self.disposeBag) 37 | } 38 | 39 | func saveUserInfo(imageData: Data) { 40 | guard let nickname = self.nickname, 41 | let height = try? self.height.value(), 42 | let weight = try? self.weight.value(), 43 | let imageURL = try? self.imageURL.value() else { return } 44 | 45 | let userProfile = UserProfile( 46 | image: imageURL, 47 | height: height, 48 | weight: weight 49 | ) 50 | 51 | self.firestoreRepository.saveAll( 52 | userProfile: userProfile, 53 | with: imageData, 54 | of: nickname 55 | ) 56 | .subscribe(onNext: { [weak self] _ in 57 | self?.saveResult.onNext(true) 58 | }) 59 | .disposed(by: self.disposeBag) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/MyPage/Protocol/MyPageUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyPageUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol MyPageUseCase { 13 | var nickname: String? { get } 14 | var imageURL: PublishSubject { get set } 15 | func loadUserInfo() 16 | func logout() 17 | func deleteUserData() -> Observable 18 | } 19 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/MyPage/Protocol/NotificationUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol NotificationUseCase { 13 | var notices: PublishSubject<[Notice]> { get set } 14 | func fetchNotices() 15 | func updateMateState(notice: Notice, isAccepted: Bool) 16 | } 17 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/MyPage/Protocol/ProfileEditUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileEditUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol ProfileEditUseCase { 13 | var nickname: String? { get } 14 | var height: BehaviorSubject { get set } 15 | var weight: BehaviorSubject { get set } 16 | var imageURL: BehaviorSubject { get set } 17 | var saveResult: PublishSubject { get set } 18 | func loadUserInfo() 19 | func saveUserInfo(imageData: Data) 20 | } 21 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Record/DefaultRecordDetailUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultRecordDetailUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | final class DefaultRecordDetailUseCase: RecordDetailUseCase { 11 | private let userRepository: UserRepository 12 | let runningResult: RunningResult 13 | let nickname: String? 14 | 15 | init(userRepository: UserRepository, with runningResult: RunningResult) { 16 | self.runningResult = runningResult 17 | self.userRepository = userRepository 18 | self.nickname = self.userRepository.fetchUserNickname() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Record/Protocol/RecordDetailUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecordDetailUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol RecordDetailUseCase { 11 | var runningResult: RunningResult { get } 12 | var nickname: String? { get } 13 | } 14 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Domain/UseCase/Record/Protocol/RecordUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecordUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/18. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol RecordUseCase { 13 | var totalRecord: PublishSubject {get set } 14 | var month: BehaviorSubject { get set } 15 | var selectedDay: BehaviorSubject { get set } 16 | var runningCount: PublishSubject { get set } 17 | var likeCount: PublishSubject { get set } 18 | var monthlyRecords: BehaviorSubject<[RunningResult]> { get set } 19 | func loadTotalRecord() 20 | func refreshRecords() 21 | func loadMonthlyRecord() 22 | func updateMonth(toNext: Bool) 23 | } 24 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/MateRunner.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.developer.applesignin 8 | 9 | Default 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Network/Protocol/RealtimeDatabaseNetworkService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealtimeDatabaseNetworkService.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol RealtimeDatabaseNetworkService { 13 | func updateChildValues(with value: [String: Any], path: [String]) -> Observable 14 | func update(with: Any, path: [String]) -> Observable 15 | func listen(path: [String]) -> Observable 16 | func stopListen(path: [String]) 17 | func fetch(of path: [String])-> Observable 18 | func fetchFCMToken(of mate: String)-> Observable 19 | } 20 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Network/Protocol/URLSessionNetworkService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLSessionNetworkService.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol URLSessionNetworkService { 13 | func post( 14 | _ data: T, 15 | url urlString: String, 16 | headers: [String: String]? 17 | ) -> Observable> 18 | func patch( 19 | _ data: T, 20 | url urlString: String, 21 | headers: [String: String]? 22 | ) -> Observable> 23 | func delete( 24 | url urlString: String, 25 | headers: [String: String]? 26 | ) -> Observable> 27 | func get( 28 | url urlString: String, 29 | headers: [String: String]? 30 | ) -> Observable> 31 | } 32 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/Common/Coordinator/DefaultAppCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppCoordinator.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/09. 6 | // 7 | // 8 | import UIKit 9 | 10 | final class DefaultAppCoordinator: AppCoordinator { 11 | weak var finishDelegate: CoordinatorFinishDelegate? 12 | var navigationController: UINavigationController 13 | var childCoordinators = [Coordinator]() 14 | var type: CoordinatorType { .app } 15 | 16 | required init(_ navigationController: UINavigationController) { 17 | self.navigationController = navigationController 18 | navigationController.setNavigationBarHidden(true, animated: true) 19 | } 20 | 21 | func start() { 22 | if UserDefaults.standard.bool(forKey: UserDefaultKey.isLoggedIn) { 23 | self.showTabBarFlow() 24 | } else { 25 | self.showLoginFlow() 26 | } 27 | } 28 | 29 | func showLoginFlow() { 30 | let loginCoordinator = DefaultLoginCoordinator(self.navigationController) 31 | loginCoordinator.finishDelegate = self 32 | loginCoordinator.start() 33 | childCoordinators.append(loginCoordinator) 34 | } 35 | 36 | func showTabBarFlow() { 37 | let tabBarCoordinator = DefaultTabBarCoordinator(self.navigationController) 38 | tabBarCoordinator.finishDelegate = self 39 | tabBarCoordinator.start() 40 | childCoordinators.append(tabBarCoordinator) 41 | } 42 | } 43 | 44 | extension DefaultAppCoordinator: CoordinatorFinishDelegate { 45 | func coordinatorDidFinish(childCoordinator: Coordinator) { 46 | self.childCoordinators = self.childCoordinators.filter({ $0.type != childCoordinator.type }) 47 | 48 | self.navigationController.view.backgroundColor = .systemBackground 49 | self.navigationController.viewControllers.removeAll() 50 | 51 | switch childCoordinator.type { 52 | case .tab: 53 | self.showLoginFlow() 54 | case .login: 55 | self.showTabBarFlow() 56 | default: 57 | break 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/Common/Coordinator/Delegate/CoordinatorFinishDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoordinatorFinishDelegate.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/09. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol CoordinatorFinishDelegate: AnyObject { 11 | func coordinatorDidFinish(childCoordinator: Coordinator) 12 | } 13 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/Common/Coordinator/Protocol/AppCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppCoordinator.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/09. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol AppCoordinator: Coordinator { 11 | func showLoginFlow() 12 | func showTabBarFlow() 13 | } 14 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/Common/Coordinator/Protocol/Coordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinator.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/09. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol Coordinator: AnyObject { 11 | var finishDelegate: CoordinatorFinishDelegate? { get set } 12 | var navigationController: UINavigationController { get set } 13 | var childCoordinators: [Coordinator] { get set } 14 | var type: CoordinatorType { get } 15 | func start() 16 | func finish() 17 | func findCoordinator(type: CoordinatorType) -> Coordinator? 18 | 19 | init(_ navigationController: UINavigationController) 20 | } 21 | 22 | extension Coordinator { 23 | func finish() { 24 | childCoordinators.removeAll() 25 | finishDelegate?.coordinatorDidFinish(childCoordinator: self) 26 | } 27 | 28 | func findCoordinator(type: CoordinatorType) -> Coordinator? { 29 | var stack: [Coordinator] = [self] 30 | 31 | while !stack.isEmpty { 32 | let currentCoordinator = stack.removeLast() 33 | if currentCoordinator.type == type { 34 | return currentCoordinator 35 | } 36 | currentCoordinator.childCoordinators.forEach({ child in 37 | stack.append(child) 38 | }) 39 | } 40 | return nil 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/Common/Coordinator/Protocol/InvitationReceivable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InvitationReceivable.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/28. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol InvitationReceivable: AnyObject { 11 | func invitationDidReceive(_ notification: Notification) 12 | func invitationDidAccept(with settingData: RunningSetting) 13 | func invitationDidReject() 14 | } 15 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/Common/Coordinator/Protocol/TabBarCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBarCoordinator.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/09. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol TabBarCoordinator: Coordinator, InvitationReceivable { 11 | var tabBarController: UITabBarController { get set } 12 | func selectPage(_ page: TabBarPage) 13 | func setSelectedIndex(_ index: Int) 14 | func currentPage() -> TabBarPage? 15 | } 16 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/Common/View/EmojiListView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiListView.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/23. 6 | // 7 | 8 | import UIKit 9 | 10 | final class EmojiListView: UIScrollView { 11 | private lazy var contentStackView: UIStackView = { 12 | let stackView = UIStackView() 13 | stackView.axis = .horizontal 14 | stackView.alignment = .center 15 | stackView.spacing = 10 16 | return stackView 17 | }() 18 | 19 | override init(frame: CGRect) { 20 | super.init(frame: frame) 21 | self.configureUI() 22 | } 23 | 24 | required init?(coder: NSCoder) { 25 | super.init(coder: coder) 26 | self.configureUI() 27 | } 28 | 29 | func bindUI(emojiList: [String: String]) { 30 | emojiList.forEach { emoji, count in 31 | let emojiView = EmojiView(emoji: emoji, count: count) 32 | self.contentStackView.addArrangedSubview(emojiView) 33 | } 34 | } 35 | } 36 | 37 | private extension EmojiListView { 38 | func configureUI() { 39 | self.showsHorizontalScrollIndicator = false 40 | 41 | self.snp.makeConstraints { make in 42 | make.height.equalTo(50) 43 | } 44 | 45 | self.addSubview(self.contentStackView) 46 | self.contentStackView.snp.makeConstraints { make in 47 | make.edges.equalToSuperview() 48 | make.height.equalToSuperview() 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/Common/View/EmojiView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiView.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/23. 6 | // 7 | 8 | import UIKit 9 | 10 | final class EmojiView: UIView { 11 | private lazy var stackView: UIStackView = { 12 | let stackView = UIStackView() 13 | stackView.axis = .horizontal 14 | stackView.alignment = .center 15 | stackView.spacing = 10 16 | return stackView 17 | }() 18 | 19 | override init(frame: CGRect) { 20 | super.init(frame: frame) 21 | } 22 | 23 | required init?(coder: NSCoder) { 24 | super.init(coder: coder) 25 | } 26 | 27 | convenience init(emoji: String, count: String) { 28 | self.init(frame: .zero) 29 | self.configureUI(emoji: emoji, count: count) 30 | } 31 | } 32 | 33 | private extension EmojiView { 34 | func configureUI(emoji: String, count: String) { 35 | let emojiLabel = UILabel() 36 | emojiLabel.font = .systemFont(ofSize: 20) 37 | emojiLabel.text = emoji 38 | 39 | let countLabel = UILabel() 40 | countLabel.font = .notoSansBoldItalic(size: 20) 41 | countLabel.text = count 42 | 43 | self.stackView.addArrangedSubview(emojiLabel) 44 | self.stackView.addArrangedSubview(countLabel) 45 | 46 | self.snp.makeConstraints { make in 47 | make.height.equalTo(50) 48 | } 49 | 50 | self.addSubview(self.stackView) 51 | self.stackView.snp.makeConstraints { make in 52 | make.left.right.equalToSuperview().inset(15) 53 | make.centerY.equalToSuperview() 54 | } 55 | 56 | self.layer.borderWidth = 2 57 | self.layer.borderColor = UIColor.systemGray5.cgColor 58 | self.layer.cornerRadius = 25 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/Common/View/MateRunnerActivityIndicatorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MateRunnerActivityIndicatorView.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/27. 6 | // 7 | 8 | import UIKit 9 | 10 | final class MateRunnerActivityIndicatorView: UIActivityIndicatorView { 11 | override init(frame: CGRect) { 12 | super.init(frame: frame) 13 | } 14 | 15 | required init(coder: NSCoder) { 16 | super.init(coder: coder) 17 | } 18 | 19 | convenience init(color: UIColor) { 20 | self.init(frame: CGRect(x: 0, y: 0, width: 90, height: 90)) 21 | self.color = color 22 | self.hidesWhenStopped = true 23 | self.style = .large 24 | self.startAnimating() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/Common/View/PickerTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PickerTextField.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/14. 6 | // 7 | 8 | import UIKit 9 | 10 | final class PickerTextField: UITextField { 11 | lazy var doneButton = UIBarButtonItem(title: "설정", style: .done, target: self, action: nil) 12 | 13 | private lazy var toolbar: UIToolbar = { 14 | let toolbar = UIToolbar() 15 | toolbar.sizeToFit() 16 | toolbar.items = [self.doneButton] 17 | toolbar.barTintColor = .systemBackground 18 | toolbar.tintColor = .mrPurple 19 | return toolbar 20 | }() 21 | 22 | lazy var pickerView: UIPickerView = { 23 | let pickerView = UIPickerView() 24 | pickerView.backgroundColor = .systemBackground 25 | return pickerView 26 | }() 27 | 28 | override init(frame: CGRect) { 29 | super.init(frame: frame) 30 | self.configureUI() 31 | } 32 | 33 | required init?(coder: NSCoder) { 34 | super.init(coder: coder) 35 | self.configureUI() 36 | } 37 | 38 | override func selectionRects(for range: UITextRange) -> [UITextSelectionRect] { 39 | return [] 40 | } 41 | 42 | override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { 43 | return false 44 | } 45 | } 46 | 47 | // MARK: - Private Functions 48 | 49 | private extension PickerTextField { 50 | func configureUI() { 51 | self.borderStyle = .roundedRect 52 | self.tintColor = .clear 53 | self.backgroundColor = .systemGray6 54 | self.font = .notoSans(size: 13, family: .regular) 55 | self.inputView = self.pickerView 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/Common/ViewController/InvitationViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InvitationViewController.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/10. 6 | // 7 | 8 | import UIKit 9 | 10 | import RxCocoa 11 | import RxSwift 12 | 13 | final class InvitationViewController: UIViewController { 14 | var viewModel: InvitationViewModel? 15 | private lazy var invitationView = InvitationView() 16 | private let disposeBag = DisposeBag() 17 | 18 | override func viewDidLoad() { 19 | super.viewDidLoad() 20 | self.configureUI() 21 | self.bindViewModel() 22 | } 23 | } 24 | 25 | // MARK: - Private Functions 26 | 27 | private extension InvitationViewController { 28 | func bindViewModel() { 29 | let input = InvitationViewModel.Input( 30 | acceptButtonDidTapEvent: self.invitationView.acceptButton.rx.tap.asObservable(), 31 | rejectButtonDidTapEvent: self.invitationView.rejectButton.rx.tap.asObservable()) 32 | 33 | let output = self.viewModel?.transform(from: input, disposeBag: self.disposeBag) 34 | guard let output = output else { return } 35 | 36 | self.invitationView.updateTitleLabel(with: output.host) 37 | self.invitationView.updateModeLabel(with: output.mode) 38 | self.invitationView.updateDistanceLabel(with: output.targetDistance) 39 | 40 | output.cancelledAlertShouldShow 41 | .filter { $0 } 42 | .subscribe(onNext: { [weak self] _ in 43 | self?.showAlert(message: "취소된 달리기입니다.") 44 | }) 45 | .disposed(by: self.disposeBag) 46 | } 47 | 48 | func configureUI() { 49 | self.view.addSubview(invitationView) 50 | self.invitationView.snp.makeConstraints { make in 51 | make.centerX.centerY.equalToSuperview() 52 | } 53 | } 54 | 55 | func showAlert(message: String) { 56 | let alert = UIAlertController(title: "알림", message: message, preferredStyle: .alert) 57 | let confirm = UIAlertAction(title: "확인", style: .default, handler: { [weak self] _ in 58 | self?.viewModel?.finish() 59 | }) 60 | alert.addAction(confirm) 61 | present(alert, animated: false, completion: nil) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/Common/ViewModel/EmojiViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiViewModel.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/11. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxCocoa 11 | import RxSwift 12 | 13 | final class EmojiViewModel { 14 | let emojiObservable = Observable.of(Emoji.allCases) 15 | var emojiUseCase: EmojiUseCase 16 | private weak var coordinator: EmojiCoordinator? 17 | 18 | struct Input { 19 | let emojiCellTapEvent: Observable 20 | } 21 | 22 | struct Output { 23 | var selectedEmoji: PublishRelay = PublishRelay() 24 | } 25 | 26 | init( 27 | coordinator: EmojiCoordinator?, 28 | emojiUseCase: EmojiUseCase 29 | ) { 30 | self.coordinator = coordinator 31 | self.emojiUseCase = emojiUseCase 32 | } 33 | 34 | func transform(from input: Input, disposeBag: DisposeBag) -> Output { 35 | let output = Output() 36 | 37 | input.emojiCellTapEvent 38 | .subscribe(onNext: { [weak self] indexPath in 39 | guard let emoji = self?.emoji(at: indexPath.row) else { return } 40 | self?.emojiUseCase.saveSentEmoji(emoji) 41 | self?.emojiUseCase.selectEmoji(emoji) 42 | self?.emojiUseCase.sendComplimentEmoji() 43 | }) 44 | .disposed(by: disposeBag) 45 | 46 | self.emojiUseCase.selectedEmoji 47 | .bind(to: output.selectedEmoji) 48 | .disposed(by: disposeBag) 49 | 50 | return output 51 | } 52 | } 53 | 54 | private extension EmojiViewModel { 55 | func emoji(at index: Int) -> Emoji { 56 | return Emoji.allCases[index] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/Common/ViewModel/Protocol/CoreLocationConvertable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreLocationCalculatable.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/12/03. 6 | // 7 | 8 | import CoreLocation 9 | import Foundation 10 | 11 | protocol CoreLocationConvertable { 12 | func pointsToCoordinate2D(from points: [Point]) -> [CLLocationCoordinate2D] 13 | func calculateRegion(from points: [CLLocationCoordinate2D]) -> Region 14 | } 15 | 16 | extension CoreLocationConvertable { 17 | func pointsToCoordinate2D(from points: [Point]) -> [CLLocationCoordinate2D] { 18 | return points.map { location in location.convertToCLLocationCoordinate2D() } 19 | } 20 | 21 | func calculateRegion(from points: [CLLocationCoordinate2D]) -> Region { 22 | guard !points.isEmpty else { return Region() } 23 | 24 | let latitudes = points.map { $0.latitude } 25 | let longitudes = points.map { $0.longitude } 26 | 27 | guard let maxLatitude = latitudes.max(), 28 | let minLatitude = latitudes.min(), 29 | let maxLongitude = longitudes.max(), 30 | let minLongitude = longitudes.min() else { return Region() } 31 | 32 | let meanLatitude = (maxLatitude + minLatitude) / 2 33 | let meanLongitude = (maxLongitude + minLongitude) / 2 34 | let coordinate = CLLocationCoordinate2DMake(meanLatitude, meanLongitude) 35 | 36 | let latitudeSpan = (maxLatitude - minLatitude) * 1.5 37 | let longitudeSpan = (maxLongitude - minLongitude) * 1.5 38 | let span = (latitudeSpan, longitudeSpan) 39 | 40 | return Region(center: coordinate, span: span) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/HomeScene/Coordinator/Protocol/HomeCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeCoordinator.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/10. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol HomeCoordinator: Coordinator { 11 | func showSettingFlow() 12 | func showRunningFlow(with initialSettingData: RunningSetting) 13 | func startRunningFromInvitation(with settingData: RunningSetting) 14 | } 15 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/HomeScene/Coordinator/Protocol/RunningCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningCoordinator.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/10. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol RunningCoordinator: EmojiCoordinator { 11 | func pushRunningViewController(with settingData: RunningSetting?) 12 | func pushRunningResultViewController(with runningResult: RunningResult?) 13 | func pushTeamRunningResultViewController(with runningResult: RunningResult?) 14 | func pushRaceRunningResultViewController(with runningResult: RunningResult?) 15 | func presentEmojiModal(connectedTo usecase: RunningResultUseCase) 16 | } 17 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/HomeScene/Coordinator/Protocol/RunningSettingCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeCoordinator.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/09. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol RunningSettingCoordinator: Coordinator { 11 | func pushRunningModeSettingViewController() 12 | func pushMateRunningModeSettingViewController(with settingData: RunningSetting?) 13 | func pushDistanceSettingViewController(with settingData: RunningSetting?) 14 | func navigateProperViewController(with settingData: RunningSetting?) 15 | func pushInvitationWaitingViewController(with settingData: RunningSetting?) 16 | func pushRunningPreparationViewController(with settingData: RunningSetting?) 17 | func pushMateSettingViewController(with settingData: RunningSetting?) 18 | func finish(with settingData: RunningSetting) 19 | } 20 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/HomeScene/Coordinator/Protocol/SettingCoordinatorDidFinishDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingCoordinatorDidFinishDelegate.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/10. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol SettingCoordinatorDidFinishDelegate: AnyObject { 11 | func settingCoordinatorDidFinish(with runningSettingData: RunningSetting) 12 | } 13 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/HomeScene/View/CursorDisabledTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CursorDisabledTextField.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/04. 6 | // 7 | 8 | import UIKit 9 | 10 | final class CursorDisabledTextField: UITextField { 11 | override func closestPosition(to point: CGPoint) -> UITextPosition? { 12 | let beginning = self.beginningOfDocument 13 | let end = self.position(from: beginning, offset: self.text?.count ?? 0) 14 | return end 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/HomeScene/View/MyResultView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyResultView.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/10. 6 | // 7 | 8 | import UIKit 9 | 10 | final class MyResultView: UIStackView { 11 | convenience init(distanceLabel: UILabel, calorieLabel: UILabel, timeLabel: UILabel) { 12 | self.init(frame: .zero) 13 | self.configureUI(distanceLabel: distanceLabel, calorieLabel: calorieLabel, timeLabel: timeLabel) 14 | } 15 | } 16 | 17 | private extension MyResultView { 18 | func configureUI(distanceLabel: UILabel, calorieLabel: UILabel, timeLabel: UILabel) { 19 | let calorieSection = self.createSectionView(valueLabel: calorieLabel, name: "칼로리") 20 | let timeSection = self.createSectionView(valueLabel: timeLabel, name: "시간") 21 | let distanceSection = self.createSectionView(valueLabel: distanceLabel, name: "킬로미터", isDistance: true) 22 | 23 | let horizontalStack = UIStackView() 24 | horizontalStack.axis = .horizontal 25 | horizontalStack.spacing = 50 26 | 27 | horizontalStack.addArrangedSubview(calorieSection) 28 | horizontalStack.addArrangedSubview(timeSection) 29 | 30 | self.axis = .vertical 31 | self.alignment = .leading 32 | self.spacing = 15 33 | 34 | self.addArrangedSubview(distanceSection) 35 | self.addArrangedSubview(horizontalStack) 36 | } 37 | 38 | func createSectionView(valueLabel: UILabel, name: String, isDistance: Bool = false) -> UIStackView { 39 | let fontSize = isDistance ? 20.0 : 18.0 40 | let nameLabel = UILabel() 41 | nameLabel.textColor = .systemGray 42 | nameLabel.text = name 43 | nameLabel.font = .notoSans(size: fontSize, family: .light) 44 | 45 | let stackView = UIStackView() 46 | stackView.axis = .vertical 47 | stackView.alignment = isDistance ? .leading : .center 48 | 49 | stackView.addArrangedSubview(valueLabel) 50 | stackView.addArrangedSubview(nameLabel) 51 | return stackView 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/HomeScene/View/RoundedButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RoundedButton.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/02. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | final class RoundedButton: UIButton { 13 | override init(frame: CGRect) { 14 | super.init(frame: frame) 15 | self.configureUI() 16 | } 17 | 18 | required init?(coder: NSCoder) { 19 | super.init(coder: coder) 20 | self.configureUI() 21 | } 22 | 23 | convenience init(title: String) { 24 | self.init(frame: .zero) 25 | self.configureUI(title: title) 26 | } 27 | } 28 | 29 | // MARK: - Private Functions 30 | 31 | private extension RoundedButton { 32 | func configureUI(title: String = "") { 33 | self.setTitle(title, for: .normal) 34 | self.titleLabel?.font = .notoSans(size: 16, family: .bold) 35 | self.layer.cornerRadius = 10 36 | self.backgroundColor = .mrPurple 37 | self.snp.makeConstraints { make in 38 | make.height.equalTo(50) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/HomeScene/View/RunningInfoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningInfoView.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/04. 6 | // 7 | 8 | import UIKit 9 | 10 | final class RunningInfoView: UIStackView { 11 | convenience init(name: String, value: String) { 12 | self.init(frame: .zero) 13 | self.configureUI(name: name, value: value) 14 | } 15 | 16 | func updateValue(newValue: String) { 17 | guard let valueLabel = self.arrangedSubviews.first as? UILabel else { return } 18 | valueLabel.text = newValue 19 | valueLabel.textColor = .black 20 | } 21 | } 22 | 23 | // MARK: - Private Functions 24 | 25 | private extension RunningInfoView { 26 | func configureUI(name: String, value: String) { 27 | let nameLabel = UILabel() 28 | let valueLabel = UILabel() 29 | 30 | nameLabel.font = .notoSans(size: 16, family: .regular) 31 | nameLabel.textColor = .darkGray 32 | nameLabel.text = name 33 | 34 | valueLabel.font = .notoSans(size: 30, family: .bold) 35 | valueLabel.text = value 36 | 37 | self.axis = .vertical 38 | self.alignment = .center 39 | 40 | self.addArrangedSubview(valueLabel) 41 | self.addArrangedSubview(nameLabel) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/HomeScene/View/RunningProgressView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningProgressView.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/04. 6 | // 7 | 8 | import UIKit 9 | 10 | final class RunningProgressView: UIProgressView { 11 | convenience init(width: CGFloat, color: UIColor = .mrPurple) { 12 | self.init(frame: .zero) 13 | self.configureUI(width: width, color: color) 14 | } 15 | } 16 | 17 | // MARK: - Private Functions 18 | 19 | private extension RunningProgressView { 20 | func configureUI(width: CGFloat, color: UIColor) { 21 | self.progressTintColor = color 22 | self.trackTintColor = .white 23 | self.setProgress(0.5, animated: false) 24 | 25 | self.snp.makeConstraints { make in 26 | make.width.equalTo(width) 27 | make.height.equalTo(5) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/HomeScene/ViewController/Delegate/BackButtonDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackButtonDelegate.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/08. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol BackButtonDelegate: AnyObject { 11 | func backButtonDidTap() 12 | } 13 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/HomeScene/ViewController/SettingViewController/MateSettingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MateSettingViewController.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/10. 6 | // 7 | 8 | import UIKit 9 | 10 | import RxRelay 11 | import RxSwift 12 | 13 | final class MateSettingViewController: MateViewController { 14 | var viewModel: MateSettingViewModel? 15 | private let disposeBag = DisposeBag() 16 | private let mateDidSelectEvent = PublishRelay() 17 | private let viewWillAppearEvent = PublishRelay() 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | self.configureNavigation() 22 | } 23 | 24 | override func viewWillAppear(_ animated: Bool) { 25 | super.viewWillAppear(animated) 26 | self.bindViewModel() 27 | self.viewWillAppearEvent.accept(()) 28 | } 29 | 30 | override func configureNavigation() { 31 | self.navigationItem.title = "친구 목록" 32 | } 33 | 34 | override func moveToNext(mate: String) { 35 | self.mateDidSelectEvent.accept(mate) 36 | } 37 | 38 | func bindViewModel() { 39 | let input = MateSettingViewModel.Input( 40 | viewWillAppearEvent: self.viewWillAppearEvent.asObservable(), 41 | mateDidSelectEvent: self.mateDidSelectEvent.asObservable() 42 | ) 43 | 44 | self.viewModel?.transform(input: input, disposeBag: self.disposeBag) 45 | .mateIsNowRunningAlertShouldShow 46 | .asDriver(onErrorJustReturn: false) 47 | .filter { $0 } 48 | .drive(onNext: { [weak self] _ in 49 | self?.showAlert(message: "해당 메이트가 달리기 중이어서 선택할 수 없습니다. 다른 메이트를 선택해주세요.") 50 | }) 51 | .disposed(by: self.disposeBag) 52 | } 53 | } 54 | 55 | private extension MateSettingViewController { 56 | func showAlert(message: String) { 57 | let alert = UIAlertController(title: "알림", message: message, preferredStyle: .alert) 58 | let confirm = UIAlertAction(title: "확인", style: .default, handler: nil) 59 | alert.addAction(confirm) 60 | present(alert, animated: false, completion: nil) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/HomeScene/ViewController/SettingViewController/RunningPreparationViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningPreparationViewController.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/03. 6 | // 7 | 8 | import UIKit 9 | 10 | import RxCocoa 11 | import RxSwift 12 | import SnapKit 13 | 14 | final class RunningPreparationViewController: UIViewController { 15 | var viewModel: RunningPreparationViewModel? 16 | let disposeBag = DisposeBag() 17 | 18 | private lazy var timeLeftLabel: UILabel = { 19 | let label = UILabel() 20 | label.font = .notoSansBoldItalic(size: 130) 21 | label.textColor = .black 22 | return label 23 | }() 24 | 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | self.configureUI() 28 | self.bindViewModel() 29 | } 30 | } 31 | 32 | private extension RunningPreparationViewController { 33 | func configureUI() { 34 | self.navigationController?.setNavigationBarHidden(true, animated: false) 35 | self.view.layer.backgroundColor = UIColor.mrYellow.cgColor 36 | self.view.addSubview(self.timeLeftLabel) 37 | self.timeLeftLabel.snp.makeConstraints { make in 38 | make.centerX.centerY.equalToSuperview() 39 | } 40 | } 41 | 42 | func bindViewModel() { 43 | let input = RunningPreparationViewModel.Input(viewDidLoadEvent: Observable.just(())) 44 | let output = self.viewModel?.transform(from: input, disposeBag: self.disposeBag) 45 | 46 | output?.timeLeft 47 | .asDriver() 48 | .drive(onNext: { [weak self] updatedTime in 49 | self?.timeLeftLabel.text = updatedTime 50 | self?.timeLeftLabel.transform = CGAffineTransform(scaleX: 0.25, y: 0.25) 51 | UIView.animate(withDuration: 0.7) { 52 | self?.timeLeftLabel.transform = CGAffineTransform(scaleX: 1, y: 1) 53 | } 54 | }) 55 | .disposed(by: self.disposeBag) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/HomeScene/ViewModel/HomeViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HomeViewModel.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/09. 6 | // 7 | 8 | import CoreLocation 9 | import Foundation 10 | 11 | import RxRelay 12 | import RxSwift 13 | 14 | final class HomeViewModel { 15 | weak var coordinator: HomeCoordinator? 16 | private let homeUseCase: HomeUseCase 17 | 18 | init(coordinator: HomeCoordinator, homeUseCase: HomeUseCase) { 19 | self.coordinator = coordinator 20 | self.homeUseCase = homeUseCase 21 | } 22 | 23 | struct Input { 24 | let viewDidLoadEvent: Observable 25 | let startButtonDidTapEvent: Observable 26 | } 27 | 28 | struct Output { 29 | let currentUserLocation = PublishRelay() 30 | let authorizationAlertShouldShow = BehaviorRelay(value: false) 31 | } 32 | 33 | func transform(input: Input, disposeBag: DisposeBag) -> Output { 34 | let output = Output() 35 | 36 | input.viewDidLoadEvent 37 | .subscribe({ [weak self] _ in 38 | self?.homeUseCase.checkAuthorization() 39 | self?.homeUseCase.observeUserLocation() 40 | }) 41 | .disposed(by: disposeBag) 42 | 43 | input.startButtonDidTapEvent 44 | .subscribe({ [weak self] _ in 45 | self?.homeUseCase.stopUpdatingLocation() 46 | self?.coordinator?.showSettingFlow() 47 | }) 48 | .disposed(by: disposeBag) 49 | 50 | self.homeUseCase.authorizationStatus 51 | .map({ $0 == .disallowed }) 52 | .bind(to: output.authorizationAlertShouldShow) 53 | .disposed(by: disposeBag) 54 | 55 | self.homeUseCase.userLocation 56 | .map({ $0.coordinate }) 57 | .bind(to: output.currentUserLocation) 58 | .disposed(by: disposeBag) 59 | 60 | return output 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/HomeScene/ViewModel/SettingViewModel/RunningModeSettingViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningModeSettingViewModel.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/02. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | import RxRelay 12 | 13 | final class RunningModeSettingViewModel { 14 | private weak var coordinator: RunningSettingCoordinator? 15 | private let runningSettingUseCase: RunningSettingUseCase 16 | 17 | struct Input { 18 | let singleButtonTapEvent: Observable 19 | let mateButtonTapEvent: Observable 20 | } 21 | 22 | struct Output { 23 | var runningMode = PublishRelay() 24 | } 25 | 26 | init(coordinator: RunningSettingCoordinator?, runningSettingUseCase: RunningSettingUseCase) { 27 | self.coordinator = coordinator 28 | self.runningSettingUseCase = runningSettingUseCase 29 | } 30 | 31 | func transform(from input: Input, disposeBag: DisposeBag) -> Output { 32 | let output = Output() 33 | input.singleButtonTapEvent 34 | .subscribe(onNext: { [weak self] _ in 35 | self?.runningSettingUseCase.updateMode(mode: .single) 36 | self?.coordinator?.pushDistanceSettingViewController( 37 | with: try? self?.runningSettingUseCase.runningSetting.value() 38 | ) 39 | }) 40 | .disposed(by: disposeBag) 41 | 42 | input.mateButtonTapEvent 43 | .subscribe(onNext: { [weak self] _ in 44 | self?.runningSettingUseCase.updateMode(mode: .race) 45 | self?.coordinator?.pushMateRunningModeSettingViewController( 46 | with: try? self?.runningSettingUseCase.runningSetting.value() 47 | ) 48 | }) 49 | .disposed(by: disposeBag) 50 | 51 | self.runningSettingUseCase.runningSetting 52 | .compactMap({ $0.mode }) 53 | .bind(to: output.runningMode) 54 | .disposed(by: disposeBag) 55 | 56 | return output 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/HomeScene/ViewModel/SettingViewModel/RunningPreparationViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningPreparationViewModel.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/03. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxRelay 11 | import RxSwift 12 | 13 | final class RunningPreparationViewModel { 14 | private weak var coordinator: RunningSettingCoordinator? 15 | private let runningSettingUseCase: RunningSettingUseCase 16 | private let runningPreparationUseCase: RunningPreparationUseCase 17 | private let maxPreparationTime = 3 18 | 19 | struct Input { 20 | let viewDidLoadEvent: Observable 21 | } 22 | struct Output { 23 | var timeLeft = BehaviorRelay(value: "") 24 | } 25 | 26 | init( 27 | coordinator: RunningSettingCoordinator?, 28 | runningSettingUseCase: RunningSettingUseCase, 29 | runningPreparationUseCase: RunningPreparationUseCase 30 | ) { 31 | self.coordinator = coordinator 32 | self.runningPreparationUseCase = runningPreparationUseCase 33 | self.runningSettingUseCase = runningSettingUseCase 34 | } 35 | 36 | func transform(from input: Input, disposeBag: DisposeBag) -> Output { 37 | let output = Output() 38 | input.viewDidLoadEvent 39 | .subscribe(onNext: { [weak self] _ in 40 | self?.runningPreparationUseCase.executeTimer() 41 | }) 42 | .disposed(by: disposeBag) 43 | 44 | self.runningPreparationUseCase.timeLeft 45 | .map({ "\($0)" }) 46 | .bind(to: output.timeLeft) 47 | .disposed(by: disposeBag) 48 | 49 | self.runningPreparationUseCase.isTimeOver 50 | .subscribe(onNext: { [weak self] isOver in 51 | self?.runningSettingUseCase.updateDateTime(date: Date()) 52 | guard isOver, let settingData = try? self?.runningSettingUseCase.runningSetting.value() else { 53 | return 54 | } 55 | self?.coordinator?.finish(with: settingData) 56 | }) 57 | .disposed(by: disposeBag) 58 | 59 | return output 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/LoginScene/Coordinator/DefaultSignUpCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultSignUpCoordinator.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/13. 6 | // 7 | 8 | import UIKit 9 | 10 | final class DefaultSignUpCoordinator: SignUpCoordinator { 11 | weak var finishDelegate: CoordinatorFinishDelegate? 12 | var navigationController: UINavigationController 13 | var signUpViewController: SignUpViewController 14 | var childCoordinators: [Coordinator] = [] 15 | var type: CoordinatorType = .signUp 16 | 17 | init(_ navigationController: UINavigationController) { 18 | self.navigationController = navigationController 19 | self.signUpViewController = SignUpViewController() 20 | } 21 | 22 | func start() {} 23 | 24 | func pushSignUpViewController(with uid: String) { 25 | self.signUpViewController.viewModel = SignUpViewModel( 26 | coordinator: self, 27 | signUpUseCase: DefaultSignUpUseCase( 28 | repository: DefaultUserRepository( 29 | realtimeDatabaseNetworkService: DefaultRealtimeDatabaseNetworkService() 30 | ), 31 | firestoreRepository: DefaultFirestoreRepository( 32 | urlSessionService: DefaultURLSessionNetworkService() 33 | ), 34 | uid: uid 35 | ) 36 | ) 37 | self.navigationController.pushViewController(self.signUpViewController, animated: true) 38 | } 39 | 40 | func finish() { 41 | self.finishDelegate?.coordinatorDidFinish(childCoordinator: self) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/LoginScene/Coordinator/Protocol/LoginCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginCoordinator.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/13. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol LoginCoordinator: Coordinator { 11 | func showSignUpFlow(with uid: String) 12 | func pushTermsViewController() 13 | } 14 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/LoginScene/Coordinator/Protocol/SignUpCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpCoordinator.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/13. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol SignUpCoordinator: Coordinator { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/MateScene/ Cooditnator/DefaultAddMateCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultAddMateCoordinator.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/17. 6 | // 7 | 8 | import UIKit 9 | 10 | final class DefaultAddMateCoordinator: AddMateCoordinator { 11 | weak var finishDelegate: CoordinatorFinishDelegate? 12 | weak var settingFinishDelegate: SettingCoordinatorDidFinishDelegate? 13 | var navigationController: UINavigationController 14 | var childCoordinators: [Coordinator] = [] 15 | var type: CoordinatorType { .addMate } 16 | 17 | func start() { 18 | self.pushAddMateViewController() 19 | } 20 | 21 | required init(_ navigationController: UINavigationController) { 22 | self.navigationController = navigationController 23 | } 24 | 25 | func pushAddMateViewController() { 26 | let addMateViewController = AddMateViewController() 27 | addMateViewController.viewModel = AddMateViewModel( 28 | coordinator: self, 29 | mateUseCase: DefaultMateUseCase( 30 | mateRepository: DefaultMateRepository( 31 | realtimeNetworkService: DefaultRealtimeDatabaseNetworkService(), 32 | urlSessionNetworkService: DefaultURLSessionNetworkService() 33 | ), firestoreRepository: DefaultFirestoreRepository( 34 | urlSessionService: DefaultURLSessionNetworkService() 35 | ), userRepository: DefaultUserRepository( 36 | realtimeDatabaseNetworkService: DefaultRealtimeDatabaseNetworkService() 37 | ) 38 | ) 39 | ) 40 | self.navigationController.pushViewController(addMateViewController, animated: true) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/MateScene/ Cooditnator/Protocol/AddMateCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddMateCoordinator.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol AddMateCoordinator: Coordinator { 11 | func pushAddMateViewController() 12 | } 13 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/MateScene/ Cooditnator/Protocol/MateCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MateCoordinator.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/15. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol MateCoordinator: Coordinator { 11 | func showAddMateFlow() 12 | func showMateProfileFlow(_ nickname: String) 13 | } 14 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/MateScene/ Cooditnator/Protocol/MateProfileCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MateProfileCoordinator.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/19. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol MateProfileCoordinator: EmojiCoordinator { 11 | func pushMateProfileViewController() 12 | func pushRecordDetailViewController(with runningResult: RunningResult) 13 | func presentEmojiModal( 14 | connectedTo usecase: ProfileUseCase, 15 | mate: String, 16 | runningID: String 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/MateScene/View/MateEmptyView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MateEmptyView.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/10. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | 12 | final class MateEmptyView: UIView { 13 | convenience init(title: String, topOffset: Int) { 14 | self.init(frame: .zero) 15 | self.configureUI(title: title, topOffset: topOffset) 16 | } 17 | } 18 | 19 | // MARK: - Private Functions 20 | 21 | private extension MateEmptyView { 22 | func configureUI(title: String, topOffset: Int) { 23 | let titleLabel = UILabel() 24 | 25 | titleLabel.font = .notoSans(size: 14, family: .medium) 26 | titleLabel.textColor = .darkGray 27 | titleLabel.text = title 28 | titleLabel.numberOfLines = 2 29 | titleLabel.textAlignment = .center 30 | 31 | self.addSubview(titleLabel) 32 | titleLabel.snp.makeConstraints { make in 33 | make.centerX.equalToSuperview() 34 | make.top.equalToSuperview().offset(topOffset) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/MateScene/View/MateHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MateHeaderView.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/09. 6 | // 7 | 8 | import UIKit 9 | 10 | final class MateHeaderView: UITableViewHeaderFooterView { 11 | static var identifier: String { 12 | return String(describing: Self.self) 13 | } 14 | 15 | private lazy var headerTitleLable: UILabel = { 16 | let label = UILabel() 17 | label.font = UIFont.notoSans(size: 18, family: .medium) 18 | return label 19 | }() 20 | 21 | override init(reuseIdentifier: String?) { 22 | super.init(reuseIdentifier: Self.identifier) 23 | self.configureUI() 24 | } 25 | 26 | required init?(coder: NSCoder) { 27 | super.init(coder: coder) 28 | self.configureUI() 29 | } 30 | 31 | func updateUI(description: String = "친구", value: Int) { 32 | self.headerTitleLable.text = "\(description) (\(value)명)" 33 | } 34 | 35 | func updateUI(nickname: String) { 36 | self.headerTitleLable.text = "\(nickname)의 달리기 기록" 37 | } 38 | } 39 | 40 | // MARK: - Private Functions 41 | 42 | private extension MateHeaderView { 43 | func configureUI() { 44 | addSubview(headerTitleLable) 45 | self.headerTitleLable.snp.makeConstraints { make in 46 | make.left.equalToSuperview().offset(20) 47 | make.centerY.equalToSuperview() 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/MateScene/ViewModel/AddMateViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddMateViewModel.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | import RxRelay 12 | 13 | final class AddMateViewModel { 14 | private let mateUseCase: MateUseCase 15 | weak var coordinator: AddMateCoordinator? 16 | typealias MateList = [(key: String, value: String)] 17 | var filteredMate: MateList = [] 18 | 19 | struct Input { 20 | let searchButtonDidTap: Observable 21 | let searchBarTextEvent: Observable 22 | } 23 | 24 | struct Output { 25 | let loadData = PublishRelay() 26 | } 27 | 28 | init(coordinator: AddMateCoordinator?, mateUseCase: MateUseCase) { 29 | self.coordinator = coordinator 30 | self.mateUseCase = mateUseCase 31 | } 32 | 33 | func transform(from input: Input, disposeBag: DisposeBag) -> Output { 34 | let output = Output() 35 | var text = "" 36 | 37 | input.searchButtonDidTap 38 | .subscribe(onNext: { [weak self] in 39 | self?.mateUseCase.fetchSearchedUser(with: text) 40 | }) 41 | .disposed(by: disposeBag) 42 | 43 | input.searchBarTextEvent 44 | .subscribe(onNext: { searchText in 45 | text = searchText 46 | }) 47 | .disposed(by: disposeBag) 48 | 49 | self.mateUseCase.mateList 50 | .subscribe(onNext: { [weak self] mate in 51 | self?.filteredMate = mate 52 | output.loadData.accept(true) 53 | }) 54 | .disposed(by: disposeBag) 55 | 56 | return output 57 | } 58 | 59 | func requestMate(to mate: String) { 60 | self.mateUseCase.sendRequestMate(to: mate) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/MyPageScene/Coordinator/Protocol/MyPageCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MyPageCoordinator.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/23. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol MyPageCoordinator: Coordinator { 11 | func pushNotificationViewController() 12 | func pushProfileEditViewController(with nickname: String) 13 | func pushLicenseViewController() 14 | func popViewController() 15 | } 16 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/MyPageScene/View/ImageEditButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageEditButton.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/25. 6 | // 7 | 8 | import UIKit 9 | 10 | final class ImageEditButton: UIView { 11 | private(set) lazy var profileImageView: UIImageView = { 12 | let imageView = UIImageView() 13 | imageView.layer.cornerRadius = 40 14 | imageView.clipsToBounds = true 15 | 16 | imageView.snp.makeConstraints { make in 17 | make.width.height.equalTo(80) 18 | } 19 | return imageView 20 | }() 21 | 22 | private lazy var cameraIconView: UIView = { 23 | let view = UIView() 24 | view.backgroundColor = .white 25 | view.layer.cornerRadius = 12 26 | view.snp.makeConstraints { make in 27 | make.width.height.equalTo(24) 28 | } 29 | 30 | let imageView = UIImageView() 31 | imageView.image = UIImage(systemName: "camera.fill") 32 | imageView.tintColor = .systemGray5 33 | imageView.snp.makeConstraints { make in 34 | make.width.height.equalTo(20) 35 | } 36 | 37 | view.addSubview(imageView) 38 | imageView.snp.makeConstraints { make in 39 | make.centerX.centerY.equalToSuperview() 40 | } 41 | return view 42 | }() 43 | 44 | override init(frame: CGRect) { 45 | super.init(frame: frame) 46 | self.configureUI() 47 | } 48 | 49 | required init?(coder: NSCoder) { 50 | super.init(coder: coder) 51 | self.configureUI() 52 | } 53 | } 54 | 55 | private extension ImageEditButton { 56 | func configureUI() { 57 | self.addSubview(self.profileImageView) 58 | self.profileImageView.snp.makeConstraints { make in 59 | make.centerX.centerY.equalToSuperview() 60 | } 61 | 62 | self.addSubview(self.cameraIconView) 63 | self.cameraIconView.snp.makeConstraints { make in 64 | make.right.bottom.equalToSuperview() 65 | } 66 | 67 | self.snp.makeConstraints { make in 68 | make.width.height.equalTo(86) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/MyPageScene/ViewController/LicenseViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LicenseViewController.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/24. 6 | // 7 | 8 | import UIKit 9 | 10 | final class LicenseViewController: UIViewController { 11 | private lazy var licenseTextView: UITextView = { 12 | let textView = UITextView() 13 | let path = FilePath.license 14 | textView.text = try? String(contentsOfFile: path) 15 | textView.font = .systemFont(ofSize: 14) 16 | textView.isEditable = false 17 | textView.isSelectable = false 18 | textView.contentInsetAdjustmentBehavior = .never 19 | return textView 20 | }() 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | self.configureUI() 25 | } 26 | } 27 | 28 | private extension LicenseViewController { 29 | func configureUI() { 30 | self.view.backgroundColor = .systemBackground 31 | self.navigationItem.title = "라이센스" 32 | 33 | self.view.addSubview(self.licenseTextView) 34 | self.licenseTextView.snp.makeConstraints { make in 35 | make.top.bottom.equalTo(self.view.safeAreaLayoutGuide) 36 | make.left.right.equalToSuperview().inset(10) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/MyPageScene/ViewModel/NotificationViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationViewModel.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxRelay 11 | import RxSwift 12 | 13 | final class NotificationViewModel { 14 | private weak var notificationCoordinator: MyPageCoordinator? 15 | private let notificationUseCase: NotificationUseCase 16 | var notices: [Notice] = [] 17 | 18 | init( 19 | coordinator: MyPageCoordinator?, 20 | notificationUseCase: NotificationUseCase 21 | ) { 22 | self.notificationCoordinator = coordinator 23 | self.notificationUseCase = notificationUseCase 24 | } 25 | 26 | struct Input { 27 | let viewDidLoadEvent: Observable 28 | } 29 | 30 | struct Output { 31 | var didLoadData = PublishRelay() 32 | } 33 | 34 | func transform(from input: Input, disposeBag: DisposeBag) -> Output { 35 | let output = Output() 36 | 37 | input.viewDidLoadEvent 38 | .subscribe(onNext: { [weak self] in 39 | self?.notificationUseCase.fetchNotices() 40 | }) 41 | .disposed(by: disposeBag) 42 | 43 | self.notificationUseCase.notices 44 | .subscribe(onNext: { [weak self] notices in 45 | self?.notices = notices.reversed() 46 | output.didLoadData.accept(true) 47 | }) 48 | .disposed(by: disposeBag) 49 | 50 | return output 51 | } 52 | 53 | func updateMateState(notice: Notice, isAccepted: Bool) { 54 | self.notificationUseCase.updateMateState(notice: notice, isAccepted: true) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/RecordScene/Coordinator/Protocol/RecordCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RecordCoordinator.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol RecordCoordinator: Coordinator { 11 | func push(with runningResult: RunningResult) 12 | } 13 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/RecordScene/View/CalendarView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CalendarView.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/18. 6 | // 7 | 8 | import UIKit 9 | 10 | final class CalendarView: UICollectionView { 11 | override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { 12 | super.init(frame: frame, collectionViewLayout: UICollectionViewLayout()) 13 | self.configureUI() 14 | } 15 | 16 | required init?(coder: NSCoder) { 17 | super.init(coder: coder) 18 | self.configureUI() 19 | } 20 | } 21 | 22 | private extension CalendarView { 23 | func configureUI() { 24 | self.collectionViewLayout = self.createCompositionalLayout() 25 | self.register(CalendarCell.self, forCellWithReuseIdentifier: CalendarCell.identifier) 26 | } 27 | 28 | func createCompositionalLayout() -> UICollectionViewCompositionalLayout { 29 | let item = NSCollectionLayoutItem( 30 | layoutSize: .init( 31 | widthDimension: .fractionalWidth(1 / 7.0), 32 | heightDimension: .fractionalHeight(1) 33 | ) 34 | ) 35 | 36 | let group = NSCollectionLayoutGroup.horizontal( 37 | layoutSize: .init( 38 | widthDimension: .fractionalWidth(1), 39 | heightDimension: .fractionalHeight(1 / 6.0) 40 | ), 41 | subitems: [item] 42 | ) 43 | let section = NSCollectionLayoutSection(group: group) 44 | return UICollectionViewCompositionalLayout(section: section) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Presentation/RecordScene/View/WeekdayView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeekdayView.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/18. 6 | // 7 | 8 | import UIKit 9 | 10 | final class WeekdayView: UIStackView { 11 | override init(frame: CGRect) { 12 | super.init(frame: frame) 13 | self.configureUI() 14 | } 15 | 16 | required init(coder: NSCoder) { 17 | super.init(coder: coder) 18 | self.configureUI() 19 | } 20 | } 21 | 22 | private extension WeekdayView { 23 | func configureUI() { 24 | self.axis = .horizontal 25 | self.distribution = .fillEqually 26 | 27 | let week = ["일", "월", "화", "수", "목", "금", "토"] 28 | week.forEach { [weak self] weekday in 29 | let label = UILabel() 30 | label.font = .notoSans(size: 10, family: .regular) 31 | label.textColor = .darkGray 32 | label.text = weekday 33 | label.textAlignment = .center 34 | self?.addArrangedSubview(label) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/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 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | {"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"}]} -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/bell.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "bell.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "bell@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "bell@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/bell.imageset/bell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/bell.imageset/bell.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/bell.imageset/bell@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/bell.imageset/bell@2x.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/bell.imageset/bell@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/bell.imageset/bell@3x.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/home.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "home.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "home@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "home@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/home.imageset/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/home.imageset/home.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/home.imageset/home@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/home.imageset/home@2x.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/home.imageset/home@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/home.imageset/home@3x.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/launchScreen.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "launchScreen.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "launchScreen-1.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "launchScreen-2.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/launchScreen.imageset/launchScreen-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/launchScreen.imageset/launchScreen-1.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/launchScreen.imageset/launchScreen-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/launchScreen.imageset/launchScreen-2.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/launchScreen.imageset/launchScreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/launchScreen.imageset/launchScreen.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/mate.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "mate.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "mate@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "mate@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/mate.imageset/mate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/mate.imageset/mate.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/mate.imageset/mate@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/mate.imageset/mate@2x.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/mate.imageset/mate@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/mate.imageset/mate@3x.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/mypage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "mypage.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "mypage@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "mypage@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/mypage.imageset/mypage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/mypage.imageset/mypage.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/mypage.imageset/mypage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/mypage.imageset/mypage@2x.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/mypage.imageset/mypage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/mypage.imageset/mypage@3x.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/person-add.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "person-add.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "person-add@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "person-add@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/person-add.imageset/person-add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/person-add.imageset/person-add.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/person-add.imageset/person-add@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/person-add.imageset/person-add@2x.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/person-add.imageset/person-add@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/person-add.imageset/person-add@3x.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/record.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "record.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "record@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "record@3x.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/record.imageset/record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/record.imageset/record.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/record.imageset/record@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/record.imageset/record@2x.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Assets.xcassets/record.imageset/record@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Assets.xcassets/record.imageset/record@3x.png -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Font/NotoSans-boldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Font/NotoSans-boldItalic.ttf -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Font/NotoSansKR-black.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Font/NotoSansKR-black.otf -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Font/NotoSansKR-bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Font/NotoSansKR-bold.otf -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Font/NotoSansKR-light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Font/NotoSansKR-light.otf -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Font/NotoSansKR-medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Font/NotoSansKR-medium.otf -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Font/NotoSansKR-regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Font/NotoSansKR-regular.otf -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Font/NotoSansKR-thin.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Font/NotoSansKR-thin.otf -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Asset/Font/RacingSansOne.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcampwm-2021/iOS06-MateRunner/07a03c931733cd99a136c4b22c1e93d28120d504/MateRunner/MateRunner/Resource/Asset/Font/RacingSansOne.ttf -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/CoreData/MateRunner.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/CoreData/MateRunner.xcdatamodeld/MateRunner.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 147491151696-p2c48sdunpoudfa8p9prqa0a87e9ebnp.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.147491151696-p2c48sdunpoudfa8p9prqa0a87e9ebnp 9 | API_KEY 10 | AIzaSyDqphGzUo7MEFacMcVTr-ALMgv8G9H6Sk4 11 | GCM_SENDER_ID 12 | 147491151696 13 | PLIST_VERSION 14 | 1 15 | BUNDLE_ID 16 | kr.codesquad.MateRunner 17 | PROJECT_ID 18 | mate-runner-e232c 19 | STORAGE_BUCKET 20 | mate-runner-e232c.appspot.com 21 | IS_ADS_ENABLED 22 | 23 | IS_ANALYTICS_ENABLED 24 | 25 | IS_APPINVITE_ENABLED 26 | 27 | IS_GCM_ENABLED 28 | 29 | IS_SIGNIN_ENABLED 30 | 31 | GOOGLE_APP_ID 32 | 1:147491151696:ios:e7bf39a6db42985ed29655 33 | DATABASE_URL 34 | https://mate-runner-e232c-default-rtdb.asia-southeast1.firebasedatabase.app 35 | 36 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FCM_SERVER_KEY 6 | $(FCM_SERVER_KEY) 7 | NSAppTransportSecurity 8 | 9 | NSAllowsArbitraryLoads 10 | 11 | 12 | UIAppFonts 13 | 14 | RacingSansOne.ttf 15 | NotoSans-boldItalic.ttf 16 | NotoSansKR-black.otf 17 | NotoSansKR-bold.otf 18 | NotoSansKR-medium.otf 19 | NotoSansKR-regular.otf 20 | NotoSansKR-light.otf 21 | NotoSansKR-thin.otf 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | UISceneStoryboardFile 37 | Main 38 | 39 | 40 | 41 | 42 | UIBackgroundModes 43 | 44 | remote-notification 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Resource/Storyboard/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Constant/CacheSizeConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CacheSizeConstants.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2022/01/23. 6 | // 7 | 8 | import Foundation 9 | 10 | enum CacheConstants { 11 | static let maximumMemoryCacheSize = 52428800 12 | static let maximumDiskCacheSize = 52428800 13 | } 14 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Constant/Configuration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Configuration.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/21. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Configuration { 11 | static let fcmServerKey: String = Bundle.main.infoDictionary?["FCM_SERVER_KEY"] as? String ?? "" 12 | } 13 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Constant/CoordinatorType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoordinatorType.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/09. 6 | // 7 | 8 | import Foundation 9 | 10 | enum CoordinatorType { 11 | case app, login, tab 12 | case home, record, mate, mypage 13 | case setting, running 14 | case signUp, addMate 15 | } 16 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Constant/Emoji.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Emoji.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/03. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Emoji: String, Codable, CaseIterable, Equatable { 11 | case tear = "🥲" 12 | case running = "🏃‍♀️" 13 | case ribbonHeart = "💝" 14 | case clap = "👏" 15 | case fire = "🔥" 16 | case burningHeart = "❤️‍🔥" 17 | case thumbsUp = "👍" 18 | case strong = "💪" 19 | case lovely = "🥰" 20 | case okay = "🙆‍♂️" 21 | case twoHandsUp = "🙌" 22 | case flower = "🌷" 23 | 24 | func text() -> String { 25 | return self.rawValue 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Constant/FilePath.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FilePath.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/26. 6 | // 7 | 8 | import Foundation 9 | 10 | enum FilePath { 11 | static let license = Bundle.main.path(forResource: "license", ofType: "txt") ?? "" 12 | static let termsOfService = Bundle.main.path(forResource: "termsOfService", ofType: "txt") ?? "" 13 | static let termsOfPrivacy = Bundle.main.path(forResource: "termsOfPrivacy", ofType: "txt") ?? "" 14 | static let termsOfLocationService = Bundle.main.path(forResource: "termsOfLocationService", ofType: "txt") ?? "" 15 | } 16 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Constant/FireStoreConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FireStoreConstants.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/12/02. 6 | // 7 | 8 | import Foundation 9 | 10 | enum FirestoreConfiguration { 11 | static let firestoreBaseURL = "https://firestore.googleapis.com/v1/" 12 | static let baseURL = "https://firestore.googleapis.com/v1/projects/mate-runner-e232c" 13 | static let documentsPath = "/databases/(default)/documents" 14 | static let queryKey = ":runQuery" 15 | static let commitKey = ":commit" 16 | static let defaultHeaders = ["Content-Type": "application/json", "Accept": "application/json"] 17 | } 18 | 19 | enum FirestoreFieldParameter { 20 | static let updateMask = "updateMask.fieldPaths=" 21 | static let readMask = "mask.fieldPaths=" 22 | } 23 | 24 | enum FirestoreCollectionPath { 25 | static let runningResultPath = "/RunningResult" 26 | static let userPath = "/User" 27 | static let recordsPath = "/records" 28 | static let emojiPath = "/emojis" 29 | static let notificationPath = "/Notification" 30 | static let uidPath = "/UID" 31 | } 32 | 33 | enum FirestoreField { 34 | static let fields = "fields" 35 | static let emoji = "emoji" 36 | static let userNickname = "userNickname" 37 | static let nickname = "nickname" 38 | static let distance = "distance" 39 | static let time = "time" 40 | static let height = "height" 41 | static let weight = "weight" 42 | static let image = "image" 43 | static let calorie = "calorie" 44 | static let mate = "mate" 45 | } 46 | 47 | enum FirebaseStorageConfiguration { 48 | static let baseURL = "https://firebasestorage.googleapis.com/v0/b" 49 | static let projectNamePath = "/mate-runner-e232c.appspot.com/o" 50 | static let profileImageName = "profile.png" 51 | static let downloadTokens = "downloadTokens" 52 | static let altMediaParameter = "alt=media" 53 | static let tokenParameter = "token=" 54 | static let mediaContentType = ["Content-Type": "image/png"] 55 | } 56 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Constant/FirebaseCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FirebaseCollection.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/18. 6 | // 7 | 8 | import Foundation 9 | 10 | enum FirebaseCollection { 11 | static let runningResult = "RunningResult" 12 | static let user = "User" 13 | static let uid = "UID" 14 | } 15 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Constant/Height.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Height.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Height { 11 | static let range = [Int](100...250) 12 | static let minimum = 100 13 | 14 | static func toRow(from height: Double) -> Int { 15 | return Int(height) - Self.minimum 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Constant/LocationAuthorizationStatus.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationAuthorizationStatus.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | enum LocationAuthorizationStatus { 11 | case allowed, disallowed, notDetermined 12 | } 13 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Constant/Mets.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mets.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/07. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Mets: Double { 11 | case stationary = 0.0 12 | case walking = 3.8 13 | case running = 10.0 14 | 15 | func value() -> Double { 16 | return self.rawValue 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Constant/NoticeMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NoticeMode.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | enum NoticeMode: String { 11 | case invite = "invititation" 12 | case requestMate = "requestMate" 13 | case receiveEmoji = "receiveEmoji" 14 | 15 | func text() -> String { 16 | return self.rawValue 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Constant/NotificationCenterKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotificationCenterKey.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/28. 6 | // 7 | 8 | import Foundation 9 | 10 | enum NotificationCenterKey { 11 | static let invitationDidReceive = Notification.Name("invitationDidReceive") 12 | static let invitation = "invitation" 13 | static let sessionID = "sessionId" 14 | static let host = "host" 15 | static let mate = "mate" 16 | static let inviteTime = "inviteTime" 17 | static let mode = "mode" 18 | static let targetDistance = "targetDistance" 19 | static let sender = "sender" 20 | } 21 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Constant/RealtimeDatabaseKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealtimeDatabaseKey.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/30. 6 | // 7 | 8 | import Foundation 9 | 10 | enum RealtimeDatabaseKey { 11 | static let state = "state" 12 | static let isRunning = "isRunning" 13 | static let fcmToken = "fcmToken" 14 | static let session = "session" 15 | static let elapsedDistance = "elapsedDistance" 16 | static let elapsedTime = "elapsedTime" 17 | static let isCancelled = "isCancelled" 18 | static let dateTime = "dateTime" 19 | static let isAccepted = "isAccepted" 20 | static let isReceived = "isReceived" 21 | static let mode = "mode" 22 | static let targetDistance = "targetDistance" 23 | } 24 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Constant/RunningMode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningMode.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/03. 6 | // 7 | 8 | import Foundation 9 | 10 | enum RunningMode: String, Codable, Equatable { 11 | case single, race, team 12 | 13 | var title: String { 14 | switch self { 15 | case .race: 16 | return "경쟁 모드" 17 | case .team: 18 | return "협동 모드" 19 | case .single: 20 | return "혼자 달리기" 21 | } 22 | } 23 | 24 | var description: String { 25 | switch self { 26 | case .race: 27 | return "정해진 거리를 누가 더 빨리 달리는지 메이트와 대결해보세요!" 28 | case .team: 29 | return "정해진 거리를 메이트와 함께 달려서 달성해보세요!" 30 | case .single: 31 | return "혼자서 달려보세요!" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Constant/SignUpValidationState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpValidationState.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/30. 6 | // 7 | 8 | import Foundation 9 | 10 | enum SignUpValidationState: Equatable { 11 | case empty 12 | case lowerboundViolated 13 | case upperboundViolated 14 | case invalidLetterIncluded 15 | case success 16 | 17 | var description: String { 18 | switch self { 19 | case .empty, .success: 20 | return "" 21 | case .lowerboundViolated: 22 | return "최소 5자 이상 입력해주세요" 23 | case .upperboundViolated: 24 | return "최대 20자까지만 가능해요" 25 | case .invalidLetterIncluded: 26 | return "영문과 숫자만 입력할 수 있어요" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Constant/TabBarPage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBarPage.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/09. 6 | // 7 | 8 | import Foundation 9 | 10 | enum TabBarPage: String, CaseIterable { 11 | case home, record, mate, mypage 12 | 13 | init?(index: Int) { 14 | switch index { 15 | case 0: self = .home 16 | case 1: self = .record 17 | case 2: self = .mate 18 | case 3: self = .mypage 19 | default: return nil 20 | } 21 | } 22 | 23 | func pageOrderNumber() -> Int { 24 | switch self { 25 | case .home: return 0 26 | case .record: return 1 27 | case .mate: return 2 28 | case .mypage: return 3 29 | } 30 | } 31 | 32 | func tabIconName() -> String { 33 | return self.rawValue 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Constant/User.swift: -------------------------------------------------------------------------------- 1 | // 2 | // User.swift 3 | // MateRunner 4 | // 5 | // Created by 김민지 on 2021/11/11. 6 | // 7 | 8 | import Foundation 9 | 10 | enum User: String { 11 | case host = "honux" 12 | case mate = "jk" 13 | } 14 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Constant/UserDefaultKey.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultKey.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/17. 6 | // 7 | 8 | import Foundation 9 | 10 | enum UserDefaultKey { 11 | static let nickname = "nickname" 12 | static let isLoggedIn = "isLoggedIn" 13 | static let fcmToken = "fcmToken" 14 | } 15 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Constant/Weight.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Weight.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/29. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Weight { 11 | static let range = [Int](20...300) 12 | static let minimum = 20 13 | 14 | static func toRow(from weight: Double) -> Int { 15 | return Int(weight) - Self.minimum 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Error/FirebaseServiceError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FirebaseNetworkError.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/18. 6 | // 7 | 8 | import Foundation 9 | 10 | enum FirebaseServiceError: Error { 11 | case nilDataError, userNicknameNotExistsError, typeMismatchError, invalidURLError 12 | } 13 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Error/ImageCacheError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageCacheError.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/24. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ImageCacheError: Error { 11 | case nilPathError 12 | case nilImageError 13 | case invalidURLError 14 | case imageNotModifiedError 15 | case networkUsageExceedError 16 | case unknownNetworkError 17 | } 18 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Error/SignUpValidationError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignUpValidationError.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/30. 6 | // 7 | 8 | import Foundation 9 | 10 | enum SignUpValidationError: Error { 11 | case nicknameDuplicatedError 12 | case requiredDataMissingError 13 | 14 | var description: String { 15 | switch self { 16 | case .nicknameDuplicatedError: 17 | return "이미 존재하는 닉네임입니다" 18 | case .requiredDataMissingError: 19 | return "알 수 없는 에러" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Extension/Double+Formatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double+Formatter.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/10. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Double { 11 | func string() -> String { 12 | return "\(floor(self * 100) / 100)" 13 | } 14 | 15 | var kilometerString: String { 16 | return "\(floor(self * 100) / 100)" 17 | } 18 | 19 | var kilometer: Double { 20 | return self / 1000 21 | } 22 | 23 | var meter: Double { 24 | return self * 1000 25 | } 26 | 27 | var totalDistanceString: String { 28 | if self >= 100 { 29 | return String(format: "%.0f", self) 30 | } else { 31 | return String(format: "%.2f", self) 32 | } 33 | } 34 | 35 | var calorieString: String { 36 | return String(format: "%.0f", self) 37 | } 38 | 39 | var percentageString: String { 40 | return String(format: "%.0f", self * 100) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Extension/Int+Formatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Int+Formatter.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/18. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Int { 11 | var timeString: String { 12 | let hour = self / 3600 13 | let minute = (self % 3600) / 60 14 | let second = self % 60 15 | 16 | if hour >= 100 { return "\(hour)" } 17 | return (hour < 1 ? "" : String(format: "%02d:", hour)) + String(format: "%02d:%02d", minute, second) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Extension/Point+CLLocationCoordinate2D.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Point+CLLocation2D.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/17. 6 | // 7 | 8 | import CoreLocation 9 | import Foundation 10 | 11 | extension Point { 12 | func convertToCLLocationCoordinate2D() -> CLLocationCoordinate2D { 13 | return CLLocationCoordinate2D( 14 | latitude: self.latitude, 15 | longitude: self.longitude 16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Extension/String+UIImageConverter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+UIImageConverter.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/30. 6 | // 7 | 8 | import UIKit 9 | 10 | extension String { 11 | func emojiToImage() -> Data? { 12 | let size = CGSize(width: 60, height: 65) 13 | UIGraphicsBeginImageContextWithOptions(size, false, 0) 14 | UIColor.clear.set() 15 | let rect = CGRect(origin: CGPoint(), size: size) 16 | UIRectFill(CGRect(origin: CGPoint(), size: size)) 17 | (self as NSString).draw(in: rect, withAttributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 60)]) 18 | let image = UIGraphicsGetImageFromCurrentImageContext() 19 | UIGraphicsEndImageContext() 20 | return image?.pngData() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Extension/UIColor+CustomColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+CustomColor.swift 3 | // MateRunner 4 | // 5 | // Created by 이정원 on 2021/11/01. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIColor { 11 | static var mrPurple: UIColor { 12 | return UIColor(red: 122.0 / 255.0, green: 126.0 / 255.0, blue: 247.0 / 255.0, alpha: 1) 13 | } 14 | 15 | static var mrYellow: UIColor { 16 | return UIColor(red: 255.0 / 255.0, green: 233.0 / 255.0, blue: 129.0 / 255.0, alpha: 1) 17 | } 18 | 19 | static var mrGray: UIColor { 20 | return UIColor(red: 200.0 / 255.0, green: 200.0 / 255.0, blue: 200.0 / 255.0, alpha: 1) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Extension/UIFont+CustomFont.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIFont+CustomFont.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/01. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIFont { 11 | enum Family: String { 12 | case black, bold, light, medium, regular, thin 13 | } 14 | 15 | static func notoSans(size: CGFloat = 10, family: Family = .regular) -> UIFont { 16 | return UIFont(name: "NotoSansKR-\(family)", size: size) ?? UIFont.systemFont(ofSize: size) 17 | } 18 | 19 | static func notoSansBoldItalic(size: CGFloat = 10) -> UIFont { 20 | return UIFont(name: "NotoSans-boldItalic", size: size) ?? UIFont.systemFont(ofSize: size) 21 | } 22 | 23 | static func racingSansOne(size: CGFloat) -> UIFont { 24 | return UIFont(name: "RacingSansOne-Regular", size: size) ?? UIFont.systemFont(ofSize: size) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Extension/UIImageView+ImageCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageView+ImageCache.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/24. 6 | // 7 | 8 | import UIKit 9 | 10 | import RxSwift 11 | 12 | extension UIImageView { 13 | func setImage(with url: String, disposeBag: DisposeBag) { 14 | DefaultImageCacheService.shared.setImage(url) 15 | .observe(on: MainScheduler.instance) 16 | .subscribe(onNext: { [weak self] image in 17 | self?.image = UIImage(data: image) 18 | }) 19 | .disposed(by: disposeBag) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Extension/UIScrollView+ObserveScroll.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIScrollView+ObserveScroll.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/29. 6 | // 7 | 8 | import UIKit 9 | 10 | import RxCocoa 11 | import RxSwift 12 | 13 | public extension Reactive where Base: UIScrollView { 14 | func scrollToBottom() -> ControlEvent { 15 | return ControlEvent( events: contentOffset.map { offset in 16 | let offsetY = offset.y 17 | let contentHeight = self.base.contentSize.height 18 | let height = self.base.frame.height 19 | 20 | return offsetY > (contentHeight - height + 150) 21 | } 22 | .distinctUntilChanged() 23 | .filter { $0 } 24 | .map { _ in () } 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Extension/UIView+ShadowEffect.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+ShadowEffect.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/02. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIView { 11 | enum Shadow { 12 | case bottom, top, left, right 13 | } 14 | 15 | func addShadow(location: Shadow, color: UIColor = .systemGray4, opacity: Float = 0.8, radius: CGFloat = 5.0) { 16 | switch location { 17 | case .bottom: 18 | addShadow(offset: CGSize(width: 0, height: 10), color: color, opacity: opacity, radius: radius) 19 | case .top: 20 | addShadow(offset: CGSize(width: 0, height: -10), color: color, opacity: opacity, radius: radius) 21 | case .left: 22 | addShadow(offset: CGSize(width: -10, height: 0), color: color, opacity: opacity, radius: radius) 23 | case .right: 24 | addShadow(offset: CGSize(width: 10, height: 0), color: color, opacity: opacity, radius: radius) 25 | } 26 | } 27 | 28 | func addShadow(offset: CGSize, color: UIColor = .black, opacity: Float = 0.1, radius: CGFloat = 3.0) { 29 | self.layer.masksToBounds = false 30 | self.layer.shadowColor = color.cgColor 31 | self.layer.shadowOffset = offset 32 | self.layer.shadowOpacity = opacity 33 | self.layer.shadowRadius = radius 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/PropertyWrapper/BehaviorRelayProperty.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BehaviorRelayProperty.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/01. 6 | // 7 | import Foundation 8 | 9 | import RxCocoa 10 | import RxSwift 11 | 12 | @propertyWrapper 13 | public struct BehaviorRelayProperty { 14 | private var subject: BehaviorRelay 15 | public var wrappedValue: Value { 16 | get { subject.value } 17 | set { subject.accept(newValue) } 18 | } 19 | 20 | public var projectedValue: BehaviorRelay { 21 | return self.subject 22 | } 23 | 24 | public init(wrappedValue: Value) { 25 | subject = BehaviorRelay(value: wrappedValue) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Service/DefaultCoreMotionService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultCoreMotionService.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/16. 6 | // 7 | 8 | import CoreMotion 9 | import Foundation 10 | 11 | import RxCocoa 12 | import RxSwift 13 | 14 | final class DefaultCoreMotionService: CoreMotionService { 15 | private let pedometer = CMPedometer() 16 | private let activityManager = CMMotionActivityManager() 17 | 18 | func startPedometer() -> Observable { 19 | return BehaviorRelay.create { [weak self] observe in 20 | self?.pedometer.startUpdates(from: Date()) { pedometerData, error in 21 | guard let pedometerData = pedometerData, error == nil else { return } 22 | if let distance = pedometerData.distance { 23 | observe.onNext(distance.doubleValue) 24 | } 25 | } 26 | return Disposables.create() 27 | } 28 | } 29 | 30 | func startActivity() -> Observable { 31 | return BehaviorRelay.create { [weak self] observe in 32 | self?.activityManager.startActivityUpdates(to: .current ?? .main) { activity in 33 | guard let activity = activity else { return } 34 | if activity.stationary { 35 | observe.onNext(Mets.stationary.value()) 36 | } 37 | if activity.walking { 38 | observe.onNext(Mets.walking.value()) 39 | } 40 | if activity.running { 41 | observe.onNext(Mets.running.value()) 42 | } 43 | } 44 | return Disposables.create() 45 | } 46 | } 47 | 48 | func stopPedometer() { 49 | self.pedometer.stopUpdates() 50 | } 51 | 52 | func stopAcitivity() { 53 | self.activityManager.stopActivityUpdates() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Service/DefaultLocationService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultLocationService.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/13. 6 | // 7 | 8 | import CoreLocation 9 | import Foundation 10 | 11 | import RxRelay 12 | import RxSwift 13 | 14 | final class DefaultLocationService: NSObject, LocationService { 15 | var locationManager: CLLocationManager? 16 | var disposeBag: DisposeBag = DisposeBag() 17 | var authorizationStatus = BehaviorRelay(value: .notDetermined) 18 | 19 | override init() { 20 | super.init() 21 | self.locationManager = CLLocationManager() 22 | self.locationManager?.distanceFilter = CLLocationDistance(3) 23 | self.locationManager?.delegate = self 24 | self.locationManager?.desiredAccuracy = kCLLocationAccuracyBest 25 | } 26 | 27 | func start() { 28 | self.locationManager?.startUpdatingLocation() 29 | } 30 | 31 | func stop() { 32 | self.locationManager?.stopUpdatingLocation() 33 | } 34 | 35 | func requestAuthorization() { 36 | self.locationManager?.requestWhenInUseAuthorization() 37 | } 38 | 39 | func observeUpdatedAuthorization() -> Observable { 40 | return self.authorizationStatus.asObservable() 41 | } 42 | 43 | func observeUpdatedLocation() -> Observable<[CLLocation]> { 44 | return PublishRelay<[CLLocation]>.create({ emitter in 45 | self.rx.methodInvoked(#selector(CLLocationManagerDelegate.locationManager(_:didUpdateLocations:))) 46 | .compactMap({ $0.last as? [CLLocation] }) 47 | .subscribe(onNext: { location in 48 | emitter.onNext(location) 49 | }) 50 | .disposed(by: self.disposeBag) 51 | return Disposables.create() 52 | }) 53 | } 54 | } 55 | 56 | extension DefaultLocationService: CLLocationManagerDelegate { 57 | func locationManager( 58 | _ manager: CLLocationManager, 59 | didUpdateLocations locations: [CLLocation]) {} 60 | 61 | func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { 62 | self.authorizationStatus.accept(status) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Service/DefaultRxTimerService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultRxTimerService.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/16. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class DefaultRxTimerService: RxTimerService { 13 | var disposeBag = DisposeBag() 14 | 15 | func start() -> Observable { 16 | return Observable 17 | .interval( 18 | RxTimeInterval.seconds(1), 19 | scheduler: MainScheduler.instance 20 | ) 21 | .map { $0 + 1 } 22 | } 23 | 24 | func stop() { 25 | self.disposeBag = DisposeBag() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Service/Protocol/CoreMotionService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreMotionService.swift 3 | // MateRunner 4 | // 5 | // Created by 이유진 on 2021/11/06. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol CoreMotionService { 13 | func startPedometer() -> Observable 14 | func startActivity() -> Observable 15 | func stopPedometer() 16 | func stopAcitivity() 17 | } 18 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Service/Protocol/LocationService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationService.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/12. 6 | // 7 | 8 | import CoreLocation 9 | import Foundation 10 | 11 | import RxRelay 12 | import RxSwift 13 | 14 | protocol LocationService { 15 | var authorizationStatus: BehaviorRelay { get set } 16 | func start() 17 | func stop() 18 | func requestAuthorization() 19 | func observeUpdatedAuthorization() -> Observable 20 | func observeUpdatedLocation() -> Observable<[CLLocation]> 21 | } 22 | -------------------------------------------------------------------------------- /MateRunner/MateRunner/Util/Service/Protocol/RxTimerService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RxTimerService.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/16. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | protocol RxTimerService { 13 | var disposeBag: DisposeBag { get set } 14 | func start() -> Observable 15 | func stop() 16 | } 17 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerUseCaseTests/Common/Mock/MockEmojiDidSelectDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockEmojiDidSelectDelegate.swift 3 | // MateRunnerUseCaseTests 4 | // 5 | // Created by 이정원 on 2021/12/02. 6 | // 7 | 8 | import Foundation 9 | 10 | final class MockEmojiDidSelectDelegate: EmojiDidSelectDelegate { 11 | func emojiDidSelect(selectedEmoji: Emoji) { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerUseCaseTests/Common/Mock/MockUserRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockUserRepository.swift 3 | // MateRunnerUseCaseTests 4 | // 5 | // Created by 이유진 on 2021/11/30. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class MockUserRepository: UserRepository { 13 | func fetchFCMToken() -> String? { 14 | return "Sfewe198scDCLsd" 15 | } 16 | 17 | func fetchFCMTokenFromServer(of nickname: String) -> Observable { 18 | if nickname == "signUpSuccess" { 19 | return Observable.error(FirebaseServiceError.nilDataError) 20 | } 21 | return Observable.just("Sfewe198scDCLsd") 22 | } 23 | 24 | func saveFCMToken(_ fcmToken: String, of nickname: String) -> Observable { 25 | return Observable.just(()) 26 | } 27 | 28 | func fetchUserNickname() -> String? { 29 | return "materunner" 30 | } 31 | 32 | func deleteFCMToken() {} 33 | func saveLoginInfo(nickname: String) {} 34 | func saveLogoutInfo() {} 35 | func deleteUserInfo() {} 36 | } 37 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerUseCaseTests/HomeScene/InvitationUseCaseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InvitationUseCaseTests.swift 3 | // MateRunnerUseCaseTests 4 | // 5 | // Created by 김민지 on 2021/11/30. 6 | // 7 | 8 | import XCTest 9 | 10 | import RxSwift 11 | import RxTest 12 | 13 | final class InvitationUseCaseTests: XCTestCase { 14 | private var invitation: Invitation! 15 | private var invitationUseCase: InvitationUseCase! 16 | private var invitationRepository: InvitationRepository! 17 | private let disposeBag = DisposeBag() 18 | 19 | override func setUp() { 20 | super.setUp() 21 | self.invitation = Invitation( 22 | sessionId: "session", 23 | host: "yujin", 24 | inviteTime: "20211129183654", 25 | mode: .team, 26 | targetDistance: 1.5, 27 | mate: "minji" 28 | ) 29 | self.invitationRepository = MockInvitationRepository() 30 | self.invitationUseCase = DefaultInvitationUseCase( 31 | invitation: self.invitation, 32 | invitationRepository: self.invitationRepository 33 | ) 34 | } 35 | 36 | func test_checkIsCancelled() { 37 | self.invitationUseCase.checkIsCancelled() 38 | .subscribe(onNext: { isCancelled in 39 | XCTAssert(isCancelled == true) 40 | }) 41 | .disposed(by: self.disposeBag) 42 | } 43 | 44 | func test_acceptInvitation() { 45 | self.invitationUseCase.acceptInvitation() 46 | .subscribe( 47 | onNext: { _ in 48 | XCTAssert(true) 49 | }, 50 | onError: { _ in 51 | XCTAssert(false) 52 | } 53 | ) 54 | .disposed(by: self.disposeBag) 55 | } 56 | 57 | func test_rejectInvitation() { 58 | self.invitationUseCase.rejectInvitation() 59 | .subscribe( 60 | onNext: { _ in 61 | XCTAssert(true) 62 | }, 63 | onError: { _ in 64 | XCTAssert(false) 65 | } 66 | ) 67 | .disposed(by: self.disposeBag) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerUseCaseTests/HomeScene/MapUseCaseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapUseCaseTests.swift 3 | // MateRunnerUseCaseTests 4 | // 5 | // Created by 전여훈 on 2021/12/02. 6 | // 7 | 8 | import XCTest 9 | 10 | import RxSwift 11 | import RxTest 12 | 13 | final class MapUseCaseTests: XCTestCase { 14 | private var useCase: MapUseCase! 15 | private var disposeBag: DisposeBag! 16 | private var scheduler: TestScheduler! 17 | 18 | override func setUpWithError() throws { 19 | self.useCase = DefaultMapUseCase( 20 | locationService: MockLocationService(), 21 | delegate: nil 22 | ) 23 | self.scheduler = TestScheduler(initialClock: 0) 24 | self.disposeBag = DisposeBag() 25 | self.useCase.executeLocationTracker() 26 | } 27 | 28 | override func tearDownWithError() throws { 29 | self.useCase.terminateLocationTracker() 30 | self.useCase = nil 31 | self.disposeBag = nil 32 | } 33 | 34 | func test_request_location() { 35 | let testableObserver = self.scheduler.createObserver(Point.self) 36 | self.scheduler.createHotObservable([ 37 | .next(10, ()) 38 | ]) 39 | .subscribe(onNext: { self.useCase.requestLocation() }) 40 | .disposed(by: self.disposeBag) 41 | 42 | self.useCase.updatedLocation 43 | .map({ Point(latitude: $0.coordinate.latitude, longitude: $0.coordinate.longitude) }) 44 | .subscribe(testableObserver) 45 | .disposed(by: self.disposeBag) 46 | 47 | self.scheduler.start() 48 | XCTAssertEqual(testableObserver.events, [ 49 | .next(10, Point(latitude: 1, longitude: 1)) 50 | ]) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerUseCaseTests/HomeScene/Mock/MockCoreMotionService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockCoreMotionService.swift 3 | // MateRunnerUseCaseTests 4 | // 5 | // Created by 전여훈 on 2021/12/02. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class MockCoreMotionService: CoreMotionService { 13 | func startPedometer() -> Observable { 14 | return Observable.just(1) 15 | } 16 | 17 | func startActivity() -> Observable { 18 | return Observable.just(1) 19 | } 20 | 21 | func stopPedometer() {} 22 | 23 | func stopAcitivity() {} 24 | } 25 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerUseCaseTests/HomeScene/Mock/MockLocationService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockLocationService.swift 3 | // MateRunnerViewModelTests 4 | // 5 | // Created by 전여훈 on 2021/12/01. 6 | // 7 | 8 | import CoreLocation 9 | import Foundation 10 | 11 | import RxRelay 12 | import RxSwift 13 | 14 | final class MockLocationService: LocationService { 15 | var authorizationStatus = BehaviorRelay(value: .notDetermined) 16 | 17 | func start() {} 18 | 19 | func stop() {} 20 | 21 | func requestAuthorization() {} 22 | 23 | func observeUpdatedAuthorization() -> Observable { 24 | return self.authorizationStatus.asObservable() 25 | } 26 | 27 | func observeUpdatedLocation() -> Observable<[CLLocation]> { 28 | return Observable.just([CLLocation(latitude: 1, longitude: 1)]) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerUseCaseTests/HomeScene/Mock/MockRunningRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockRunningRepository.swift 3 | // MateRunnerUseCaseTests 4 | // 5 | // Created by 전여훈 on 2021/12/02. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class MockRunningRepository: RunningRepository { 13 | func listen(sessionId: String, mate: String) -> Observable { 14 | return Observable.of(RunningRealTimeData(elapsedDistance: 1, elapsedTime: 1)) 15 | } 16 | 17 | func listenIsCancelled(of sessionId: String) -> Observable { 18 | return sessionId == "canceled" ? Observable.of(true) : Observable.of(false) 19 | } 20 | 21 | func saveRunningRealTimeData(_ domain: RunningRealTimeData, sessionId: String, user: String) -> Observable { 22 | return Observable.just(()) 23 | } 24 | 25 | func cancelSession(of runningSetting: RunningSetting) -> Observable { 26 | return Observable.just(()) 27 | } 28 | 29 | func stopListen(sessionId: String, mate: String) {} 30 | 31 | func saveRunningStatus(of user: String, isRunning: Bool) -> Observable { 32 | return Observable.just(()) 33 | } 34 | 35 | func fetchRunningStatus(of mate: String) -> Observable { 36 | return mate == "fail" ? Observable.of(true) : Observable.of(false) 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerUseCaseTests/HomeScene/Mock/MockTimerService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockTimerService.swift 3 | // MateRunnerUseCaseTests 4 | // 5 | // Created by 전여훈 on 2021/12/02. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | class MockTimerService: RxTimerService { 13 | var disposeBag: DisposeBag = DisposeBag() 14 | 15 | func start() -> Observable { 16 | return Observable.from([1, 2, 3, 4, 5]) 17 | } 18 | 19 | func stop() { 20 | self.disposeBag = DisposeBag() 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerUseCaseTests/HomeScene/MockInvitationRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockInvitationRepository.swift 3 | // MateRunnerUseCaseTests 4 | // 5 | // Created by 김민지 on 2021/11/30. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class MockInvitationRepository: InvitationRepository { 13 | func fetchCancellationStatus(of invitation: Invitation) -> Observable { 14 | return Observable.just(true) 15 | } 16 | 17 | func saveInvitationResponse(accept: Bool, invitation: Invitation) -> Observable { 18 | return Observable.just(()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerUseCaseTests/HomeScene/RunningPreparationUseCaseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningPreparationUseCaseTests.swift 3 | // MateRunnerUseCaseTests 4 | // 5 | // Created by 전여훈 on 2021/12/01. 6 | // 7 | 8 | import Foundation 9 | 10 | import XCTest 11 | 12 | import RxSwift 13 | import RxTest 14 | 15 | final class RunningPreparationUseCaseTests: XCTestCase { 16 | private var useCase: RunningPreparationUseCase! 17 | private var disposeBag: DisposeBag! 18 | private var scheduler: TestScheduler! 19 | 20 | override func setUpWithError() throws { 21 | self.useCase = DefaultRunningPreparationUseCase() 22 | self.scheduler = TestScheduler(initialClock: 0) 23 | self.disposeBag = DisposeBag() 24 | } 25 | 26 | override func tearDownWithError() throws { 27 | self.useCase = nil 28 | self.disposeBag = nil 29 | } 30 | 31 | func test_timeleft_emmit() { 32 | let timerTestableObserver = self.scheduler.createObserver(Int.self) 33 | let expectation = XCTestExpectation(description: "TimerExpectation") 34 | 35 | self.useCase.executeTimer() 36 | 37 | self.useCase.timeLeft 38 | .subscribe( 39 | onNext: { timerTestableObserver.onNext($0) }, 40 | onCompleted: { expectation.fulfill() }) 41 | .disposed(by: self.disposeBag) 42 | 43 | self.scheduler.start() 44 | wait(for: [expectation], timeout: 10.0) 45 | 46 | XCTAssertEqual(timerTestableObserver.events, [ 47 | .next(0, 3), 48 | .next(0, 2), 49 | .next(0, 1) 50 | ]) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerUseCaseTests/MateScene/Mock/MockMateRepository.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockMateRepository.swift 3 | // MateRunnerViewModelTests 4 | // 5 | // Created by 이유진 on 2021/11/30. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class MockMateRepository: MateRepository { 13 | func sendRequestMate(from sender: String, fcmToken: String) -> Observable { 14 | return Observable.just(()) 15 | } 16 | 17 | func fetchFCMToken(of mate: String) -> Observable { 18 | return Observable.just("2341asdgf1ddf") 19 | } 20 | 21 | func sendEmoji(from sender: String, fcmToken: String) -> Observable { 22 | return Observable.just(()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerUseCaseTests/MyPageScene/MypageUseCaseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MypageUseCaseTests.swift 3 | // MateRunnerUseCaseTests 4 | // 5 | // Created by 이유진 on 2021/12/01. 6 | // 7 | 8 | import XCTest 9 | 10 | import RxSwift 11 | import RxTest 12 | 13 | final class MypageUseCaseTests: XCTestCase { 14 | private var mypageUseCase: MyPageUseCase! 15 | private var userRepository: UserRepository! 16 | private var firestoreRepository: FirestoreRepository! 17 | private var scheduler: TestScheduler! 18 | private var disposeBag: DisposeBag! 19 | 20 | override func setUpWithError() throws { 21 | self.mypageUseCase = DefaultMyPageUseCase( 22 | userRepository: MockUserRepository(), 23 | firestoreRepository: MockFirestoreRepository() 24 | ) 25 | self.scheduler = TestScheduler(initialClock: 0) 26 | self.disposeBag = DisposeBag() 27 | } 28 | 29 | override func tearDownWithError() throws { 30 | self.mypageUseCase.logout() 31 | self.mypageUseCase = nil 32 | self.disposeBag = nil 33 | } 34 | 35 | func test_load_user_success() { 36 | self.mypageUseCase.loadUserInfo() 37 | self.mypageUseCase.imageURL 38 | .subscribe( 39 | onNext: { _ in 40 | XCTAssert(true) 41 | }, 42 | onError: { _ in 43 | XCTAssert(false) 44 | } 45 | ) 46 | .disposed(by: self.disposeBag) 47 | } 48 | 49 | func test_delete_userdata_success() { 50 | self.mypageUseCase.deleteUserData() 51 | .subscribe( 52 | onNext: { success in 53 | success 54 | ? XCTAssert(true) 55 | : XCTAssert(false) 56 | }, 57 | onError: { _ in 58 | XCTAssert(false) 59 | } 60 | ) 61 | .disposed(by: self.disposeBag) 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerViewModelTests/Common/Mock/MockInvitationUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockInvitationUseCase.swift 3 | // MateRunnerViewModelTests 4 | // 5 | // Created by 이유진 on 2021/12/02. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class MockInvitationUseCase: InvitationUseCase { 13 | var invitation: Invitation 14 | var isCancelled: PublishSubject 15 | 16 | init() { 17 | self.isCancelled = PublishSubject() 18 | self.invitation = Invitation( 19 | sessionId: "session-id", 20 | host: "materunner", 21 | inviteTime: "2:00", 22 | mode: .race, 23 | targetDistance: 5.00, 24 | mate: "runner" 25 | ) 26 | } 27 | 28 | func checkIsCancelled() -> Observable { 29 | return Observable.just(false) 30 | } 31 | 32 | func acceptInvitation() -> Observable { 33 | self.isCancelled.onNext(false) 34 | return Observable.just(()) 35 | } 36 | 37 | func rejectInvitation() -> Observable { 38 | self.isCancelled.onNext(true) 39 | return Observable.just(()) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerViewModelTests/HomeScene/Mock/MockDistanceSettingUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockDistanceSettingUseCase.swift 3 | // DistanceSettingTests 4 | // 5 | // Created by 전여훈 on 2021/11/05. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | class MockDistanceSettingUseCase: DistanceSettingUseCase { 13 | var validatedText: BehaviorSubject = BehaviorSubject(value: "5.00") 14 | 15 | func validate(text: String) { 16 | if text.count >= 10 { 17 | self.validatedText.onNext(nil) 18 | } else { 19 | self.validatedText.onNext(text) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerViewModelTests/HomeScene/Mock/MockInvitationWaitingUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockInvitationWaitingUseCase.swift 3 | // MateRunnerViewModelTests 4 | // 5 | // Created by 김민지 on 2021/12/03. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxRelay 11 | import RxSwift 12 | 13 | final class MockInvitationWaitingUseCase: InvitationWaitingUseCase { 14 | var runningSetting: RunningSetting 15 | var requestSuccess: PublishRelay = PublishRelay() 16 | var requestStatus: PublishSubject<(Bool, Bool)> = PublishSubject<(Bool, Bool)>() 17 | var isAccepted: PublishSubject = PublishSubject() 18 | var isRejected: PublishSubject = PublishSubject() 19 | var isCanceled: PublishSubject = PublishSubject() 20 | 21 | init(runningSetting: RunningSetting) { 22 | self.runningSetting = runningSetting 23 | } 24 | 25 | func inviteMate() { 26 | switch self.runningSetting.sessionId { 27 | case "accepted-session": 28 | self.requestStatus.onNext((true, true)) 29 | self.requestSuccess.accept(true) 30 | self.isAccepted.onNext(true) 31 | case "rejected-session": 32 | self.requestStatus.onNext((true, false)) 33 | self.requestSuccess.accept(true) 34 | self.isRejected.onNext(true) 35 | case "canceled-session": 36 | self.requestStatus.onNext((false, false)) 37 | self.requestSuccess.accept(true) 38 | self.isCanceled.onNext(true) 39 | default: 40 | break 41 | } 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerViewModelTests/HomeScene/Mock/MockMapUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockMapUseCase.swift 3 | // MateRunnerViewModelTests 4 | // 5 | // Created by 전여훈 on 2021/12/01. 6 | // 7 | 8 | import Foundation 9 | import CoreLocation 10 | 11 | import RxSwift 12 | import RxRelay 13 | 14 | class MockMapUseCase: MapUseCase { 15 | var updatedLocation: PublishRelay 16 | var disposeBag: DisposeBag = DisposeBag() 17 | 18 | init() { 19 | self.updatedLocation = PublishRelay() 20 | } 21 | 22 | required init(locationService: LocationService, delegate: LocationDidUpdateDelegate?) { 23 | self.updatedLocation = PublishRelay() 24 | } 25 | 26 | func executeLocationTracker() { 27 | self.updatedLocation.accept(CLLocation(latitude: 0, longitude: 0)) 28 | } 29 | 30 | func terminateLocationTracker() { 31 | self.disposeBag = DisposeBag() 32 | } 33 | 34 | func requestLocation() { 35 | self.updatedLocation.accept(CLLocation(latitude: 1, longitude: 1)) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerViewModelTests/HomeScene/Mock/MockRunningPreparationUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockRunningPreparationUseCase.swift 3 | // MateRunner 4 | // 5 | // Created by 전여훈 on 2021/11/03. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | class MockRunningPreparationUseCase: RunningPreparationUseCase { 13 | var timeLeft: BehaviorSubject = BehaviorSubject(value: 0) 14 | var isTimeOver: BehaviorSubject = BehaviorSubject(value: false) 15 | 16 | func executeTimer() { 17 | self.timeLeft.onNext(1) 18 | self.timeLeft.onNext(2) 19 | self.timeLeft.onNext(3) 20 | self.isTimeOver.onNext(true) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerViewModelTests/HomeScene/Mock/MockRunningResultUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockRunningResultUseCase.swift 3 | // MateRunnerViewModelTests 4 | // 5 | // Created by 전여훈 on 2021/12/03. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxRelay 11 | import RxSwift 12 | 13 | final class MockRunningResultUseCase: RunningResultUseCase { 14 | enum MockError: Error { 15 | case testError 16 | } 17 | var runningResult: RunningResult 18 | var selectedEmoji = PublishRelay() 19 | 20 | init(runningResult: RunningResult) { 21 | self.runningResult = runningResult 22 | } 23 | 24 | func saveRunningResult() -> Observable { 25 | return Observable.error(MockError.testError) 26 | } 27 | 28 | func emojiDidSelect(selectedEmoji: Emoji) { 29 | self.selectedEmoji.accept(.burningHeart) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerViewModelTests/HomeScene/RunningPreparationViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningPreparationViewModelTests.swift 3 | // RunningPreparationViewModelTests 4 | // 5 | // Created by 전여훈 on 2021/11/03. 6 | // 7 | 8 | import XCTest 9 | 10 | import RxRelay 11 | import RxSwift 12 | import RxTest 13 | 14 | class RunningPreparationViewModelTests: XCTestCase { 15 | private var viewModel: RunningPreparationViewModel! 16 | private var disposeBag: DisposeBag! 17 | private var scheduler: TestScheduler! 18 | private var input: RunningPreparationViewModel.Input! 19 | private var output: RunningPreparationViewModel.Output! 20 | 21 | override func setUpWithError() throws { 22 | self.viewModel = RunningPreparationViewModel( 23 | coordinator: nil, 24 | runningSettingUseCase: MockRunningSettingUseCase(), 25 | runningPreparationUseCase: MockRunningPreparationUseCase() 26 | ) 27 | self.disposeBag = DisposeBag() 28 | self.scheduler = TestScheduler(initialClock: 0) 29 | let testableObservable = scheduler.createColdObservable([.next(10, ())]) 30 | self.input = RunningPreparationViewModel.Input(viewDidLoadEvent: testableObservable.asObservable()) 31 | self.output = viewModel.transform(from: input, disposeBag: self.disposeBag) 32 | } 33 | 34 | override func tearDownWithError() throws { 35 | self.viewModel = nil 36 | self.disposeBag = DisposeBag() 37 | } 38 | 39 | func test_convert_integer_time_to_string_sucess() { 40 | // observe output 41 | let timeLeft = self.scheduler.createObserver(String?.self) 42 | self.output.timeLeft 43 | .subscribe(timeLeft) 44 | .disposed(by: self.disposeBag) 45 | 46 | // begin 47 | self.scheduler.start() 48 | 49 | // test 50 | XCTAssertEqual(timeLeft.events, [ 51 | .next(0, "0"), 52 | .next(10, "1"), 53 | .next(10, "2"), 54 | .next(10, "3") 55 | ]) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerViewModelTests/LoginScene/Mock/MockLoginUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockLoginUseCase.swift 3 | // MateRunnerViewModelTests 4 | // 5 | // Created by 이정원 on 2021/12/03. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class MockLoginUseCase: LoginUseCase { 13 | var isRegistered = PublishSubject() 14 | var isSaved = PublishSubject() 15 | 16 | func checkRegistration(uid: String) { 17 | if uid == "registeredMockUID" { 18 | self.isRegistered.onNext(true) 19 | } else { 20 | self.isRegistered.onNext(false) 21 | } 22 | } 23 | 24 | func saveLoginInfo(uid: String) { 25 | self.isSaved.onNext(true) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerViewModelTests/LoginScene/Mock/MockSignUpUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockSignUpUseCase.swift 3 | // MateRunnerViewModelTests 4 | // 5 | // Created by 이정원 on 2021/12/03. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class MockSignUpUseCase: SignUpUseCase { 13 | var nickname: String = "" 14 | var selectedProfileEmoji = BehaviorSubject(value: "👩🏻‍🚀") 15 | var nicknameValidationState = BehaviorSubject(value: .empty) 16 | var height = BehaviorSubject(value: 170) 17 | var weight = BehaviorSubject(value: 60) 18 | 19 | func validate(text: String) { 20 | self.nickname = text 21 | self.updateValidationState(of: text) 22 | } 23 | 24 | func signUp() -> Observable { 25 | return Observable.just(true) 26 | } 27 | 28 | func saveLoginInfo() { 29 | 30 | } 31 | 32 | func shuffleProfileEmoji() { 33 | self.selectedProfileEmoji.onNext(self.createRandomEmoji()) 34 | } 35 | 36 | private func createRandomEmoji() -> String { 37 | let emojis = [UInt32](0x1F601...0x1F64F).compactMap { UnicodeScalar($0)?.description } 38 | return emojis.randomElement() ?? "👩🏻‍🚀" 39 | } 40 | 41 | private func updateValidationState(of nicknameText: String) { 42 | guard !nicknameText.isEmpty else { 43 | self.nicknameValidationState.onNext(.empty) 44 | return 45 | } 46 | guard nicknameText.count >= 5 else { 47 | self.nicknameValidationState.onNext(.lowerboundViolated) 48 | return 49 | } 50 | guard nicknameText.count <= 20 else { 51 | self.nicknameValidationState.onNext(.upperboundViolated) 52 | return 53 | } 54 | guard nicknameText.range(of: "[^a-zA-Z0-9]", options: .regularExpression) == nil else { 55 | self.nicknameValidationState.onNext(.invalidLetterIncluded) 56 | return 57 | } 58 | 59 | self.nicknameValidationState.onNext(.success) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerViewModelTests/MateScene/AddMateViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddMateViewModelTests.swift 3 | // MateRunnerViewModelTests 4 | // 5 | // Created by 이유진 on 2021/12/04. 6 | // 7 | 8 | import XCTest 9 | 10 | import RxRelay 11 | import RxSwift 12 | import RxTest 13 | 14 | final class AddMateViewModelTests: XCTestCase { 15 | private var viewModel: AddMateViewModel! 16 | private var disposeBag: DisposeBag! 17 | private var scheduler: TestScheduler! 18 | private var input: AddMateViewModel.Input! 19 | private var output: AddMateViewModel.Output! 20 | 21 | override func setUpWithError() throws { 22 | self.viewModel = AddMateViewModel( 23 | coordinator: nil, 24 | mateUseCase: MockMateUseCase() 25 | ) 26 | self.disposeBag = DisposeBag() 27 | self.scheduler = TestScheduler(initialClock: 0) 28 | } 29 | 30 | override func tearDownWithError() throws { 31 | self.viewModel.requestMate(to: "materunner") 32 | self.viewModel = nil 33 | self.disposeBag = nil 34 | } 35 | 36 | func test_search_text_event() { 37 | let searchbarTextEventTestableObservable = self.scheduler.createHotObservable([ 38 | .next(10, "mate") 39 | ]) 40 | let searchbarButtonEventTestableObservable = self.scheduler.createHotObservable([ 41 | .next(10, ()) 42 | ]) 43 | 44 | let loadTestableObservable = self.scheduler.createObserver(Bool.self) 45 | 46 | self.input = AddMateViewModel.Input( 47 | searchButtonDidTap: searchbarButtonEventTestableObservable.asObservable(), 48 | searchBarTextEvent: searchbarTextEventTestableObservable.asObservable() 49 | ) 50 | 51 | self.viewModel.transform(from: input, disposeBag: self.disposeBag) 52 | .loadData 53 | .subscribe(loadTestableObservable) 54 | .disposed(by: self.disposeBag) 55 | 56 | self.scheduler.start() 57 | 58 | XCTAssertEqual(loadTestableObservable.events, [ 59 | .next(10, true) 60 | ]) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerViewModelTests/MateScene/EmojiViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EmojiViewModelTests.swift 3 | // MateRunnerViewModelTests 4 | // 5 | // Created by 이유진 on 2021/12/02. 6 | // 7 | 8 | import XCTest 9 | 10 | import RxRelay 11 | import RxSwift 12 | import RxTest 13 | 14 | final class EmojiViewModelTests: XCTestCase { 15 | private var viewModel: EmojiViewModel! 16 | private var disposeBag: DisposeBag! 17 | private var scheduler: TestScheduler! 18 | private var input: EmojiViewModel.Input! 19 | private var output: EmojiViewModel.Output! 20 | 21 | override func setUpWithError() throws { 22 | self.viewModel = EmojiViewModel( 23 | coordinator: nil, 24 | emojiUseCase: MockEmojiUseCase() 25 | ) 26 | self.disposeBag = DisposeBag() 27 | self.scheduler = TestScheduler(initialClock: 0) 28 | } 29 | 30 | override func tearDownWithError() throws { 31 | self.viewModel = nil 32 | self.disposeBag = nil 33 | } 34 | 35 | func test_emoji_selection() { 36 | let emojiCellTestableObservable = self.scheduler.createHotObservable([ 37 | .next(20, IndexPath(row: 2, section: 0)) 38 | ]) 39 | 40 | let emojiSelectionTestableObserver = self.scheduler.createObserver(Emoji?.self) 41 | 42 | self.input = EmojiViewModel.Input( 43 | emojiCellTapEvent: emojiCellTestableObservable.asObservable() 44 | ) 45 | 46 | self.viewModel.transform(from: input, disposeBag: self.disposeBag) 47 | .selectedEmoji 48 | .skip(1) 49 | .subscribe(emojiSelectionTestableObserver) 50 | .disposed(by: self.disposeBag) 51 | 52 | self.scheduler.start() 53 | 54 | XCTAssertEqual(emojiSelectionTestableObserver.events, [ 55 | .next(20, Emoji.ribbonHeart) 56 | ]) 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerViewModelTests/MateScene/Mock/MockEmojiUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockEmojiUseCase.swift 3 | // MateRunnerViewModelTests 4 | // 5 | // Created by 이유진 on 2021/12/02. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class MockEmojiUseCase: EmojiUseCase { 13 | var selectedEmoji: PublishSubject 14 | var runningID: String? 15 | var mateNickname: String? 16 | 17 | init() { 18 | self.selectedEmoji = PublishSubject() 19 | self.runningID = "running-id" 20 | self.mateNickname = "materunner" 21 | } 22 | 23 | func saveSentEmoji(_ emoji: Emoji) { 24 | self.selectedEmoji.onNext(emoji) 25 | } 26 | 27 | func selectEmoji(_ emoji: Emoji) { 28 | self.selectedEmoji.onNext(emoji) 29 | } 30 | 31 | func sendComplimentEmoji() {} 32 | } 33 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerViewModelTests/MateScene/Mock/MockMateUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockMateUseCase.swift 3 | // MateRunnerViewModelTests 4 | // 5 | // Created by 이유진 on 2021/12/04. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class MockMateUseCase: MateUseCase { 13 | var mateList: PublishSubject 14 | var didLoadMate: PublishSubject 15 | var didRequestMate: PublishSubject 16 | 17 | init() { 18 | self.mateList = PublishSubject() 19 | self.didLoadMate = PublishSubject() 20 | self.didRequestMate = PublishSubject() 21 | } 22 | 23 | func fetchMateList() { 24 | self.mateList.onNext([(key: "mateRunner", value: "profile")]) 25 | self.didLoadMate.onNext(true) 26 | } 27 | 28 | func fetchMateImage(from mate: [String]) { 29 | self.mateList.onNext([(key: "mateRunner", value: "profile")]) 30 | self.didLoadMate.onNext(true) 31 | } 32 | 33 | func fetchSearchedUser(with nickname: String) { 34 | self.mateList.onNext([(key: "mateRunner", value: "profile")]) 35 | self.didLoadMate.onNext(true) 36 | } 37 | 38 | func sendRequestMate(to mate: String) {} 39 | 40 | func filterMate(base mate: MateList, from text: String) { 41 | self.mateList.onNext([(key: "mateRunner", value: "profile")]) 42 | self.didLoadMate.onNext(true) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerViewModelTests/MyPageScene/Mock/MockMyPageUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockMyPageUseCase.swift 3 | // MateRunnerViewModelTests 4 | // 5 | // Created by 이유진 on 2021/12/02. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class MockMyPageUseCase: MyPageUseCase { 13 | var nickname: String? 14 | var imageURL: PublishSubject 15 | 16 | init() { 17 | self.nickname = "materunner" 18 | self.imageURL = PublishSubject() 19 | } 20 | 21 | func loadUserInfo() { 22 | self.imageURL.onNext("materunner-profile.png") 23 | } 24 | 25 | func logout() { 26 | self.nickname = nil 27 | } 28 | 29 | func deleteUserData() -> Observable { 30 | return Observable.just(true) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerViewModelTests/MyPageScene/Mock/MockNotificationUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockNotificationUseCase.swift 3 | // MateRunnerViewModelTests 4 | // 5 | // Created by 이정원 on 2021/12/03. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class MockNotificationUseCase: NotificationUseCase { 13 | var notices = PublishSubject<[Notice]>() 14 | private var mockNotices = [ 15 | Notice( 16 | id: "notice-1", 17 | sender: "Jungwon", 18 | receiver: "minji", 19 | mode: .requestMate, 20 | isReceived: false 21 | ), 22 | Notice( 23 | id: "notice-2", 24 | sender: "yujin", 25 | receiver: "hunhun", 26 | mode: .invite, 27 | isReceived: true 28 | ), 29 | Notice( 30 | id: "notice-3", 31 | sender: "Jungwon", 32 | receiver: "hunhun", 33 | mode: .receiveEmoji, 34 | isReceived: true 35 | ) 36 | ] 37 | 38 | func fetchNotices() { 39 | self.notices.onNext(self.mockNotices) 40 | } 41 | 42 | func updateMateState(notice: Notice, isAccepted: Bool) { 43 | self.notices.onNext(self.mockNotices.map { $0.copyUpdatedReceived() }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerViewModelTests/MyPageScene/Mock/MockProfileEditUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockProfileEditUseCase.swift 3 | // MateRunnerViewModelTests 4 | // 5 | // Created by 이유진 on 2021/12/02. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class MockProfileEditUseCase: ProfileEditUseCase { 13 | var nickname: String? 14 | var height: BehaviorSubject 15 | var weight: BehaviorSubject 16 | var imageURL: BehaviorSubject 17 | var saveResult: PublishSubject 18 | 19 | init() { 20 | self.nickname = "" 21 | self.height = BehaviorSubject(value: 170.0) 22 | self.weight = BehaviorSubject(value: 60.0) 23 | self.imageURL = BehaviorSubject(value: "image.png") 24 | self.saveResult = PublishSubject() 25 | } 26 | 27 | func loadUserInfo() { 28 | self.nickname = "materunner" 29 | self.height.onNext(160.0) 30 | self.weight.onNext(50.0) 31 | self.imageURL.onNext("materunner.png") 32 | } 33 | 34 | func saveUserInfo(imageData: Data) { 35 | self.saveResult.onNext(true) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /MateRunner/MateRunnerViewModelTests/RecordScene/Mock/MockRecordDetailUseCase.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockRecordDetailUseCase.swift 3 | // MateRunnerViewModelTests 4 | // 5 | // Created by 이유진 on 2021/12/04. 6 | // 7 | 8 | import Foundation 9 | 10 | import RxSwift 11 | 12 | final class MockRecordDetailUseCase: RecordDetailUseCase { 13 | var runningResult: RunningResult 14 | var nickname: String? 15 | 16 | init(runningResult: RunningResult) { 17 | self.runningResult = runningResult 18 | self.nickname = "mate" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /MateRunner/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | inhibit_all_warnings! 3 | platform :ios, '13.0' 4 | 5 | target 'MateRunner' do 6 | # Comment the next line if you don't want to use dynamic frameworks 7 | use_frameworks! 8 | pod 'SnapKit' 9 | pod 'RxSwift' 10 | pod 'RxCocoa' 11 | pod 'RxGesture' 12 | pod 'SwiftLint' 13 | pod 'Firebase/Analytics' 14 | pod 'Firebase/Database' 15 | pod 'Firebase/Messaging' 16 | pod 'FirebaseUI/OAuth' 17 | # Pods for MateRunner 18 | target 'MateRunnerViewModelTests' do 19 | inherit! :search_paths 20 | pod 'RxTest' 21 | pod 'RxSwift' 22 | pod 'RxRelay' 23 | end 24 | target 'MateRunnerUseCaseTests' do 25 | inherit! :search_paths 26 | pod 'RxTest' 27 | pod 'RxSwift' 28 | pod 'RxRelay' 29 | end 30 | end 31 | 32 | post_install do |installer| 33 | installer.pods_project.targets.each do |target| 34 | target.build_configurations.each do |config| 35 | config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '9.0' 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### 📕 Issue Number 2 | 3 | Close # 4 | 5 | 6 | ### 📙 작업 내역 7 | 8 | > 구현 내용 및 작업 했던 내역 9 | 10 | - [x] 작업 내역 작성 11 | 12 | 13 | ### 📘 작업 유형 14 | 15 | - [ ] 신규 기능 추가 16 | - [ ] 버그 수정 17 | - [ ] 리펙토링 18 | - [ ] 문서 업데이트 19 | 20 | 21 | ### 📋 체크리스트 22 | 23 | - [ ] Merge 하는 브랜치가 올바른가? 24 | - [ ] 코딩컨벤션을 준수하는가? 25 | - [ ] PR과 관련없는 변경사항이 없는가? 26 | - [ ] 내 코드에 대한 자기 검토가 되었는가? 27 | - [ ] 변경사항이 효과적이거나 동작이 작동한다는 것을 보증하는 테스트를 추가하였는가? 28 | - [ ] 새로운 테스트와 기존의 테스트가 변경사항에 대해 만족하는가? 29 |
30 | 31 | ### 📝 PR 특이 사항 32 | 33 | > PR을 볼 때 주의깊게 봐야하거나 말하고 싶은 점 34 | 35 | - 특이 사항 1 36 | - 특이 사항 2 37 | 38 |

39 | --------------------------------------------------------------------------------