├── .gitIgnore ├── .github ├── ISSUE_TEMPLATE │ ├── discussion-issue-template.md │ ├── feature.md │ └── scrum---wrap-up-issue-template.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── build_and_unit_test_ios_project.yml ├── BoostRunClub.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ └── BoostRunClub.xcscheme ├── BoostRunClub.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── BoostRunClub ├── .swiftformat ├── .swiftlint.yml ├── AppDelegate.swift ├── Coordinators │ ├── ActivityCoordinator.swift │ ├── ActivityDetailCoordinator.swift │ ├── ActivityListCoordinator.swift │ ├── AppCoordinator.swift │ ├── BasicCoordinator.swift │ ├── Coordinator.swift │ ├── PausedRunningCoordinator.swift │ ├── PrepareRunCoordinator.swift │ ├── ProfileCoordinator.swift │ ├── RunningCoordinator.swift │ ├── RunningInfoCoordinator.swift │ ├── RunningMapCoordinator.swift │ ├── RunningPageCoordinator.swift │ ├── SplitsViewCoordinator.swift │ └── TabBarCoordinator.swift ├── CoreData │ ├── BRCModel.xcdatamodeld │ │ └── BRCModel.xcdatamodel │ │ │ └── contents │ ├── ZActivity+Activity.swift │ └── ZActivityDetail+ActivityDetail.swift ├── CoreML │ ├── DataSet │ │ ├── Motion │ │ │ ├── running │ │ │ │ ├── 10.csv │ │ │ │ ├── 11.csv │ │ │ │ ├── 4.csv │ │ │ │ ├── 5.csv │ │ │ │ ├── 6.csv │ │ │ │ ├── 7.csv │ │ │ │ ├── 8.csv │ │ │ │ ├── 9.csv │ │ │ │ ├── er1.csv │ │ │ │ ├── er10.csv │ │ │ │ ├── er2.csv │ │ │ │ ├── er3.csv │ │ │ │ ├── er4.csv │ │ │ │ ├── er5.csv │ │ │ │ ├── er6.csv │ │ │ │ ├── er7.csv │ │ │ │ ├── er8.csv │ │ │ │ ├── er9.csv │ │ │ │ ├── q1.csv │ │ │ │ ├── q11.csv │ │ │ │ ├── q12.csv │ │ │ │ ├── q13.csv │ │ │ │ ├── q14.csv │ │ │ │ ├── q15.csv │ │ │ │ ├── q16.csv │ │ │ │ ├── q17.csv │ │ │ │ ├── q18.csv │ │ │ │ ├── q19.csv │ │ │ │ ├── q2.csv │ │ │ │ ├── q3.csv │ │ │ │ ├── q4.csv │ │ │ │ ├── q5.csv │ │ │ │ ├── q6.csv │ │ │ │ ├── r1.csv │ │ │ │ ├── r10.csv │ │ │ │ ├── r2.csv │ │ │ │ ├── r3.csv │ │ │ │ ├── r4.csv │ │ │ │ ├── r5.csv │ │ │ │ ├── r6.csv │ │ │ │ ├── r7.csv │ │ │ │ ├── r8.csv │ │ │ │ ├── r9.csv │ │ │ │ ├── we1.csv │ │ │ │ ├── we10.csv │ │ │ │ ├── we11.csv │ │ │ │ ├── we2.csv │ │ │ │ ├── we3.csv │ │ │ │ ├── we4.csv │ │ │ │ ├── we5.csv │ │ │ │ ├── we6.csv │ │ │ │ ├── we7.csv │ │ │ │ ├── we8.csv │ │ │ │ ├── we9.csv │ │ │ │ ├── z0.csv │ │ │ │ ├── z1.csv │ │ │ │ └── z2.csv │ │ │ └── standing │ │ │ │ ├── 10.csv │ │ │ │ ├── 11.csv │ │ │ │ ├── 23E915F4-1705-40F3-BC0F-F3B64635B3F2.csv │ │ │ │ ├── 4.csv │ │ │ │ ├── 5.csv │ │ │ │ ├── 572988F2-0C99-4614-8189-0936FA181D9C.csv │ │ │ │ ├── 6.csv │ │ │ │ ├── 7.csv │ │ │ │ ├── 8.csv │ │ │ │ ├── 9.csv │ │ │ │ ├── 9E813B22-822F-469D-9697-F0DF62A39C1F.csv │ │ │ │ ├── E567C633-BF4B-4AAF-9C44-C8E4560BAF82.csv │ │ │ │ ├── F809AD11-504E-42A6-877C-C6C6037E3D81.csv │ │ │ │ ├── ad1.csv │ │ │ │ ├── ad2.csv │ │ │ │ ├── q10.csv │ │ │ │ ├── q11.csv │ │ │ │ ├── q12.csv │ │ │ │ ├── q13.csv │ │ │ │ ├── q14.csv │ │ │ │ ├── q15.csv │ │ │ │ ├── q16.csv │ │ │ │ ├── q5.csv │ │ │ │ ├── q6.csv │ │ │ │ ├── q7.csv │ │ │ │ ├── q8.csv │ │ │ │ ├── q9.csv │ │ │ │ ├── s1.csv │ │ │ │ ├── s2.csv │ │ │ │ ├── s3.csv │ │ │ │ ├── s4.csv │ │ │ │ ├── s5.csv │ │ │ │ ├── s6.csv │ │ │ │ ├── sw1.csv │ │ │ │ ├── w0.csv │ │ │ │ ├── w1.csv │ │ │ │ ├── w2.csv │ │ │ │ ├── w3.csv │ │ │ │ ├── we1.csv │ │ │ │ ├── we2.csv │ │ │ │ └── ws2.csv │ │ └── TEST │ │ │ ├── running │ │ │ ├── 1.csv │ │ │ ├── 2.csv │ │ │ ├── 3.csv │ │ │ ├── q10.csv │ │ │ ├── q7.csv │ │ │ ├── q8.csv │ │ │ └── q9.csv │ │ │ └── standing │ │ │ ├── 1.csv │ │ │ ├── 2.csv │ │ │ ├── 3.csv │ │ │ ├── q1.csv │ │ │ ├── q2.csv │ │ │ ├── q3.csv │ │ │ └── q4.csv │ └── RunningMotionClassifier.mlmodel ├── Extensions │ ├── CaseIterable+.swift │ ├── Data+SaveAndLoadData.swift │ ├── Date │ │ ├── Date+DateRange.swift │ │ ├── Date+String.swift │ │ ├── DateFormatter+Common.swift │ │ └── TimerInterval+String.swift │ ├── MapKit │ │ ├── CLLocationCoordinate+computeSplitCoordinate.swift │ │ └── MKCoordinateRegion+Make.swift │ ├── Math │ │ ├── Clamp.swift │ │ └── Double+Radian+Degree.swift │ ├── Notification.name.swift │ ├── String+Regex.swift │ ├── UIKit │ │ ├── UIButton+setArrowImage.swift │ │ ├── UIButton+setSFSymbol.swift │ │ ├── UIColor+Components.swift │ │ ├── UIImage+CustomAnnotation.swift │ │ ├── UIImage+SFSymbol.swift │ │ ├── UILabel+Make.swift │ │ ├── UINavigationController+setStatusBar.swift │ │ ├── UIStackView+Make.swift │ │ ├── UIView+identifier.swift │ │ └── UIView+notificationFeedback.swift │ └── URL+Documents.swift ├── Factory │ ├── DependencyFactory.swift │ └── Extensions │ │ ├── Factory+ActivityDetailScene.swift │ │ ├── Factory+ActivityListScene.swift │ │ ├── Factory+ActivityScene.swift │ │ ├── Factory+EditProfileScene.swift │ │ ├── Factory+LoginScene.swift │ │ ├── Factory+PausedRunningScene.swift │ │ ├── Factory+PrepareRunScene.swift │ │ ├── Factory+ProfileScene.swift │ │ ├── Factory+RouteDetailScene.swift │ │ ├── Factory+RunningInfoScene.swift │ │ ├── Factory+RunningMapScene.swift │ │ ├── Factory+RunningPageContainer.swift │ │ ├── Factory+SplitScene.swift │ │ └── Factory+TabBarContainer.swift ├── Models │ ├── Activity.swift │ ├── ActivityDetail.swift │ ├── ActivityFilterType.swift │ ├── DateRange.swift │ ├── GoalType.swift │ ├── Location.swift │ ├── MotionType.swift │ ├── Profile.swift │ ├── RunningEvent.swift │ ├── RunningInfoType.swift │ ├── RunningSlice.swift │ ├── RunningSplit.swift │ └── RunningState.swift ├── SceneDelegate.swift ├── Services │ ├── CoreDataStorage │ │ ├── ActivityStorageService.swift │ │ └── CoreDataService.swift │ ├── Provider │ │ ├── DefaultsManagable.swift │ │ ├── DefaultsProvider.swift │ │ ├── EventTimeProvidable.swift │ │ ├── EventTimeProvider.swift │ │ ├── LocationProvidable.swift │ │ ├── LocationProvider.swift │ │ ├── MotionDataModelProvidable.swift │ │ ├── MotionDataModelProvider.swift │ │ ├── PedometerProvidable.swift │ │ ├── PedometerProvider.swift │ │ └── RunningSnapShotProvider │ │ │ ├── MapSnapShotSubscription.swift │ │ │ ├── RouteDrawer.swift │ │ │ ├── RouteSnapShotProcessor.swift │ │ │ ├── RunningSnapShotProvidable.swift │ │ │ └── RunningSnapShotProvider.swift │ └── RunningService │ │ ├── RunningDashBoardService.swift │ │ ├── RunningDashBoardServiceable.swift │ │ ├── RunningMotionService.swift │ │ ├── RunningMotionServiecable.swift │ │ ├── RunningRecordService.swift │ │ ├── RunningRecordServiceable.swift │ │ ├── RunningService.swift │ │ └── RunningServiceType.swift ├── SupportingFiles │ ├── Assets.xcassets │ │ ├── Accent.colorset │ │ │ └── Contents.json │ │ ├── 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 │ │ ├── Color.colorset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Image.imageset │ │ │ └── Contents.json │ │ ├── accent2.colorset │ │ │ └── Contents.json │ │ ├── activity4.imageset │ │ │ ├── Contents.json │ │ │ └── activity4.png │ │ ├── brcYellow.colorset │ │ │ └── Contents.json │ │ ├── customBackground.colorset │ │ │ └── Contents.json │ │ ├── profile4.imageset │ │ │ ├── Contents.json │ │ │ └── profile4.png │ │ ├── running4.imageset │ │ │ ├── Contents.json │ │ │ └── running4.png │ │ └── tableBackground.colorset │ │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Futura LT Condensed Extra Bold Oblique.ttf │ └── Info.plist ├── Utilities.swift ├── ViewModels │ ├── ActivityDateFilterViewModel.swift │ ├── ActivityDetailViewModel.swift │ ├── ActivityListViewModel.swift │ ├── ActivityViewModel.swift │ ├── Configurable │ │ ├── ActivityDetailConfig.swift │ │ ├── ActivityListItem.swift │ │ └── ActivityTotalConfig.swift │ ├── EditProfileViewModel.swift │ ├── GoalTypeViewModel.swift │ ├── GoalTypeViewModelTest.swift │ ├── GoalValueSetupViewModel.swift │ ├── GoalValueSetupViewModelTest.swift │ ├── PausedRunningViewModel.swift │ ├── PrepareRunViewModel.swift │ ├── PrepareRunViewModelTest.swift │ ├── ProfileViewModel.swift │ ├── RouteDetailViewModel.swift │ ├── RunningInfoViewModel.swift │ ├── RunningMapViewModel.swift │ ├── RunningPageViewModel.swift │ ├── RunningSplitCellViewModel.swift │ ├── SplitInfoDetailViewModel.swift │ └── SplitsViewModel.swift └── Views │ ├── ActivityDetailScene │ ├── DetailMapView.swift │ ├── DetailSplitsTableView.swift │ ├── DetailSplitsView.swift │ ├── DetailTitleView.swift │ ├── DetailTotalView.swift │ └── SimpleSplitViewCell.swift │ ├── ActivityScene │ ├── ActivitiesContainerCellView.swift │ ├── ActivityCellView.swift │ ├── ActivityCollectionView.swift │ ├── ActivityFooterView.swift │ ├── ActivityListHeaderView.swift │ ├── ActivityStatisticCellView.swift │ ├── ActivityTableView.swift │ ├── ActivityTotalView.swift │ └── DateFilterSheetView.swift │ ├── Cells │ ├── GoalTypeCell.swift │ └── RunningSplitCell.swift │ ├── CircleButton.swift │ ├── CircleLongPressButton.swift │ ├── Controllers │ ├── ActivityDateFilterViewController.swift │ ├── ActivityDetailViewController.swift │ ├── ActivityListViewController.swift │ ├── ActivityViewController.swift │ ├── EditProfileViewController.swift │ ├── GoalTypeViewController.swift │ ├── GoalValueSetupViewController.swift │ ├── PausedRunningViewController.swift │ ├── PrepareRunViewController.swift │ ├── ProfileViewController.swift │ ├── RouteDetailViewController.swift │ ├── RunningInfoViewController.swift │ ├── RunningMapViewController.swift │ ├── RunningPageViewController.swift │ ├── SplitInfoDetailViewController.swift │ └── SplitsViewController.swift │ ├── CountDownView.swift │ ├── DataSource │ ├── ActivityDataSource.swift │ ├── ActivityDetailDataSource.swift │ ├── ActivityListDataSource.swift │ ├── SplitDatailSplitDataSource.swift │ └── SplitInfoDetailDataSource.swift │ ├── GoalValueView.swift │ ├── NikeLabel.swift │ ├── ProfileButton.swift.swift │ ├── Renderer │ ├── BasicRouteOverlay.swift │ ├── BasicRouteRenderer.swift │ ├── GradientRouteOverlay.swift │ └── GradientRouteRenderer.swift │ ├── RoundSegmentControl.swift │ ├── RunDataView.swift │ └── SplitInfoDetailScene │ ├── SplitDatailSplitCell.swift │ ├── SplitDetailDateInfoView.swift │ ├── SplitDetailInfoCell.swift │ ├── SplitDetailSplitHeaderView.swift │ └── SplitHeaderView.swift ├── BoostRunClubTests ├── BoostRunClubTests.swift ├── Info.plist └── Mocks │ ├── DefaultsProviderMock.swift │ ├── EventTimeProvidableMock.swift │ ├── LocationProviderMock.swift │ ├── MotionDataModelProvidableMock.swift │ ├── PedometerProviderMock.swift │ ├── RunningDashBoardServiceableMock.swift │ ├── RunningMotionServiceableMock.swift │ ├── RunningRecordServiceableMock.swift │ └── RunningServiceTypeMock.swift ├── Docs └── prototype.xd ├── LICENSE ├── Podfile ├── Podfile.lock └── README.md /.github/ISSUE_TEMPLATE/discussion-issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Discussion issue template 3 | about: 의논사항에 대한 이슈 작성시 사용하는 템플릿 4 | title: '' 5 | labels: "🏷️ discussion 🗨️" 6 | assignees: whrlgus, SHIVVVPP, seoulboy 7 | 8 | --- 9 | 10 | ### 의논거리 🤔 11 | - 12 | 13 | ### 관련 PR or Issue Number 14 | 15 | - PR : 16 | - Issue : 17 | 18 | ### 이미지 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: feature request template 3 | about: feature 추가시 사용하는 기본 템플릿 4 | title: story를 적어주세요 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | # story를 적어주세요 11 | 12 | - 필요시 추가 설명 작성 13 | 14 | ## 완료 조건 ✅ 15 | 16 | - [ ] task1 17 | - [ ] task2 18 | 19 | ## 관련 이슈 📎 20 | 21 | 관련 이슈 없음 22 | 23 | ## 레퍼런스 📚 24 | 25 | 레퍼런스 없음 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/scrum---wrap-up-issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Scrum 및 wrap-up issue template 3 | about: 데일리 스크럼 및 wrap-up 이슈 작성시 사용하는 템플릿 4 | title: 'Day 00 Scrum 및 Wrap Up ' 5 | labels: "daily scrum & Wrap up 👨‍👩‍👧‍👦" 6 | assignees: whrlgus, SHIVVVPP, seoulboy 7 | 8 | --- 9 | 10 | ## 스크럼 / Wrap-up 택 1 11 | 12 | 스크럼 템플릿 13 | 14 | ### 1. 어제 한 일 🌙 15 | - ... 16 | ### 2. 오늘 할 일 🔥 17 | - ... 18 | ### 3. 공유할 이슈 🙌 19 |
20 | 21 | wrap-up 템플릿 22 | 23 | ### 오늘의 회고 🎈 24 | ``` 25 | 초기 프로젝트 셋팅 때문에 많이들 힘들텐데 열심히 하는모습이 대견하다! 26 | wiki 정리가 생각보다 많이 길어졌다. wiki를 정리하며, 템플릿을 만드는데 거의 하루종일 소비했다. 27 | CI/CD 에 대해 진행을 오늘 하루종일 못했다. 내일은 정말 CI/CD를 공부한다!😎 28 | ``` 29 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Issue Number 2 | Close # 3 | 4 | ### 변경사항 5 | 6 | - 의존성 목록 7 | 8 | ### 새로운 기능 9 | 10 | - 기능 목록 11 | 12 | ### 작업 유형 13 | - [x] 신규 기능 추가 14 | - [ ] 버그 수정 15 | - [ ] 리펙토링 16 | - [ ] 문서 업데이트 17 | 18 | ### 체크리스트 19 | - [ ] Merge 하는 브랜치가 올바른가? 20 | - [ ] 코딩컨벤션을 준수하는가? 21 | - [ ] PR과 관련없는 변경사항이 없는가? 22 | - [ ] 내 코드에 대한 자기 검토가 되었는가? 23 | - [ ] 변경사항이 효과적이거나 동작이 작동한다는 것을 보증하는 테스트를 추가하였는가? 24 | - [ ] 새로운 테스트와 기존의 테스트가 변경사항에 대해 만족하는가? 25 | -------------------------------------------------------------------------------- /.github/workflows/build_and_unit_test_ios_project.yml: -------------------------------------------------------------------------------- 1 | name: iOS CI workflow 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | build: 8 | name: Build, Install Pods, and Run Unit Tests 9 | env: 10 | SCHEME: "BoostRunClub" 11 | DEVICE: "iPhone 12" 12 | WORKSPACE: "BoostRunClub.xcworkspace" 13 | 14 | runs-on: macOS-latest 15 | 16 | steps: 17 | 18 | - name: Checkout project 19 | uses: actions/checkout@v1 20 | 21 | - name: CocoaPod Install 22 | run: pod install 23 | 24 | - name: Select Xcode 12 25 | run: sudo xcode-select -switch /Applications/Xcode_12.2.app 26 | 27 | - name: Build 28 | run: | 29 | set -eo pipefail && xcodebuild build-for-testing \ 30 | -scheme $SCHEME \ 31 | -workspace $WORKSPACE \ 32 | -destination "platform=iOS Simulator,name=$DEVICE" | 33 | xcpretty --color --simple 34 | - name: Run unit tests 35 | run: | 36 | set -eo pipefail && xcodebuild test-without-building \ 37 | -scheme $SCHEME \ 38 | -workspace $WORKSPACE \ 39 | -destination "platform=iOS Simulator,name=$DEVICE" | 40 | xcpretty --color --simple 41 | -------------------------------------------------------------------------------- /BoostRunClub.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /BoostRunClub.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BoostRunClub/.swiftformat: -------------------------------------------------------------------------------- 1 | # file options 2 | 3 | --swiftversion 5 4 | -------------------------------------------------------------------------------- /BoostRunClub/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - trailing_comma 3 | - opening_brace 4 | 5 | excluded: 6 | - AppDelegate.swift 7 | - SceneDelegate.swift 8 | 9 | weak_delegate: 10 | excluded: ".*Test\\.swift" 11 | -------------------------------------------------------------------------------- /BoostRunClub/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/11/19. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 13 | return true 14 | } 15 | 16 | // MARK: UISceneSession Lifecycle 17 | 18 | func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration { 19 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 20 | } 21 | 22 | func application(_: UIApplication, didDiscardSceneSessions _: Set) {} 23 | } 24 | -------------------------------------------------------------------------------- /BoostRunClub/Coordinators/ActivityListCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityAllCoordinator.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/09. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | final class ActivityListCoordinator: BasicCoordinator { 12 | let factory: ActivityListSceneFactory 13 | 14 | init(navigationController: UINavigationController, factory: ActivityListSceneFactory = DependencyFactory.shared) { 15 | self.factory = factory 16 | super.init(navigationController: navigationController) 17 | navigationController.view.backgroundColor = .systemBackground 18 | navigationController.setNavigationBarHidden(false, animated: true) 19 | } 20 | 21 | override func start() { 22 | showActivityListViewController() 23 | } 24 | 25 | func showActivityListViewController() { 26 | let listVM = factory.makeActivityListVM() 27 | let listVC = factory.makeActivityListVC(with: listVM) 28 | 29 | listVM.outputs.closeSignal 30 | .receive(on: RunLoop.main) 31 | .sink { [weak self, weak listVC] in 32 | listVC?.navigationController?.popViewController(animated: true) 33 | self?.closeSignal.send() 34 | } 35 | .store(in: &cancellables) 36 | 37 | listVM.outputs.showActivityDetails 38 | .receive(on: RunLoop.main) 39 | .sink { [weak self] in 40 | self?.showActivityDetailScene(activity: $0) 41 | } 42 | .store(in: &cancellables) 43 | 44 | navigationController.pushViewController(listVC, animated: true) 45 | } 46 | 47 | func showActivityDetailScene(activity: Activity) { 48 | let activityDetailCoordinator = ActivityDetailCoordinator( 49 | navigationController: navigationController, 50 | activity: activity 51 | ) 52 | 53 | let uuid = activityDetailCoordinator.identifier 54 | closeSubscription[uuid] = coordinate(coordinator: activityDetailCoordinator) 55 | .receive(on: RunLoop.main) 56 | .sink { [weak self] in self?.release(coordinator: activityDetailCoordinator) } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /BoostRunClub/Coordinators/AppCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppCoordinator.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/11/23. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | final class AppCoordinator: BasicCoordinator { 12 | override func start() { 13 | return showMainFlow() 14 | } 15 | 16 | func showMainFlow() { 17 | let mainTabBarCoordinator = MainTabBarCoordinator(navigationController: navigationController) 18 | 19 | let uuid = mainTabBarCoordinator.identifier 20 | closeSubscription[uuid] = coordinate(coordinator: mainTabBarCoordinator) 21 | .receive(on: RunLoop.main) 22 | .sink { [weak self] in 23 | switch $0 { 24 | case let .running(info): 25 | self?.showRunningScene(goalType: info.type, goalValue: info.value) 26 | } 27 | self?.release(coordinator: mainTabBarCoordinator) 28 | } 29 | } 30 | 31 | func showActivityDetail(activity: Activity, detail: ActivityDetail) { 32 | let mainTabBarCoordinator = MainTabBarCoordinator(navigationController: navigationController) 33 | mainTabBarCoordinator.start(activity: activity, detail: detail) 34 | 35 | let uuid = mainTabBarCoordinator.identifier 36 | closeSubscription[uuid] = mainTabBarCoordinator.closeSignal 37 | .receive(on: RunLoop.main) 38 | .sink { [weak self] in 39 | switch $0 { 40 | case let .running(info): 41 | self?.showRunningScene(goalType: info.type, goalValue: info.value) 42 | } 43 | self?.release(coordinator: mainTabBarCoordinator) 44 | } 45 | } 46 | 47 | func showRunningScene(goalType: GoalType, goalValue: String) { 48 | let goalInfo = GoalInfo(type: goalType, value: goalValue) 49 | let runningPageCoordinator = RunningPageCoordinator( 50 | navigationController: navigationController, 51 | goalInfo: goalInfo 52 | ) 53 | 54 | let uuid = runningPageCoordinator.identifier 55 | closeSubscription[uuid] = coordinate(coordinator: runningPageCoordinator) 56 | .receive(on: RunLoop.main) 57 | .sink { [weak self] in 58 | switch $0 { 59 | case let .activityDetail(activity, detail): 60 | self?.showActivityDetail(activity: activity, detail: detail) 61 | case .prepareRun: 62 | self?.showMainFlow() 63 | } 64 | self?.release(coordinator: runningPageCoordinator) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /BoostRunClub/Coordinators/BasicCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicCoordinator.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/20. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | class BasicCoordinator: Coordinator { 12 | typealias CoordinationResult = ResultType 13 | 14 | let identifier = UUID() 15 | var navigationController: UINavigationController 16 | 17 | var childCoordinators = [UUID: Coordinator]() 18 | var closeSubscription = [UUID: AnyCancellable]() 19 | 20 | var closeSignal = PassthroughSubject() 21 | 22 | var cancellables = Set() 23 | 24 | init(navigationController: UINavigationController) { 25 | self.navigationController = navigationController 26 | navigationController.setNavigationBarHidden(true, animated: true) 27 | print("[Memory \(Date())] 🌈Coordinator🌈 \(Self.self) started") 28 | } 29 | 30 | private func store(coordinator: BasicCoordinator) { 31 | childCoordinators[coordinator.identifier] = coordinator 32 | } 33 | 34 | @discardableResult 35 | func coordinate(coordinator: BasicCoordinator) -> AnyPublisher { 36 | childCoordinators[coordinator.identifier] = coordinator 37 | coordinator.start() 38 | return coordinator.closeSignal.eraseToAnyPublisher() 39 | } 40 | 41 | func release(coordinator: BasicCoordinator) { 42 | let uuid = coordinator.identifier 43 | childCoordinators[uuid] = nil 44 | closeSubscription[uuid]?.cancel() 45 | closeSubscription.removeValue(forKey: uuid) 46 | } 47 | 48 | func start() { 49 | fatalError("start() method must be implemented") 50 | } 51 | 52 | deinit { 53 | print("[Memory \(Date())] 🌈Coordinator💀 \(Self.self) deallocated.") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /BoostRunClub/Coordinators/Coordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Coordinator.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/11/23. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | protocol Coordinator: AnyObject { 12 | var identifier: UUID { get } 13 | var navigationController: UINavigationController { get set } 14 | var childCoordinators: [UUID: Coordinator] { get set } 15 | func start() 16 | } 17 | 18 | extension Coordinator { 19 | func clear() { 20 | childCoordinators.removeAll() 21 | navigationController.children.forEach { $0.removeFromParent() } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BoostRunClub/Coordinators/PausedRunningCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PausedRunningCoordinator.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/01. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | enum PausedRunCoordinationResult { 12 | case runInfo, prepareRun, activityDetail(activity: Activity, detail: ActivityDetail) 13 | } 14 | 15 | final class PausedRunningCoordinator: BasicCoordinator { 16 | let factory: PausedRunningSceneFactory 17 | 18 | init(navigationController: UINavigationController, factory: PausedRunningSceneFactory = DependencyFactory.shared) { 19 | self.factory = factory 20 | super.init(navigationController: navigationController) 21 | } 22 | 23 | override func start() { 24 | showPausedRunningViewController() 25 | } 26 | 27 | func showPausedRunningViewController() { 28 | let pausedRunningVM = factory.makePausedRunningVM() 29 | 30 | pausedRunningVM.outputs.showRunningInfoSignal 31 | .receive(on: RunLoop.main) 32 | .sink { [weak self] in 33 | let result = PausedRunCoordinationResult.runInfo 34 | self?.closeSignal.send(result) 35 | self?.navigationController.popViewController(animated: false) 36 | } 37 | .store(in: &cancellables) 38 | 39 | pausedRunningVM.outputs.showPrepareRunningSignal 40 | .receive(on: RunLoop.main) 41 | .sink { [weak self] in 42 | let result = PausedRunCoordinationResult.prepareRun 43 | self?.closeSignal.send(result) 44 | self?.navigationController.popViewController(animated: false) 45 | } 46 | .store(in: &cancellables) 47 | 48 | pausedRunningVM.outputs.showActivityDetailSignal 49 | .receive(on: RunLoop.main) 50 | .sink { [weak self] in 51 | let result = PausedRunCoordinationResult.activityDetail(activity: $0.activity, detail: $0.detail) 52 | self?.closeSignal.send(result) 53 | self?.navigationController.popViewController(animated: false) 54 | } 55 | .store(in: &cancellables) 56 | 57 | let pausedRunningVC = factory.makePausedRunningVC(with: pausedRunningVM) 58 | navigationController.pushViewController(pausedRunningVC, animated: false) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /BoostRunClub/Coordinators/ProfileCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileCoordinator.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/11/23. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | final class ProfileCoordinator: BasicCoordinator { 12 | let factory: ProfileSceneFactory & EditProfileSceneFactory 13 | 14 | init(navigationController: UINavigationController, 15 | factory: ProfileSceneFactory & EditProfileSceneFactory = DependencyFactory.shared) 16 | { 17 | self.factory = factory 18 | super.init(navigationController: navigationController) 19 | } 20 | 21 | override func start() { 22 | showProfileViewController() 23 | } 24 | 25 | func showProfileViewController() { 26 | let profileVM = factory.makeProfileVM() 27 | profileVM.outputs.showEditProfileSignal 28 | .receive(on: RunLoop.main) 29 | .compactMap { [weak self] in self?.showEditProfileScene() } 30 | .flatMap { $0 } 31 | .sink { [weak profileVM] (profile: Profile) in 32 | profileVM?.inputs.didEditProfile(profile) 33 | } 34 | .store(in: &cancellables) 35 | 36 | let profileVC = factory.makeProfileVC(with: profileVM) 37 | navigationController.pushViewController(profileVC, animated: true) 38 | } 39 | 40 | func showEditProfileScene() -> AnyPublisher { 41 | let editProfileVM = factory.makeEditProfileVM() 42 | let editProfileVC = factory.makeEditProfileVC(with: editProfileVM) 43 | 44 | editProfileVC.modalPresentationStyle = .overFullScreen 45 | navigationController.present(editProfileVC, animated: true, completion: nil) 46 | 47 | return editProfileVM.outputs.closeSignal 48 | .receive(on: RunLoop.main) 49 | .map { [weak editProfileVC] (profile: Profile) -> Profile in 50 | editProfileVC?.dismiss(animated: true, completion: nil) 51 | return profile 52 | } 53 | .eraseToAnyPublisher() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /BoostRunClub/Coordinators/RunningCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningCoordinator.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/01. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | enum RunningCoordinationResult { 12 | case prepareRun 13 | case activityDetail(activity: Activity, detail: ActivityDetail) 14 | } 15 | 16 | final class RunningCoordinator: BasicCoordinator { 17 | override init(navigationController: UINavigationController) { 18 | super.init(navigationController: navigationController) 19 | navigationController.pushViewController(UIViewController(), animated: false) 20 | } 21 | 22 | override func start() { 23 | showRunningInfoScene(isResume: false) 24 | } 25 | 26 | func showRunningInfoScene(isResume: Bool) { 27 | let runInfoCoordinator = RunningInfoCoordinator(navigationController: navigationController, isResume: isResume) 28 | 29 | let uuid = runInfoCoordinator.identifier 30 | closeSubscription[uuid] = coordinate(coordinator: runInfoCoordinator) 31 | .receive(on: RunLoop.main) 32 | .sink { [weak self] in 33 | switch $0 { 34 | case .pausedRun: 35 | self?.showPausedRunningScene() 36 | } 37 | self?.release(coordinator: runInfoCoordinator) 38 | } 39 | } 40 | 41 | func showPausedRunningScene() { 42 | let pausedRunningCoordinator = PausedRunningCoordinator(navigationController: navigationController) 43 | 44 | let uuid = pausedRunningCoordinator.identifier 45 | closeSubscription[uuid] = coordinate(coordinator: pausedRunningCoordinator) 46 | .receive(on: RunLoop.main) 47 | .sink { [weak self] in 48 | switch $0 { 49 | case .runInfo: 50 | self?.showRunningInfoScene(isResume: true) 51 | case .prepareRun: 52 | let result = RunningCoordinationResult.prepareRun 53 | self?.closeSignal.send(result) 54 | case let .activityDetail(activity, detail): 55 | let result = RunningCoordinationResult.activityDetail(activity: activity, detail: detail) 56 | self?.closeSignal.send(result) 57 | } 58 | self?.release(coordinator: pausedRunningCoordinator) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /BoostRunClub/Coordinators/RunningInfoCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningInfoCoordinator.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/11/30. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | enum RunningInfoCoordinationResult { 12 | case pausedRun 13 | } 14 | 15 | final class RunningInfoCoordinator: BasicCoordinator { 16 | let factory: RunningInfoSceneFactory 17 | var isResumed: Bool 18 | init(navigationController: UINavigationController, isResume: Bool, factory: RunningInfoSceneFactory = DependencyFactory.shared) { 19 | isResumed = isResume 20 | self.factory = factory 21 | super.init(navigationController: navigationController) 22 | } 23 | 24 | override func start() { 25 | showRunningInfoViewController() 26 | } 27 | 28 | func showRunningInfoViewController() { 29 | let runningInfoVM = factory.makeRunningInfoVM(isResumed: isResumed) 30 | 31 | runningInfoVM.outputs.showPausedRunningSignal 32 | .receive(on: RunLoop.main) 33 | .sink { [weak self] in 34 | let result = RunningInfoCoordinationResult.pausedRun 35 | self?.closeSignal.send(result) 36 | self?.navigationController.popViewController(animated: false) 37 | } 38 | .store(in: &cancellables) 39 | 40 | let runningInfoVC = factory.makeRunningInfoVC(with: runningInfoVM) 41 | navigationController.pushViewController(runningInfoVC, animated: false) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /BoostRunClub/Coordinators/RunningMapCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningMapCoordinator.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/11/30. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | final class RunningMapCoordinator: BasicCoordinator { 12 | let factory: RunningMapSceneFactory 13 | 14 | init(navigationController: UINavigationController, factory: RunningMapSceneFactory = DependencyFactory.shared) { 15 | self.factory = factory 16 | super.init(navigationController: navigationController) 17 | } 18 | 19 | override func start() { 20 | showRunningMapViewController() 21 | } 22 | 23 | func showRunningMapViewController() { 24 | let runningMapVM = factory.makeRunningMapVM() 25 | runningMapVM.outputs.closeSignal 26 | .sink { [weak self] in self?.closeSignal.send() } 27 | .store(in: &cancellables) 28 | 29 | let runningMapVC = factory.makeRunningMapVC(with: runningMapVM) 30 | navigationController.pushViewController(runningMapVC, animated: true) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /BoostRunClub/Coordinators/RunningPageCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningPageCoordinator.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/11/30. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | enum RunningPageCoordinationResult { 12 | case prepareRun 13 | case activityDetail(activity: Activity, detail: ActivityDetail) 14 | } 15 | 16 | final class RunningPageCoordinator: BasicCoordinator { 17 | let factory: RunningPageContainerFactory 18 | let goalInfo: GoalInfo 19 | init( 20 | navigationController: UINavigationController, 21 | factory: RunningPageContainerFactory = DependencyFactory.shared, 22 | goalInfo: GoalInfo 23 | ) { 24 | self.factory = factory 25 | self.goalInfo = goalInfo 26 | super.init(navigationController: navigationController) 27 | } 28 | 29 | override func start() { 30 | prepareRunningPageController() 31 | } 32 | 33 | private func prepareRunningPageController() { 34 | let mapCoordinator = RunningMapCoordinator(navigationController: UINavigationController()) 35 | let runningCoordinator = RunningCoordinator(navigationController: UINavigationController()) 36 | let splitsCoordinator = SplitsCoordinator(navigationController: UINavigationController()) 37 | 38 | let closablePublisherWithoutRelease = coordinate(coordinator: mapCoordinator) 39 | coordinate(coordinator: splitsCoordinator) 40 | let closablePublisher = coordinate(coordinator: runningCoordinator) 41 | 42 | let runningPageVM = factory.makeRunningPageVM(goalInfo: goalInfo) 43 | let runningPageVC = factory.makeRunningPageVC( 44 | with: runningPageVM, 45 | viewControllers: [ 46 | mapCoordinator.navigationController, 47 | runningCoordinator.navigationController, 48 | splitsCoordinator.navigationController, 49 | ] 50 | ) 51 | 52 | navigationController.viewControllers = [runningPageVC] 53 | 54 | let uuid = runningCoordinator.identifier 55 | closeSubscription[uuid] = closablePublisher 56 | .receive(on: RunLoop.main) 57 | .sink { [weak self] in 58 | switch $0 { 59 | case let .activityDetail(activity, detail): 60 | let result = RunningPageCoordinationResult.activityDetail(activity: activity, detail: detail) 61 | self?.closeSignal.send(result) 62 | case .prepareRun: 63 | let result = RunningPageCoordinationResult.prepareRun 64 | self?.closeSignal.send(result) 65 | } 66 | self?.release(coordinator: mapCoordinator) 67 | self?.release(coordinator: runningCoordinator) 68 | self?.release(coordinator: splitsCoordinator) 69 | } 70 | 71 | closeSubscription[mapCoordinator.identifier] = closablePublisherWithoutRelease 72 | .sink { runningPageVM.inputs.didTapGoBackButton() } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /BoostRunClub/Coordinators/SplitsViewCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitsViewCoordinator.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/11/30. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | final class SplitsCoordinator: BasicCoordinator { 12 | let factory: SplitSceneFactory 13 | 14 | init(navigationController: UINavigationController, factory: SplitSceneFactory = DependencyFactory.shared) { 15 | self.factory = factory 16 | super.init(navigationController: navigationController) 17 | navigationController.setNavigationBarHidden(false, animated: false) 18 | } 19 | 20 | override func start() { 21 | showSplitsViewController() 22 | } 23 | 24 | func showSplitsViewController() { 25 | let splitsVM = factory.makeSplitVM() 26 | let splitsVC = factory.makeSplitVC(with: splitsVM) 27 | navigationController.pushViewController(splitsVC, animated: true) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /BoostRunClub/CoreData/BRCModel.xcdatamodeld/BRCModel.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /BoostRunClub/CoreData/ZActivity+Activity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZActivity+Activity.swift 3 | // BoostRunClub 4 | // 5 | // Created by 조기현 on 2020/12/06. 6 | // 7 | 8 | import CoreData 9 | import Foundation 10 | 11 | @objc(ZActivity) 12 | public class ZActivity: NSManagedObject { 13 | @NSManaged public var avgPace: Int32 14 | @NSManaged public var createdAt: Date? 15 | @NSManaged public var finishedAt: Date? 16 | @NSManaged public var distance: Double 17 | @NSManaged public var duration: Double 18 | @NSManaged public var thumbnail: Data? 19 | @NSManaged public var uuid: UUID? 20 | @NSManaged public var elevation: Double 21 | } 22 | 23 | extension ZActivity { 24 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 25 | return NSFetchRequest(entityName: "ZActivity") 26 | } 27 | 28 | @discardableResult 29 | convenience init(context: NSManagedObjectContext, activity: Activity) { 30 | self.init(context: context) 31 | avgPace = Int32(activity.avgPace) 32 | distance = activity.distance 33 | uuid = activity.uuid 34 | thumbnail = activity.thumbnail 35 | createdAt = activity.createdAt 36 | finishedAt = activity.finishedAt 37 | duration = activity.duration 38 | } 39 | 40 | var activity: Activity? { 41 | Activity( 42 | avgPace: Int(avgPace), 43 | distance: distance, 44 | duration: duration, 45 | elevation: elevation, 46 | thumbnail: thumbnail, 47 | createdAt: createdAt, 48 | finishedAt: finishedAt, 49 | uuid: uuid 50 | ) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /BoostRunClub/CoreData/ZActivityDetail+ActivityDetail.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZActivityDetail+ActivityDetail.swift 3 | // BoostRunClub 4 | // 5 | // Created by 조기현 on 2020/12/06. 6 | // 7 | 8 | import CoreData 9 | import Foundation 10 | 11 | @objc(ZActivityDetail) 12 | public class ZActivityDetail: NSManagedObject { 13 | @NSManaged public var activityUUID: UUID? 14 | @NSManaged public var avgBPM: Int32 15 | @NSManaged public var cadence: Int32 16 | @NSManaged public var calorie: Int32 17 | @NSManaged public var elevation: Int32 18 | @NSManaged public var locations: Data? 19 | @NSManaged public var splits: Data? 20 | } 21 | 22 | extension ZActivityDetail { 23 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 24 | return NSFetchRequest(entityName: "ZActivityDetail") 25 | } 26 | 27 | public class func fetchRequest(activityId: UUID) -> NSFetchRequest { 28 | let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "ZActivityDetail") 29 | fetchRequest.predicate = NSPredicate(format: "activityUUID == %@", activityId as CVarArg) 30 | return fetchRequest 31 | } 32 | 33 | @discardableResult 34 | convenience init(context: NSManagedObjectContext, activityDetail: ActivityDetail) { 35 | self.init(context: context) 36 | activityUUID = activityDetail.activityUUID 37 | avgBPM = Int32(activityDetail.avgBPM) 38 | cadence = Int32(activityDetail.cadence) 39 | calorie = Int32(activityDetail.calorie) 40 | elevation = Int32(activityDetail.elevation) 41 | let encoder = JSONEncoder() 42 | locations = try? encoder.encode(activityDetail.locations) 43 | splits = try? encoder.encode(activityDetail.splits) 44 | } 45 | 46 | var activityDetail: ActivityDetail? { 47 | var location: [Location] = [] 48 | var split: [RunningSplit] = [] 49 | let decoder = JSONDecoder() 50 | 51 | if 52 | let locationData = locations, 53 | let locations = try? decoder.decode([Location].self, from: locationData) 54 | { 55 | location.append(contentsOf: locations) 56 | } 57 | if 58 | let splitData = splits, 59 | let splits = try? decoder.decode([RunningSplit].self, from: splitData) 60 | { 61 | split.append(contentsOf: splits) 62 | } 63 | 64 | return ActivityDetail( 65 | activityUUID: activityUUID, 66 | avgBPM: Int(avgBPM), 67 | cadence: Int(cadence), 68 | calorie: Int(calorie), 69 | elevation: Int(elevation), 70 | locations: location, 71 | splits: split 72 | ) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /BoostRunClub/CoreML/RunningMotionClassifier.mlmodel: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project18-B-iOS-BoostRunClub/61a9e892e3a363ad381ee1ca12c4147c17947975/BoostRunClub/CoreML/RunningMotionClassifier.mlmodel -------------------------------------------------------------------------------- /BoostRunClub/Extensions/CaseIterable+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CaseIterable+.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/11/27. 6 | // 7 | 8 | import Foundation 9 | 10 | extension CaseIterable where Self: Equatable, AllCases: BidirectionalCollection { 11 | func next() -> Self? { 12 | let all = Self.allCases 13 | let idx = all.firstIndex(of: self)! 14 | let next = all.index(after: idx) 15 | return next == all.endIndex ? nil : all[next] 16 | } 17 | 18 | func circularNext() -> Self { 19 | return next() ?? Self.allCases.first! 20 | } 21 | 22 | func prev() -> Self? { 23 | let all = Self.allCases 24 | let idx = all.firstIndex(of: self)! 25 | return idx == all.startIndex ? nil : all[all.index(before: idx)] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/Data+SaveAndLoadData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Data+SaveAndLoadData.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/10. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Data { 11 | static func loadImageDataFromDocumentsDirectory(fileName: String) -> Data? { 12 | let fileURL = URL.documents.appendingPathComponent(fileName) 13 | do { 14 | let imageData = try Data(contentsOf: fileURL) 15 | return imageData 16 | } catch { 17 | print(error) 18 | } 19 | return nil 20 | } 21 | 22 | static func saveImageDataToDocumentsDirectory(fileName: String, imageData: Data) { 23 | let url = URL.documents.appendingPathComponent(fileName) 24 | do { 25 | try imageData.write(to: url) 26 | } catch { 27 | print(error) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/Date/Date+DateRange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date+.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/08. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Date { 11 | func rangeOf(type: ActivityFilterType) -> DateRange? { 12 | switch type { 13 | case .week: 14 | return rangeOfWeek 15 | case .month: 16 | return rangeOfMonth 17 | case .year: 18 | return rangeOfYear 19 | case .all: 20 | return nil 21 | } 22 | } 23 | 24 | var rangeOfWeek: DateRange? { 25 | var calendar = Calendar.current 26 | calendar.firstWeekday = 2 27 | let components = calendar.dateComponents([.yearForWeekOfYear, .weekOfYear], from: self) 28 | guard let startOfWeek = calendar.date(from: components), 29 | let endOfWeek = calendar.date(byAdding: .day, value: 7, to: startOfWeek) 30 | else { return nil } 31 | return DateRange(start: startOfWeek, end: endOfWeek) 32 | } 33 | 34 | var rangeOfMonth: DateRange? { 35 | let calendar = Calendar.current 36 | let startComponents = calendar.dateComponents([.year, .month], from: self) 37 | var endComponents = DateComponents() 38 | endComponents.month = 1 39 | endComponents.second = -1 40 | guard let startOfMonth = calendar.date(from: startComponents), 41 | let endOfMonth = calendar.date(byAdding: endComponents, to: startOfMonth) 42 | else { return nil } 43 | 44 | return DateRange(start: startOfMonth, end: endOfMonth) 45 | } 46 | 47 | var rangeOfYear: DateRange? { 48 | let calendar = Calendar.current 49 | let startComponents = calendar.dateComponents([.year], from: self) 50 | var endComponents = DateComponents() 51 | endComponents.year = 1 52 | endComponents.second = -1 53 | 54 | guard let startOfMonth = calendar.date(from: startComponents), 55 | let endOfMonth = calendar.date(byAdding: endComponents, to: startOfMonth) 56 | else { return nil } 57 | 58 | return DateRange(start: startOfMonth, end: endOfMonth) 59 | } 60 | 61 | static func numberOfWeeks(range: DateRange) -> Int? { 62 | let calendar = Calendar.current 63 | let components = calendar.dateComponents([.weekOfMonth], from: range.start, to: range.end) 64 | return components.weekOfMonth == 0 ? 1 : components.weekOfMonth 65 | } 66 | 67 | static func isSameWeek(date: Date, dateOfWeek: Date) -> Bool { 68 | dateOfWeek.rangeOfWeek?.contains(date: date) ?? false 69 | } 70 | 71 | static func isSameYear(date: Date, dateOfYear: Date) -> Bool { 72 | dateOfYear.rangeOfYear?.contains(date: date) ?? false 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/Date/Date+String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date+String.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/08. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Date { 11 | typealias YearMonthDay = (year: Int, month: Int, day: Int) 12 | typealias HourMinSec = (hour: Int, min: Int, sec: Int) 13 | 14 | var toMDString: String { 15 | DateFormatter.MDFormatter.string(from: self) 16 | } 17 | 18 | var toYMDHMString: String { 19 | DateFormatter.YMDHMFormatter.string(from: self) 20 | } 21 | 22 | var toYMDString: String { 23 | DateFormatter.YMDFormatter.string(from: self) 24 | } 25 | 26 | var toYMString: String { 27 | DateFormatter.YMFormatter.string(from: self) 28 | } 29 | 30 | var toYString: String { 31 | DateFormatter.YFormatter.string(from: self) 32 | } 33 | 34 | var toHMString: String { 35 | DateFormatter.HMFormatter.string(from: self) 36 | } 37 | 38 | var toDayOfWeekString: String { 39 | DateFormatter.KRDayOfWeekFormatter.string(from: self) 40 | } 41 | 42 | var toMDEString: String { 43 | DateFormatter.MDEFormatter.string(from: self) 44 | } 45 | 46 | var toPHM: String { 47 | period + " " + toHMString 48 | } 49 | 50 | var yearMonthDay: YearMonthDay? { 51 | let str = DateFormatter.YMDFormatter.string(from: self) 52 | let components = str.components(separatedBy: ".") 53 | guard 54 | components.count >= 3, 55 | let year = Int(components[0]), 56 | let day = Int(components[1]), 57 | let month = Int(components[2]) 58 | else { return nil } 59 | 60 | return (year, day, month) 61 | } 62 | 63 | var hourMinSec: HourMinSec? { 64 | let str = DateFormatter.HMSFormatter.string(from: self) 65 | let components = str.components(separatedBy: ":") 66 | guard 67 | components.count >= 3, 68 | let hour = Int(components[0]), 69 | let min = Int(components[1]), 70 | let sec = Int(components[2]) 71 | else { return nil } 72 | 73 | return (hour, min, sec) 74 | } 75 | 76 | var period: String { 77 | guard let hourMinSec = self.hourMinSec else { return "알수없는 시간대" } 78 | switch hourMinSec.hour { 79 | case 22 ... 24, 0 ..< 3: 80 | return "야간" 81 | case 3 ..< 6: 82 | return "새벽" 83 | case 6 ..< 12: 84 | return "오전" 85 | case 12 ..< 18: 86 | return "오후" 87 | case 18 ..< 22: 88 | return "저녁" 89 | default: 90 | return "" 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/Date/DateFormatter+Common.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateFormatter.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/08. 6 | // 7 | 8 | import Foundation 9 | 10 | extension DateFormatter { 11 | static var YMDHMFormatter: DateFormatter = { 12 | let fmt = DateFormatter() 13 | fmt.dateFormat = "yyyy.MM.dd HH:mm" 14 | return fmt 15 | }() 16 | 17 | static var MDFormatter: DateFormatter = { 18 | let fmt = DateFormatter() 19 | fmt.dateFormat = "MM.dd." 20 | return fmt 21 | }() 22 | 23 | static var YMFormatter: DateFormatter = { 24 | let fmt = DateFormatter() 25 | fmt.dateFormat = "yyyy년 MM월" 26 | return fmt 27 | }() 28 | 29 | static var YFormatter: DateFormatter = { 30 | let fmt = DateFormatter() 31 | fmt.dateFormat = "yyyy년" 32 | return fmt 33 | }() 34 | 35 | static var YMDFormatter: DateFormatter = { 36 | let fmt = DateFormatter() 37 | fmt.dateFormat = "yyyy.MM.dd" 38 | return fmt 39 | }() 40 | 41 | static var HMSFormatter: DateFormatter = { 42 | let fmt = DateFormatter() 43 | fmt.dateFormat = "HH:mm:ss" 44 | return fmt 45 | }() 46 | 47 | static var HMFormatter: DateFormatter = { 48 | let fmt = DateFormatter() 49 | fmt.dateFormat = "h:mm" 50 | return fmt 51 | }() 52 | 53 | static var MDEFormatter: DateFormatter = { 54 | let fmt = DateFormatter() 55 | fmt.locale = Locale(identifier: "ko") 56 | fmt.dateFormat = "M월 d일 EEEE" 57 | return fmt 58 | }() 59 | 60 | static var KRDayOfWeekFormatter: DateFormatter = { 61 | let fmt = DateFormatter() 62 | fmt.locale = Locale(identifier: "ko") 63 | fmt.dateFormat = "EEEE" 64 | return fmt 65 | }() 66 | } 67 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/Date/TimerInterval+String.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimerInterval+String.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/01. 6 | // 7 | 8 | import Foundation 9 | 10 | extension TimeInterval { 11 | var simpleFormattedString: String { 12 | let interval = Int(self) 13 | let times = (interval / 60) / 60 14 | let seconds = interval % 60 15 | let minutes = (interval / 60) % 60 16 | if times > 0 { 17 | return String(format: "%02d:%02d", times, minutes) 18 | } 19 | return String(format: "%02d:%02d", minutes, seconds) 20 | } 21 | 22 | var fullFormattedString: String { 23 | let interval = Int(self) 24 | let times = (interval / 60) / 60 25 | let seconds = interval % 60 26 | let minutes = (interval / 60) % 60 27 | return String(format: "%02d:%02d:%02d", times, minutes, seconds) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/MapKit/CLLocationCoordinate+computeSplitCoordinate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CLLocationCoordinate+computeSplitCoordinate.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/15. 6 | // 7 | 8 | import CoreLocation 9 | import Foundation 10 | 11 | extension CLLocationCoordinate2D { 12 | static func computeSplitCoordinate(from points: [CLLocationCoordinate2D], distance: Double) -> [CLLocationCoordinate2D] { 13 | guard let first = points.first else { return [CLLocationCoordinate2D]() } 14 | var previousPoint = first 15 | let initialValue: Double = 0.0 16 | var splitCoordinates = [CLLocationCoordinate2D]() 17 | 18 | points.reduce(initialValue) { (acculmatedDistance, currentPoint) -> Double in 19 | let addedDistance = acculmatedDistance + CLLocation( 20 | latitude: previousPoint.latitude, 21 | longitude: previousPoint.longitude 22 | ).distance(from: CLLocation( 23 | latitude: currentPoint.latitude, 24 | longitude: currentPoint.longitude 25 | )) 26 | 27 | if addedDistance >= distance { 28 | splitCoordinates.append(currentPoint) 29 | return 0 30 | } else { 31 | previousPoint = currentPoint 32 | return addedDistance 33 | } 34 | } 35 | 36 | return splitCoordinates 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/Math/Clamp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Clamp.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/13. 6 | // 7 | 8 | import Foundation 9 | 10 | func clamped(value: T, minValue: T, maxValue: T) -> T where T: Comparable { 11 | return max(minValue, min(value, maxValue)) 12 | } 13 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/Math/Double+Radian+Degree.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Double+Radian+Degree.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/19. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Double { 11 | static func degreesToRadians(_ degrees: Double) -> Double { 12 | return degrees * Double.pi / 180.0 13 | } 14 | 15 | static func radiansToDegrees(_ radians: Double) -> Double { 16 | return radians * 180.0 / Double.pi 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/Notification.name.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Notification.name.swift 3 | // BoostRunClub 4 | // 5 | // Created by 조기현 on 2020/11/25. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Notification.Name { 11 | static let showRunningScene = Notification.Name("showRunningScene") 12 | static let showPausedRunningScene = Notification.Name("showPausedRunningScene") 13 | static let showRunningInfoScene = Notification.Name("showRunningInfoScene") 14 | static let showPrepareRunningScene = Notification.Name("showPrepareRunningScene") 15 | } 16 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/String+Regex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Regex.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/11/26. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | enum RegexPattern { 12 | case distance, time 13 | var patternString: String { 14 | switch self { 15 | case .distance: 16 | return "^([0-9]{0,2}(\\.[0-9]{0,2})?|\\.?\\[0-9]{1,2})$" 17 | case .time: 18 | return "" 19 | } 20 | } 21 | } 22 | 23 | static func ~= (lhs: String, rhs: String) -> Bool { 24 | guard let regex = try? NSRegularExpression(pattern: rhs) else { return false } 25 | let range = NSRange(location: 0, length: lhs.utf16.count) 26 | return regex.firstMatch(in: lhs, options: [], range: range) != nil 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/UIKit/UIButton+setArrowImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIButton+setArrowImage.swift 3 | // BoostRunClub 4 | // 5 | // Created by 조기현 on 2020/12/15. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIButton { 11 | enum ArrowDirection: String { 12 | case left, right 13 | } 14 | 15 | func setArrowImage(dir: ArrowDirection, color: UIColor = .black) { 16 | let arrow = UIImage(systemName: "arrow.\(dir.rawValue)") 17 | setImage(arrow, for: .normal) 18 | switch dir { 19 | case .left: 20 | semanticContentAttribute = .forceLeftToRight 21 | contentEdgeInsets.left = 20 22 | contentEdgeInsets.right = 30 23 | titleEdgeInsets.left = 5 24 | titleEdgeInsets.right = -5 25 | 26 | case .right: 27 | semanticContentAttribute = .forceRightToLeft 28 | contentEdgeInsets.left = 30 29 | contentEdgeInsets.right = 20 30 | titleEdgeInsets.left = -5 31 | titleEdgeInsets.right = 5 32 | } 33 | tintColor = color 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/UIKit/UIButton+setSFSymbol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIButton+setSFSymbol.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/02. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIButton { 11 | func setSFSymbol(iconName: String, size: CGFloat, weight: UIImage.SymbolWeight = .regular, 12 | scale: UIImage.SymbolScale = .default, tintColor: UIColor, backgroundColor: UIColor) 13 | { 14 | let buttonImage = UIImage.SFSymbol( 15 | name: iconName, 16 | size: size, 17 | weight: weight, 18 | scale: scale, 19 | color: tintColor 20 | ) 21 | setImage(buttonImage, for: .normal) 22 | self.tintColor = tintColor 23 | self.backgroundColor = backgroundColor 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/UIKit/UIColor+Components.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor+RGBA.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/15. 6 | // 7 | 8 | import UIKit 9 | 10 | typealias ColorRGBA = (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) 11 | typealias ColorHSBA = (hue: CGFloat, saturation: CGFloat, brightness: CGFloat, alpha: CGFloat) 12 | 13 | extension UIColor { 14 | var rgba: ColorRGBA { 15 | var red: CGFloat = 0 16 | var green: CGFloat = 0 17 | var blue: CGFloat = 0 18 | var alpha: CGFloat = 0 19 | getRed(&red, green: &green, blue: &blue, alpha: &alpha) 20 | return (red, green, blue, alpha) 21 | } 22 | 23 | var hsba: ColorHSBA { 24 | var hue: CGFloat = 0 25 | var saturation: CGFloat = 0 26 | var brightness: CGFloat = 0 27 | var alpha: CGFloat = 0 28 | getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) 29 | return (hue, saturation, brightness, alpha) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/UIKit/UIImage+CustomAnnotation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+CustomAnnotation.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/13. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension UIImage { 12 | static func customSplitAnnotation(type: CustomAnnotationType, title: String = " ", color: UIColor = .white) -> UIImage { 13 | let label = UILabel() 14 | label.textColor = .black 15 | label.text = title 16 | label.textAlignment = .center 17 | 18 | switch type { 19 | case .split: 20 | label.frame.size = CGSize(width: label.intrinsicContentSize.width + CGFloat(23), height: label.intrinsicContentSize.height) 21 | label.backgroundColor = color 22 | label.layer.cornerRadius = label.bounds.height * 6 / 10 23 | case .point: 24 | label.frame.size = CGSize(width: CGFloat(10), height: CGFloat(10)) 25 | label.layer.cornerRadius = label.bounds.height / 2 26 | label.layer.borderWidth = 5 27 | label.layer.borderColor = UIColor.white.cgColor 28 | label.backgroundColor = color 29 | } 30 | 31 | label.layer.masksToBounds = true 32 | UIGraphicsBeginImageContextWithOptions(label.bounds.size, false, 0.0) 33 | label.layer.render(in: UIGraphicsGetCurrentContext()!) 34 | 35 | guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return UIImage() } 36 | return image 37 | } 38 | 39 | enum CustomAnnotationType { 40 | case point, split 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/UIKit/UIImage+SFSymbol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage+SFSymbol.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/03. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIImage { 11 | static func SFSymbol( 12 | name: String, 13 | size: CGFloat, 14 | weight: UIImage.SymbolWeight, 15 | scale: UIImage.SymbolScale, 16 | color: UIColor = .label, 17 | renderingMode: UIImage.RenderingMode = .automatic 18 | ) -> UIImage? { 19 | let configuration = UIImage.SymbolConfiguration(pointSize: size, 20 | weight: weight, 21 | scale: scale) 22 | return UIImage.SFSymbol(name: name, 23 | configuration: configuration, 24 | color: color, 25 | renderingMode: renderingMode) 26 | } 27 | 28 | static func SFSymbol( 29 | name: String, 30 | configuration: UIImage.SymbolConfiguration? = nil, 31 | color: UIColor = .label, 32 | renderingMode: UIImage.RenderingMode = .alwaysOriginal 33 | ) -> UIImage? { 34 | if let configuration = configuration { 35 | return UIImage(systemName: name, withConfiguration: configuration)? 36 | .withTintColor(color, renderingMode: renderingMode) 37 | } else { 38 | return UIImage(systemName: name)?.withTintColor(color, renderingMode: renderingMode) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/UIKit/UILabel+Make.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UILabel+Make.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/12. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UILabel { 11 | static func makeBold(text: String = "Value", size: CGFloat = 20, color: UIColor = .label) -> UILabel { 12 | let label = UILabel() 13 | label.font = UIFont.boldSystemFont(ofSize: size) 14 | label.textColor = color 15 | label.text = text 16 | return label 17 | } 18 | 19 | static func makeNormal(text: String = "Value", size: CGFloat = 17, color: UIColor = .systemGray) -> UILabel { 20 | let label = UILabel() 21 | label.font = UIFont.systemFont(ofSize: size) 22 | label.textColor = color 23 | label.text = text 24 | return label 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/UIKit/UINavigationController+setStatusBar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UINavigationController+setStatusBar.swift 3 | // BoostRunClub 4 | // 5 | // Created by 조기현 on 2020/12/13. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UINavigationController { 11 | // 사용하는 뷰컨에서 viewDidDisappear 시 removeFromParent로 없애줄 것 12 | func setStatusBar(backgroundColor: UIColor) -> UIView { 13 | let statusBarFrame: CGRect 14 | if #available(iOS 13.0, *) { 15 | statusBarFrame = view.window?.windowScene?.statusBarManager?.statusBarFrame ?? CGRect.zero 16 | } else { 17 | statusBarFrame = UIApplication.shared.statusBarFrame 18 | } 19 | let statusBarView = UIView(frame: statusBarFrame) 20 | statusBarView.backgroundColor = backgroundColor 21 | view.addSubview(statusBarView) 22 | return statusBarView 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/UIKit/UIStackView+Make.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIStackView+Make.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/07. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIStackView { 11 | static func make( 12 | with subviews: [UIView], 13 | axis: NSLayoutConstraint.Axis = .horizontal, 14 | alignment: Alignment = .fill, 15 | distribution: Distribution = .fill, 16 | spacing: CGFloat = 0 17 | ) -> UIStackView { 18 | let view = UIStackView(arrangedSubviews: subviews) 19 | view.axis = axis 20 | view.alignment = alignment 21 | view.distribution = distribution 22 | view.spacing = spacing 23 | return view 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/UIKit/UIView+identifier.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+identifier.swift 3 | // BoostRunClub 4 | // 5 | // Created by 조기현 on 2020/12/09. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIView { 11 | static var identifier: String { 12 | String(describing: self) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/UIKit/UIView+notificationFeedback.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+otificationFeedback.swift 3 | // BoostRunClub 4 | // 5 | // Created by 조기현 on 2020/12/03. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIView { 11 | func notificationFeedback() { 12 | print("\(#function), \(Date())") 13 | let notificationFeedbackGenerator = UINotificationFeedbackGenerator() 14 | notificationFeedbackGenerator.prepare() 15 | notificationFeedbackGenerator.notificationOccurred(.success) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BoostRunClub/Extensions/URL+Documents.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL+Documents.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/10. 6 | // 7 | 8 | import Foundation 9 | 10 | extension URL { 11 | static var documents: URL { 12 | return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BoostRunClub/Factory/DependencyFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceProvider.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/01. 6 | // 7 | 8 | import Foundation 9 | 10 | class DependencyFactory { 11 | static let shared = DependencyFactory() 12 | 13 | // Service For Storage 14 | lazy var defaultsProvider = DefaultsProvider() 15 | lazy var coreDataService = CoreDataService() 16 | lazy var activityStorageService = ActivityStorageService(coreDataService: coreDataService) 17 | 18 | // Provider For RunningService 19 | lazy var locationProvider = LocationProvider() 20 | lazy var pedometerProvider = PedometerProvider() 21 | lazy var motionDataModelProvider = MotionDataModelProvider() 22 | lazy var runningSnapShotProvider = RunningSnapShotProvider() 23 | 24 | // Running Service 25 | lazy var runningDataService = RunningService( 26 | motionProvider: runningMotionService, 27 | dashBoard: runningDashBoardService, 28 | recoder: runningRecordService 29 | ) 30 | lazy var runningMotionService = RunningMotionService( 31 | motionDataModelProvider: motionDataModelProvider, 32 | locationProvider: locationProvider 33 | ) 34 | lazy var runningDashBoardService = RunningDashBoardService( 35 | eventTimer: EventTimeProvider(), 36 | locationProvider: locationProvider, 37 | pedometerProvider: pedometerProvider 38 | ) 39 | lazy var runningRecordService = RunningRecordService( 40 | activityWriter: activityStorageService, 41 | mapSnapShotter: runningSnapShotProvider 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /BoostRunClub/Factory/Extensions/Factory+ActivityDetailScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Factory+ActivityDetailScene.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/12. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol ActivityDetailSceneFactory { 11 | func makeActivityDetailVM(activity: Activity, detail: ActivityDetail?) -> ActivityDetailViewModelTypes? 12 | func makeActivityDetailVC(with: ActivityDetailViewModelTypes) -> UIViewController 13 | 14 | func makeSplitInfoDetailVM(activity: Activity) -> SplitInfoDetailViewModelType? 15 | func makeSplitInfoDetailVC(with viewModel: SplitInfoDetailViewModelType) -> UIViewController 16 | } 17 | 18 | extension DependencyFactory: ActivityDetailSceneFactory { 19 | func makeActivityDetailVM(activity: Activity, detail: ActivityDetail?) -> ActivityDetailViewModelTypes? { 20 | ActivityDetailViewModel(activity: activity, detail: detail, activityService: activityStorageService) 21 | } 22 | 23 | func makeActivityDetailVC(with viewModel: ActivityDetailViewModelTypes) -> UIViewController { 24 | ActivityDetailViewController(with: viewModel) 25 | } 26 | 27 | func makeSplitInfoDetailVM(activity: Activity) -> SplitInfoDetailViewModelType? { 28 | SplitInfoDetailViewModel(activity: activity, activityReader: activityStorageService) 29 | } 30 | 31 | func makeSplitInfoDetailVC(with viewModel: SplitInfoDetailViewModelType) -> UIViewController { 32 | SplitInfoDetailViewController(with: viewModel) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /BoostRunClub/Factory/Extensions/Factory+ActivityListScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Factory+AllActivitiesScene.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/09. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol ActivityListSceneFactory { 11 | func makeActivityListVC(with viewModel: ActivityListViewModelTypes) -> UIViewController 12 | func makeActivityListVM() -> ActivityListViewModelTypes 13 | } 14 | 15 | extension DependencyFactory: ActivityListSceneFactory { 16 | func makeActivityListVC(with viewModel: ActivityListViewModelTypes) -> UIViewController { 17 | ActivityListViewController(with: viewModel) 18 | } 19 | 20 | func makeActivityListVM() -> ActivityListViewModelTypes { 21 | ActivityListViewModel(activityReader: activityStorageService) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BoostRunClub/Factory/Extensions/Factory+ActivityScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Factory+ActivityScene.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/06. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol ActivitySceneFactory { 11 | func makeActivityVC(with viewModel: ActivityViewModelTypes) -> UIViewController 12 | func makeActivityVM() -> ActivityViewModelTypes 13 | 14 | func makeActivityDateFilterVC( 15 | with viewModel: ActivityDateFilterViewModelTypes, 16 | tabHeight: CGFloat 17 | ) -> UIViewController 18 | 19 | func makeActivityDateFilterVM( 20 | filterType: ActivityFilterType, 21 | dateRanges: [DateRange], 22 | currentRange: DateRange 23 | ) -> ActivityDateFilterViewModelTypes 24 | } 25 | 26 | extension DependencyFactory: ActivitySceneFactory { 27 | func makeActivityVC(with viewModel: ActivityViewModelTypes) -> UIViewController { 28 | return ActivityViewController(with: viewModel) 29 | } 30 | 31 | func makeActivityVM() -> ActivityViewModelTypes { 32 | return ActivityViewModel(activityReader: activityStorageService) 33 | } 34 | 35 | func makeActivityDateFilterVC( 36 | with viewModel: ActivityDateFilterViewModelTypes, 37 | tabHeight: CGFloat 38 | ) -> UIViewController { 39 | return ActivityDateFilterViewController(with: viewModel, tabHeight: tabHeight) 40 | } 41 | 42 | func makeActivityDateFilterVM( 43 | filterType: ActivityFilterType, 44 | dateRanges: [DateRange], 45 | currentRange: DateRange 46 | ) -> ActivityDateFilterViewModelTypes { 47 | return ActivityDateFilterViewModel( 48 | filterType: filterType, 49 | dateRanges: dateRanges, 50 | currentRange: currentRange 51 | ) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /BoostRunClub/Factory/Extensions/Factory+EditProfileScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Factory+EditProfileScene.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/09. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol EditProfileSceneFactory { 11 | func makeEditProfileVC(with viewModel: EditProfileViewModelTypes) -> UIViewController 12 | func makeEditProfileVM() -> EditProfileViewModelTypes 13 | } 14 | 15 | extension DependencyFactory: EditProfileSceneFactory { 16 | func makeEditProfileVC(with viewModel: EditProfileViewModelTypes) -> UIViewController { 17 | EditProfileViewController(with: viewModel) 18 | } 19 | 20 | func makeEditProfileVM() -> EditProfileViewModelTypes { 21 | EditProfileViewModel(defaults: defaultsProvider) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BoostRunClub/Factory/Extensions/Factory+LoginScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Factory+LoginScene.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/06. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol LoginSceneFactory { 11 | // func makeLoginVC(with viewModel: LoginViewModelTypes) -> UIViewController 12 | // func makeLoginVM() -> LoginViewModelTypes 13 | } 14 | 15 | extension DependencyFactory: LoginSceneFactory {} 16 | -------------------------------------------------------------------------------- /BoostRunClub/Factory/Extensions/Factory+PausedRunningScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Factory+PausedRunningScene.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/06. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol PausedRunningSceneFactory { 11 | func makePausedRunningVC(with viewModel: PausedRunningViewModelTypes) -> UIViewController 12 | func makePausedRunningVM() -> PausedRunningViewModelTypes 13 | } 14 | 15 | extension DependencyFactory: PausedRunningSceneFactory { 16 | func makePausedRunningVC(with viewModel: PausedRunningViewModelTypes) -> UIViewController { 17 | PausedRunningViewController(with: viewModel) 18 | } 19 | 20 | func makePausedRunningVM() -> PausedRunningViewModelTypes { 21 | PausedRunningViewModel(runningService: runningDataService) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BoostRunClub/Factory/Extensions/Factory+PrepareRunScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Factory+PrepareRunScene.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/06. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol PrepareRunSceneFactory { 11 | func makePrepareRunVC(with viewModel: PrepareRunViewModelTypes) -> UIViewController 12 | func makePrepareRunVM() -> PrepareRunViewModelTypes 13 | 14 | func makeGoalValueSetupVC(with viewModel: GoalValueSetupViewModelTypes) -> UIViewController 15 | func makeGoalValueSetupVM(goalType: GoalType, goalValue: String) -> GoalValueSetupViewModelTypes 16 | 17 | func makeGoalTypeVC(with viewModel: GoalTypeViewModelTypes) -> UIViewController 18 | func makeGoalTypeVM(goalType: GoalType) -> GoalTypeViewModelTypes 19 | } 20 | 21 | extension DependencyFactory: PrepareRunSceneFactory { 22 | func makePrepareRunVC(with viewModel: PrepareRunViewModelTypes) -> UIViewController { 23 | PrepareRunViewController(with: viewModel) 24 | } 25 | 26 | func makePrepareRunVM() -> PrepareRunViewModelTypes { 27 | PrepareRunViewModel(locationProvider: locationProvider) 28 | } 29 | 30 | func makeGoalTypeVC(with viewModel: GoalTypeViewModelTypes) -> UIViewController { 31 | GoalTypeViewController(with: viewModel) 32 | } 33 | 34 | func makeGoalTypeVM(goalType: GoalType) -> GoalTypeViewModelTypes { 35 | GoalTypeViewModel(goalType: goalType) 36 | } 37 | 38 | func makeGoalValueSetupVC(with viewModel: GoalValueSetupViewModelTypes) -> UIViewController { 39 | GoalValueSetupViewController(with: viewModel) 40 | } 41 | 42 | func makeGoalValueSetupVM(goalType: GoalType, goalValue: String) -> GoalValueSetupViewModelTypes { 43 | GoalValueSetupViewModel(goalType: goalType, goalValue: goalValue) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /BoostRunClub/Factory/Extensions/Factory+ProfileScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Factory+ProfileScene.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/06. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol ProfileSceneFactory { 11 | func makeProfileVC(with viewModel: ProfileViewModelTypes) -> UIViewController 12 | func makeProfileVM() -> ProfileViewModelTypes 13 | } 14 | 15 | extension DependencyFactory: ProfileSceneFactory { 16 | func makeProfileVC(with viewModel: ProfileViewModelTypes) -> UIViewController { 17 | ProfileViewController(with: viewModel) 18 | } 19 | 20 | func makeProfileVM() -> ProfileViewModelTypes { 21 | ProfileViewModel(defaults: defaultsProvider) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BoostRunClub/Factory/Extensions/Factory+RouteDetailScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Factory+RouteDetailScene.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/13. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol RouteDetailSceneFactory { 11 | func makeRouteDetailVC(with viewModel: RouteDetailViewModelTypes) -> UIViewController 12 | func makeRouteDetailVM(activityDetailConfig: ActivityDetailConfig) -> RouteDetailViewModelTypes 13 | } 14 | 15 | extension DependencyFactory: RouteDetailSceneFactory { 16 | func makeRouteDetailVC(with viewModel: RouteDetailViewModelTypes) -> UIViewController { 17 | RouteDetailViewController(with: viewModel) 18 | } 19 | 20 | func makeRouteDetailVM(activityDetailConfig: ActivityDetailConfig) -> RouteDetailViewModelTypes { 21 | RouteDetailViewModel(activityDetailConfig: activityDetailConfig) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BoostRunClub/Factory/Extensions/Factory+RunningInfoScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Factory+RunningInfoScene.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/06. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol RunningInfoSceneFactory { 11 | func makeRunningInfoVC(with viewModel: RunningInfoViewModelTypes) -> UIViewController 12 | func makeRunningInfoVM(isResumed: Bool) -> RunningInfoViewModelTypes 13 | } 14 | 15 | extension DependencyFactory: RunningInfoSceneFactory { 16 | func makeRunningInfoVC(with viewModel: RunningInfoViewModelTypes) -> UIViewController { 17 | RunningInfoViewController(with: viewModel) 18 | } 19 | 20 | func makeRunningInfoVM(isResumed: Bool) -> RunningInfoViewModelTypes { 21 | RunningInfoViewModel(runningService: runningDataService, resumed: isResumed) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BoostRunClub/Factory/Extensions/Factory+RunningMapScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Factory+RunningMapScene.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/06. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol RunningMapSceneFactory { 11 | func makeRunningMapVC(with viewModel: RunningMapViewModelTypes) -> UIViewController 12 | func makeRunningMapVM() -> RunningMapViewModelTypes 13 | } 14 | 15 | extension DependencyFactory: RunningMapSceneFactory { 16 | func makeRunningMapVC(with viewModel: RunningMapViewModelTypes) -> UIViewController { 17 | RunningMapViewController(with: viewModel) 18 | } 19 | 20 | func makeRunningMapVM() -> RunningMapViewModelTypes { 21 | RunningMapViewModel(runningService: runningDataService) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BoostRunClub/Factory/Extensions/Factory+RunningPageContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Factory+RunningPageContainer.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/06. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol RunningPageContainerFactory { 11 | func makeRunningPageVC( 12 | with viewModel: RunningPageViewModelTypes, 13 | viewControllers: [UIViewController] 14 | ) 15 | -> UIViewController 16 | func makeRunningPageVM(goalInfo: GoalInfo?) -> RunningPageViewModelTypes 17 | } 18 | 19 | extension DependencyFactory: RunningPageContainerFactory { 20 | func makeRunningPageVC( 21 | with viewModel: RunningPageViewModelTypes, 22 | viewControllers: [UIViewController] 23 | ) -> UIViewController { 24 | RunningPageViewController(with: viewModel, viewControllers: viewControllers) 25 | } 26 | 27 | func makeRunningPageVM(goalInfo: GoalInfo? = nil) -> RunningPageViewModelTypes { 28 | runningDataService.setGoal(goalInfo) 29 | return RunningPageViewModel(runningService: runningDataService) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /BoostRunClub/Factory/Extensions/Factory+SplitScene.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Factory+SplitScene.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/06. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol SplitSceneFactory { 11 | func makeSplitVC(with viewModel: SplitsViewModelTypes) -> UIViewController 12 | func makeSplitVM() -> SplitsViewModelTypes 13 | func makeRunningSplitCellVM() -> RunningSplitCellViewModelType 14 | } 15 | 16 | extension DependencyFactory: SplitSceneFactory { 17 | func makeRunningSplitCellVM() -> RunningSplitCellViewModelType { 18 | RunningSplitCellViewModel() 19 | } 20 | 21 | func makeSplitVC(with viewModel: SplitsViewModelTypes) -> UIViewController { 22 | SplitsViewController(with: viewModel) 23 | } 24 | 25 | func makeSplitVM() -> SplitsViewModelTypes { 26 | SplitsViewModel(runningService: runningDataService) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /BoostRunClub/Factory/Extensions/Factory+TabBarContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Factory+TabBarContainer.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/06. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol TabBarContainerFactory { 11 | func makeTabBarVC(with viewControllers: [UIViewController], selectedIndex: Int) -> UITabBarController 12 | } 13 | 14 | extension DependencyFactory: TabBarContainerFactory { 15 | func makeTabBarVC(with viewControllers: [UIViewController], selectedIndex: Int) -> UITabBarController { 16 | let tabBarController = UITabBarController() 17 | tabBarController.setViewControllers(viewControllers, animated: true) 18 | tabBarController.selectedIndex = selectedIndex 19 | // tabBarController.tabBar.isTranslucent = false // TODO: false true 비교 20 | tabBarController.tabBar.tintColor = TabBarPage.selectColor 21 | tabBarController.tabBar.unselectedItemTintColor = TabBarPage.unselectColor 22 | tabBarController.tabBar.barTintColor = .systemBackground 23 | viewControllers[0].tabBarItem = UITabBarItem(title: nil, image: UIImage(named: "activity4"), tag: 0) 24 | viewControllers[1].tabBarItem = UITabBarItem(title: nil, image: UIImage(named: "running4"), tag: 1) 25 | viewControllers[2].tabBarItem = UITabBarItem(title: nil, image: UIImage(named: "profile4"), tag: 2) 26 | viewControllers.forEach { 27 | $0.tabBarItem.imageInsets = UIEdgeInsets(top: 10, left: 0, bottom: -10, right: 0) 28 | } 29 | return tabBarController 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /BoostRunClub/Models/Activity.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Activity.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/06. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Activity { 11 | var avgPace: Int 12 | var distance: Double 13 | var duration: Double 14 | var thumbnail: Data? 15 | var createdAt: Date 16 | var finishedAt: Date 17 | var uuid: UUID 18 | var elevation: Double 19 | 20 | init?( 21 | avgPace: Int, 22 | distance: Double, 23 | duration: Double, 24 | elevation: Double, 25 | thumbnail: Data?, 26 | createdAt: Date?, 27 | finishedAt: Date?, 28 | uuid: UUID? 29 | ) { 30 | guard 31 | let createdAt = createdAt, 32 | let finishedAt = finishedAt, 33 | let uuid = uuid 34 | else { return nil } 35 | self.avgPace = avgPace 36 | self.distance = distance 37 | self.duration = duration 38 | self.elevation = elevation 39 | self.thumbnail = thumbnail 40 | self.createdAt = createdAt 41 | self.finishedAt = finishedAt 42 | self.uuid = uuid 43 | } 44 | 45 | init( 46 | avgPace: Int, 47 | distance: Double, 48 | duration: Double, 49 | elevation: Double, 50 | thumbnail: Data?, 51 | createdAt: Date, 52 | finishedAt: Date, 53 | uuid: UUID 54 | ) { 55 | self.avgPace = avgPace 56 | self.distance = distance 57 | self.duration = duration 58 | self.elevation = elevation 59 | self.thumbnail = thumbnail 60 | self.createdAt = createdAt 61 | self.finishedAt = finishedAt 62 | self.uuid = uuid 63 | } 64 | } 65 | 66 | extension Activity: Comparable { 67 | static func < (lhs: Activity, rhs: Activity) -> Bool { 68 | lhs.createdAt < rhs.createdAt 69 | } 70 | } 71 | 72 | extension Activity { 73 | var titleText: String { 74 | "\(createdAt.toDayOfWeekString) \(createdAt.period) 러닝 " 75 | } 76 | 77 | func dateText(with date: Date) -> String { 78 | if Date.isSameWeek(date: createdAt, dateOfWeek: date) { 79 | return createdAt.toDayOfWeekString 80 | } else { 81 | return createdAt.toYMDString 82 | } 83 | } 84 | 85 | // TODO: avgPace, distance, time 등 단위 변환이 겹치는 것 공통으로 처리하도록 하기 86 | var avgPaceText: String { 87 | String(format: "%d'%d\"", avgPace / 60, avgPace % 60) 88 | } 89 | 90 | var distanceText: String { 91 | String(format: "%.2f", distance / 1000) 92 | } 93 | 94 | var runningTimeText: String { 95 | TimeInterval(duration).fullFormattedString 96 | } 97 | 98 | var elapsedTimeText: String { 99 | (finishedAt.timeIntervalSinceReferenceDate - createdAt.timeIntervalSinceReferenceDate).fullFormattedString 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /BoostRunClub/Models/ActivityDetail.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityDetail.swift 3 | // BoostRunClub 4 | // 5 | // Created by 조기현 on 2020/12/06. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ActivityDetail { 11 | var activityUUID: UUID 12 | var avgBPM: Int 13 | var cadence: Int 14 | var calorie: Int 15 | var elevation: Int 16 | var locations: [Location] 17 | var splits: [RunningSplit] 18 | 19 | init?( 20 | activityUUID: UUID?, 21 | avgBPM: Int, 22 | cadence: Int, 23 | calorie: Int, 24 | elevation: Int, 25 | locations: [Location], 26 | splits: [RunningSplit] 27 | ) { 28 | guard let activityUUID = activityUUID else { return nil } 29 | self.activityUUID = activityUUID 30 | self.avgBPM = avgBPM 31 | self.cadence = cadence 32 | self.calorie = calorie 33 | self.elevation = elevation 34 | self.locations = locations 35 | self.splits = splits 36 | } 37 | 38 | init( 39 | activityUUID: UUID, 40 | avgBPM: Int, 41 | cadence: Int, 42 | calorie: Int, 43 | elevation: Int, 44 | locations: [Location], 45 | splits: [RunningSplit] 46 | ) { 47 | self.activityUUID = activityUUID 48 | self.avgBPM = avgBPM 49 | self.cadence = cadence 50 | self.calorie = calorie 51 | self.elevation = elevation 52 | self.locations = locations 53 | self.splits = splits 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /BoostRunClub/Models/ActivityFilterType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityFilterType.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/07. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ActivityFilterType: Int { 11 | case week, month, year, all 12 | 13 | func groupDateRanges(from activities: [Activity]) -> [DateRange] { 14 | guard !activities.isEmpty else { return [] } 15 | 16 | if self == .all { 17 | return [ 18 | DateRange( 19 | start: activities.last?.createdAt ?? activities.first!.createdAt, 20 | end: activities.first!.createdAt 21 | ), 22 | ] 23 | } 24 | 25 | return activities 26 | .reduce(into: [DateRange]()) { result, activity in 27 | if 28 | result.isEmpty, 29 | let range = activity.createdAt.rangeOf(type: self) 30 | { 31 | result.append(range) 32 | } else if 33 | let lastRange = result.last, 34 | !lastRange.contains(date: activity.createdAt), 35 | let newRange = activity.createdAt.rangeOf(type: self) 36 | { 37 | result.append(newRange) 38 | } 39 | } 40 | } 41 | 42 | func rangeDescription(at range: DateRange, from date: Date = Date()) -> String { 43 | switch self { 44 | case .week: 45 | if Date.isSameWeek(date: range.start, dateOfWeek: date) { 46 | return "이번 주" 47 | } else if 48 | let lastWeekDate = Calendar.current.date(byAdding: .day, value: -7, to: date), 49 | Date.isSameWeek(date: range.start, dateOfWeek: lastWeekDate) 50 | { 51 | return "저번 주" 52 | } 53 | 54 | if Date.isSameYear(date: range.start, dateOfYear: date) { 55 | return range.start.toMDString + "~" + range.end.toMDString 56 | } 57 | return range.start.toYMDString + "~" + range.end.toYMDString 58 | case .month: 59 | return range.end.toYMString 60 | case .year: 61 | return range.end.toYString 62 | case .all: 63 | let from = range.start.toYString 64 | let end = range.end.toYString 65 | return from == end ? end : from + "-" + end 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /BoostRunClub/Models/DateRange.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateRange.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/08. 6 | // 7 | 8 | import Foundation 9 | 10 | struct DateRange: Equatable { 11 | let start: Date 12 | let end: Date 13 | } 14 | 15 | extension DateRange { 16 | func contains(date: Date) -> Bool { 17 | start <= date && date <= end 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /BoostRunClub/Models/GoalType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GoalType.swift 3 | // BoostRunClub 4 | // 5 | // Created by 조기현 on 2020/11/26. 6 | // 7 | 8 | enum GoalType: Int { 9 | case distance, time, speed, none 10 | 11 | var description: String { 12 | switch self { 13 | case .distance: 14 | return "거리" 15 | case .time: 16 | return "시간" 17 | case .speed: 18 | return "속도" 19 | case .none: 20 | return "목표 설정" 21 | } 22 | } 23 | 24 | var initialValue: String { 25 | switch self { 26 | case .distance: 27 | return "5.00" 28 | case .time: 29 | return "00:30" 30 | case .speed, .none: 31 | return "" 32 | } 33 | } 34 | 35 | var unit: String { 36 | switch self { 37 | case .distance: 38 | return "킬로미터" 39 | case .time: 40 | return "시간 : 분" 41 | case .speed, .none: 42 | return "" 43 | } 44 | } 45 | } 46 | 47 | struct GoalInfo: Equatable { 48 | let type: GoalType 49 | let value: String 50 | } 51 | -------------------------------------------------------------------------------- /BoostRunClub/Models/Location.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Location.swift 3 | // BoostRunClub 4 | // 5 | // Created by 조기현 on 2020/12/06. 6 | // 7 | 8 | import CoreLocation 9 | import Foundation 10 | 11 | struct Location: Codable { 12 | let longitude: Double 13 | let latitude: Double 14 | let altitude: Double 15 | let speed: Int 16 | let timestamp: Date 17 | } 18 | 19 | extension Location { 20 | init(clLocation: CLLocation) { 21 | self.init(longitude: clLocation.coordinate.longitude, 22 | latitude: clLocation.coordinate.latitude, 23 | altitude: Double(clLocation.altitude), 24 | speed: Int(clLocation.speed), 25 | timestamp: clLocation.timestamp) 26 | } 27 | 28 | var coord2d: CLLocationCoordinate2D { 29 | CLLocationCoordinate2D(latitude: latitude, longitude: longitude) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /BoostRunClub/Models/MotionType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MotionType.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/15. 6 | // 7 | 8 | import Foundation 9 | 10 | enum MotionType { 11 | case running, standing 12 | 13 | var METFactor: Double { 14 | switch self { 15 | case .running: 16 | return 1.035 17 | case .standing: 18 | return 0 19 | // case _ where walking: 20 | // return 0.655 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BoostRunClub/Models/Profile.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Profile.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/09. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Profile: Equatable { 11 | var image: Data? 12 | var lastName: String 13 | var firstName: String 14 | var hometown: String 15 | var bio: String 16 | } 17 | -------------------------------------------------------------------------------- /BoostRunClub/Models/RunningEvent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningEvent.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/14. 6 | // 7 | 8 | import Foundation 9 | 10 | enum RunningEvent { 11 | case start, resume, pause, stop, goal(String) 12 | 13 | var text: String { 14 | switch self { 15 | case .start: 16 | return "Welcome to Boost Run Club. Starting Workout." 17 | case .resume: 18 | return "Resume Workout." 19 | case .pause: 20 | return "Pause Workout" 21 | case .stop: 22 | return "Ending Workout. Great job today." 23 | case let .goal(text): 24 | return text 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /BoostRunClub/Models/RunningInfoType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningInfoType.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/11/27. 6 | // 7 | 8 | import Foundation 9 | 10 | enum RunningInfoType: CaseIterable { 11 | case pace, averagePace, bpm, calorie, time, kilometer, cadence, interval, meter 12 | 13 | var initialValue: String { 14 | switch self { 15 | case .pace: 16 | return "0\'00\"" 17 | case .averagePace: 18 | return "0\'00\"" 19 | case .bpm: 20 | return "0" 21 | case .calorie: 22 | return "0" 23 | case .time: 24 | return "00:00" 25 | case .kilometer: 26 | return "0" 27 | case .cadence: 28 | return "0" 29 | case .interval: 30 | return "0" 31 | case .meter: 32 | return "0" 33 | } 34 | } 35 | 36 | var name: String { 37 | switch self { 38 | case .pace: 39 | return "페이스" 40 | case .averagePace: 41 | return "평균 페이스" 42 | case .bpm: 43 | return "BPM" 44 | case .calorie: 45 | return "칼로리" 46 | case .time: 47 | return "시간" 48 | case .kilometer: 49 | return "킬로미터" 50 | case .cadence: 51 | return "케이던스" 52 | case .interval: 53 | return "인터벌" 54 | case .meter: 55 | return "미터" 56 | } 57 | } 58 | 59 | static func getPossibleTypes(from goalType: GoalType) -> [Self] { 60 | switch goalType { 61 | case .distance, .time, .none: 62 | return RunningInfoType.allCases.filter { $0 != .meter && $0 != .interval } 63 | case .speed: 64 | return RunningInfoType.allCases 65 | } 66 | } 67 | } 68 | 69 | struct RunningInfo { 70 | let type: RunningInfoType 71 | let value: String 72 | 73 | init(type: RunningInfoType, value: String) { 74 | self.type = type 75 | self.value = value 76 | } 77 | 78 | init(type: RunningInfoType) { 79 | self.type = type 80 | value = type.initialValue 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /BoostRunClub/Models/RunningSlice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningSlice.swift 3 | // BoostRunClub 4 | // 5 | // Created by 조기현 on 2020/12/03. 6 | // 7 | 8 | import CoreLocation.CLLocation 9 | import Foundation 10 | 11 | struct RunningSlice: Codable { 12 | var startIndex: Int = 0 13 | var endIndex: Int = -1 14 | var distance: Double = 0 15 | var isRunning: Bool = false 16 | 17 | mutating func setupSlice(with runningStates: [RunningState]) { 18 | if startIndex < endIndex { 19 | distance = runningStates[endIndex].distance - runningStates[startIndex].distance 20 | } else { 21 | distance = 0 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /BoostRunClub/Models/RunningSplit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningSplit.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/03. 6 | // 7 | 8 | import CoreLocation.CLLocation 9 | import Foundation 10 | 11 | struct RunningSplit: Codable { 12 | var avgPace: Int = 0 13 | var distance: Double = 0 14 | var elevation: Int = 0 15 | var runningSlices = [RunningSlice]() 16 | 17 | mutating func setupSplit(avgPace: Int, elevation: Int) { 18 | self.avgPace = avgPace 19 | self.elevation = elevation 20 | distance = runningSlices.reduce(into: Double(0)) { $0 += $1.distance } 21 | } 22 | 23 | mutating func setup(with states: [RunningState]) { 24 | guard 25 | let start = runningSlices.first?.startIndex, 26 | let end = runningSlices.last?.endIndex 27 | else { return } 28 | let endIdx = end < states.count ? end : states.count - 1 29 | 30 | let sumOutput = states[start ... endIdx] 31 | .reduce( 32 | into: ( 33 | distance: Double(0), 34 | pace: Int(0), 35 | minElv: Double(9999), 36 | maxElv: Double(0) 37 | )) { 38 | $0.distance += $1.distance 39 | $0.pace += $1.pace 40 | $0.minElv = min($0.minElv, $1.location.altitude) 41 | $0.maxElv = max($0.maxElv, $1.location.altitude) 42 | } 43 | distance = states[endIdx].distance - states[start].distance 44 | avgPace = sumOutput.pace / (endIdx - start + 1) 45 | elevation = Int(sumOutput.maxElv - sumOutput.minElv) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /BoostRunClub/Models/RunningState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningState.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/16. 6 | // 7 | 8 | import CoreLocation 9 | import Foundation 10 | 11 | struct RunningState { 12 | var location: CLLocation 13 | var runningTime: TimeInterval 14 | var calorie: Double 15 | var pace: Int 16 | var avgPace: Int 17 | var cadence: Int 18 | var distance: Double 19 | var isRunning: Bool = false 20 | } 21 | -------------------------------------------------------------------------------- /BoostRunClub/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/11/19. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | var window: UIWindow? 12 | var appCoordinator: AppCoordinator? 13 | 14 | func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) { 15 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 16 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 17 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 18 | guard let windowScene = (scene as? UIWindowScene) else { return } 19 | window = UIWindow(windowScene: windowScene) 20 | let navigationController = UINavigationController() 21 | window?.rootViewController = navigationController 22 | window?.makeKeyAndVisible() 23 | let appCoordinator = AppCoordinator(navigationController: navigationController) 24 | appCoordinator.start() 25 | self.appCoordinator = appCoordinator 26 | } 27 | 28 | func sceneDidDisconnect(_: UIScene) {} 29 | 30 | func sceneDidBecomeActive(_: UIScene) {} 31 | 32 | func sceneWillResignActive(_: UIScene) {} 33 | 34 | func sceneWillEnterForeground(_: UIScene) {} 35 | 36 | func sceneDidEnterBackground(_: UIScene) {} 37 | } 38 | -------------------------------------------------------------------------------- /BoostRunClub/Services/CoreDataStorage/ActivityStorageService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityStorageService.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/06. 6 | // 7 | 8 | import Combine 9 | import CoreData 10 | import Foundation 11 | 12 | protocol ActivityWritable { 13 | func addActivity(activity: Activity, activityDetail: ActivityDetail) 14 | func editActivity(activity: Activity) 15 | } 16 | 17 | protocol ActivityReadable { 18 | var activityChangeSignal: PassthroughSubject { get } 19 | 20 | func fetchActivities() -> [Activity] 21 | func fetchActivityDetail(activityId: UUID) -> ActivityDetail? 22 | } 23 | 24 | protocol ActivityStorageServiceable { 25 | var reader: ActivityReadable { get } 26 | var writer: ActivityWritable { get } 27 | } 28 | 29 | class ActivityStorageService: ActivityWritable, ActivityReadable { 30 | let coreDataService: CoreDataServiceable 31 | 32 | init(coreDataService: CoreDataServiceable) { 33 | self.coreDataService = coreDataService 34 | } 35 | 36 | func addActivity(activity: Activity, activityDetail: ActivityDetail) { 37 | ZActivity(context: coreDataService.context, activity: activity) 38 | ZActivityDetail(context: coreDataService.context, activityDetail: activityDetail) 39 | 40 | do { 41 | try coreDataService.context.save() 42 | activityChangeSignal.send() 43 | } catch { 44 | print(error.localizedDescription) 45 | } 46 | } 47 | 48 | func editActivity(activity _: Activity) {} 49 | 50 | func fetchActivities() -> [Activity] { 51 | let request = NSFetchRequest(entityName: "ZActivity") 52 | 53 | do { 54 | let result = try coreDataService.context.fetch(request) 55 | return result.compactMap { $0.activity } 56 | } catch { 57 | print(error.localizedDescription) 58 | } 59 | return [] 60 | } 61 | 62 | func fetchActivityDetail(activityId: UUID) -> ActivityDetail? { 63 | let request: NSFetchRequest = ZActivityDetail.fetchRequest(activityId: activityId) 64 | do { 65 | let result = try coreDataService.context.fetch(request) 66 | return result.compactMap { $0.activityDetail }.first 67 | } catch { 68 | print(error.localizedDescription) 69 | } 70 | return nil 71 | } 72 | 73 | var activityChangeSignal = PassthroughSubject() 74 | } 75 | 76 | extension ActivityStorageService: ActivityStorageServiceable { 77 | var reader: ActivityReadable { self } 78 | var writer: ActivityWritable { self } 79 | } 80 | -------------------------------------------------------------------------------- /BoostRunClub/Services/CoreDataStorage/CoreDataService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataService.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/06. 6 | // 7 | 8 | import CoreData 9 | import Foundation 10 | 11 | protocol CoreDataServiceable { 12 | var context: NSManagedObjectContext { get } 13 | } 14 | 15 | class CoreDataService: CoreDataServiceable { 16 | lazy var persistentContainer: NSPersistentContainer = { 17 | let container = NSPersistentContainer(name: "BRCModel") 18 | container.loadPersistentStores { description, error in 19 | print(description) 20 | guard let error = error as NSError? else { return } 21 | fatalError("persistent store error: \(error)") 22 | } 23 | return container 24 | }() 25 | 26 | var context: NSManagedObjectContext { 27 | persistentContainer.viewContext 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /BoostRunClub/Services/Provider/DefaultsManagable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultsManagable.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/20. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol DefaultsWritable { 11 | func set(_ value: Any?, forKey: String) 12 | } 13 | 14 | protocol DefaultsReadable { 15 | func string(forKey: String) -> String? 16 | } 17 | 18 | protocol DefaultsManagable { 19 | var reader: DefaultsReadable { get } 20 | var writer: DefaultsWritable { get } 21 | } 22 | -------------------------------------------------------------------------------- /BoostRunClub/Services/Provider/DefaultsProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultsProvider.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/11. 6 | // 7 | 8 | import Foundation 9 | 10 | class DefaultsProvider: DefaultsWritable, DefaultsReadable { 11 | var defaults = UserDefaults.standard 12 | 13 | func set(_ value: Any?, forKey: String) { 14 | defaults.set(value, forKey: forKey) 15 | } 16 | 17 | func string(forKey: String) -> String? { 18 | defaults.string(forKey: forKey) 19 | } 20 | } 21 | 22 | extension DefaultsProvider: DefaultsManagable { 23 | var reader: DefaultsReadable { self } 24 | var writer: DefaultsWritable { self } 25 | } 26 | -------------------------------------------------------------------------------- /BoostRunClub/Services/Provider/EventTimeProvidable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventTimeProvidable.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/20. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | protocol EventTimeProvidable { 12 | var timeIntervalSubject: PassthroughSubject { get } 13 | func start() 14 | func stop() 15 | } 16 | -------------------------------------------------------------------------------- /BoostRunClub/Services/Provider/EventTimeProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventTimer.swift 3 | // BoostRunClub 4 | // 5 | // Created by 조기현 on 2020/12/02. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | class EventTimeProvider: EventTimeProvidable { 12 | var timeIntervalSubject = PassthroughSubject() 13 | 14 | private var cancellable: AnyCancellable? 15 | 16 | func start() { 17 | cancellable = Timer.TimerPublisher(interval: 0.8, runLoop: RunLoop.main, mode: .common) 18 | .autoconnect() 19 | .sink { date in 20 | self.timeIntervalSubject.send(date.timeIntervalSinceReferenceDate) 21 | } 22 | } 23 | 24 | func stop() { 25 | cancellable?.cancel() 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /BoostRunClub/Services/Provider/LocationProvidable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationProvidable.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/20. 6 | // 7 | 8 | import Combine 9 | import CoreLocation 10 | import Foundation 11 | 12 | protocol LocationProvidable { 13 | var locationSubject: PassthroughSubject { get } 14 | func startBackgroundTask() 15 | func stopBackgroundTask() 16 | } 17 | -------------------------------------------------------------------------------- /BoostRunClub/Services/Provider/LocationProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationProvider.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/11/24. 6 | // 7 | 8 | import Combine 9 | import CoreLocation 10 | import Foundation 11 | 12 | class LocationProvider: NSObject, LocationProvidable { 13 | let locationManager: CLLocationManager 14 | private(set) var locationSubject = PassthroughSubject() 15 | 16 | init(locationManager: CLLocationManager = CLLocationManager()) { 17 | self.locationManager = locationManager 18 | super.init() 19 | configureLocationManager() 20 | } 21 | 22 | private func configureLocationManager() { 23 | locationManager.delegate = self 24 | locationManager.requestWhenInUseAuthorization() 25 | locationManager.requestAlwaysAuthorization() 26 | locationManager.startUpdatingLocation() 27 | locationManager.allowsBackgroundLocationUpdates = false 28 | locationManager.showsBackgroundLocationIndicator = true 29 | } 30 | 31 | func startBackgroundTask() { 32 | locationManager.allowsBackgroundLocationUpdates = true 33 | locationManager.pausesLocationUpdatesAutomatically = false 34 | } 35 | 36 | func stopBackgroundTask() { 37 | locationManager.allowsBackgroundLocationUpdates = false 38 | locationManager.pausesLocationUpdatesAutomatically = true 39 | } 40 | } 41 | 42 | // MARK: - CLLocationManagerDelegate Implementation 43 | 44 | extension LocationProvider: CLLocationManagerDelegate { 45 | func locationManager(_: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 46 | guard let location = locations.last, 47 | location.horizontalAccuracy.sign == .plus, 48 | // location.verticalAccuracy.sign == .plus, 49 | location.speedAccuracy.sign == .plus, 50 | location.courseAccuracy.sign == .plus 51 | else { return } 52 | 53 | print("[CORE LOCATION] \(location.coordinate), \(location.speed), \(location.horizontalAccuracy)") 54 | if location.horizontalAccuracy > 30 { 55 | return 56 | } 57 | 58 | locationSubject.send(location) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /BoostRunClub/Services/Provider/MotionDataModelProvidable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MotionDataModelProvidable.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/20. 6 | // 7 | 8 | import Combine 9 | import CoreML 10 | import CoreMotion 11 | import Foundation 12 | 13 | protocol MotionDataModelProvidable { 14 | func start() 15 | func stop() 16 | var motionTypeSubject: PassthroughSubject { get } 17 | } 18 | -------------------------------------------------------------------------------- /BoostRunClub/Services/Provider/PedometerProvidable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PedometerProvidable.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/20. 6 | // 7 | 8 | import Combine 9 | import CoreMotion 10 | import Foundation 11 | 12 | protocol PedometerProvidable { 13 | func start() 14 | func stop() 15 | var cadenceSubject: PassthroughSubject { get } 16 | } 17 | -------------------------------------------------------------------------------- /BoostRunClub/Services/Provider/PedometerProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PedometerProvider.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/16. 6 | // 7 | 8 | import Combine 9 | import CoreMotion 10 | import Foundation 11 | 12 | final class PedometerProvider: PedometerProvidable { 13 | private let pedometer = CMPedometer() 14 | private(set) var cadenceSubject = PassthroughSubject() 15 | private var isActive = false 16 | 17 | func start() { 18 | if CMPedometer.isStepCountingAvailable() { 19 | startCountingSteps() 20 | } 21 | } 22 | 23 | func stop() { 24 | pedometer.stopUpdates() 25 | } 26 | 27 | func startCountingSteps() { 28 | if isActive { return } 29 | 30 | isActive = true 31 | 32 | pedometer.startUpdates(from: Date()) { [weak self] pedometerData, error in 33 | 34 | guard 35 | let self = self, 36 | let cadence = pedometerData?.currentCadence, 37 | error == nil 38 | else { return } 39 | 40 | self.cadenceSubject.send(Int(truncating: cadence) * 60) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /BoostRunClub/Services/Provider/RunningSnapShotProvider/MapSnapShotSubscription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapSnapShotSubscription.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/10. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import MapKit 11 | 12 | class MapSnapShotSubscription: Subscription where S.Input == Data?, S.Failure == Error { 13 | private var subscriber: S? 14 | private let snapshotter: MKMapSnapshotter 15 | private let processor: MapSnapShotProcessor 16 | private let queue = DispatchQueue(label: "MapSnapShot", qos: .background, attributes: .concurrent) 17 | 18 | init(subscriber: S, snapshotter: MKMapSnapshotter, processor: MapSnapShotProcessor) { 19 | self.subscriber = subscriber 20 | self.snapshotter = snapshotter 21 | self.processor = processor 22 | start() 23 | } 24 | 25 | func request(_: Subscribers.Demand) {} 26 | 27 | func cancel() { 28 | snapshotter.cancel() 29 | subscriber = nil 30 | } 31 | 32 | private func start() { 33 | snapshotter.start(with: queue) { [subscriber, processor, queue] snapshot, error in 34 | dispatchPrecondition(condition: .onQueue(queue)) 35 | if let error = error { 36 | subscriber?.receive(completion: .failure(error)) 37 | } else { 38 | _ = subscriber?.receive(processor.process(snapShot: snapshot)) 39 | } 40 | } 41 | } 42 | } 43 | 44 | struct MapSnapShotPublisher: Publisher { 45 | typealias Output = Data? 46 | typealias Failure = Error 47 | 48 | private let snapshotter: MKMapSnapshotter 49 | private let processor: MapSnapShotProcessor 50 | 51 | init(snapshotter: MKMapSnapshotter, processor: MapSnapShotProcessor) { 52 | self.snapshotter = snapshotter 53 | self.processor = processor 54 | } 55 | 56 | func receive(subscriber: S) where S: Subscriber, Self.Failure == S.Failure, Self.Output == S.Input { 57 | let subscription = MapSnapShotSubscription( 58 | subscriber: subscriber, 59 | snapshotter: snapshotter, 60 | processor: processor 61 | ) 62 | subscriber.receive(subscription: subscription) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /BoostRunClub/Services/Provider/RunningSnapShotProvider/RouteDrawer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteDrawable.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/10. 6 | // 7 | 8 | import Foundation 9 | import MapKit 10 | import UIKit.UIImage 11 | 12 | protocol RouteDrawable { 13 | func draw( 14 | context: CGContext, 15 | size: CGSize, 16 | locations: [CLLocation], 17 | snapshot: MKMapSnapshotter.Snapshot 18 | ) 19 | } 20 | 21 | struct RouteDrawer: RouteDrawable { 22 | struct Style { 23 | let lineWidth: CGFloat 24 | let lineColors: [UIColor] 25 | } 26 | 27 | let style: Style 28 | 29 | func draw(context: CGContext, size _: CGSize, locations: [CLLocation], snapshot: MKMapSnapshotter.Snapshot) { 30 | guard !style.lineColors.isEmpty else { return } 31 | 32 | context.setLineWidth(style.lineWidth) 33 | context.setLineCap(.round) 34 | context.setLineJoin(.round) 35 | 36 | let points = locations.map { snapshot.point(for: $0.coordinate) } 37 | let path = CGMutablePath() 38 | path.addLines(between: points) 39 | 40 | let lineColor = style.lineColors.first ?? UIColor.black 41 | 42 | context.setStrokeColor(lineColor.cgColor) 43 | context.beginPath() 44 | context.addPath(path) 45 | context.strokePath() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /BoostRunClub/Services/Provider/RunningSnapShotProvider/RouteSnapShotProcessor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteSnapShotProcessor.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/10. 6 | // 7 | 8 | import Foundation 9 | import MapKit 10 | import UIKit.UIImage 11 | 12 | protocol MapSnapShotProcessor { 13 | func process(snapShot: MKMapSnapshotter.Snapshot?) -> Data? 14 | } 15 | 16 | class RouteSnapShotProcessor: MapSnapShotProcessor { 17 | private let drawable: RouteDrawable 18 | private let locations: [CLLocation] 19 | 20 | init(locations: [CLLocation], drawable: RouteDrawable) { 21 | self.drawable = drawable 22 | self.locations = locations 23 | } 24 | 25 | func process(snapShot: MKMapSnapshotter.Snapshot?) -> Data? { 26 | guard let snapShot = snapShot else { return nil } 27 | defer { 28 | UIGraphicsEndImageContext() 29 | } 30 | 31 | UIGraphicsBeginImageContextWithOptions(snapShot.image.size, false, UIScreen.main.scale) 32 | 33 | snapShot.image.draw(at: .zero) 34 | 35 | guard 36 | let context = UIGraphicsGetCurrentContext(), 37 | locations.count > 1 38 | else { 39 | return UIGraphicsGetImageFromCurrentImageContext()?.pngData() 40 | } 41 | 42 | drawable.draw( 43 | context: context, 44 | size: snapShot.image.size, 45 | locations: locations, 46 | snapshot: snapShot 47 | ) 48 | 49 | return UIGraphicsGetImageFromCurrentImageContext()?.pngData() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /BoostRunClub/Services/Provider/RunningSnapShotProvider/RunningSnapShotProvidable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningSnapShotProvidable.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/20. 6 | // 7 | 8 | import Combine 9 | import CoreLocation.CLLocation 10 | import Foundation 11 | 12 | protocol RunningSnapShotProvidable { 13 | func takeSnapShot(from locations: [CLLocation], dimension: Double) -> AnyPublisher 14 | } 15 | -------------------------------------------------------------------------------- /BoostRunClub/Services/Provider/RunningSnapShotProvider/RunningSnapShotProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MapSnapShotService.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/10. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import MapKit 11 | 12 | class RunningSnapShotProvider: RunningSnapShotProvidable { 13 | func takeSnapShot( 14 | from locations: [CLLocation], 15 | dimension: Double 16 | ) -> AnyPublisher { 17 | let region = MKCoordinateRegion.make(from: locations) 18 | let drawable = RouteDrawer(style: .init(lineWidth: 3, lineColors: [.label])) 19 | let processor = RouteSnapShotProcessor(locations: locations, drawable: drawable) 20 | let size = CGSize(width: dimension, height: dimension) 21 | return MKMapSnapshotter.Publisher(region: region, size: size, processor: processor) 22 | .eraseToAnyPublisher() 23 | } 24 | } 25 | 26 | extension MKMapSnapshotter { 27 | // swiftlint:disable:next identifier_name 28 | static func Publisher( 29 | region: MKCoordinateRegion, 30 | size: CGSize, 31 | processor: MapSnapShotProcessor 32 | ) -> MapSnapShotPublisher { 33 | let options = MKMapSnapshotter.Options() 34 | options.region = region 35 | options.size = size 36 | let snapshotter = MKMapSnapshotter(options: options) 37 | return MapSnapShotPublisher(snapshotter: snapshotter, processor: processor) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /BoostRunClub/Services/RunningService/RunningDashBoardServiceable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningDashBoardServicable.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/20. 6 | // 7 | 8 | import Combine 9 | import CoreLocation 10 | import Foundation 11 | 12 | protocol RunningDashBoardServiceable { 13 | var runningStateSubject: PassthroughSubject { get } 14 | var runningTime: CurrentValueSubject { get } 15 | 16 | var location: CLLocation? { get } 17 | var calorie: Double { get } 18 | var pace: Double { get } 19 | var cadence: Int { get } 20 | var distance: Double { get } 21 | var avgPace: Double { get } 22 | 23 | func setState(isRunning: Bool) 24 | func start() 25 | func stop() 26 | func clear() 27 | } 28 | -------------------------------------------------------------------------------- /BoostRunClub/Services/RunningService/RunningMotionService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MotionClassifier.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/19. 6 | // 7 | 8 | import Combine 9 | import CoreMotion 10 | import Foundation 11 | 12 | class RunningMotionService: RunningMotionServiceable { 13 | private var motionDataModelProvider: MotionDataModelProvidable 14 | private var locationProvider: LocationProvidable 15 | 16 | var motionTypeSubject = PassthroughSubject() 17 | var cancellables = Set() 18 | 19 | init( 20 | motionDataModelProvider: MotionDataModelProvidable, 21 | locationProvider: LocationProvidable 22 | ) { 23 | self.motionDataModelProvider = motionDataModelProvider 24 | self.locationProvider = locationProvider 25 | } 26 | 27 | func start() { 28 | bindProvider() 29 | motionDataModelProvider.start() 30 | } 31 | 32 | private func bindProvider() { 33 | motionDataModelProvider.motionTypeSubject 34 | .receive(on: RunLoop.main) 35 | .sink { 36 | self.motionTypeSubject.send($0) 37 | } 38 | .store(in: &cancellables) 39 | 40 | // locationProvider.locationSubject 41 | } 42 | 43 | func stop() { 44 | cancellables.forEach { $0.cancel() } 45 | cancellables.removeAll() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /BoostRunClub/Services/RunningService/RunningMotionServiecable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningMotionServicable.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/20. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | protocol RunningMotionServiceable { 12 | var motionTypeSubject: PassthroughSubject { get } 13 | func start() 14 | func stop() 15 | } 16 | -------------------------------------------------------------------------------- /BoostRunClub/Services/RunningService/RunningRecordServiceable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningRecordServicable.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/20. 6 | // 7 | 8 | import Combine 9 | import CoreLocation 10 | import Foundation 11 | 12 | protocol RunningRecordServiceable { 13 | func addState(_ state: RunningState) 14 | func save(startTime: Date?, endTime: Date?) -> (activity: Activity, detail: ActivityDetail)? 15 | func clear() 16 | 17 | var locations: [CLLocation] { get } 18 | var routes: [RunningSlice] { get } 19 | var didAddSplitSignal: PassthroughSubject { get } 20 | } 21 | -------------------------------------------------------------------------------- /BoostRunClub/Services/RunningService/RunningServiceType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningServiceType.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/16. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | protocol RunningServiceType { 12 | var dashBoardService: RunningDashBoardServiceable { get } 13 | var recordService: RunningRecordServiceable { get } 14 | var runningResultSubject: PassthroughSubject<(activity: Activity, detail: ActivityDetail)?, Never> { get } 15 | var runningStateSubject: CurrentValueSubject { get } 16 | var runningEventSubject: PassthroughSubject { get } 17 | var isStarted: Bool { get } 18 | 19 | func start() 20 | func stop() 21 | func pause(autoResume: Bool) 22 | func resume() 23 | 24 | func setGoal(_ goalInfo: GoalInfo?) 25 | } 26 | -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/Accent.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.932", 10 | "red" : "0.976" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.000", 27 | "green" : "0.000", 28 | "red" : "0.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/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 | -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project18-B-iOS-BoostRunClub/61a9e892e3a363ad381ee1ca12c4147c17947975/BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project18-B-iOS-BoostRunClub/61a9e892e3a363ad381ee1ca12c4147c17947975/BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project18-B-iOS-BoostRunClub/61a9e892e3a363ad381ee1ca12c4147c17947975/BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project18-B-iOS-BoostRunClub/61a9e892e3a363ad381ee1ca12c4147c17947975/BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project18-B-iOS-BoostRunClub/61a9e892e3a363ad381ee1ca12c4147c17947975/BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project18-B-iOS-BoostRunClub/61a9e892e3a363ad381ee1ca12c4147c17947975/BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project18-B-iOS-BoostRunClub/61a9e892e3a363ad381ee1ca12c4147c17947975/BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project18-B-iOS-BoostRunClub/61a9e892e3a363ad381ee1ca12c4147c17947975/BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project18-B-iOS-BoostRunClub/61a9e892e3a363ad381ee1ca12c4147c17947975/BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project18-B-iOS-BoostRunClub/61a9e892e3a363ad381ee1ca12c4147c17947975/BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project18-B-iOS-BoostRunClub/61a9e892e3a363ad381ee1ca12c4147c17947975/BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "29.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "58.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "87.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "80.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "120.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "57.png", 47 | "idiom" : "iphone", 48 | "scale" : "1x", 49 | "size" : "57x57" 50 | }, 51 | { 52 | "filename" : "114.png", 53 | "idiom" : "iphone", 54 | "scale" : "2x", 55 | "size" : "57x57" 56 | }, 57 | { 58 | "filename" : "120.png", 59 | "idiom" : "iphone", 60 | "scale" : "2x", 61 | "size" : "60x60" 62 | }, 63 | { 64 | "filename" : "180.png", 65 | "idiom" : "iphone", 66 | "scale" : "3x", 67 | "size" : "60x60" 68 | }, 69 | { 70 | "filename" : "1024.png", 71 | "idiom" : "ios-marketing", 72 | "scale" : "1x", 73 | "size" : "1024x1024" 74 | } 75 | ], 76 | "info" : { 77 | "author" : "xcode", 78 | "version" : 1 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/Color.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "1.000", 27 | "green" : "1.000", 28 | "red" : "1.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/Image.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "scale" : "2x" 10 | }, 11 | { 12 | "idiom" : "universal", 13 | "scale" : "3x" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | }, 20 | "properties" : { 21 | "on-demand-resource-tags" : [ 22 | "Icons" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/accent2.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "display-p3", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.000", 10 | "red" : "0.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.000", 27 | "green" : "0.932", 28 | "red" : "0.976" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/activity4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "activity4.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/activity4.imageset/activity4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project18-B-iOS-BoostRunClub/61a9e892e3a363ad381ee1ca12c4147c17947975/BoostRunClub/SupportingFiles/Assets.xcassets/activity4.imageset/activity4.png -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/brcYellow.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.000", 9 | "green" : "0.932", 10 | "red" : "0.976" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.260", 27 | "green" : "0.777", 28 | "red" : "0.800" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/customBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "1.000", 9 | "green" : "1.000", 10 | "red" : "1.000" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "display-p3", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.000", 27 | "green" : "0.000", 28 | "red" : "0.000" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/profile4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "profile4.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/profile4.imageset/profile4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project18-B-iOS-BoostRunClub/61a9e892e3a363ad381ee1ca12c4147c17947975/BoostRunClub/SupportingFiles/Assets.xcassets/profile4.imageset/profile4.png -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/running4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "running4.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/running4.imageset/running4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project18-B-iOS-BoostRunClub/61a9e892e3a363ad381ee1ca12c4147c17947975/BoostRunClub/SupportingFiles/Assets.xcassets/running4.imageset/running4.png -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Assets.xcassets/tableBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.969", 9 | "green" : "0.949", 10 | "red" : "0.949" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | }, 15 | { 16 | "appearances" : [ 17 | { 18 | "appearance" : "luminosity", 19 | "value" : "dark" 20 | } 21 | ], 22 | "color" : { 23 | "color-space" : "srgb", 24 | "components" : { 25 | "alpha" : "1.000", 26 | "blue" : "0.077", 27 | "green" : "0.077", 28 | "red" : "0.077" 29 | } 30 | }, 31 | "idiom" : "universal" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Futura LT Condensed Extra Bold Oblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project18-B-iOS-BoostRunClub/61a9e892e3a363ad381ee1ca12c4147c17947975/BoostRunClub/SupportingFiles/Futura LT Condensed Extra Bold Oblique.ttf -------------------------------------------------------------------------------- /BoostRunClub/SupportingFiles/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSLocationAlwaysAndWhenInUseUsageDescription 24 | 사용자 위치 정보를 확인합니다. 25 | NSLocationWhenInUseUsageDescription 26 | 사용자 위치 정보를 확인합니다. 27 | NSMotionUsageDescription 28 | 케이던스 및 모션 정보를 가져오기 위해 만보기에 대한 접근이 필요합니다. 29 | NSPhotoLibraryUsageDescription 30 | 사진에 대한 접근이 필요합니다. 31 | UIAppFonts 32 | 33 | Futura LT Condensed Extra Bold Oblique.ttf 34 | 35 | UIApplicationSceneManifest 36 | 37 | UIApplicationSupportsMultipleScenes 38 | 39 | UISceneConfigurations 40 | 41 | UIWindowSceneSessionRoleApplication 42 | 43 | 44 | UISceneConfigurationName 45 | Default Configuration 46 | UISceneDelegateClassName 47 | $(PRODUCT_MODULE_NAME).SceneDelegate 48 | 49 | 50 | 51 | 52 | UIApplicationSupportsIndirectInputEvents 53 | 54 | UIBackgroundModes 55 | 56 | location 57 | 58 | UILaunchStoryboardName 59 | LaunchScreen 60 | UIRequiredDeviceCapabilities 61 | 62 | armv7 63 | 64 | UISupportedInterfaceOrientations 65 | 66 | UIInterfaceOrientationPortrait 67 | 68 | UISupportedInterfaceOrientations~ipad 69 | 70 | UIInterfaceOrientationPortrait 71 | UIInterfaceOrientationPortraitUpsideDown 72 | UIInterfaceOrientationLandscapeLeft 73 | UIInterfaceOrientationLandscapeRight 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /BoostRunClub/Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utilities.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/13. 6 | // 7 | 8 | import Foundation 9 | -------------------------------------------------------------------------------- /BoostRunClub/ViewModels/Configurable/ActivityDetailConfig.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityDetailConfig.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/13. 6 | // 7 | 8 | import CoreLocation 9 | import Foundation 10 | 11 | struct ActivityDetailConfig { 12 | let titleDate: String 13 | let title: String 14 | 15 | let distance: Double 16 | let avgPace: String 17 | let runningTime: String 18 | let calorie: Int 19 | let elevation: Int 20 | let avgBPM: Int 21 | let cadence: Int 22 | 23 | let locations: [Location] 24 | let splits: [RunningSplit] 25 | 26 | init(activity: Activity, detail: ActivityDetail) { 27 | let timeTexts = activity.createdAt.toYMDHMString.components(separatedBy: " ") 28 | let periodText = activity.createdAt.period 29 | titleDate = "\(timeTexts[0]) \(periodText) \(timeTexts[1])" 30 | title = "\(activity.createdAt.toDayOfWeekString) \(periodText) 러닝" 31 | 32 | distance = activity.distance 33 | avgPace = activity.avgPaceText 34 | runningTime = activity.runningTimeText 35 | calorie = detail.calorie 36 | elevation = Int(activity.elevation) 37 | avgBPM = detail.avgBPM 38 | cadence = detail.cadence 39 | 40 | locations = detail.locations 41 | splits = detail.splits 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /BoostRunClub/ViewModels/Configurable/ActivityListItem.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityListItem.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/09. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ActivityListItem { 11 | let total: ActivityTotalConfig 12 | let items: [Activity] 13 | } 14 | 15 | extension ActivityListItem: Comparable { 16 | static func == (lhs: ActivityListItem, rhs: ActivityListItem) -> Bool { 17 | lhs.total.totalRange.start == rhs.total.totalRange.start 18 | } 19 | 20 | static func < (lhs: ActivityListItem, rhs: ActivityListItem) -> Bool { 21 | lhs.total.totalRange.start < rhs.total.totalRange.start 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BoostRunClub/ViewModels/GoalTypeViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GoalTypeViewModel.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/11/24. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | protocol GoalTypeViewModelTypes { 12 | var inputs: GoalTypeViewModelInputs { get } 13 | var outputs: GoalTypeViewModelOutputs { get } 14 | } 15 | 16 | protocol GoalTypeViewModelInputs { 17 | func didSelectGoalType(_ goalType: GoalType) 18 | func didTapBackgroundView() 19 | } 20 | 21 | protocol GoalTypeViewModelOutputs { 22 | // Data For Configure 23 | var goalTypeSubject: CurrentValueSubject { get } 24 | 25 | // Signal For Coordinate 26 | var closeSignal: PassthroughSubject { get } 27 | } 28 | 29 | class GoalTypeViewModel: GoalTypeViewModelInputs, GoalTypeViewModelOutputs { 30 | init(goalType: GoalType) { 31 | goalTypeSubject = CurrentValueSubject(goalType) 32 | } 33 | 34 | deinit { 35 | print("[Memory \(Date())] 🌙ViewModel⭐️ \(Self.self) deallocated.") 36 | } 37 | 38 | // MARK: Inputs 39 | 40 | func didTapBackgroundView() { 41 | closeSignal.send(goalTypeSubject.value) 42 | } 43 | 44 | func didSelectGoalType(_ goalType: GoalType) { 45 | goalTypeSubject.send(goalType) 46 | closeSignal.send(goalTypeSubject.value) 47 | } 48 | 49 | // MARK: Ouputs 50 | 51 | private(set) var closeSignal = PassthroughSubject() 52 | let goalTypeSubject: CurrentValueSubject 53 | } 54 | 55 | // MARK: - Types 56 | 57 | extension GoalTypeViewModel: GoalTypeViewModelTypes { 58 | var inputs: GoalTypeViewModelInputs { self } 59 | var outputs: GoalTypeViewModelOutputs { self } 60 | } 61 | -------------------------------------------------------------------------------- /BoostRunClub/ViewModels/GoalTypeViewModelTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GoalTypeViewModelTest.swift 3 | // BoostRunClubTests 4 | // 5 | // Created by 조기현 on 2020/11/29. 6 | // 7 | 8 | @testable import BoostRunClub 9 | import Combine 10 | import XCTest 11 | 12 | class GoalTypeViewModelTest: XCTestCase { 13 | var goalTypeVM: GoalTypeViewModel! 14 | var cancellables: Set! 15 | 16 | override func setUp() { 17 | goalTypeVM = GoalTypeViewModel(goalType: .none) 18 | cancellables = [] 19 | } 20 | 21 | override func tearDown() { 22 | cancellables.removeAll() 23 | } 24 | 25 | func testDidTapBackgroundView() { 26 | let receivedSignal = expectation(description: "received signal to close action sheet") 27 | let expectedGoalType: GoalType = .distance 28 | 29 | goalTypeVM.goalTypeSubject.send(expectedGoalType) 30 | 31 | let cancellable = goalTypeVM.closeSignal 32 | .sink { 33 | if $0 == expectedGoalType { 34 | receivedSignal.fulfill() 35 | } else { 36 | XCTFail("BackgroundView를 탭한 후 goalTypeVM에서 전송하는 값과 GoalType이 들어오는 값이 일치하지 않음") 37 | } 38 | } 39 | 40 | goalTypeVM.didTapBackgroundView() 41 | waitForExpectations(timeout: 1, handler: nil) 42 | cancellable.cancel() 43 | } 44 | 45 | func testDidSelectGoalType() { 46 | let allCases: [GoalType] = [.distance, .speed, .time] 47 | 48 | allCases.forEach { goalType in 49 | let receivedSignal = expectation(description: "received signal that user selected goal type") 50 | 51 | let cancellable = Publishers.CombineLatest( 52 | goalTypeVM.goalTypeSubject, 53 | goalTypeVM.closeSignal 54 | ) 55 | .sink { 56 | if $0 == goalType, 57 | $1 == goalType 58 | { 59 | receivedSignal.fulfill() 60 | } else { 61 | XCTFail("GoalType 선택 후 전송하는 값과 들어오는 값이 일치하지 않음") 62 | } 63 | } 64 | 65 | goalTypeVM.didSelectGoalType(goalType) 66 | waitForExpectations(timeout: 1, handler: nil) 67 | cancellable.cancel() 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /BoostRunClub/ViewModels/ProfileViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileViewModel.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/08. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import UIKit 11 | 12 | protocol ProfileViewModelTypes: AnyObject { 13 | var inputs: ProfileViewModelInputs { get } 14 | var outputs: ProfileViewModelOutputs { get } 15 | } 16 | 17 | protocol ProfileViewModelInputs { 18 | func didTapEditProfileButton() 19 | func didEditProfile(_ profile: Profile) 20 | 21 | // Life Cycle 22 | func viewDidLoad() 23 | } 24 | 25 | protocol ProfileViewModelOutputs { 26 | var profileSubject: PassthroughSubject { get } 27 | 28 | var showEditProfileSignal: PassthroughSubject { get } 29 | } 30 | 31 | final class ProfileViewModel: ProfileViewModelInputs, ProfileViewModelOutputs { 32 | private var defaults: DefaultsReadable 33 | private var profile: Profile 34 | 35 | init(defaults: DefaultsReadable) { 36 | self.defaults = defaults 37 | profile = Profile(image: Data.loadImageDataFromDocumentsDirectory(fileName: "profile.png"), 38 | lastName: defaults.string(forKey: "LastName") ?? "", 39 | firstName: defaults.string(forKey: "FirstName") ?? "", 40 | hometown: defaults.string(forKey: "Hometown") ?? "", 41 | bio: defaults.string(forKey: "Bio") ?? "") 42 | } 43 | 44 | deinit { 45 | print("[Memory \(Date())] 🌙ViewModel⭐️ \(Self.self) deallocated.") 46 | } 47 | 48 | // inputs 49 | 50 | func viewDidLoad() { 51 | profileSubject.send(profile) 52 | } 53 | 54 | func didTapEditProfileButton() { 55 | showEditProfileSignal.send() 56 | } 57 | 58 | func didEditProfile(_ profile: Profile) { 59 | profileSubject.send(profile) 60 | } 61 | 62 | // outputs 63 | 64 | var showEditProfileSignal = PassthroughSubject() 65 | var profileSubject = PassthroughSubject() 66 | } 67 | 68 | extension ProfileViewModel: ProfileViewModelTypes { 69 | var inputs: ProfileViewModelInputs { self } 70 | var outputs: ProfileViewModelOutputs { self } 71 | } 72 | -------------------------------------------------------------------------------- /BoostRunClub/ViewModels/RouteDetailViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteDetailViewModel.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/13. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | import MapKit 11 | import UIKit 12 | 13 | protocol RouteDetailViewModelTypes: AnyObject { 14 | var inputs: RouteDetailViewModelInputs { get } 15 | var outputs: RouteDetailViewModelOutputs { get } 16 | } 17 | 18 | protocol RouteDetailViewModelInputs { 19 | func didTapCloseButton() 20 | 21 | // Life Cycle 22 | func viewDidLoad() 23 | } 24 | 25 | protocol RouteDetailViewModelOutputs { 26 | var detailConfigSubject: PassthroughSubject { get } 27 | 28 | var closeSignal: PassthroughSubject { get } 29 | } 30 | 31 | final class RouteDetailViewModel: RouteDetailViewModelInputs, RouteDetailViewModelOutputs { 32 | let activityDetailConfig: ActivityDetailConfig 33 | 34 | init(activityDetailConfig: ActivityDetailConfig) { 35 | self.activityDetailConfig = activityDetailConfig 36 | } 37 | 38 | // inputs 39 | func viewDidLoad() { 40 | detailConfigSubject.send(activityDetailConfig) 41 | } 42 | 43 | func didTapCloseButton() { 44 | closeSignal.send() 45 | } 46 | 47 | // outputs 48 | var closeSignal = PassthroughSubject() 49 | var detailConfigSubject = PassthroughSubject() 50 | 51 | deinit { 52 | print("[\(Date())] 🌙ViewModel⭐️ \(Self.self) deallocated.") 53 | } 54 | } 55 | 56 | extension RouteDetailViewModel: RouteDetailViewModelTypes { 57 | var inputs: RouteDetailViewModelInputs { self } 58 | var outputs: RouteDetailViewModelOutputs { self } 59 | } 60 | -------------------------------------------------------------------------------- /BoostRunClub/ViewModels/RunningSplitCellViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningSplitCellViewModel.swift 3 | // BoostRunClub 4 | // 5 | // Created by 조기현 on 2020/12/09. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | protocol CellViewModelTypeBase {} 12 | 13 | protocol RunningSplitCellViewModelType: CellViewModelTypeBase { 14 | var kilometerSubject: CurrentValueSubject { get } 15 | var paceSubject: CurrentValueSubject { get } 16 | var changeSubject: CurrentValueSubject { get } 17 | } 18 | 19 | class RunningSplitCellViewModel: RunningSplitCellViewModelType { 20 | var kilometerSubject = CurrentValueSubject("") 21 | var paceSubject = CurrentValueSubject("") 22 | var changeSubject = CurrentValueSubject(nil) 23 | } 24 | 25 | struct ValueChange { 26 | enum Status { 27 | case incresed, equal, decreased 28 | } 29 | 30 | let status: Status 31 | let value: String 32 | } 33 | -------------------------------------------------------------------------------- /BoostRunClub/ViewModels/SplitsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitsViewModel.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/11/27. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | protocol SplitsViewModelTypes { 12 | var inputs: SplitsViewModelInputs { get } 13 | var outputs: SplitsViewModelOutputs { get } 14 | } 15 | 16 | protocol SplitsViewModelInputs {} 17 | 18 | protocol SplitsViewModelOutputs { 19 | var rowViewModelSubject: CurrentValueSubject<[RunningSplitCellViewModelType], Never> { get } 20 | } 21 | 22 | class SplitsViewModel: SplitsViewModelInputs, SplitsViewModelOutputs { 23 | let runningService: RunningServiceType 24 | let factory: SplitSceneFactory 25 | var cancellables = Set() 26 | var avgPaces = [Int]() 27 | 28 | init(runningService: RunningServiceType, factory: SplitSceneFactory = DependencyFactory.shared) { 29 | self.runningService = runningService 30 | self.factory = factory 31 | 32 | runningService.recordService.didAddSplitSignal 33 | .receive(on: RunLoop.main) 34 | .sink { [weak self] in self?.newSplitAction(split: $0) } 35 | .store(in: &cancellables) 36 | } 37 | 38 | deinit { 39 | print("[Memory \(Date())] 🌙ViewModel⭐️ \(Self.self) deallocated.") 40 | } 41 | 42 | // outputs 43 | var rowViewModelSubject = CurrentValueSubject<[RunningSplitCellViewModelType], Never>([]) 44 | } 45 | 46 | extension SplitsViewModel { 47 | func newSplitAction(split: RunningSplit) { 48 | let currRowVM = factory.makeRunningSplitCellVM() 49 | let kilometer = rowViewModelSubject.value.count + 1 50 | let currPace = split.avgPace 51 | let valueChange: ValueChange? 52 | if let prevPace = avgPaces.last { 53 | let status: ValueChange.Status = prevPace == currPace ? .equal : prevPace < currPace ? .incresed : .decreased 54 | valueChange = ValueChange(status: status, 55 | value: abs(currPace - prevPace).formattedString) 56 | } else { 57 | valueChange = nil 58 | } 59 | 60 | currRowVM.kilometerSubject.send("\(kilometer)") 61 | currRowVM.paceSubject.send(currPace.formattedString) 62 | currRowVM.changeSubject.send(valueChange) 63 | 64 | avgPaces.append(split.avgPace) 65 | rowViewModelSubject.value.append(currRowVM) 66 | } 67 | } 68 | 69 | extension SplitsViewModel: SplitsViewModelTypes { 70 | var inputs: SplitsViewModelInputs { self } 71 | var outputs: SplitsViewModelOutputs { self } 72 | } 73 | 74 | extension Int { 75 | var formattedString: String { 76 | String(format: "%d'%d\"", self / 60, self % 60) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /BoostRunClub/Views/ActivityDetailScene/DetailSplitsTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailSplitsTableView.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/12. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | class DetailSplitsTableView: UITableView { 12 | private(set) var didIntrinsicSizeChangedSignal = PassthroughSubject() 13 | 14 | init() { 15 | super.init(frame: .zero, style: .plain) 16 | commonInit() 17 | } 18 | 19 | required init?(coder: NSCoder) { 20 | super.init(coder: coder) 21 | commonInit() 22 | } 23 | 24 | override func layoutSubviews() { 25 | super.layoutSubviews() 26 | 27 | if bounds.size != intrinsicContentSize { 28 | invalidateIntrinsicContentSize() 29 | didIntrinsicSizeChangedSignal.send(intrinsicContentSize) 30 | } 31 | } 32 | 33 | override var intrinsicContentSize: CGSize { 34 | contentSize 35 | } 36 | } 37 | 38 | // MARK: - Configure 39 | 40 | extension DetailSplitsTableView { 41 | private func commonInit() { 42 | configureTableView() 43 | } 44 | 45 | private func configureTableView() { 46 | rowHeight = SimpleSplitViewCell.Constant.cellHeight 47 | allowsSelection = false 48 | alwaysBounceVertical = false 49 | isScrollEnabled = false 50 | separatorStyle = .none 51 | backgroundColor = .clear 52 | register(SimpleSplitViewCell.self, forCellReuseIdentifier: "\(SimpleSplitViewCell.self)") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /BoostRunClub/Views/ActivityDetailScene/DetailSplitsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailSplitsView.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/12. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | class DetailSplitsView: UIView { 12 | private(set) var didHeightChangeSignal = PassthroughSubject() 13 | private(set) var didTapInfoButtonSignal = PassthroughSubject() 14 | 15 | private var titleLabel = UILabel.makeBold(text: "구간", size: 30) 16 | private(set) var tableView = DetailSplitsTableView() 17 | private lazy var detailInfoButton = makeDetailInfoButton() 18 | 19 | private lazy var tableHeightConstraint = tableView.heightAnchor.constraint(equalToConstant: 0) 20 | 21 | private var cancellables = Set() 22 | 23 | override init(frame: CGRect = .zero) { 24 | super.init(frame: frame) 25 | commonInit() 26 | } 27 | 28 | required init?(coder: NSCoder) { 29 | super.init(coder: coder) 30 | commonInit() 31 | } 32 | } 33 | 34 | // MARK: - Actions 35 | 36 | extension DetailSplitsView { 37 | @objc 38 | func didTapInfoButton() { 39 | didTapInfoButtonSignal.send() 40 | } 41 | } 42 | 43 | // MARK: - Configure 44 | 45 | extension DetailSplitsView { 46 | private func commonInit() { 47 | tableView.didIntrinsicSizeChangedSignal 48 | .sink { [weak self] _ in 49 | self?.didHeightChangeSignal.send() 50 | } 51 | .store(in: &cancellables) 52 | 53 | configureLayout() 54 | } 55 | 56 | private func configureLayout() { 57 | let vStack = UIStackView.make( 58 | with: [titleLabel, tableView, detailInfoButton], 59 | axis: .vertical, alignment: .fill, distribution: .equalSpacing, spacing: 10 60 | ) 61 | addSubview(vStack) 62 | vStack.translatesAutoresizingMaskIntoConstraints = false 63 | NSLayoutConstraint.activate([ 64 | vStack.topAnchor.constraint(equalTo: topAnchor), 65 | vStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20), 66 | vStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20), 67 | vStack.bottomAnchor.constraint(equalTo: bottomAnchor), 68 | ]) 69 | 70 | detailInfoButton.translatesAutoresizingMaskIntoConstraints = false 71 | NSLayoutConstraint.activate([ 72 | detailInfoButton.heightAnchor.constraint(equalToConstant: 60), 73 | ]) 74 | } 75 | 76 | private func makeDetailInfoButton() -> UIButton { 77 | let button = UIButton(type: .system) 78 | button.setTitle("상세 정보", for: .normal) 79 | button.setTitleColor(.label, for: .normal) 80 | button.titleLabel?.font = UIFont.systemFont(ofSize: 20) 81 | button.layer.borderWidth = 1 82 | button.layer.borderColor = UIColor.systemGray.cgColor 83 | button.layer.cornerRadius = 30 84 | button.addTarget(self, action: #selector(didTapInfoButton), for: .touchUpInside) 85 | return button 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /BoostRunClub/Views/ActivityDetailScene/DetailTitleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DetailTitleView.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/12. 6 | // 7 | 8 | import UIKit 9 | 10 | class DetailTitleView: UIView { 11 | private var dateLabel = UILabel.makeNormal() 12 | private var titleLabel = UILabel.makeBold() 13 | private var dividerView = UIView() 14 | 15 | override init(frame: CGRect = .zero) { 16 | super.init(frame: frame) 17 | commonInit() 18 | } 19 | 20 | required init?(coder: NSCoder) { 21 | super.init(coder: coder) 22 | commonInit() 23 | } 24 | 25 | func configure(dateText: String, title: String) { 26 | dateLabel.text = dateText 27 | titleLabel.text = title 28 | } 29 | } 30 | 31 | // MARK: - Configure 32 | 33 | extension DetailTitleView { 34 | private func commonInit() { 35 | dividerView.backgroundColor = .systemGray 36 | configureLayout() 37 | } 38 | 39 | private func configureLayout() { 40 | NSLayoutConstraint.activate([ 41 | dividerView.heightAnchor.constraint(equalToConstant: 1), 42 | dividerView.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.width - 40), 43 | ]) 44 | 45 | let titleVStack = UIStackView.make( 46 | with: [dateLabel, titleLabel, dividerView], 47 | axis: .vertical, alignment: .leading, distribution: .fill, spacing: 10 48 | ) 49 | 50 | addSubview(titleVStack) 51 | titleVStack.translatesAutoresizingMaskIntoConstraints = false 52 | NSLayoutConstraint.activate([ 53 | titleVStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20), 54 | titleVStack.topAnchor.constraint(equalTo: topAnchor, constant: 20), 55 | titleVStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20), 56 | titleVStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10), 57 | ]) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /BoostRunClub/Views/ActivityScene/ActivitiesContainerCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityCollectionContainerView.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/09. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | class ActivitiesContainerCellView: UITableViewCell { 12 | lazy var collectionView = ActivityCollectionView() 13 | 14 | var didHeightChangeSignal = PassthroughSubject() 15 | var didItemSelectedSignal = PassthroughSubject() 16 | var cancellables = Set() 17 | 18 | init() { 19 | super.init(style: .default, reuseIdentifier: "\(Self.self)") 20 | commonInit() 21 | } 22 | 23 | required init?(coder: NSCoder) { 24 | super.init(coder: coder) 25 | commonInit() 26 | } 27 | 28 | private func commonInit() { 29 | backgroundColor = .clear 30 | contentView.backgroundColor = .clear 31 | 32 | collectionView.delegate = self 33 | collectionView.didHeightChangeSignal 34 | .sink { 35 | self.bounds.size.height = $0 36 | self.didHeightChangeSignal.send(self) 37 | } 38 | .store(in: &cancellables) 39 | 40 | contentView.addSubview(collectionView) 41 | collectionView.translatesAutoresizingMaskIntoConstraints = false 42 | NSLayoutConstraint.activate([ 43 | collectionView.topAnchor.constraint(equalTo: contentView.topAnchor), 44 | collectionView.leftAnchor.constraint(equalTo: contentView.leftAnchor), 45 | collectionView.rightAnchor.constraint(equalTo: contentView.rightAnchor), 46 | collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), 47 | ]) 48 | } 49 | 50 | override func layoutSubviews() { 51 | super.layoutSubviews() 52 | } 53 | } 54 | 55 | // MARK: - UICollectionViewDelegate Implementation 56 | 57 | extension ActivitiesContainerCellView: UICollectionViewDelegate { 58 | func collectionView(_: UICollectionView, didSelectItemAt indexPath: IndexPath) { 59 | didItemSelectedSignal.send(indexPath) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /BoostRunClub/Views/ActivityScene/ActivityCollectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityCollectionView.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/09. 6 | // 7 | 8 | import Combine 9 | import UIKit 10 | 11 | class ActivityCollectionView: UICollectionView { 12 | var didHeightChangeSignal = PassthroughSubject() 13 | 14 | init(frame _: CGRect = .zero) { 15 | let layout = ActivityCollectionView.makeLayout() 16 | super.init(frame: .zero, collectionViewLayout: layout) 17 | commonInit() 18 | } 19 | 20 | required init?(coder: NSCoder) { 21 | super.init(coder: coder) 22 | commonInit() 23 | } 24 | 25 | override func layoutSubviews() { 26 | super.layoutSubviews() 27 | 28 | if bounds.size != intrinsicContentSize { 29 | invalidateIntrinsicContentSize() 30 | didHeightChangeSignal.send(intrinsicContentSize.height) 31 | } 32 | } 33 | 34 | override var intrinsicContentSize: CGSize { 35 | collectionViewLayout.collectionViewContentSize 36 | } 37 | 38 | private func commonInit() { 39 | register(ActivityCellView.self, forCellWithReuseIdentifier: "\(ActivityCellView.self)") 40 | layoutMargins = UIEdgeInsets.zero 41 | backgroundColor = .clear 42 | isScrollEnabled = false 43 | layer.masksToBounds = false 44 | showsHorizontalScrollIndicator = false 45 | showsVerticalScrollIndicator = false 46 | } 47 | 48 | static func makeLayout() -> UICollectionViewCompositionalLayout { 49 | let size = NSCollectionLayoutSize( 50 | widthDimension: NSCollectionLayoutDimension.fractionalWidth(1), 51 | heightDimension: NSCollectionLayoutDimension.estimated(80) 52 | ) 53 | let item = NSCollectionLayoutItem(layoutSize: size) 54 | let group = NSCollectionLayoutGroup.horizontal(layoutSize: size, subitems: [item]) 55 | let section = NSCollectionLayoutSection(group: group) 56 | section.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5) 57 | section.interGroupSpacing = 10 58 | let layout = UICollectionViewCompositionalLayout(section: section) 59 | return layout 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /BoostRunClub/Views/ActivityScene/ActivityFooterView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityFooterView.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/08. 6 | // 7 | 8 | import UIKit 9 | 10 | class ActivityFooterView: UIView { 11 | private var showAllActivityButton: UIButton = { 12 | let button = UIButton(type: .system) 13 | button.setTitle("모든 활동", for: .normal) 14 | button.setTitleColor(.label, for: .normal) 15 | button.titleLabel?.font = UIFont.systemFont(ofSize: 20) 16 | button.layer.borderWidth = 1 17 | button.layer.borderColor = UIColor.systemGray.cgColor 18 | button.layer.cornerRadius = 30 19 | button.addTarget(self, action: #selector(didTapShowAllActivity), for: .touchUpInside) 20 | return button 21 | }() 22 | 23 | var didTapAllActivityButton: (() -> Void)? 24 | 25 | required init?(coder: NSCoder) { 26 | super.init(coder: coder) 27 | commonInit() 28 | } 29 | 30 | init() { 31 | super.init(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 100)) 32 | commonInit() 33 | } 34 | } 35 | 36 | // MARK: - Actions 37 | 38 | extension ActivityFooterView { 39 | @objc 40 | func didTapShowAllActivity() { 41 | didTapAllActivityButton?() 42 | } 43 | } 44 | 45 | // MARK: - Configure 46 | 47 | extension ActivityFooterView { 48 | private func commonInit() { 49 | backgroundColor = .clear 50 | configureLayout() 51 | } 52 | 53 | private func configureLayout() { 54 | addSubview(showAllActivityButton) 55 | showAllActivityButton.translatesAutoresizingMaskIntoConstraints = false 56 | NSLayoutConstraint.activate([ 57 | showAllActivityButton.widthAnchor.constraint(equalToConstant: bounds.width - 40), 58 | showAllActivityButton.centerXAnchor.constraint(equalTo: centerXAnchor), 59 | showAllActivityButton.centerYAnchor.constraint(equalTo: centerYAnchor), 60 | showAllActivityButton.heightAnchor.constraint(equalToConstant: 60), 61 | ]) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /BoostRunClub/Views/ActivityScene/ActivityListHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityListHeaderView.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/09. 6 | // 7 | 8 | import UIKit 9 | 10 | class ActivityListHeaderView: UICollectionReusableView { 11 | lazy var categoryLabel = makeValueLabel() 12 | lazy var numRunningLabel = makeNormalLabel() 13 | lazy var distancelabel = makeNormalLabel() 14 | lazy var paceLabel = makeNormalLabel() 15 | 16 | override init(frame: CGRect) { 17 | super.init(frame: frame) 18 | commonInit() 19 | } 20 | 21 | required init?(coder: NSCoder) { 22 | super.init(coder: coder) 23 | commonInit() 24 | } 25 | 26 | func configure(with config: ActivityTotalConfig) { 27 | categoryLabel.text = config.totalRange.start.toYMString 28 | numRunningLabel.text = "러닝 \(config.numRunningText)회" 29 | distancelabel.text = "\(config.totalDistanceText)km" 30 | paceLabel.text = "\(config.avgPaceText)/km" 31 | } 32 | 33 | private func commonInit() { 34 | backgroundColor = .systemGroupedBackground 35 | configureLayout() 36 | } 37 | 38 | private func configureLayout() { 39 | let valueHStack = UIStackView.make( 40 | with: [numRunningLabel, distancelabel, paceLabel], 41 | axis: .horizontal, 42 | alignment: .leading, 43 | distribution: .equalSpacing, 44 | spacing: 10 45 | ) 46 | 47 | let totalVStack = UIStackView.make( 48 | with: [categoryLabel, valueHStack], 49 | axis: .vertical, 50 | alignment: .leading, 51 | distribution: .equalSpacing, 52 | spacing: 5 53 | ) 54 | 55 | addSubview(totalVStack) 56 | totalVStack.translatesAutoresizingMaskIntoConstraints = false 57 | 58 | NSLayoutConstraint.activate([ 59 | totalVStack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 20), 60 | totalVStack.topAnchor.constraint(equalTo: topAnchor, constant: 20), 61 | totalVStack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -20), 62 | totalVStack.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -20), 63 | ]) 64 | } 65 | } 66 | 67 | // MARK: - Configure 68 | 69 | extension ActivityListHeaderView { 70 | private func makeValueLabel() -> UILabel { 71 | let label = UILabel() 72 | label.font = UIFont.boldSystemFont(ofSize: 20) 73 | label.textColor = .label 74 | label.text = "Value" 75 | return label 76 | } 77 | 78 | private func makeNormalLabel() -> UILabel { 79 | let label = UILabel() 80 | label.textColor = .systemGray 81 | label.text = "타이틀" 82 | return label 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /BoostRunClub/Views/ActivityScene/ActivityStatisticCellView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityStatisticCellView.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/07. 6 | // 7 | 8 | import UIKit 9 | 10 | class ActivityStatisticCellView: UITableViewCell { 11 | private lazy var titleLabel = makeNormalLabel() 12 | private lazy var valueLabel = makeValueLabel() 13 | 14 | required init?(coder: NSCoder) { 15 | super.init(coder: coder) 16 | commonInit() 17 | } 18 | 19 | init(title: String, value: String) { 20 | super.init(style: .default, reuseIdentifier: String(describing: Self.self)) 21 | commonInit() 22 | configure(title: title, value: value) 23 | } 24 | 25 | init() { 26 | super.init(style: .default, reuseIdentifier: String(describing: Self.self)) 27 | commonInit() 28 | } 29 | 30 | func configure(title: String, value: String) { 31 | titleLabel.text = title 32 | valueLabel.text = value 33 | } 34 | } 35 | 36 | // MARK: - Configure 37 | 38 | extension ActivityStatisticCellView { 39 | private func commonInit() { 40 | selectionStyle = .none 41 | configureLayout() 42 | } 43 | 44 | private func configureLayout() { 45 | let stackView = UIStackView.make( 46 | with: [titleLabel, valueLabel], 47 | axis: .vertical, 48 | alignment: .leading, 49 | distribution: .fill, 50 | spacing: 10 51 | ) 52 | contentView.addSubview(stackView) 53 | stackView.translatesAutoresizingMaskIntoConstraints = false 54 | 55 | NSLayoutConstraint.activate([ 56 | stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), 57 | stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), 58 | stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20), 59 | stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -20), 60 | ]) 61 | } 62 | 63 | private func makeValueLabel() -> UILabel { 64 | let label = UILabel() 65 | label.font = UIFont.boldSystemFont(ofSize: 30) 66 | label.textColor = .label 67 | label.text = "Value" 68 | return label 69 | } 70 | 71 | private func makeNormalLabel() -> UILabel { 72 | let label = UILabel() 73 | label.textColor = .systemGray 74 | label.text = "타이틀" 75 | return label 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /BoostRunClub/Views/ActivityScene/ActivityTableView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityTableView.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/07. 6 | // 7 | 8 | import UIKit 9 | 10 | class ActivityTableView: UITableView { 11 | init() { 12 | super.init(frame: .zero, style: .insetGrouped) 13 | commonInit() 14 | } 15 | 16 | required init?(coder: NSCoder) { 17 | super.init(coder: coder) 18 | commonInit() 19 | } 20 | } 21 | 22 | // MARK: - Configure 23 | 24 | extension ActivityTableView { 25 | private func commonInit() { 26 | backgroundColor = UIColor(named: "tableBackground") 27 | estimatedRowHeight = 200 28 | allowsSelection = true 29 | alwaysBounceVertical = true 30 | isScrollEnabled = true 31 | sectionHeaderHeight = 5 32 | sectionFooterHeight = 5 33 | separatorInset.right = 20 34 | rowHeight = UITableView.automaticDimension 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /BoostRunClub/Views/CircleButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CircleButton.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/02. 6 | // 7 | 8 | import UIKit 9 | 10 | class CircleButton: UIButton { 11 | enum Style { 12 | case start, stop, pause, resume, locate, exit 13 | } 14 | 15 | init(with buttonStyle: Style) { 16 | super.init(frame: .zero) 17 | commonInit(with: buttonStyle) 18 | } 19 | 20 | required init?(coder: NSCoder) { 21 | super.init(coder: coder) 22 | commonInit() 23 | } 24 | 25 | private func commonInit(with buttonStyle: Style = .start) { 26 | setTitleColor(.label, for: .normal) 27 | titleLabel?.font = UIFont.boldSystemFont(ofSize: 25) 28 | imageView?.contentMode = .scaleAspectFill 29 | 30 | switch buttonStyle { 31 | case .start: 32 | backgroundColor = UIColor(named: "accent") 33 | setTitle("시작", for: .normal) 34 | setTitleColor(UIColor(named: "accent2"), for: .normal) 35 | case .stop: 36 | setSFSymbol(iconName: "stop.fill", size: 25, tintColor: .systemBackground, backgroundColor: .label) 37 | case .pause: 38 | setSFSymbol(iconName: "pause.fill", size: 25, tintColor: .systemBackground, backgroundColor: .label) 39 | setTitleColor(UIColor(named: "accent2"), for: .normal) 40 | case .resume: 41 | setSFSymbol(iconName: "play.fill", 42 | size: 25, 43 | tintColor: .label, 44 | backgroundColor: UIColor(named: "brcYellow")!) 45 | case .locate: 46 | setSFSymbol(iconName: "location.fill", size: 25, tintColor: .systemBackground, backgroundColor: .label) 47 | case .exit: 48 | setSFSymbol(iconName: "xmark", size: 25, tintColor: .systemBackground, backgroundColor: .label) 49 | } 50 | } 51 | 52 | override func layoutSubviews() { 53 | super.layoutSubviews() 54 | layer.cornerRadius = bounds.height / 2 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /BoostRunClub/Views/CountDownView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CountDownView.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/02. 6 | // 7 | 8 | import UIKit 9 | 10 | class CountDownView: UIView { 11 | let numberLabel: NikeLabel = { 12 | let label = NikeLabel(with: 180) 13 | label.textAlignment = .center 14 | label.textColor = #colorLiteral(red: 0.9763557315, green: 0.9324046969, blue: 0, alpha: 1) 15 | return label 16 | }() 17 | 18 | override init(frame: CGRect) { 19 | super.init(frame: frame) 20 | commonInit() 21 | } 22 | 23 | required init?(coder: NSCoder) { 24 | super.init(coder: coder) 25 | commonInit() 26 | } 27 | 28 | private func commonInit() { 29 | backgroundColor = .black 30 | 31 | addSubview(numberLabel) 32 | numberLabel.translatesAutoresizingMaskIntoConstraints = false 33 | NSLayoutConstraint.activate([ 34 | numberLabel.centerXAnchor.constraint(equalTo: centerXAnchor), 35 | numberLabel.centerYAnchor.constraint(equalTo: centerYAnchor), 36 | ]) 37 | } 38 | 39 | func startCountingAnimation(count: Int, completion: @escaping () -> Void) { 40 | if count <= 0 { 41 | completion() 42 | return 43 | } 44 | numberLabel.text = "\(count)" 45 | numberLabel.transform = numberLabel.transform.scaledBy(x: 0.5, y: 0.5) 46 | UIView.animate(withDuration: 1) { 47 | self.numberLabel.transform = .identity 48 | } completion: { _ in 49 | self.startCountingAnimation(count: count - 1, completion: completion) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /BoostRunClub/Views/DataSource/ActivityDetailDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityDetailDataSource.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/13. 6 | // 7 | 8 | import UIKit 9 | 10 | class ActivityDetailDataSource: NSObject, UITableViewDataSource { 11 | private var maxPace: CGFloat = 0 12 | private var minPace: CGFloat = 0 13 | private var splits = [RunningSplit]() 14 | private var totalDistance: Double = 0 15 | 16 | func loadData(splits: [RunningSplit], distance: Double) { 17 | self.splits = splits 18 | totalDistance = distance 19 | let minMaxValue = splits.reduce(into: (min: CGFloat.infinity, max: CGFloat(0))) { 20 | let value = CGFloat($1.avgPace) 21 | $0.max = max($0.max, value) 22 | $0.min = min($0.min, value) 23 | } 24 | maxPace = minMaxValue.max 25 | minPace = minMaxValue.min 26 | } 27 | 28 | func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { 29 | if splits.isEmpty { 30 | return 0 31 | } else { 32 | return splits.count + 1 33 | } 34 | } 35 | 36 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 37 | let cell = tableView.dequeueReusableCell(withIdentifier: "\(SimpleSplitViewCell.self)", for: indexPath) 38 | 39 | if let cell = cell as? SimpleSplitViewCell { 40 | switch indexPath.row { 41 | case 0: 42 | cell.configure(style: .description) 43 | default: 44 | let idx = indexPath.row - 1 45 | let split = splits[idx] 46 | 47 | let distanceText: String 48 | if idx < splits.count - 1 { 49 | distanceText = "\(idx + 1)" 50 | } else { 51 | distanceText = String(format: "%.2f", Double(Int(totalDistance) % 1000) / 1000) 52 | } 53 | 54 | let paceText = String(format: "%d'%d\"", split.avgPace / 60, split.avgPace % 60) 55 | let elevationText = String(split.elevation) 56 | 57 | let paceRatio: CGFloat 58 | if maxPace - minPace > 0 { 59 | paceRatio = (maxPace - CGFloat(split.avgPace)) / (maxPace - minPace) 60 | } else { 61 | paceRatio = 0 62 | } 63 | 64 | cell.configure( 65 | style: .value, 66 | distance: distanceText, 67 | pace: paceText, 68 | elevation: elevationText, 69 | paceRatio: paceRatio 70 | ) 71 | } 72 | } 73 | 74 | return cell 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /BoostRunClub/Views/DataSource/ActivityListDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActivityListDataSource.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/09. 6 | // 7 | 8 | import UIKit 9 | 10 | class ActivityListDataSource: NSObject, UICollectionViewDataSource { 11 | var listItem = [ActivityListItem]() 12 | 13 | func loadData(_ listItem: [ActivityListItem]) { 14 | self.listItem = listItem 15 | } 16 | 17 | func numberOfSections(in _: UICollectionView) -> Int { 18 | listItem.count 19 | } 20 | 21 | func collectionView(_: UICollectionView, numberOfItemsInSection section: Int) -> Int { 22 | listItem[section].items.count 23 | } 24 | 25 | func collectionView( 26 | _ collectionView: UICollectionView, 27 | cellForItemAt indexPath: IndexPath 28 | ) -> UICollectionViewCell { 29 | let cell = collectionView.dequeueReusableCell( 30 | withReuseIdentifier: "\(ActivityCellView.self)", 31 | for: indexPath 32 | ) 33 | 34 | if let cell = cell as? ActivityCellView { 35 | cell.configure(with: listItem[indexPath.section].items[indexPath.row]) 36 | } 37 | 38 | return cell 39 | } 40 | 41 | func collectionView( 42 | _ collectionView: UICollectionView, 43 | viewForSupplementaryElementOfKind kind: String, 44 | at indexPath: IndexPath 45 | ) -> UICollectionReusableView { 46 | guard kind == UICollectionView.elementKindSectionHeader else { return UICollectionReusableView() } 47 | 48 | let header = collectionView.dequeueReusableSupplementaryView( 49 | ofKind: UICollectionView.elementKindSectionHeader, 50 | withReuseIdentifier: "\(ActivityListHeaderView.self)", 51 | for: indexPath 52 | ) 53 | 54 | if let header = header as? ActivityListHeaderView { 55 | header.configure(with: listItem[indexPath.section].total) 56 | } 57 | 58 | return header 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /BoostRunClub/Views/DataSource/SplitDatailSplitDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitDatailSplitDataSource.swift 3 | // BoostRunClub 4 | // 5 | // Created by 조기현 on 2020/12/14. 6 | // 7 | 8 | import UIKit 9 | 10 | struct SplitRow { 11 | let distance: Double 12 | let kilometer: String 13 | let avgPace: String 14 | let change: ValueChange? 15 | let elevation: String 16 | } 17 | 18 | class SplitDatailSplitDataSource: NSObject, UITableViewDataSource { 19 | var data = [SplitRow]() 20 | var totalDistance: Double = 0 21 | 22 | func update(_ data: [SplitRow]) { 23 | self.data = data 24 | totalDistance = data.reduce(into: Double(0)) { $0 += $1.distance } 25 | } 26 | 27 | func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { 28 | data.count 29 | } 30 | 31 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 32 | let cell = tableView.dequeueReusableCell( 33 | withIdentifier: SplitDatailSplitCell.identifier, 34 | for: indexPath 35 | ) 36 | 37 | if let cell = cell as? SplitDatailSplitCell { 38 | // cell.kilometerLabel.text = data[indexPath.row].kilometer 39 | cell.kilometerLabel.text = indexPath.row < data.count - 1 ? "\(indexPath.row + 1)" : String(format: "%.2f", Double(Int(totalDistance) % 1000) / 1000) 40 | cell.paceLabel.text = data[indexPath.row].avgPace 41 | cell.changeLabel.applyChange(data[indexPath.row].change) 42 | cell.elevationLabel.text = data[indexPath.row].elevation 43 | } 44 | 45 | return cell 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /BoostRunClub/Views/DataSource/SplitInfoDetailDataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitInfoDetailDataSource.swift 3 | // BoostRunClub 4 | // 5 | // Created by 조기현 on 2020/12/14. 6 | // 7 | 8 | import UIKit 9 | 10 | struct SplitInfo { 11 | let title: String 12 | let value: String 13 | } 14 | 15 | class SplitInfoDetailDataSource: NSObject, UITableViewDataSource { 16 | var data = [SplitInfo]() 17 | 18 | func update(_ data: [SplitInfo]) { 19 | self.data = data 20 | } 21 | 22 | func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { 23 | data.count 24 | } 25 | 26 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 27 | let cell = tableView.dequeueReusableCell( 28 | withIdentifier: SplitDetailInfoCell.identifier, 29 | for: indexPath 30 | ) 31 | 32 | if let cell = cell as? SplitDetailInfoCell { 33 | cell.titleLabel.text = data[indexPath.row].title 34 | cell.valueLabel.text = data[indexPath.row].value 35 | } 36 | 37 | return cell 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /BoostRunClub/Views/NikeLabel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // nikeLabel.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/11/28. 6 | // 7 | 8 | import UIKit 9 | 10 | class NikeLabel: UILabel { 11 | init(with size: CGFloat = 17) { 12 | super.init(frame: .zero) 13 | commonInit(with: size) 14 | } 15 | 16 | required init?(coder: NSCoder) { 17 | super.init(coder: coder) 18 | commonInit() 19 | } 20 | 21 | private func commonInit(with size: CGFloat = 17) { 22 | font = UIFont(name: "FuturaLT-CondExtraBoldObl", size: size) 23 | } 24 | 25 | override var intrinsicContentSize: CGSize { 26 | var size = super.intrinsicContentSize 27 | 28 | if 29 | let length = text?.count, 30 | length > 0 31 | { 32 | let adder = size.width / CGFloat(length) / 5 33 | size.width += adder 34 | } 35 | 36 | return size 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /BoostRunClub/Views/ProfileButton.swift.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileButton.swift.swift 3 | // BoostRunClub 4 | // 5 | // Created by Imho Jang on 2020/12/15. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIBarButtonItem { 11 | static func makeProfileButton() -> UIBarButtonItem { 12 | var profileImage = UIImage() 13 | let button = UIButton(type: .custom) 14 | if let imageData = Data.loadImageDataFromDocumentsDirectory(fileName: "profile.png") { 15 | profileImage = UIImage(data: imageData) ?? UIImage() 16 | } else { 17 | profileImage = UIImage.SFSymbol(name: "person.circle", 18 | size: 27, 19 | weight: .regular, 20 | scale: .default, 21 | color: .systemGray, 22 | renderingMode: .automatic) ?? UIImage() 23 | } 24 | button.setImage(profileImage, for: .normal) 25 | button.tintColor = .systemGray 26 | button.imageView?.contentMode = .scaleAspectFill 27 | button.frame = CGRect(x: 0, y: 0, width: 35, height: 35) 28 | button.layer.cornerRadius = button.frame.height / 2 29 | button.layer.masksToBounds = true 30 | 31 | let barButton = UIBarButtonItem(customView: button) 32 | 33 | barButton.customView?.translatesAutoresizingMaskIntoConstraints = false 34 | barButton.customView?.widthAnchor.constraint(equalToConstant: 35).isActive = true 35 | barButton.customView?.heightAnchor.constraint(equalToConstant: 35).isActive = true 36 | 37 | return barButton 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /BoostRunClub/Views/Renderer/BasicRouteOverlay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RouteOverlay.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/15. 6 | // 7 | 8 | import MapKit 9 | 10 | class BasicRouteOverlay: NSObject, MKOverlay { 11 | var boundingMapRect: MKMapRect 12 | var coordinate: CLLocationCoordinate2D { 13 | CLLocationCoordinate2D(latitude: boundingMapRect.midX, longitude: boundingMapRect.midY) 14 | } 15 | 16 | var locations: [Location] 17 | 18 | init(locations: [Location], mapRect _: MKMapRect) { 19 | self.locations = locations 20 | boundingMapRect = .world 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /BoostRunClub/Views/Renderer/BasicRouteRenderer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BasicRouteRenderer.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/15. 6 | // 7 | 8 | import MapKit 9 | 10 | class BasicRouteRenderer: MKOverlayPathRenderer { 11 | var routeOverlay: BasicRouteOverlay 12 | 13 | init(overlay: BasicRouteOverlay) { 14 | routeOverlay = overlay 15 | super.init(overlay: routeOverlay) 16 | } 17 | 18 | override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) { 19 | let baseWidth = lineWidth / zoomScale 20 | 21 | let locations = routeOverlay.locations 22 | 23 | context.setLineWidth(baseWidth) 24 | context.setLineJoin(.round) 25 | context.setLineCap(.round) 26 | 27 | let points = locations.map { self.point(for: MKMapPoint($0.coord2d)) } 28 | let paths = CGMutablePath() 29 | paths.addLines(between: points) 30 | 31 | let lineColor = strokeColor?.cgColor ?? UIColor.black.cgColor 32 | context.setStrokeColor(lineColor) 33 | context.beginPath() 34 | context.addPath(paths) 35 | context.strokePath() 36 | 37 | super.draw(mapRect, zoomScale: zoomScale, in: context) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /BoostRunClub/Views/Renderer/GradientRouteOverlay.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientRouteOverlay.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/15. 6 | // 7 | 8 | import MapKit 9 | 10 | class PaceGradientRouteOverlay: BasicRouteOverlay { 11 | let maxSpeed: CGFloat 12 | let minSpeed: CGFloat 13 | var maxHue: CGFloat 14 | var minHue: CGFloat 15 | var colors = [(start: CGColor, end: CGColor)]() 16 | var slices = [RunningSlice]() 17 | 18 | init( 19 | locations: [Location], 20 | splits: [RunningSplit], 21 | mapRect: MKMapRect, 22 | colorMin: UIColor, 23 | colorMax: UIColor 24 | ) { 25 | slices = splits.reduce(into: []) { $0.append(contentsOf: $1.runningSlices) } 26 | let minMaxValue = locations.reduce(into: (min: Int.max, max: Int.min)) { 27 | $0.min = min($0.min, $1.speed) 28 | $0.max = max($0.max, $1.speed) 29 | } 30 | maxSpeed = CGFloat(minMaxValue.max) 31 | minSpeed = CGFloat(minMaxValue.min) 32 | 33 | minHue = colorMin.hsba.hue 34 | maxHue = colorMax.hsba.hue 35 | maxHue += minHue > maxHue ? 1 : 0 36 | 37 | super.init(locations: locations, mapRect: mapRect) 38 | 39 | setColors() 40 | } 41 | 42 | private func setColors() { 43 | let speedFactor: CGFloat = 1 / (maxSpeed - minSpeed) 44 | let hueFactor: CGFloat = (maxHue - minHue) 45 | 46 | slices.forEach { slice in 47 | if !slice.isRunning { 48 | self.colors.append((start: UIColor.black.cgColor, end: UIColor.black.cgColor)) 49 | return 50 | } 51 | let startIdx = clamped(value: slice.startIndex, minValue: 0, maxValue: locations.count - 2) 52 | let endIdx = clamped(value: slice.endIndex, minValue: startIdx, maxValue: locations.count - 1) 53 | 54 | let startHue = minHue + (CGFloat(locations[startIdx].speed) - minSpeed) * speedFactor * hueFactor 55 | let endHue = minHue + (CGFloat(locations[endIdx].speed) - minSpeed) * speedFactor * hueFactor 56 | self.colors.append(( 57 | start: UIColor(hue: startHue, saturation: 1, brightness: 1, alpha: 1).cgColor, 58 | end: UIColor(hue: endHue, saturation: 1, brightness: 1, alpha: 1).cgColor 59 | ) 60 | ) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /BoostRunClub/Views/Renderer/GradientRouteRenderer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GradientRouteRenderer.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/15. 6 | // 7 | 8 | import MapKit 9 | 10 | class GradientRouteRenderer: MKOverlayPathRenderer { 11 | var routeOverlay: PaceGradientRouteOverlay 12 | 13 | var borderColor: UIColor = .white 14 | 15 | init(overlay: PaceGradientRouteOverlay) { 16 | routeOverlay = overlay 17 | super.init(overlay: overlay) 18 | } 19 | 20 | override func draw(_: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) { 21 | guard routeOverlay.locations.count > 2 else { return } 22 | let baseWidth = lineWidth / zoomScale 23 | let points = routeOverlay.locations.map { self.point(for: MKMapPoint($0.coord2d)) } 24 | 25 | let borderPath = CGMutablePath() 26 | borderPath.addLines(between: points) 27 | 28 | context.setLineWidth(baseWidth * 2) 29 | context.setLineJoin(.round) 30 | context.setLineCap(.round) 31 | context.addPath(borderPath) 32 | context.setStrokeColor(borderColor.cgColor) 33 | context.strokePath() 34 | 35 | for (idx, slice) in routeOverlay.slices.enumerated() { 36 | let startIdx = clamped(value: slice.startIndex, minValue: 0, maxValue: points.count - 2) 37 | let endIdx = clamped(value: slice.endIndex, minValue: startIdx + 1, maxValue: points.count - 1) 38 | let startColor = routeOverlay.colors[idx].start 39 | let endColor = routeOverlay.colors[idx].end 40 | 41 | context.setLineWidth(baseWidth) 42 | context.setLineJoin(.round) 43 | context.setLineCap(.round) 44 | 45 | let path = CGMutablePath() 46 | path.move(to: points[startIdx]) 47 | path.addLines(between: points[startIdx ... endIdx].map { $0 }) 48 | 49 | context.addPath(path) 50 | context.saveGState() 51 | 52 | context.replacePathWithStrokedPath() 53 | context.clip() 54 | 55 | guard 56 | let gradient = CGGradient( 57 | colorsSpace: nil, 58 | colors: [startColor, endColor] as CFArray, 59 | locations: [0, 1] 60 | ) 61 | else { 62 | context.restoreGState() 63 | continue 64 | } 65 | 66 | context.drawLinearGradient( 67 | gradient, start: points[startIdx], 68 | end: points[endIdx], 69 | options: [.drawsAfterEndLocation] 70 | ) 71 | context.restoreGState() 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /BoostRunClub/Views/RunDataView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunDataView.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/11/28. 6 | // 7 | 8 | import UIKit 9 | 10 | final class RunDataView: UIStackView { 11 | enum Style { 12 | case main, sub 13 | } 14 | 15 | lazy var valueLabel = makeValueLabel() 16 | private lazy var descriptionLabel = maekDescriptionLabel() 17 | let style: Style 18 | var tapAction: (() -> Void)? 19 | 20 | init(style: Style = .sub) { 21 | self.style = style 22 | super.init(frame: .zero) 23 | commonInit() 24 | } 25 | 26 | required init(coder: NSCoder) { 27 | style = .sub 28 | super.init(coder: coder) 29 | commonInit() 30 | } 31 | 32 | func setValue(value: String) { 33 | valueLabel.text = value 34 | } 35 | 36 | func setType(type: String) { 37 | descriptionLabel.text = type 38 | } 39 | } 40 | 41 | // MARK: - Actions 42 | 43 | extension RunDataView { 44 | @objc 45 | private func execute() { 46 | tapAction?() 47 | } 48 | 49 | func startBounceAnimation() { 50 | transform = transform.scaledBy(x: 0.5, y: 0.5) 51 | UIView.animate(withDuration: 0.3) { 52 | self.transform = .identity 53 | } 54 | } 55 | } 56 | 57 | // MARK: - Configure 58 | 59 | extension RunDataView { 60 | private func commonInit() { 61 | distribution = .equalSpacing 62 | alignment = .center 63 | axis = .vertical 64 | 65 | switch style { 66 | case .main: 67 | valueLabel.font = valueLabel.font.withSize(120) 68 | descriptionLabel.font = descriptionLabel.font.withSize(30) 69 | case .sub: 70 | valueLabel.font = valueLabel.font.withSize(35) 71 | descriptionLabel.font = descriptionLabel.font.withSize(20) 72 | } 73 | 74 | addArrangedSubview(valueLabel) 75 | addArrangedSubview(descriptionLabel) 76 | 77 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(execute)) 78 | 79 | addGestureRecognizer(tapGesture) 80 | } 81 | 82 | private func makeValueLabel() -> UILabel { 83 | let label: UILabel 84 | switch style { 85 | case .main: 86 | label = NikeLabel() 87 | case .sub: 88 | label = UILabel() 89 | label.font = UIFont.boldSystemFont(ofSize: 17) 90 | } 91 | label.textColor = UIColor(named: "accent2") 92 | label.textAlignment = .center 93 | label.text = "00:00" 94 | return label 95 | } 96 | 97 | private func maekDescriptionLabel() -> UILabel { 98 | let label = UILabel() 99 | label.textColor = UIColor.systemGray2.withAlphaComponent(0.7) 100 | label.textAlignment = .center 101 | label.font = UIFont.boldSystemFont(ofSize: 17) 102 | label.text = "시간" 103 | return label 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /BoostRunClub/Views/SplitInfoDetailScene/SplitDatailSplitCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitDatailSplitCell.swift 3 | // BoostRunClub 4 | // 5 | // Created by 조기현 on 2020/12/14. 6 | // 7 | 8 | import UIKit 9 | 10 | class SplitDatailSplitCell: UITableViewCell { 11 | let kilometerLabel = UILabel() 12 | let paceLabel = UILabel() 13 | let changeLabel = UILabel() 14 | let elevationLabel = UILabel() 15 | 16 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 17 | super.init(style: style, reuseIdentifier: reuseIdentifier) 18 | commonInit() 19 | kilometerLabel.text = "asdf" 20 | } 21 | 22 | required init?(coder: NSCoder) { 23 | super.init(coder: coder) 24 | } 25 | 26 | private func commonInit() { 27 | let labels = [kilometerLabel, paceLabel, changeLabel, elevationLabel] 28 | labels.enumerated().forEach { idx, label in 29 | label.font = UIFont.systemFont(ofSize: 17) 30 | label.textColor = .label 31 | label.setTextAlignment(idx: idx, total: labels.count) 32 | } 33 | kilometerLabel.textColor = .systemGray 34 | 35 | let stackView = UIStackView.make(with: labels, distribution: .fillEqually) 36 | contentView.addSubview(stackView) 37 | stackView.translatesAutoresizingMaskIntoConstraints = false 38 | NSLayoutConstraint.activate([ 39 | stackView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), 40 | stackView.widthAnchor.constraint(equalTo: contentView.widthAnchor, constant: -40), 41 | stackView.topAnchor.constraint(equalTo: contentView.topAnchor), 42 | stackView.centerYAnchor.constraint(equalTo: centerYAnchor), 43 | stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), 44 | stackView.heightAnchor.constraint(equalToConstant: 60), 45 | ]) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /BoostRunClub/Views/SplitInfoDetailScene/SplitDetailDateInfoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitDetailTotalHeaderView.swift 3 | // BoostRunClub 4 | // 5 | // Created by 조기현 on 2020/12/14. 6 | // 7 | 8 | import UIKit 9 | 10 | class SplitDetailDateInfoView: UIView { 11 | let dateLabel = UILabel() 12 | let timeLabel = UILabel() 13 | 14 | override init(frame: CGRect) { 15 | super.init(frame: frame) 16 | configure() 17 | } 18 | 19 | required init?(coder: NSCoder) { 20 | super.init(coder: coder) 21 | } 22 | 23 | var stackView: UIStackView? 24 | 25 | private func configure() { 26 | backgroundColor = .systemBackground 27 | dateLabel.font = UIFont.boldSystemFont(ofSize: 24) 28 | timeLabel.font = UIFont.systemFont(ofSize: 17) 29 | 30 | let stackView = UIStackView.make(with: [dateLabel, timeLabel], 31 | axis: .vertical, 32 | spacing: 10) 33 | addSubview(stackView) 34 | stackView.translatesAutoresizingMaskIntoConstraints = false 35 | NSLayoutConstraint.activate([ 36 | stackView.centerYAnchor.constraint(equalTo: centerYAnchor), 37 | stackView.centerXAnchor.constraint(equalTo: centerXAnchor), 38 | stackView.widthAnchor.constraint(equalTo: widthAnchor, constant: -40), 39 | ]) 40 | self.stackView = stackView 41 | } 42 | } 43 | 44 | extension UILabel { 45 | func setTextAlignment(idx: Int, total: Int) { 46 | if total == 4, idx == 1 { 47 | textAlignment = .left 48 | } else { 49 | textAlignment = [.left, .center, .right][idx == 0 ? 0 : 1 + (idx + 1) / total] 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /BoostRunClub/Views/SplitInfoDetailScene/SplitDetailInfoCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitDetailInfoCell.swift 3 | // BoostRunClub 4 | // 5 | // Created by 조기현 on 2020/12/14. 6 | // 7 | 8 | import UIKit 9 | 10 | class SplitDetailInfoCell: UITableViewCell { 11 | lazy var titleLabel: UILabel = { 12 | let label = UILabel() 13 | label.font = UIFont.systemFont(ofSize: fontSize) 14 | label.textColor = .lightGray 15 | label.textAlignment = .left 16 | return label 17 | }() 18 | 19 | lazy var valueLabel: UILabel = { 20 | let label = UILabel() 21 | label.font = UIFont.boldSystemFont(ofSize: fontSize) 22 | label.textColor = .label 23 | label.textAlignment = .right 24 | return label 25 | }() 26 | 27 | private let fontSize: CGFloat = 17 28 | 29 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 30 | super.init(style: style, reuseIdentifier: reuseIdentifier) 31 | commonInit() 32 | } 33 | 34 | required init?(coder: NSCoder) { 35 | super.init(coder: coder) 36 | } 37 | 38 | func commonInit() { 39 | let stackView = UIStackView.make(with: [titleLabel, valueLabel], distribution: .fillEqually) 40 | contentView.addSubview(stackView) 41 | stackView.translatesAutoresizingMaskIntoConstraints = false 42 | NSLayoutConstraint.activate([ 43 | stackView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), 44 | stackView.widthAnchor.constraint(equalTo: contentView.widthAnchor, constant: -40), 45 | stackView.topAnchor.constraint(equalTo: contentView.topAnchor), 46 | stackView.centerYAnchor.constraint(equalTo: centerYAnchor), 47 | stackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), 48 | stackView.heightAnchor.constraint(equalToConstant: 60), 49 | ]) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /BoostRunClub/Views/SplitInfoDetailScene/SplitDetailSplitHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitDetailSplitHeaderView.swift 3 | // BoostRunClub 4 | // 5 | // Created by 조기현 on 2020/12/14. 6 | // 7 | 8 | import UIKit 9 | 10 | class SplitDetailSplitHeaderView: UIView { 11 | private lazy var titleLabel: UILabel = { 12 | let label = UILabel() 13 | label.text = "구간" 14 | label.font = UIFont.boldSystemFont(ofSize: 30) 15 | label.textColor = .label 16 | return label 17 | }() 18 | 19 | private let secondView = SplitHeaderView(titles: ["KM", "평균 페이스", "+/-", "고도"], with: false, inset: 0) 20 | private let inset: CGFloat = 20 21 | 22 | override init(frame: CGRect) { 23 | super.init(frame: frame) 24 | configure() 25 | } 26 | 27 | required init?(coder: NSCoder) { 28 | super.init(coder: coder) 29 | } 30 | 31 | private func configure() { 32 | addSubview(titleLabel) 33 | titleLabel.translatesAutoresizingMaskIntoConstraints = false 34 | NSLayoutConstraint.activate([ 35 | titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: inset), 36 | titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor, constant: inset), 37 | titleLabel.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -inset), 38 | ]) 39 | 40 | addSubview(secondView) 41 | secondView.translatesAutoresizingMaskIntoConstraints = false 42 | NSLayoutConstraint.activate([ 43 | secondView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor), 44 | secondView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: inset), 45 | secondView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -inset), 46 | secondView.bottomAnchor.constraint(equalTo: bottomAnchor), 47 | ]) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /BoostRunClub/Views/SplitInfoDetailScene/SplitHeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitHeaderView.swift 3 | // BoostRunClub 4 | // 5 | // Created by 조기현 on 2020/12/09. 6 | // 7 | 8 | import UIKit 9 | 10 | class SplitHeaderView: UIView { 11 | var inset: CGFloat = -40 12 | 13 | init(titles: [String], with bottomBorder: Bool = true, inset: CGFloat = -40) { 14 | super.init(frame: .zero) 15 | self.inset = inset 16 | configure(titles: titles, bottomBorder: bottomBorder) 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 | func configure(titles: [String], bottomBorder: Bool) { 28 | backgroundColor = .systemBackground 29 | 30 | let labels: [UILabel] = titles.enumerated().map { idx, title in 31 | let label = UILabel() 32 | label.text = title 33 | label.font = label.font.withSize(17) 34 | label.textColor = .lightGray 35 | label.setTextAlignment(idx: idx, total: titles.count) 36 | return label 37 | } 38 | 39 | let stackView = UIStackView.make(with: labels, distribution: .fillEqually) 40 | addSubview(stackView) 41 | stackView.translatesAutoresizingMaskIntoConstraints = false 42 | NSLayoutConstraint.activate([ 43 | stackView.centerYAnchor.constraint(equalTo: centerYAnchor), 44 | stackView.centerXAnchor.constraint(equalTo: centerXAnchor), 45 | stackView.widthAnchor.constraint(equalTo: widthAnchor, constant: inset), 46 | stackView.heightAnchor.constraint(equalToConstant: 60), 47 | ]) 48 | 49 | if bottomBorder { 50 | addBottomBorder() 51 | } 52 | } 53 | 54 | func addBottomBorder() { 55 | let bottomBorder = UIView() 56 | bottomBorder.backgroundColor = .lightGray 57 | addSubview(bottomBorder) 58 | bottomBorder.translatesAutoresizingMaskIntoConstraints = false 59 | NSLayoutConstraint.activate([ 60 | bottomBorder.centerXAnchor.constraint(equalTo: centerXAnchor), 61 | bottomBorder.widthAnchor.constraint(equalTo: widthAnchor, constant: inset), 62 | bottomBorder.bottomAnchor.constraint(equalTo: bottomAnchor), 63 | bottomBorder.heightAnchor.constraint(equalToConstant: 0.5), 64 | ]) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /BoostRunClubTests/BoostRunClubTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoostRunClubTests.swift 3 | // BoostRunClubTests 4 | // 5 | // Created by 김신우 on 2020/11/28. 6 | // 7 | 8 | import XCTest 9 | 10 | class BoostRunClubTests: XCTestCase { 11 | override func setUpWithError() throws { 12 | // Put setup code here. This method is called before the invocation of each test method in the class. 13 | } 14 | 15 | override func tearDownWithError() throws { 16 | // Put teardown code here. This method is called after the invocation of each test method in the class. 17 | } 18 | 19 | func testExample() throws { 20 | // This is an example of a functional test case. 21 | // Use XCTAssert and related functions to verify your tests produce the correct results. 22 | } 23 | 24 | func testPerformanceExample() throws { 25 | // This is an example of a performance test case. 26 | measure { 27 | // Put the code you want to measure the time of here. 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /BoostRunClubTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleVersion 18 | 1 19 | 20 | 21 | -------------------------------------------------------------------------------- /BoostRunClubTests/Mocks/DefaultsProviderMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DefaultsProviderMock.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/20. 6 | // 7 | 8 | import Foundation 9 | 10 | class DefaultsProviderMock: DefaultsReadable, DefaultsWritable { 11 | func set(_: Any?, forKey _: String) {} 12 | 13 | func string(forKey _: String) -> String? { 14 | return "" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BoostRunClubTests/Mocks/EventTimeProvidableMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EventTimeProvidableMock.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/20. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | class EventTimeProvidableMock: EventTimeProvidable { 12 | var isStarted = false 13 | 14 | var timeIntervalSubject = PassthroughSubject() 15 | 16 | func start() { 17 | isStarted = true 18 | } 19 | 20 | func stop() { 21 | isStarted = false 22 | } 23 | 24 | func send(_ interval: Double) { 25 | timeIntervalSubject.send(interval) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /BoostRunClubTests/Mocks/LocationProviderMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockLocationProvider.swift 3 | // BoostRunClubTests 4 | // 5 | // Created by 김신우 on 2020/11/28. 6 | // 7 | 8 | import Combine 9 | import CoreLocation 10 | import Foundation 11 | 12 | class LocationProviderMock: LocationProvidable { 13 | var isRunningBackgroundTask = false 14 | 15 | func startBackgroundTask() { 16 | isRunningBackgroundTask = true 17 | } 18 | 19 | func stopBackgroundTask() { 20 | isRunningBackgroundTask = false 21 | } 22 | 23 | var locationSubject = PassthroughSubject() 24 | 25 | func send(_ location: CLLocation) { 26 | locationSubject.send(location) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /BoostRunClubTests/Mocks/MotionDataModelProvidableMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MotionDataModelProvidableMock.swift 3 | // BoostRunClubTests 4 | // 5 | // Created by 김신우 on 2020/12/20. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | class MotionDataModelProvidableMock: MotionDataModelProvidable { 12 | var isStarted = false 13 | 14 | func start() { 15 | isStarted = true 16 | } 17 | 18 | func stop() { 19 | isStarted = false 20 | } 21 | 22 | var motionTypeSubject = PassthroughSubject() 23 | 24 | func send(_ motionType: MotionType) { 25 | motionTypeSubject.send(motionType) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /BoostRunClubTests/Mocks/PedometerProviderMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PedometerProviderMock.swift 3 | // BoostRunClub 4 | // 5 | // Created by 김신우 on 2020/12/20. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | class PedometerProviderMock: PedometerProvidable { 12 | var isStarted = false 13 | 14 | func start() { 15 | isStarted = true 16 | } 17 | 18 | func stop() { 19 | isStarted = false 20 | } 21 | 22 | var cadenceSubject = PassthroughSubject() 23 | 24 | func send(_ cadence: Int) { 25 | cadenceSubject.send(cadence) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /BoostRunClubTests/Mocks/RunningDashBoardServiceableMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningDashBoardServiceableMock.swift 3 | // BoostRunClubTests 4 | // 5 | // Created by 김신우 on 2020/12/20. 6 | // 7 | 8 | import Combine 9 | import CoreLocation 10 | import Foundation 11 | 12 | class RunningDashBoardServiceableMock: RunningDashBoardServiceable { 13 | var runningStateSubject = PassthroughSubject() 14 | var runningTime = CurrentValueSubject(0) 15 | 16 | var location: CLLocation? 17 | var calorie: Double = 0 18 | var pace: Double = 0 19 | var cadence: Int = 0 20 | var distance: Double = 0 21 | var avgPace: Double = 0 22 | 23 | var isRunning = false 24 | func setState(isRunning: Bool) { 25 | self.isRunning = isRunning 26 | } 27 | 28 | var isStarted = false 29 | func start() { 30 | isStarted = true 31 | } 32 | 33 | func stop() { 34 | isStarted = false 35 | } 36 | 37 | func clear() { 38 | runningTime.value = 0 39 | location = nil 40 | calorie = 0 41 | pace = 0 42 | cadence = 0 43 | distance = 0 44 | avgPace = 0 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /BoostRunClubTests/Mocks/RunningMotionServiceableMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningMotionServiceableMock.swift 3 | // BoostRunClubTests 4 | // 5 | // Created by 김신우 on 2020/12/20. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | class RunningMotionServiceableMock: RunningMotionServiceable { 12 | var isStarted = false 13 | 14 | var motionTypeSubject = PassthroughSubject() 15 | func start() { 16 | isStarted = true 17 | } 18 | 19 | func stop() { 20 | isStarted = false 21 | } 22 | 23 | func send(_ motionType: MotionType) { 24 | motionTypeSubject.send(motionType) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /BoostRunClubTests/Mocks/RunningRecordServiceableMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningRecordServiceableMock.swift 3 | // BoostRunClubTests 4 | // 5 | // Created by 김신우 on 2020/12/20. 6 | // 7 | 8 | import Combine 9 | import CoreLocation 10 | import Foundation 11 | 12 | class RunningRecordServiceableMock: RunningRecordServiceable { 13 | func addState(_: RunningState) {} 14 | 15 | func save(startTime _: Date?, endTime _: Date?) -> (activity: Activity, detail: ActivityDetail)? { 16 | return nil 17 | } 18 | 19 | func clear() {} 20 | 21 | var locations: [CLLocation] = [] 22 | var routes: [RunningSlice] = [] 23 | var didAddSplitSignal = PassthroughSubject() 24 | } 25 | -------------------------------------------------------------------------------- /BoostRunClubTests/Mocks/RunningServiceTypeMock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RunningServiceTypeMock.swift 3 | // BoostRunClubTests 4 | // 5 | // Created by 김신우 on 2020/12/20. 6 | // 7 | 8 | import Combine 9 | import Foundation 10 | 11 | class RunningServiceTypeMock: RunningServiceType { 12 | var dashBoardService: RunningDashBoardServiceable 13 | var recordService: RunningRecordServiceable 14 | 15 | init( 16 | dashBoard: RunningDashBoardServiceable, 17 | recorder: RunningRecordServiceable, 18 | motionProvider _: RunningMotionServiceable 19 | ) { 20 | dashBoardService = dashBoard 21 | recordService = recorder 22 | } 23 | 24 | var runningResultSubject = PassthroughSubject<(activity: Activity, detail: ActivityDetail)?, Never>() 25 | var runningStateSubject = CurrentValueSubject(.standing) 26 | var runningEventSubject = PassthroughSubject() 27 | var isStarted: Bool = false 28 | var isPaused: Bool = false 29 | var autoResume: Bool = true 30 | 31 | func start() { 32 | isStarted = true 33 | } 34 | 35 | func stop() { 36 | isStarted = false 37 | } 38 | 39 | func pause(autoResume: Bool) { 40 | self.autoResume = autoResume 41 | isPaused = true 42 | } 43 | 44 | func resume() { 45 | isPaused = false 46 | } 47 | 48 | var goalInfo: GoalInfo? 49 | func setGoal(_ goalInfo: GoalInfo?) { 50 | self.goalInfo = goalInfo 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Docs/prototype.xd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boostcamp-2020/Project18-B-iOS-BoostRunClub/61a9e892e3a363ad381ee1ca12c4147c17947975/Docs/prototype.xd -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 부스트캠프 2020 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | target 'BoostRunClub' do 2 | use_frameworks! 3 | pod 'SwiftLint' 4 | pod 'SwiftFormat/CLI' 5 | end 6 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - SwiftFormat/CLI (0.47.4) 3 | - SwiftLint (0.40.3) 4 | 5 | DEPENDENCIES: 6 | - SwiftFormat/CLI 7 | - SwiftLint 8 | 9 | SPEC REPOS: 10 | trunk: 11 | - SwiftFormat 12 | - SwiftLint 13 | 14 | SPEC CHECKSUMS: 15 | SwiftFormat: 43c9302b7dbca1cda59acfc2ac63ac86e281fb14 16 | SwiftLint: dfd554ff0dff17288ee574814ccdd5cea85d76f7 17 | 18 | PODFILE CHECKSUM: 62520c5a7451c3d363ff5e01d4c076f458209ad4 19 | 20 | COCOAPODS: 1.9.3 21 | --------------------------------------------------------------------------------