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