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