├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ └── ownmyway-issue-template.md
└── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── OwnMyWay
├── .swiftlint.yml
├── OwnMyWay.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── OwnMyWay.xcscheme
├── OwnMyWay
│ ├── Application
│ │ ├── AppDelegate.swift
│ │ └── SceneDelegate.swift
│ ├── Data
│ │ ├── Network
│ │ │ └── DataMapping
│ │ │ │ └── LandmarkDTO.swift
│ │ ├── Persistent
│ │ │ └── CoreData
│ │ │ │ ├── DataMapping
│ │ │ │ ├── LandmarkMO+CoreDataClass.swift
│ │ │ │ ├── LocationMO+CoreDataClass.swift
│ │ │ │ ├── RecordMO+CoreDataClass.swift
│ │ │ │ └── TravelMO+CoreDataClass.swift
│ │ │ │ ├── LandmarkMO+CoreDataProperties.swift
│ │ │ │ ├── LocationMO+CoreDataProperties.swift
│ │ │ │ ├── OwnMyWay.xcdatamodeld
│ │ │ │ ├── .xccurrentversion
│ │ │ │ └── OwnMyWay.xcdatamodel
│ │ │ │ │ └── contents
│ │ │ │ ├── RecordMO+CoreDataProperties.swift
│ │ │ │ └── TravelMO+CoreDataProperties.swift
│ │ └── Repositories
│ │ │ ├── CoreDataTravelRepository.swift
│ │ │ └── LocalJSONLandmarkRepository.swift
│ ├── Domain
│ │ ├── Entities
│ │ │ ├── Landmark.swift
│ │ │ ├── Location.swift
│ │ │ ├── Record.swift
│ │ │ └── Travel.swift
│ │ ├── Interfaces
│ │ │ └── Repositories
│ │ │ │ ├── LandmarkRepository.swift
│ │ │ │ └── TravelRepository.swift
│ │ └── Usecases
│ │ │ ├── DefaultAddRecordUsecase.swift
│ │ │ ├── DefaultCompleteCreationUsecase.swift
│ │ │ ├── DefaultCompleteEditingUsecase.swift
│ │ │ ├── DefaultDetailRecordUsecase.swift
│ │ │ ├── DefaultEnterDateUsecase.swift
│ │ │ ├── DefaultEnterTitleUsecase.swift
│ │ │ ├── DefaultHomeUsecase.swift
│ │ │ ├── DefaultReservedTravelUsecase.swift
│ │ │ ├── DefaultSearchLandmarkUsecase.swift
│ │ │ └── DefaultStartedTravelUsecase.swift
│ ├── Infrastructure
│ │ ├── Error
│ │ │ ├── EnterTitleError.swift
│ │ │ ├── ErrorManager.swift
│ │ │ ├── JSONError.swift
│ │ │ ├── ModelError.swift
│ │ │ └── RepositoryError.swift
│ │ ├── File
│ │ │ └── ImageFileManager.swift
│ │ └── Location
│ │ │ ├── LocationManager.swift
│ │ │ └── Utils
│ │ │ └── Extension
│ │ │ └── CLLocationManager+.swift
│ ├── Presentation
│ │ ├── AddLanmarkScene
│ │ │ ├── Coordinator
│ │ │ │ └── AddLandmarkCoordinator.swift
│ │ │ ├── View
│ │ │ │ ├── AddLandmark.storyboard
│ │ │ │ └── AddLandmarkViewController.swift
│ │ │ └── ViewModel
│ │ │ │ └── AddLandmarkViewModel.swift
│ │ ├── AddRecordScene
│ │ │ ├── Coordinator
│ │ │ │ └── AddRecordCoordinator.swift
│ │ │ ├── Interface
│ │ │ │ └── Usecase
│ │ │ │ │ └── AddRecordUsecase.swift
│ │ │ ├── View
│ │ │ │ ├── AddRecord.storyboard
│ │ │ │ └── AddRecordViewController.swift
│ │ │ └── ViewModel
│ │ │ │ └── AddRecordViewModel.swift
│ │ ├── Common
│ │ │ ├── CollectionView
│ │ │ │ ├── CommentCell.swift
│ │ │ │ ├── CommentCell.xib
│ │ │ │ ├── DateHeaderView.swift
│ │ │ │ ├── DateHeaderView.xib
│ │ │ │ ├── HomeBackgroundView.swift
│ │ │ │ ├── HomeBackgroundView.xib
│ │ │ │ ├── LandmarkCardCell.swift
│ │ │ │ ├── LandmarkCardCell.xib
│ │ │ │ ├── MessageCell.swift
│ │ │ │ ├── MessageCell.xib
│ │ │ │ ├── OMWCollectionViewCell.swift
│ │ │ │ ├── PhotoCell.swift
│ │ │ │ ├── PhotoCell.xib
│ │ │ │ ├── PlusCell.swift
│ │ │ │ ├── PlusCell.xib
│ │ │ │ ├── RecordCardCell.swift
│ │ │ │ ├── RecordCardCell.xib
│ │ │ │ ├── TravelCardCell.swift
│ │ │ │ ├── TravelCardCell.xib
│ │ │ │ ├── TravelSectionHeader.swift
│ │ │ │ └── TravelSectionHeader.xib
│ │ │ ├── LocationTableViewCell.swift
│ │ │ ├── LocationTableViewCell.xib
│ │ │ ├── MapView
│ │ │ │ ├── LandmarkAnnotationView.swift
│ │ │ │ ├── OMWMapView.swift
│ │ │ │ └── RecordAnnotationView.swift
│ │ │ ├── NextButton.swift
│ │ │ ├── OMWCalendar
│ │ │ │ ├── CalendarCell.swift
│ │ │ │ ├── CalendarCell.xib
│ │ │ │ ├── CalendarDataSource.swift
│ │ │ │ └── OMWCalendar.swift
│ │ │ └── OMWSegmentedControl.swift
│ │ ├── CompleteScene
│ │ │ ├── CompleteCreation
│ │ │ │ ├── Coordinator
│ │ │ │ │ └── CompleteCreationCoordinator.swift
│ │ │ │ ├── Interface
│ │ │ │ │ └── Usecase
│ │ │ │ │ │ └── CompleteCreationUsecase.swift
│ │ │ │ ├── View
│ │ │ │ │ ├── CompleteCreation.storyboard
│ │ │ │ │ └── CompleteCreationViewController.swift
│ │ │ │ └── ViewModel
│ │ │ │ │ └── CompleteCreationViewModel.swift
│ │ │ └── CompleteEditing
│ │ │ │ ├── Coordinator
│ │ │ │ └── CompleteEditingCoordinator.swift
│ │ │ │ ├── Interface
│ │ │ │ └── Usecase
│ │ │ │ │ └── CompleteEditingUsecase.swift
│ │ │ │ ├── View
│ │ │ │ ├── CompleteEditing.storyboard
│ │ │ │ └── CompleteEditingViewController.swift
│ │ │ │ └── ViewModel
│ │ │ │ └── CompleteEditingViewModel.swift
│ │ ├── DetailImageScene
│ │ │ ├── Coordinator
│ │ │ │ └── DetailImageCoordinator.swift
│ │ │ ├── View
│ │ │ │ ├── DetailImage.storyboard
│ │ │ │ └── DetailImageViewController.swift
│ │ │ └── ViewModel
│ │ │ │ └── DetailImageViewModel.swift
│ │ ├── DetailRecordScene
│ │ │ ├── Coordinator
│ │ │ │ └── DetailRecordCoordinator.swift
│ │ │ ├── Interface
│ │ │ │ └── Usecase
│ │ │ │ │ └── DetailRecordUsecase.swift
│ │ │ ├── View
│ │ │ │ ├── DetailRecord.storyboard
│ │ │ │ └── DetailRecordViewController.swift
│ │ │ └── ViewModel
│ │ │ │ └── DetailRecordViewModel.swift
│ │ ├── EnterDateScene
│ │ │ ├── Coordinator
│ │ │ │ └── EnterDateCoordinator.swift
│ │ │ ├── Interface
│ │ │ │ └── Usecase
│ │ │ │ │ └── EnterDateUsecase.swift
│ │ │ ├── View
│ │ │ │ ├── EnterDate.storyboard
│ │ │ │ └── EnterDateViewController.swift
│ │ │ └── ViewModel
│ │ │ │ └── EnterDateViewModel.swift
│ │ ├── EnterTitleScene
│ │ │ ├── Coordinator
│ │ │ │ └── EnterTitleCoordinator.swift
│ │ │ ├── Interface
│ │ │ │ └── Usecase
│ │ │ │ │ └── EnterTitleUsecase.swift
│ │ │ ├── View
│ │ │ │ ├── EnterTitle.storyboard
│ │ │ │ └── EnterTitleViewController.swift
│ │ │ └── ViewModel
│ │ │ │ └── EnterTitleViewModel.swift
│ │ ├── HomeScene
│ │ │ ├── Coordinator
│ │ │ │ └── HomeCoordinator.swift
│ │ │ ├── Interface
│ │ │ │ └── Usecase
│ │ │ │ │ └── HomeUsecase.swift
│ │ │ ├── View
│ │ │ │ ├── Base.lproj
│ │ │ │ │ └── Home.storyboard
│ │ │ │ └── HomeViewController.swift
│ │ │ └── ViewModel
│ │ │ │ └── HomeViewModel.swift
│ │ ├── LandmarkCartScene
│ │ │ ├── Coordinator
│ │ │ │ └── LandmarkCartCoordinator.swift
│ │ │ ├── View
│ │ │ │ ├── LandmarkCart.storyboard
│ │ │ │ └── LandmarkCartViewController.swift
│ │ │ └── ViewModel
│ │ │ │ └── LandmarkCartViewModel.swift
│ │ ├── SearchLandmarkScene
│ │ │ ├── Coordinator
│ │ │ │ └── SearchLandmarkCoordinator.swift
│ │ │ ├── Interface
│ │ │ │ └── Usecase
│ │ │ │ │ └── SearchLandmarkUsecase.swift
│ │ │ ├── View
│ │ │ │ ├── SearchLandmark.storyboard
│ │ │ │ └── SearchLandmarkViewController.swift
│ │ │ └── ViewModel
│ │ │ │ └── SearchLandmarkViewModel.swift
│ │ ├── SearchLocationScene
│ │ │ ├── Coordinator
│ │ │ │ └── SearchLocationCoordinator.swift
│ │ │ ├── View
│ │ │ │ ├── SearchLocation.storyboard
│ │ │ │ └── SearchLocationViewController.swift
│ │ │ └── ViewModel
│ │ │ │ └── SearchLocationViewModel.swift
│ │ ├── TravelScene
│ │ │ ├── ReservedTravel
│ │ │ │ ├── Coordinator
│ │ │ │ │ └── ReservedTravelCoordinator.swift
│ │ │ │ ├── Interface
│ │ │ │ │ └── Usecase
│ │ │ │ │ │ └── ReservedTravelUsecase.swift
│ │ │ │ ├── View
│ │ │ │ │ ├── ReservedTravel.storyboard
│ │ │ │ │ └── ReservedTravelViewController.swift
│ │ │ │ └── ViewModel
│ │ │ │ │ └── ReservedTravelViewModel.swift
│ │ │ └── StartedTravel
│ │ │ │ ├── Interface
│ │ │ │ └── Usecase
│ │ │ │ │ └── StartedTravelUsecase.swift
│ │ │ │ ├── OngoingTravel
│ │ │ │ ├── Coordinator
│ │ │ │ │ └── OngoingTravelCoordinator.swift
│ │ │ │ ├── View
│ │ │ │ │ ├── OngoingTravel.storyboard
│ │ │ │ │ └── OngoingTravelViewController.swift
│ │ │ │ └── ViewModel
│ │ │ │ │ └── OngoingTravelViewModel.swift
│ │ │ │ ├── OutdatedTravel
│ │ │ │ ├── Coordinator
│ │ │ │ │ └── OutdatedTravelCoordinator.swift
│ │ │ │ ├── View
│ │ │ │ │ ├── OutdatedTravel.storyboard
│ │ │ │ │ └── OutdatedTravelViewController.swift
│ │ │ │ └── ViewModel
│ │ │ │ │ └── OutdatedTravelViewModel.swift
│ │ │ │ ├── StartedTravelCoordinatingDelegate.swift
│ │ │ │ └── StartedTravelViewModel.swift
│ │ └── Utils
│ │ │ ├── Coordinator.swift
│ │ │ ├── Extension
│ │ │ ├── Date+.swift
│ │ │ ├── UIButton+.swift
│ │ │ ├── UIImageView+.swift
│ │ │ ├── UIView+.swift
│ │ │ ├── UIViewController+.swift
│ │ │ └── URLSessionDataTask+.swift
│ │ │ └── Protocol
│ │ │ ├── Instantiable.swift
│ │ │ ├── LandmarkDeletable.swift
│ │ │ ├── MapAvailable.swift
│ │ │ ├── RecordUpdatable.swift
│ │ │ ├── TravelEditable.swift
│ │ │ ├── TravelFetchable.swift
│ │ │ └── TravelUpdatable.swift
│ └── Resource
│ │ ├── Assets.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ ├── 100.png
│ │ │ ├── 1024.png
│ │ │ ├── 114.png
│ │ │ ├── 120.png
│ │ │ ├── 144.png
│ │ │ ├── 152.png
│ │ │ ├── 167.png
│ │ │ ├── 180.png
│ │ │ ├── 20.png
│ │ │ ├── 29.png
│ │ │ ├── 40.png
│ │ │ ├── 50.png
│ │ │ ├── 57.png
│ │ │ ├── 58.png
│ │ │ ├── 60.png
│ │ │ ├── 72.png
│ │ │ ├── 76.png
│ │ │ ├── 80.png
│ │ │ ├── 87.png
│ │ │ └── Contents.json
│ │ ├── Color
│ │ │ ├── Contents.json
│ │ │ ├── HomeBackground.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── HomeContent.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── IdentityBlue.colorset
│ │ │ │ └── Contents.json
│ │ │ ├── PlusCard.colorset
│ │ │ │ └── Contents.json
│ │ │ └── WJGreen.colorset
│ │ │ │ └── Contents.json
│ │ ├── Contents.json
│ │ └── Image
│ │ │ ├── AddButton.imageset
│ │ │ ├── AddButton-1x.png
│ │ │ ├── AddButton-2x.png
│ │ │ ├── AddButton-3x.png
│ │ │ └── Contents.json
│ │ │ ├── AppName.imageset
│ │ │ ├── Contents.json
│ │ │ └── OwnMyWay.svg
│ │ │ ├── Contents.json
│ │ │ ├── LandmarkPin.imageset
│ │ │ ├── Contents.json
│ │ │ └── LandmarkPin.png
│ │ │ ├── MainIcon.imageset
│ │ │ ├── Contents.json
│ │ │ └── mainIcon.svg
│ │ │ ├── RecordPin.imageset
│ │ │ ├── Contents.json
│ │ │ └── RecordPin.svg
│ │ │ ├── Setting.imageset
│ │ │ ├── Contents.json
│ │ │ ├── setting-2x.png
│ │ │ ├── setting-3x.png
│ │ │ └── setting.png
│ │ │ └── airplane.imageset
│ │ │ ├── Contents.json
│ │ │ └── airplane.jpeg
│ │ ├── Base.lproj
│ │ └── LaunchScreen.storyboard
│ │ ├── BundleFont
│ │ ├── DancingScript-Bold.ttf
│ │ ├── DancingScript-Regular.ttf
│ │ └── nanum_handwriting.ttf
│ │ ├── BundleImage
│ │ └── addImage.png
│ │ ├── BundleJSON
│ │ └── landmark.json
│ │ └── Info.plist
├── UsecaseTest
│ └── AddRecordUsecaseTest.swift
└── ViewModelTest
│ ├── AddLandmarkViewModelTest.swift
│ ├── AddRecordViewModelTest.swift
│ ├── DetailRecordViewModelTest.swift
│ ├── EnterDateViewModelTest.swift
│ ├── EnterTitleViewModelTest.swift
│ ├── HomeViewModelTest.swift
│ ├── LandmarkCartViewModelTest.swift
│ ├── ReservedTravelViewModelTest.swift
│ ├── SearchLandmarkViewModelTest.swift
│ └── StartedTravelViewModelTest.swift
└── README.md
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.pbxproj binary merge=union
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/ownmyway-issue-template.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: OwnMyWay Issue Template
3 | about: 앞으로 해야할 일을 적어놓는 Issue
4 | title: "[S0-T0] Issue Title"
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### 작업 목록
11 | - task1
12 | - task2
13 |
14 | ### 예상 시간
15 | - Time
16 |
17 | ### 비고
18 | - Assignee 설정, Label (Priority & Bug/Feature/Test) 설정
19 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Task 🔨
2 | - [ ] [S0-T0] task1
3 | - [ ] [S0-T0] task2
4 | - [ ] [S0-T0] task3
5 |
6 | ### 체크리스트 ✅
7 |
8 | - [ ] Merge하는 브랜치 확인
9 | - [ ] 코드컨벤션 준수
10 | - [ ] 커밋컨벤션 준수
11 | - [ ] 커밋 세분화
12 | - [ ] 코드를 검증하는 테스트 추가
13 | - [ ] 관련된 기존 테스트 통과여부 확인
14 | - [ ] 리뷰어와 Label을 올바르게 설정
15 |
16 | ### 도큐먼트 📖
17 | - CollectionView
18 | - [CollectionView](Link)
19 |
--------------------------------------------------------------------------------
/OwnMyWay/.swiftlint.yml:
--------------------------------------------------------------------------------
1 | opt_in_rules:
2 | - closure_spacing
3 | - fatal_error_message
4 | - force_unwrapping
5 | - implicitly_unwrapped_optional
6 | - legacy_multiple
7 | - legacy_random
8 | - operator_usage_whitespace
9 |
10 | disabled_rules:
11 | - multiple_closures_with_trailing_closure
12 |
13 | indentation: 4
14 | line_length: 100
15 | identifier_name:
16 | min_length: 4
17 | excluded:
18 | - id
19 | - url
20 | - lhs
21 | - rhs
22 | type_name:
23 | min_length: 4
24 | function_parameter_count: 6
25 |
26 | excluded:
27 | - UsecaseTest/
28 | - ViewModelTest/
29 | - OwnMyWay/Application/AppDelegate.swift
30 | - OwnMyWay/Application/SceneDelegate.swift
31 | - OwnMyWay/Data/Persistent/CoreData/DataMapping/TravelMO+CoreDataClass.swift
32 | - OwnMyWay/Data/Persistent/CoreData/TravelMO+CoreDataProperties.swift
33 | - OwnMyWay/Data/Persistent/CoreData/DataMapping/RecordMO+CoreDataClass.swift
34 | - OwnMyWay/Data/Persistent/CoreData/RecordMO+CoreDataProperties.swift
35 | - OwnMyWay/Data/Persistent/CoreData/DataMapping/LandmarkMO+CoreDataClass.swift
36 | - OwnMyWay/Data/Persistent/CoreData/LandmarkMO+CoreDataProperties.swift
37 | - OwnMyWay/Data/Persistent/CoreData/DataMapping/LocationMO+CoreDataClass.swift
38 | - OwnMyWay/Data/Persistent/CoreData/LocationMO+CoreDataProperties.swift
39 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Application/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/01.
6 | //
7 |
8 | import UIKit
9 |
10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
11 |
12 | var window: UIWindow?
13 | var coordinator: Coordinator?
14 |
15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
16 | guard let windowScene = (scene as? UIWindowScene) else {
17 | return
18 | }
19 | window = UIWindow(frame: UIScreen.main.bounds)
20 | window?.windowScene = windowScene
21 | let navigationController = UINavigationController()
22 | navigationController.navigationBar.tintColor = .label
23 | navigationController.navigationBar.setBackgroundImage(UIImage(), for: .default)
24 | navigationController.navigationBar.clipsToBounds = true
25 |
26 | coordinator = HomeCoordinator(navigationController: navigationController)
27 | coordinator?.start()
28 | window?.rootViewController = navigationController
29 | window?.makeKeyAndVisible()
30 | }
31 |
32 | func sceneDidDisconnect(_ scene: UIScene) {
33 | // Called as the scene is being released by the system.
34 | // This occurs shortly after the scene enters the background, or when its session is discarded.
35 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
36 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).
37 | }
38 |
39 | func sceneDidBecomeActive(_ scene: UIScene) {
40 | // Called when the scene has moved from an inactive state to an active state.
41 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
42 | }
43 |
44 | func sceneWillResignActive(_ scene: UIScene) {
45 | // Called when the scene will move from an active state to an inactive state.
46 | // This may occur due to temporary interruptions (ex. an incoming phone call).
47 | }
48 |
49 | func sceneWillEnterForeground(_ scene: UIScene) {
50 | // Called as the scene transitions from the background to the foreground.
51 | // Use this method to undo the changes made on entering the background.
52 | }
53 |
54 | func sceneDidEnterBackground(_ scene: UIScene) {
55 | // Called as the scene transitions from the foreground to the background.
56 | // Use this method to save data, release shared resources, and store enough scene-specific state information
57 | // to restore the scene back to its current state.
58 |
59 | // Save changes in the application's managed object context when the application transitions to the background.
60 | (UIApplication.shared.delegate as? AppDelegate)?.saveContext()
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Data/Network/DataMapping/LandmarkDTO.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LandmarkDTO.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/02.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - LandmarkDTO: JSON에서 뽑아주는 Landmark 형식, CoreData에서 사용하는 형식과 다름
11 | struct LandmarkDTO: Decodable {
12 | let id: Int
13 | let title: String
14 | let image: String
15 | let latitude, longitude: Double
16 |
17 | func toLandmark() -> Landmark {
18 | let url = URL(string: self.image)
19 | return Landmark(
20 | uuid: UUID(),
21 | image: url,
22 | latitude: self.latitude,
23 | longitude: self.longitude,
24 | title: self.title)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Data/Persistent/CoreData/DataMapping/LandmarkMO+CoreDataClass.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LandmarkMO+CoreDataClass.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/04.
6 | //
7 | //
8 | import CoreData
9 | import Foundation
10 |
11 | @objc(LandmarkMO)
12 | final public class LandmarkMO: NSManagedObject {
13 |
14 | // MO 객체에서 Landmark 생성
15 | func toLandmark() -> Landmark {
16 | return Landmark(
17 | uuid: self.uuid,
18 | image: self.image,
19 | latitude: self.latitude,
20 | longitude: self.longitude,
21 | title: self.title
22 | )
23 | }
24 |
25 | // 입력받은 Landmark 객체로 MO 생성
26 | func fromLandmark(landmark: Landmark) {
27 | self.uuid = landmark.uuid
28 | self.image = landmark.image
29 | self.latitude = landmark.latitude ?? 0
30 | self.longitude = landmark.longitude ?? 0
31 | self.title = landmark.title
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Data/Persistent/CoreData/DataMapping/LocationMO+CoreDataClass.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LocationMO+CoreDataClass.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/11.
6 | //
7 | //
8 | import CoreData
9 | import Foundation
10 |
11 | @objc(LocationMO)
12 | final public class LocationMO: NSManagedObject {
13 |
14 | func toLocation() -> Location {
15 | return Location(latitude: self.latitude, longitude: self.longitude)
16 | }
17 |
18 | func fromLocation(location: Location) {
19 | self.latitude = location.latitude ?? 0
20 | self.longitude = location.longitude ?? 0
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Data/Persistent/CoreData/DataMapping/RecordMO+CoreDataClass.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecordMO+CoreDataClass.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/15.
6 | //
7 | //
8 | import CoreData
9 | import Foundation
10 |
11 | @objc(RecordMO)
12 | final public class RecordMO: NSManagedObject {
13 |
14 | func toRecord() -> Record {
15 | return Record(
16 | uuid: self.uuid, title: self.title, content: self.content, date: self.date,
17 | latitude: self.latitude, longitude: self.longitude,
18 | photoIDs: self.photoIDs, placeDescription: self.placeDescription
19 | )
20 | }
21 |
22 | func fromRecord(record: Record) {
23 | self.uuid = record.uuid
24 | self.title = record.title
25 | self.content = record.content
26 | self.date = record.date
27 | self.latitude = record.latitude ?? 0
28 | self.longitude = record.longitude ?? 0
29 | self.photoIDs = record.photoIDs
30 | self.placeDescription = record.placeDescription
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Data/Persistent/CoreData/DataMapping/TravelMO+CoreDataClass.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TravelMO+CoreDataClass.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/04.
6 | //
7 | //
8 | import CoreData
9 | import Foundation
10 |
11 | @objc(TravelMO)
12 | final public class TravelMO: NSManagedObject {
13 | func toTravel() -> Travel {
14 | var result = Travel(
15 | uuid: self.uuid,
16 | flag: Int(self.flag),
17 | title: self.title,
18 | startDate: self.startDate,
19 | endDate: self.endDate,
20 | landmarks: [],
21 | records: [],
22 | locations: []
23 | )
24 | guard let landmarks = self.landmarks?.array as? [LandmarkMO],
25 | let records = self.records?.array as? [RecordMO],
26 | let locations = self.locations?.array as? [LocationMO]
27 | else { return result }
28 |
29 | result.landmarks = landmarks.map{ $0.toLandmark() }
30 | result.records = records.map{ $0.toRecord() }
31 | result.locations = locations.map { $0.toLocation() }
32 | return result
33 | }
34 |
35 | func fromTravel(travel: Travel) {
36 | self.uuid = travel.uuid
37 | self.flag = Int64(travel.flag)
38 | self.title = travel.title
39 | self.startDate = travel.startDate
40 | self.endDate = travel.endDate
41 | self.landmarks = NSOrderedSet(array: travel.landmarks)
42 | self.records = NSOrderedSet(array: travel.records)
43 | self.locations = NSOrderedSet(array: travel.locations)
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Data/Persistent/CoreData/LandmarkMO+CoreDataProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LandmarkMO+CoreDataProperties.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/04.
6 | //
7 | //
8 | import CoreData
9 | import Foundation
10 |
11 | extension LandmarkMO {
12 |
13 | @nonobjc public class func fetchRequest() -> NSFetchRequest {
14 | return NSFetchRequest(entityName: "LandmarkMO")
15 | }
16 |
17 | @NSManaged public var image: URL?
18 | @NSManaged public var latitude: Double
19 | @NSManaged public var longitude: Double
20 | @NSManaged public var title: String?
21 | @NSManaged public var uuid: UUID?
22 | @NSManaged public var travel: TravelMO?
23 |
24 | }
25 |
26 | extension LandmarkMO : Identifiable {
27 | static func == (lhs: LandmarkMO, rhs: LandmarkMO) -> Bool {
28 | return lhs.uuid == rhs.uuid
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Data/Persistent/CoreData/LocationMO+CoreDataProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LocationMO+CoreDataProperties.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/11.
6 | //
7 | //
8 | import CoreData
9 | import Foundation
10 |
11 | extension LocationMO {
12 |
13 | @nonobjc public class func fetchRequest() -> NSFetchRequest {
14 | return NSFetchRequest(entityName: "LocationMO")
15 | }
16 |
17 | @NSManaged public var latitude: Double
18 | @NSManaged public var longitude: Double
19 | @NSManaged public var travel: TravelMO?
20 |
21 | }
22 |
23 | extension LocationMO: Identifiable {
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Data/Persistent/CoreData/OwnMyWay.xcdatamodeld/.xccurrentversion:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | _XCCurrentVersionName
6 | OwnMyWay.xcdatamodel
7 |
8 |
9 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Data/Persistent/CoreData/RecordMO+CoreDataProperties.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecordMO+CoreDataProperties.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/25.
6 | //
7 | //
8 | import Foundation
9 | import CoreData
10 |
11 |
12 | extension RecordMO {
13 |
14 | @nonobjc public class func fetchRequest() -> NSFetchRequest {
15 | return NSFetchRequest(entityName: "RecordMO")
16 | }
17 |
18 | @NSManaged public var content: String?
19 | @NSManaged public var date: Date?
20 | @NSManaged public var latitude: Double
21 | @NSManaged public var longitude: Double
22 | @NSManaged public var photoIDs: [String]?
23 | @NSManaged public var placeDescription: String?
24 | @NSManaged public var title: String?
25 | @NSManaged public var uuid: UUID?
26 | @NSManaged public var travel: TravelMO?
27 |
28 | }
29 |
30 | extension RecordMO : Identifiable {
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Data/Repositories/LocalJSONLandmarkRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LandmarkDTORepository.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/03.
6 | //
7 |
8 | import Foundation
9 |
10 | final class LocalJSONLandmarkRepository: LandmarkRepository {
11 |
12 | func fetchLandmarks(completion: @escaping (Result<[Landmark], Error>) -> Void) {
13 | guard let path = Bundle.main.path(forResource: "landmark", ofType: "json"),
14 | let jsonString = try? String(contentsOfFile: path),
15 | let data = jsonString.data(using: .utf8)
16 | else {
17 | completion(.failure(JSONError.fileError))
18 | return
19 | }
20 |
21 | do {
22 | let landmarkDTOs = try JSONDecoder()
23 | .decode([LandmarkDTO].self, from: data)
24 | completion(.success(landmarkDTOs.map { $0.toLandmark() }))
25 | } catch {
26 | completion(.failure(JSONError.decodeError))
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Domain/Entities/Landmark.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Landmark.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/04.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Landmark {
11 | var uuid: UUID?
12 | var image: URL?
13 | var latitude: Double?
14 | var longitude: Double?
15 | var title: String?
16 | }
17 |
18 | extension Landmark: Hashable {
19 | static func == (lhs: Self, rhs: Self) -> Bool {
20 | return lhs.uuid == rhs.uuid
21 | }
22 |
23 | func hash(into hasher: inout Hasher) {
24 | hasher.combine(uuid)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Domain/Entities/Location.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Location.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/11.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Location: Equatable {
11 | var latitude: Double?
12 | var longitude: Double?
13 | }
14 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Domain/Entities/Record.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Record.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/04.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Record {
11 | var uuid: UUID?
12 | var title: String?
13 | var content: String?
14 | var date: Date?
15 | var latitude: Double?
16 | var longitude: Double?
17 | var photoIDs: [String]?
18 | var placeDescription: String?
19 |
20 | static func dummy() -> Record {
21 | return Record(
22 | uuid: UUID()
23 | )
24 | }
25 | }
26 |
27 | extension Record: Hashable {
28 | static func == (lhs: Self, rhs: Self) -> Bool {
29 | return lhs.uuid == rhs.uuid &&
30 | lhs.title == rhs.title &&
31 | lhs.content == rhs.content &&
32 | lhs.date == rhs.date &&
33 | lhs.latitude == rhs.latitude &&
34 | lhs.longitude == rhs.longitude &&
35 | lhs.photoIDs == rhs.photoIDs &&
36 | lhs.placeDescription == rhs.placeDescription
37 | }
38 |
39 | func hash(into hasher: inout Hasher) {
40 | hasher.combine(uuid)
41 | hasher.combine(title)
42 | hasher.combine(content)
43 | hasher.combine(date)
44 | hasher.combine(latitude)
45 | hasher.combine(longitude)
46 | hasher.combine(photoIDs)
47 | hasher.combine(placeDescription)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Domain/Entities/Travel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Travel.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/04.
6 | //
7 |
8 | import Foundation
9 |
10 | struct Travel {
11 |
12 | enum Section: Int, CaseIterable {
13 | case dummy = -1
14 | case reserved = 0
15 | case ongoing = 1
16 | case outdated = 2
17 |
18 | var index: Int {
19 | return self.rawValue
20 | }
21 | }
22 |
23 | static func dummy(section: Section) -> Travel {
24 | return Travel(
25 | uuid: UUID(), flag: section.index, title: nil,
26 | startDate: nil, endDate: nil,
27 | landmarks: [], records: [], locations: []
28 | )
29 | }
30 |
31 | var uuid: UUID?
32 | var flag: Int // 0: 예정된 여행, 1: 진행중인 여행, 2:완료된 여행
33 | var title: String?
34 | var startDate: Date?
35 | var endDate: Date?
36 | var landmarks: [Landmark]
37 | var records: [Record]
38 | var locations: [Location]
39 | }
40 |
41 | extension Travel: Hashable {
42 | static func == (lhs: Self, rhs: Self) -> Bool {
43 | return lhs.uuid == rhs.uuid &&
44 | lhs.flag == rhs.flag &&
45 | lhs.title == rhs.title &&
46 | lhs.startDate == rhs.startDate &&
47 | lhs.endDate == rhs.endDate &&
48 | lhs.landmarks == rhs.landmarks &&
49 | lhs.records == rhs.records &&
50 | lhs.locations == rhs.locations
51 | }
52 |
53 | func hash(into hasher: inout Hasher) {
54 | hasher.combine(uuid)
55 | hasher.combine(flag)
56 | hasher.combine(title)
57 | hasher.combine(startDate)
58 | hasher.combine(endDate)
59 | hasher.combine(landmarks)
60 | }
61 | }
62 |
63 | extension Travel {
64 | func classifyRecords() -> [[Record]] {
65 | let dict = Dictionary(grouping: self.records) { $0.date?.localize() }.sorted(by: {
66 | guard let lhs = $0.key, let rhs = $1.key
67 | else { return true }
68 | return lhs < rhs
69 | })
70 | var array: [[Record]] = []
71 | dict.forEach { _, value in
72 | array.append(value.sorted(by: {
73 | guard let lhs = $0.date, let rhs = $1.date
74 | else { return true }
75 | return lhs < rhs
76 | }))
77 | }
78 | return array
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Domain/Interfaces/Repositories/LandmarkRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LandmarkRepository.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/30.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol LandmarkRepository {
11 | func fetchLandmarks(completion: @escaping (Result<[Landmark], Error>) -> Void)
12 | }
13 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Domain/Interfaces/Repositories/TravelRepository.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TravelRepository.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/30.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol TravelRepository {
11 | func fetchAllTravels(completion: @escaping (Result<[Travel], Error>) -> Void)
12 | func fetchTravel(of travel: Travel, completion: @escaping (Result) -> Void)
13 | func addTravel(
14 | title: String, startDate: Date, endDate: Date,
15 | completion: @escaping (Result) -> Void
16 | )
17 | func save(travel: Travel, completion: @escaping (Result) -> Void)
18 | func addLandmark(
19 | to travel: Travel, uuid: UUID?, title: String?,
20 | image: URL?, latitude: Double?, longitude: Double?,
21 | completion: @escaping (Result) -> Void
22 | )
23 | func addRecord(
24 | to travel: Travel, with record: Record,
25 | completion: @escaping (Result) -> Void
26 | )
27 | func addLocation(
28 | to travel: Travel, latitude: Double?, longitude: Double?,
29 | completion: @escaping (Result) -> Void
30 | )
31 | func update(travel: Travel, completion: @escaping (Result) -> Void)
32 | func updateRecord(at record: Record, completion: @escaping (Result) -> Void)
33 | func delete(travel: Travel, completion: @escaping (Result) -> Void)
34 | func deleteLandmark(at landmark: Landmark, completion: @escaping (Result) -> Void)
35 | func deleteRecord(at record: Record, completion: @escaping (Result) -> Void)
36 | }
37 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Domain/Usecases/DefaultAddRecordUsecase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddRecordUsecase.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/10.
6 | //
7 |
8 | import Foundation
9 |
10 | struct DefaultAddRecordUsecase: AddRecordUsecase {
11 |
12 | private let repository: TravelRepository
13 | private let imageFileManager: ImageFileManager
14 |
15 | init(repository: TravelRepository, imageFileManager: ImageFileManager) {
16 | self.repository = repository
17 | self.imageFileManager = imageFileManager
18 | }
19 |
20 | func executeValidationTitle(with title: String?) -> Bool {
21 | return (1...20) ~= (title ?? "").count
22 | }
23 |
24 | func executeValidationDate(with date: Date?) -> Bool {
25 | return date != nil
26 | }
27 |
28 | func executeValidationCoordinate(with coordinate: Location) -> Bool {
29 | guard let latitude = coordinate.latitude,
30 | let longitude = coordinate.longitude
31 | else { return false }
32 | return (-90...90) ~= latitude && (-180...180) ~= longitude
33 | }
34 |
35 | func executePickingPhoto(with url: URL, completion: (Result) -> Void) {
36 | self.imageFileManager.copyPhoto(from: url, completion: completion)
37 | }
38 |
39 | func executeRemovingPhoto(of photoID: String, completion: (Result) -> Void) {
40 | self.imageFileManager.removePhoto(of: photoID) { result in
41 | completion(result)
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Domain/Usecases/DefaultCompleteCreationUsecase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompleteCreationUsecase.swift
3 | // OwnMyWay
4 | //
5 | // Created by 이청수 on 2021/11/09.
6 | //
7 |
8 | import Foundation
9 |
10 | struct DefaultCompleteCreationUsecase: CompleteCreationUsecase {
11 |
12 | private let repository: TravelRepository
13 |
14 | init(repository: TravelRepository) {
15 | self.repository = repository
16 | }
17 |
18 | func executeCreation(travel: Travel, completion: @escaping (Result) -> Void) {
19 | self.repository.save(travel: travel) { result in
20 | completion(result)
21 | }
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Domain/Usecases/DefaultCompleteEditingUsecase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompleteEditingUsecase.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/17.
6 | //
7 |
8 | import Foundation
9 |
10 | struct DefaultCompleteEditingUsecase: CompleteEditingUsecase {
11 |
12 | private let repository: TravelRepository
13 |
14 | init(repository: TravelRepository) {
15 | self.repository = repository
16 | }
17 |
18 | func executeUpdate(travel: Travel, completion: @escaping (Result) -> Void) {
19 | self.repository.update(travel: travel) { result in
20 | completion(result)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Domain/Usecases/DefaultDetailRecordUsecase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DetailRecordUsecase.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/10.
6 | //
7 |
8 | import Foundation
9 |
10 | struct DefaultDetailRecordUsecase: DetailRecordUsecase {
11 |
12 | private let repository: TravelRepository
13 |
14 | init(repository: TravelRepository) {
15 | self.repository = repository
16 | }
17 |
18 | func executeRecordUpdate(record: Record, completion: @escaping (Result) -> Void) {
19 | self.repository.updateRecord(at: record) { result in
20 | completion(result)
21 | }
22 | }
23 |
24 | func executeRecordDeletion(at record: Record,
25 | completion: @escaping ( Result) -> Void
26 | ) {
27 | self.repository.deleteRecord(at: record) { result in
28 | completion(result)
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Domain/Usecases/DefaultEnterDateUsecase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EnterDateUsecase.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/23.
6 | //
7 |
8 | import Foundation
9 |
10 | struct DefaultEnterDateUsecase: EnterDateUsecase {
11 | func executeEnteringDate(firstDate: Date, secondDate: Date, completion: ([Date]) -> Void) {
12 | completion(Array(Set([firstDate, secondDate])).sorted())
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Domain/Usecases/DefaultEnterTitleUsecase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CreateTravelUsecase.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/02.
6 | //
7 |
8 | import Foundation
9 |
10 | struct DefaultEnterTitleUsecase: EnterTitleUsecase {
11 |
12 | func executeTitleValidation(
13 | with title: String,
14 | completion: @escaping (Result) -> Void
15 | ) {
16 | if !title.isEmpty {
17 | completion(.success(title))
18 | } else {
19 | completion(.failure(EnterTitleError.nilTitle))
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Domain/Usecases/DefaultHomeUsecase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeUsecase.swift
3 | // OwnMyWay
4 | //
5 | // Created by 이청수 on 2021/11/04.
6 | //
7 |
8 | import Foundation
9 |
10 | struct DefaultHomeUsecase: HomeUsecase {
11 |
12 | let repository: TravelRepository
13 |
14 | init(repository: TravelRepository) {
15 | self.repository = repository
16 | }
17 |
18 | func executeFetch(completion: @escaping (Result<[Travel], Error>) -> Void) {
19 | self.repository.fetchAllTravels { result in
20 | completion(result)
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Domain/Usecases/DefaultReservedTravelUsecase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReservedTravelUsecase.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/04.
6 | //
7 |
8 | import Foundation
9 |
10 | struct DefaultReservedTravelUsecase: ReservedTravelUsecase {
11 |
12 | let repository: TravelRepository
13 |
14 | init(repository: TravelRepository) {
15 | self.repository = repository
16 | }
17 |
18 | func executeDeletion(of travel: Travel, completion: @escaping (Result) -> Void) {
19 | self.repository.delete(travel: travel) { result in
20 | completion(result)
21 | }
22 | }
23 |
24 | func executeLandmarkAddition(
25 | of travel: Travel,
26 | completion: @escaping (Result) -> Void
27 | ) {
28 | guard let newLandmark = travel.landmarks.last
29 | else {
30 | completion(.failure(ModelError.landmarkError))
31 | return
32 | }
33 |
34 | self.repository.addLandmark(
35 | to: travel,
36 | uuid: newLandmark.uuid,
37 | title: newLandmark.title,
38 | image: newLandmark.image,
39 | latitude: newLandmark.latitude,
40 | longitude: newLandmark.longitude
41 | ) { result in
42 | switch result {
43 | case .success:
44 | completion(.success(()))
45 | case .failure(let error):
46 | completion(.failure(error))
47 | }
48 | }
49 | }
50 |
51 | func executeLandmarkDeletion(
52 | at landmark: Landmark,
53 | completion: @escaping (Result) -> Void
54 | ) {
55 | self.repository.deleteLandmark(at: landmark) { result in
56 | completion(result)
57 | }
58 | }
59 |
60 | func executeFlagUpdate(
61 | of travel: Travel,
62 | completion: @escaping (Result) -> Void
63 | ) {
64 | self.repository.update(travel: travel) { result in
65 | completion(result)
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Domain/Usecases/DefaultSearchLandmarkUsecase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchLandmarkUsecase.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/03.
6 | //
7 |
8 | import Foundation
9 |
10 | struct DefaultSearchLandmarkUsecase: SearchLandmarkUsecase {
11 |
12 | private let repository: LandmarkRepository
13 |
14 | init(repository: LandmarkRepository) {
15 | self.repository = repository
16 | }
17 |
18 | func executeFetch(completion: @escaping (Result<[Landmark], Error>) -> Void) {
19 | repository.fetchLandmarks { result in
20 | completion(result)
21 | }
22 | }
23 |
24 | func executeSearch(by text: String, completion: @escaping (Result<[Landmark], Error>) -> Void) {
25 | repository.fetchLandmarks { result in
26 | switch result {
27 | case .success(let landmarks):
28 | let searchResult = landmarks.filter {
29 | guard let title = $0.title
30 | else { return false }
31 | return title.contains(text)
32 | }
33 | completion(.success(searchResult))
34 | case .failure(let error):
35 | completion(.failure(error))
36 | }
37 | }
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Domain/Usecases/DefaultStartedTravelUsecase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OngoingUsecase.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/10.
6 | //
7 |
8 | import Foundation
9 |
10 | struct DefaultStartedTravelUsecase: StartedTravelUsecase {
11 |
12 | private let repository: TravelRepository
13 |
14 | init(repository: TravelRepository) {
15 | self.repository = repository
16 | }
17 |
18 | func executeFetch(of travel: Travel, completion: @escaping (Result) -> Void) {
19 | self.repository.fetchTravel(of: travel) { result in
20 | completion(result)
21 | }
22 | }
23 |
24 | func executeFlagUpdate(of travel: Travel,
25 | completion: @escaping (Result) -> Void
26 | ) {
27 | self.repository.update(travel: travel) { result in
28 | completion(result)
29 | }
30 | }
31 |
32 | func executeDeletion(of travel: Travel, completion: @escaping (Result) -> Void) {
33 | self.repository.delete(travel: travel) { result in
34 | completion(result)
35 | }
36 | }
37 |
38 | func executeLocationUpdate(
39 | of travel: Travel, latitude: Double, longitude: Double,
40 | completion: @escaping (Result) -> Void
41 | ) {
42 | self.repository.addLocation(
43 | to: travel, latitude: latitude, longitude: longitude
44 | ) { result in
45 | completion(result)
46 | }
47 | }
48 |
49 | func executeRecordAddition(
50 | to travel: Travel, with record: Record,
51 | completion: @escaping (Result) -> Void
52 | ) {
53 | self.repository.addRecord(to: travel, with: record) { result in
54 | completion(result)
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Infrastructure/Error/EnterTitleError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EnterTitleError.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/30.
6 | //
7 |
8 | import Foundation
9 |
10 | enum EnterTitleError: Error {
11 | case nilTitle
12 | }
13 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Infrastructure/Error/ErrorManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ErrorManager.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/23.
6 | //
7 |
8 | import UIKit
9 |
10 | final class ErrorManager {
11 | static func showToast(with error: Error, to viewController: UIViewController?) {
12 | viewController?.showToast(message: error.localizedDescription)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Infrastructure/Error/JSONError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // JSONError.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/30.
6 | //
7 |
8 | import Foundation
9 |
10 | enum JSONError: Error {
11 | case fileError
12 | case decodeError
13 | }
14 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Infrastructure/Error/ModelError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ModelError.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/30.
6 | //
7 |
8 | import Foundation
9 |
10 | enum ModelError: Error, LocalizedError {
11 | case landmarkError
12 | case recordError
13 | case indexError
14 |
15 | var errorDescription: String? {
16 | switch self {
17 | case .landmarkError:
18 | return "관광명소가 존재하지 않아요."
19 | case .recordError:
20 | return "게시물이 존재하지 않아요."
21 | case .indexError:
22 | return "잘못된 위치를 참조하였어요."
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Infrastructure/Error/RepositoryError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RepositoryError.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/30.
6 | //
7 |
8 | import Foundation
9 |
10 | enum RepositoryError: Error, LocalizedError {
11 | case saveError
12 | case fetchError
13 | case uuidError
14 | case locationError
15 | case recordError
16 | case landmarkError
17 |
18 | var errorDescription: String? {
19 | switch self {
20 | case .saveError:
21 | return "저장에 실패했어요."
22 | case .fetchError:
23 | return "저장에 실패했어요."
24 | case .uuidError:
25 | return "저장에 실패했어요."
26 | case .locationError:
27 | return "위치 저장에 실패했어요."
28 | case .recordError:
29 | return "게시물 저장에 실패했어요."
30 | case .landmarkError:
31 | return "관광명소 저장에 실패했어요."
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Infrastructure/File/ImageFileManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageFileManager.swift
3 | // OwnMyWay
4 | //
5 | // Created by 이청수 on 2021/11/16.
6 | //
7 |
8 | import Foundation
9 |
10 | final class ImageFileManager {
11 | private let fileManager: FileManager
12 | private let appDirectory: String
13 | private let cache: URLCache
14 |
15 | static let shared = ImageFileManager(fileManager: FileManager.default)
16 |
17 | private init(fileManager: FileManager) {
18 | self.fileManager = fileManager
19 | self.appDirectory = "OwnMyWay"
20 | let cachesURL = self.fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0]
21 | let diskCacheURL = cachesURL.appendingPathComponent("DownloadCache")
22 | cache = .init(memoryCapacity: 0, diskCapacity: 100_000_000, directory: diskCacheURL)
23 | do {
24 | try self.configureAppURL()
25 | } catch let error {
26 | print(error)
27 | }
28 | }
29 |
30 | func imageInDocuemtDirectory(image: String) -> URL? {
31 | return self.appURL()?.appendingPathComponent(image)
32 | }
33 |
34 | func copyPhoto(from source: URL, completion: (Result) -> Void) {
35 | guard let destinationURL = self.appURL()?
36 | .appendingPathComponent(UUID().uuidString)
37 | .appendingPathExtension(source.pathExtension)
38 | else { return }
39 | do {
40 | if !self.photoExists(at: destinationURL) {
41 | try self.fileManager.copyItem(at: source, to: destinationURL)
42 | }
43 | } catch let error {
44 | completion(.failure(error))
45 | }
46 | completion(.success(destinationURL.lastPathComponent))
47 | }
48 |
49 | func removePhoto(of photoID: String, completion: (Result) -> Void) {
50 | guard let url = self.imageInDocuemtDirectory(image: photoID) else {
51 | completion(.failure(NSError.init()))
52 | return
53 | }
54 | do {
55 | if self.photoExists(at: url) {
56 | try self.fileManager.removeItem(at: url)
57 | }
58 | } catch let error {
59 | completion(.failure(error))
60 | }
61 | completion(.success(()))
62 | }
63 |
64 | private func configureAppURL() throws {
65 | guard let appURL = self.appURL(),
66 | !self.fileManager.fileExists(atPath: appURL.absoluteString)
67 | else { return }
68 | try self.fileManager.createDirectory(at: appURL, withIntermediateDirectories: true)
69 | }
70 |
71 | private func photoExists(at url: URL) -> Bool {
72 | return self.fileManager.fileExists(atPath: url.path)
73 | }
74 |
75 | private func documentURL() -> URL? {
76 | return self.fileManager.urls(
77 | for: .documentDirectory, in: .userDomainMask
78 | ).first
79 | }
80 |
81 | private func appURL() -> URL? {
82 | return self.documentURL()?.appendingPathComponent(self.appDirectory)
83 | }
84 |
85 | func cachedData(request: URLRequest) -> Data? {
86 | return self.cache.cachedResponse(for: request)?.data
87 | }
88 |
89 | func saveToCache(request: URLRequest, response: URLResponse, data: Data) {
90 | self.cache.storeCachedResponse(
91 | CachedURLResponse(response: response, data: data), for: request
92 | )
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Infrastructure/Location/LocationManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LocationManager.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/15.
6 | //
7 |
8 | import CoreLocation
9 |
10 | final class LocationManager: CLLocationManager {
11 |
12 | private var repository: CoreDataTravelRepository?
13 |
14 | private var travel: Travel?
15 | private(set) var isUpdatingLocation = false
16 |
17 | static let shared = LocationManager()
18 |
19 | private override init() {
20 | super.init()
21 | self.configureLocationManager()
22 | }
23 |
24 | override func startUpdatingLocation() {
25 | super.startUpdatingLocation()
26 | self.isUpdatingLocation = true
27 | }
28 |
29 | override func stopUpdatingLocation() {
30 | super.stopUpdatingLocation()
31 | self.isUpdatingLocation = false
32 | }
33 |
34 | func bind(repository: CoreDataTravelRepository) {
35 | self.repository = repository
36 | }
37 |
38 | func currentTravel(to travel: Travel?) {
39 | self.travel = travel
40 | }
41 |
42 | private func configureLocationManager() {
43 | self.delegate = self
44 | self.desiredAccuracy = kCLLocationAccuracyBest
45 | self.distanceFilter = 10
46 | self.allowsBackgroundLocationUpdates = true
47 | self.pausesLocationUpdatesAutomatically = false
48 | self.showsBackgroundLocationIndicator = true
49 | }
50 | }
51 |
52 | extension LocationManager: CLLocationManagerDelegate {
53 | func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
54 | let lastLocation = locations.last
55 | guard let latitude = lastLocation?.coordinate.latitude,
56 | let longitude = lastLocation?.coordinate.longitude,
57 | let travel = self.travel
58 | else { return }
59 | self.repository?.addLocation(to: travel, latitude: latitude, longitude: longitude) { _ in }
60 | }
61 |
62 | func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
63 | switch manager.fetchAuthorizationStatus() {
64 | case .restricted, .denied:
65 | self.stopUpdatingLocation()
66 | default:
67 | break
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Infrastructure/Location/Utils/Extension/CLLocationManager+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CLLocationManager+.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/18.
6 | //
7 |
8 | import Foundation
9 | import CoreLocation
10 |
11 | extension CLLocationManager {
12 | func fetchAuthorizationStatus() -> CLAuthorizationStatus {
13 | if #available(iOS 14.0, *) {
14 | return self.authorizationStatus
15 | } else {
16 | return LocationManager.authorizationStatus()
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/AddLanmarkScene/Coordinator/AddLandmarkCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddLandmarkCoordinator.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/08.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol AddLandmarkCoordinatingDelegate: AnyObject {
11 | func pushToCompleteCreation(travel: Travel)
12 | func pushToCompleteEditing(travel: Travel)
13 | func popToEnterDate(travel: Travel)
14 | }
15 |
16 | final class AddLandmarkCoordinator: Coordinator, AddLandmarkCoordinatingDelegate {
17 |
18 | var childCoordinators: [Coordinator]
19 | var navigationController: UINavigationController
20 | private var travel: Travel
21 | private var isEditingMode: Bool
22 |
23 | init(navigationController: UINavigationController, travel: Travel, isEditingMode: Bool) {
24 | self.childCoordinators = []
25 | self.navigationController = navigationController
26 | self.travel = travel
27 | self.isEditingMode = isEditingMode
28 | }
29 |
30 | func start() {
31 | let addLandmarkVM = DefaultAddLandmarkViewModel(
32 | travel: self.travel, coordinatingDelegate: self, isEditingMode: self.isEditingMode
33 | )
34 | let addLandmarkVC = AddLandmarkViewController.instantiate(storyboardName: "AddLandmark")
35 | let landmarkCartCoordinator = LandmarkCartCoordinator(
36 | navigationController: self.navigationController,
37 | travel: self.travel
38 | )
39 | let cartVC = landmarkCartCoordinator.pass(from: .create)
40 | self.childCoordinators.append(landmarkCartCoordinator)
41 | addLandmarkVC.bind(viewModel: addLandmarkVM) { cartView in
42 | addLandmarkVC.addChild(cartVC)
43 | cartView.addSubview(cartVC.view)
44 | cartVC.view.translatesAutoresizingMaskIntoConstraints = false
45 | cartVC.view.topAnchor.constraint(equalTo: cartView.topAnchor).isActive = true
46 | cartVC.view.leadingAnchor.constraint(equalTo: cartView.leadingAnchor).isActive = true
47 | cartVC.view.trailingAnchor.constraint(equalTo: cartView.trailingAnchor).isActive = true
48 | cartVC.view.bottomAnchor.constraint(equalTo: cartView.bottomAnchor).isActive = true
49 | cartVC.didMove(toParent: addLandmarkVC)
50 | }
51 | self.navigationController.pushViewController(addLandmarkVC, animated: true)
52 | }
53 |
54 | func pushToCompleteCreation(travel: Travel) {
55 | let completeCreationCoordinator = CompleteCreationCoordinator(
56 | navigationController: self.navigationController, travel: travel
57 | )
58 | self.childCoordinators.append(completeCreationCoordinator)
59 | completeCreationCoordinator.start()
60 | }
61 |
62 | func pushToCompleteEditing(travel: Travel) {
63 | let completeEditingCoordinator = CompleteEditingCoordinator(
64 | navigationController: self.navigationController, travel: travel
65 | )
66 | self.childCoordinators.append(completeEditingCoordinator)
67 | completeEditingCoordinator.start()
68 | }
69 |
70 | func popToEnterDate(travel: Travel) {
71 | guard let enterDateVC = self.navigationController.children.last
72 | as? EnterDateViewController else { return }
73 |
74 | enterDateVC.travelDidChanged(to: travel)
75 | }
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/AddLanmarkScene/View/AddLandmarkViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TravelCartViewController.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/03.
6 | //
7 |
8 | import Combine
9 | import UIKit
10 |
11 | final class AddLandmarkViewController: UIViewController,
12 | Instantiable,
13 | TravelUpdatable,
14 | LandmarkDeletable {
15 |
16 | @IBOutlet private weak var contentView: UIView!
17 | @IBOutlet private weak var cartView: UIView!
18 | @IBOutlet private weak var nextButtonHeightConstraint: NSLayoutConstraint!
19 |
20 | private var bindContainerVC: ((UIView) -> Void)?
21 | private var viewModel: AddLandmarkViewModel?
22 | private var cancellables: Set = []
23 |
24 | override func viewDidLoad() {
25 | super.viewDidLoad()
26 | self.bindContainerVC?(self.cartView)
27 | self.configureCancellables()
28 | }
29 |
30 | override func viewDidDisappear(_ animated: Bool) {
31 | super.viewDidDisappear(animated)
32 | self.bindContainerVC = nil
33 | if self.isMovingFromParent {
34 | self.viewModel?.didTouchBackButton()
35 | }
36 | }
37 |
38 | override func viewWillAppear(_ animated: Bool) {
39 | super.viewWillAppear(animated)
40 | self.configureNavigationController()
41 | }
42 |
43 | override func viewWillLayoutSubviews() {
44 | super.viewWillLayoutSubviews()
45 | self.configureButtonConstraint()
46 | }
47 |
48 | func bind(viewModel: AddLandmarkViewModel, closure: @escaping (UIView) -> Void) {
49 | self.viewModel = viewModel
50 | self.bindContainerVC = closure
51 | }
52 |
53 | func didUpdateTravel(to travel: Travel) {
54 | self.viewModel?.didUpdateTravel(to: travel)
55 | }
56 |
57 | func didDeleteLandmark(at landmark: Landmark) {
58 | self.viewModel?.didDeleteLandmark(at: landmark)
59 | }
60 |
61 | private func configureCancellables() {
62 | self.viewModel?.errorPublisher
63 | .receive(on: DispatchQueue.main)
64 | .sink { [weak self] optionalError in
65 | guard let error = optionalError else { return }
66 | ErrorManager.showToast(with: error, to: self)
67 | }
68 | .store(in: &self.cancellables)
69 | }
70 |
71 | private func configureButtonConstraint() {
72 | let bottomPadding = self.view.safeAreaInsets.bottom
73 | self.nextButtonHeightConstraint.constant = 60 + bottomPadding
74 | }
75 |
76 | private func configureNavigationController() {
77 | self.navigationController?.navigationBar.topItem?.title = ""
78 | guard let isEditingMode = self.viewModel?.isEditingMode else { return }
79 | self.navigationItem.title = isEditingMode ? "여행 편집하기" : "새로운 여행"
80 | }
81 |
82 | @IBAction func didTouchNextButton(_ sender: Any) {
83 | self.viewModel?.didTouchNextButton()
84 | }
85 |
86 | }
87 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/AddLanmarkScene/ViewModel/AddLandmarkViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddLandmarkViewModel.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/08.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol AddLandmarkViewModel {
11 | var travel: Travel { get }
12 | var errorPublisher: Published.Publisher { get }
13 | var isEditingMode: Bool { get }
14 |
15 | func didTouchNextButton()
16 | func didTouchBackButton()
17 | func didUpdateTravel(to travel: Travel)
18 | func didDeleteLandmark(at landmark: Landmark)
19 | }
20 |
21 | final class DefaultAddLandmarkViewModel: AddLandmarkViewModel {
22 | var errorPublisher: Published.Publisher { $error }
23 | private(set) var travel: Travel
24 |
25 | private(set) var isEditingMode: Bool
26 | @Published private var error: Error?
27 |
28 | private weak var coordinatingDelegate: AddLandmarkCoordinatingDelegate?
29 |
30 | init(
31 | travel: Travel, coordinatingDelegate: AddLandmarkCoordinatingDelegate, isEditingMode: Bool
32 | ) {
33 | self.travel = travel
34 | self.coordinatingDelegate = coordinatingDelegate
35 | self.isEditingMode = isEditingMode
36 | }
37 |
38 | func didTouchNextButton() {
39 | if self.isEditingMode {
40 | self.coordinatingDelegate?.pushToCompleteEditing(travel: travel)
41 | } else {
42 | self.coordinatingDelegate?.pushToCompleteCreation(travel: travel)
43 | }
44 | }
45 |
46 | func didTouchBackButton() {
47 | self.coordinatingDelegate?.popToEnterDate(travel: travel)
48 | }
49 |
50 | func didUpdateTravel(to travel: Travel) {
51 | self.travel = travel
52 | }
53 |
54 | func didDeleteLandmark(at landmark: Landmark) {
55 | guard let index = self.travel.landmarks.firstIndex(of: landmark) else {
56 | self.error = ModelError.indexError
57 | return
58 | }
59 | self.travel.landmarks.remove(at: index)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/AddRecordScene/Coordinator/AddRecordCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddRecordCoordinator.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/10.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol AddRecordCoordinatingDelegate: AnyObject {
11 | func popToParent(with record: Record)
12 | func presentToSearchLocation()
13 | }
14 |
15 | final class AddRecordCoordinator: Coordinator, AddRecordCoordinatingDelegate {
16 |
17 | var childCoordinators: [Coordinator]
18 | var navigationController: UINavigationController
19 | private var record: Record?
20 | private var isEditingMode: Bool
21 |
22 | init(navigationController: UINavigationController, record: Record?, isEditingMode: Bool) {
23 | self.childCoordinators = []
24 | self.navigationController = navigationController
25 | self.record = record
26 | self.isEditingMode = isEditingMode
27 | }
28 |
29 | func start() {
30 | guard let appDelegate = UIApplication.shared.delegate as? AppDelegate
31 | else { return }
32 | let repository = CoreDataTravelRepository(contextFetcher: appDelegate)
33 | let usecase = DefaultAddRecordUsecase(
34 | repository: repository,
35 | imageFileManager: ImageFileManager.shared
36 | )
37 | let addRecordVM = DefaultAddRecordViewModel(
38 | record: self.record,
39 | usecase: usecase,
40 | coordinatingDelegate: self,
41 | isEditingMode: self.isEditingMode
42 | )
43 | let addRecordVC = AddRecordViewController.instantiate(storyboardName: "AddRecord")
44 | addRecordVC.bind(viewModel: addRecordVM)
45 | self.navigationController.pushViewController(addRecordVC, animated: true)
46 | }
47 |
48 | func popToParent(with record: Record) {
49 | self.navigationController.popViewController(animated: true)
50 | guard let upperVC = self.navigationController.viewControllers.last
51 | as? RecordUpdatable & UIViewController else { return }
52 | upperVC.didUpdateRecord(record: record)
53 | }
54 |
55 | func presentToSearchLocation() {
56 | let searchLocationCoordinator = SearchLocationCoordinator(
57 | navigationController: self.navigationController
58 | )
59 | self.childCoordinators.append(searchLocationCoordinator)
60 | searchLocationCoordinator.start()
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/AddRecordScene/Interface/Usecase/AddRecordUsecase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddRecordUsecase.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/30.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol AddRecordUsecase {
11 | func executeValidationTitle(with title: String?) -> Bool
12 | func executeValidationDate(with date: Date?) -> Bool
13 | func executeValidationCoordinate(with coordinate: Location) -> Bool
14 | func executePickingPhoto(with url: URL, completion: (Result) -> Void)
15 | func executeRemovingPhoto(of photoID: String, completion: (Result) -> Void)
16 | }
17 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/CollectionView/CommentCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CommentCell.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/11.
6 | //
7 |
8 | import UIKit
9 |
10 | final class CommentCell: UICollectionViewCell {
11 | static let identifier = "CommentCell"
12 |
13 | @IBOutlet private weak var commentLabel: UILabel!
14 |
15 | func configure(text: String) {
16 | self.commentLabel.text = text
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/CollectionView/CommentCell.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/CollectionView/DateHeaderView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DateHeaderView.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/10.
6 | //
7 |
8 | import UIKit
9 |
10 | final class DateHeaderView: UICollectionReusableView {
11 | static let identifier = "DateHeaderView"
12 |
13 | @IBOutlet private weak var dateLabel: UILabel!
14 |
15 | func configure(with text: String) {
16 | self.dateLabel.text = text
17 | self.configureAccessibility(with: text)
18 | }
19 |
20 | func configureAccessibility(with text: String) {
21 | self.isAccessibilityElement = true
22 | self.accessibilityTraits = .header
23 | self.accessibilityValue = text
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/CollectionView/DateHeaderView.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/CollectionView/HomeBackgroundView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeBackgroundView.swift
3 | // OwnMyWay
4 | //
5 | // Created by 이청수 on 2021/11/22.
6 | //
7 |
8 | import UIKit
9 |
10 | final class HomeBackgroundView: UICollectionReusableView {
11 | static let identifier = "HomeBackgroundView"
12 |
13 | @IBOutlet private weak var backgroundView: UIView!
14 |
15 | override init(frame: CGRect) {
16 | super.init(frame: frame)
17 | self.configure()
18 | }
19 |
20 | required init?(coder: NSCoder) {
21 | super.init(coder: coder)
22 | self.configure()
23 | }
24 |
25 | private func configure() {
26 | self.layer.cornerRadius = 25
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/CollectionView/HomeBackgroundView.xib:
--------------------------------------------------------------------------------
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 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/CollectionView/LandmarkCardCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LandmarkCardCell.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/02.
6 | //
7 |
8 | import UIKit
9 |
10 | final class LandmarkCardCell: OMWCollectionViewCell {
11 | static let identifier: String = "LandmarkCardCell"
12 |
13 | @IBOutlet weak var imageView: UIImageView!
14 | @IBOutlet weak var titleLabel: UILabel!
15 |
16 | override func prepareForReuse() {
17 | super.prepareForReuse()
18 | self.imageView.image = nil
19 | }
20 |
21 | override init(frame: CGRect) {
22 | super.init(frame: frame)
23 | self.layer.cornerRadius = 10.0
24 | }
25 |
26 | required init?(coder: NSCoder) {
27 | super.init(coder: coder)
28 | self.layer.cornerRadius = 10.0
29 | }
30 |
31 | func configure(with landmark: Landmark) {
32 | self.task = self.imageView.setNetworkImage(with: landmark.image)
33 | self.titleLabel.text = landmark.title
34 | self.configureAccessibility(with: landmark)
35 | }
36 |
37 | func configureAccessibility(with landmark: Landmark) {
38 | guard let title = landmark.title
39 | else { return }
40 | self.isAccessibilityElement = true
41 | self.accessibilityValue = "관광명소: \(title)"
42 | }
43 |
44 | func shake() {
45 | let shakeAnimation = CABasicAnimation(keyPath: "transform.rotation")
46 | shakeAnimation.duration = 0.05
47 | shakeAnimation.repeatCount = 2
48 | shakeAnimation.autoreverses = true
49 | let startAngle: Float = -3.14159 / 180
50 | let stopAngle = -startAngle
51 | shakeAnimation.fromValue = NSNumber(value: startAngle as Float)
52 | shakeAnimation.toValue = NSNumber(value: 2 * stopAngle as Float)
53 | shakeAnimation.autoreverses = true
54 | shakeAnimation.duration = 0.15
55 | shakeAnimation.repeatCount = 10000
56 | shakeAnimation.timeOffset = 290 * Double.random(in: 0...1)
57 |
58 | let layer: CALayer = self.layer
59 | layer.add(shakeAnimation, forKey: "shaking")
60 | }
61 |
62 | func stopShaking() {
63 | let layer: CALayer = self.layer
64 | layer.removeAnimation(forKey: "shaking")
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/CollectionView/MessageCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MessageCell.swift
3 | // OwnMyWay
4 | //
5 | // Created by 이청수 on 2021/11/23.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol MessageCellDelegate: AnyObject {
11 | func didTouchButton()
12 | func didTouchCloseButton()
13 | }
14 |
15 | final class MessageCell: UICollectionViewCell {
16 | static let identifier = "MessageCell"
17 |
18 | @IBOutlet private weak var createTravelButton: UIButton!
19 | private weak var delegate: MessageCellDelegate?
20 |
21 | override func layoutSubviews() {
22 | super.layoutSubviews()
23 | self.createTravelButton.layer.cornerRadius = 10
24 | }
25 |
26 | func bind(delegate: MessageCellDelegate) {
27 | self.delegate = delegate
28 | }
29 |
30 | @IBAction func didTouchCreateTravelButton(_ sender: UIButton) {
31 | self.delegate?.didTouchButton()
32 | }
33 |
34 | @IBAction func didTouchCloseButton(_ sender: UIButton) {
35 | self.delegate?.didTouchCloseButton()
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/CollectionView/OMWCollectionViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OMWCollectionViewCell.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/25.
6 | //
7 |
8 | import UIKit
9 |
10 | class OMWCollectionViewCell: UICollectionViewCell {
11 | var task: Cancellable?
12 |
13 | override func prepareForReuse() {
14 | super.prepareForReuse()
15 | self.task?.cancelFetch()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/CollectionView/PhotoCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoCell.swift
3 | // OwnMyWay
4 | //
5 | // Created by 이청수 on 2021/11/16.
6 | //
7 |
8 | import UIKit
9 |
10 | final class PhotoCell: UICollectionViewCell {
11 | static let identifier = "PhotoCell"
12 |
13 | @IBOutlet weak var imageView: UIImageView!
14 |
15 | func configure(url: URL?) {
16 | self.layer.cornerRadius = 10
17 | self.imageView.setLocalImage(with: url)
18 | }
19 |
20 | func configureAccessibility(index: Int) {
21 | self.isAccessibilityElement = true
22 | self.accessibilityValue = index == 0 ? "이미지 추가 버튼" : "\(index)번째 사진"
23 | self.accessibilityHint = index == 0 ? "눌러서 이미지를 추가하세요." : "눌러서 이미지를 제거할 수 있어요."
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/CollectionView/PhotoCell.xib:
--------------------------------------------------------------------------------
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 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/CollectionView/PlusCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PlusCell.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/02.
6 | //
7 |
8 | import UIKit
9 |
10 | final class PlusCell: UICollectionViewCell {
11 | static let identifier = "PlusCell"
12 |
13 | private var dashedLayer: CAShapeLayer?
14 |
15 | override func layoutSubviews() {
16 | super.layoutSubviews()
17 | self.drawDottedLayer()
18 | }
19 |
20 | private func drawDottedLayer() {
21 | self.dashedLayer?.removeFromSuperlayer()
22 |
23 | let shapeLayer: CAShapeLayer = {
24 | let shapeLayer = CAShapeLayer()
25 |
26 | shapeLayer.frame = self.bounds
27 | shapeLayer.fillColor = UIColor.clear.cgColor
28 | shapeLayer.strokeColor = UIColor.init(named: "PlusCard")?.cgColor
29 | shapeLayer.lineWidth = 2
30 | shapeLayer.lineDashPattern = [6, 3]
31 | shapeLayer.path = UIBezierPath(roundedRect: self.bounds, cornerRadius: 10).cgPath
32 |
33 | return shapeLayer
34 | }()
35 |
36 | let maskLayer: CAShapeLayer = {
37 | let maskLayer = CAShapeLayer()
38 |
39 | maskLayer.frame = self.bounds
40 | maskLayer.path = UIBezierPath(roundedRect: self.bounds, cornerRadius: 10).cgPath
41 |
42 | return maskLayer
43 | }()
44 |
45 | self.dashedLayer = shapeLayer
46 | self.layer.mask = maskLayer
47 | self.layer.addSublayer(shapeLayer)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/CollectionView/RecordCardCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecordCardCell.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/10.
6 | //
7 |
8 | import UIKit
9 |
10 | final class RecordCardCell: UICollectionViewCell {
11 |
12 | static let identifier = "RecordCardCell"
13 |
14 | @IBOutlet private weak var recordCardImageView: UIImageView!
15 | @IBOutlet private weak var recordContentLabel: UILabel!
16 |
17 | func configure(with record: Record) {
18 | self.makeShadow()
19 | guard let photos = record.photoIDs else { return }
20 | let photoURLs = photos.map { ImageFileManager.shared.imageInDocuemtDirectory(image: $0) }
21 | self.recordCardImageView.setLocalImage(with: photoURLs.first ?? nil)
22 | self.recordContentLabel.text = record.title
23 | self.configureAccessibility(with: record)
24 | }
25 |
26 | func configureAccessibility(with record: Record) {
27 | guard let title = record.title else { return }
28 | self.isAccessibilityElement = true
29 | self.accessibilityValue = "기록: \(title)"
30 | self.accessibilityHint = "눌러서 기록을 자세히 볼 수 있어요."
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/CollectionView/TravelCardCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TravelCardCell.swift
3 | // OwnMyWay
4 | //
5 | // Created by 이청수 on 2021/11/04.
6 | //
7 |
8 | import UIKit
9 |
10 | final class TravelCardCell: OMWCollectionViewCell {
11 | static let identifier = "TravelCardCell"
12 |
13 | @IBOutlet private weak var travelTitleLabel: UILabel!
14 | @IBOutlet private weak var travelDateLabel: UILabel!
15 | @IBOutlet weak var travelCardImageView: UIImageView!
16 |
17 | override func prepareForReuse() {
18 | super.prepareForReuse()
19 | }
20 |
21 | func configure(with travel: Travel) {
22 | self.travelTitleLabel.text = travel.title
23 | self.travelDateLabel.text = travel.startDate?.format(endDate: travel.endDate) ?? ""
24 | if let landmark = travel.landmarks.randomElement() {
25 | self.task = self.travelCardImageView.setNetworkImage(with: landmark.image)
26 | }
27 | self.travelCardImageView.layer.cornerRadius = 10
28 | self.travelCardImageView.clipsToBounds = true
29 | self.configureAccessibility(with: travel)
30 | }
31 |
32 | private func configureAccessibility(with travel: Travel) {
33 | guard let title = travel.title,
34 | let startDate = travel.startDate,
35 | let endDate = travel.endDate
36 | else { return }
37 | let dateString = startDate == endDate ?
38 | "\(startDate.toKorean()) 당일치기" :
39 | "\(startDate.toKorean())부터 \(endDate.toKorean())까지"
40 | self.isAccessibilityElement = true
41 | self.accessibilityValue = "여행: \(title), \(dateString)"
42 | self.accessibilityHint = "눌러서 여행 정보를 볼 수 있어요."
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/CollectionView/TravelSectionHeader.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TravelSectionHeader.swift
3 | // OwnMyWay
4 | //
5 | // Created by 이청수 on 2021/11/07.
6 | //
7 |
8 | import UIKit
9 |
10 | final class TravelSectionHeader: UICollectionReusableView {
11 | static let identifier = "TravelSectionHeader"
12 |
13 | @IBOutlet private weak var sectionTitleLabel: UILabel!
14 |
15 | func configure(sectionTitle: String) {
16 | self.sectionTitleLabel.text = sectionTitle
17 | self.configureAccessibility(with: sectionTitle)
18 | }
19 |
20 | func configureAccessibility(with sectionTitle: String) {
21 | self.isAccessibilityElement = sectionTitle != ""
22 | self.accessibilityTraits = .header
23 | self.accessibilityValue = sectionTitle
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/CollectionView/TravelSectionHeader.xib:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/LocationTableViewCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LocationTableViewCell.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/18.
6 | //
7 |
8 | import UIKit
9 |
10 | final class LocationTableViewCell: UITableViewCell {
11 | static let identifier = "LocationTableViewCell"
12 |
13 | @IBOutlet weak var titleLabel: UILabel!
14 | @IBOutlet weak var subTitleLabel: UILabel!
15 |
16 | func configure(title: String?, subTitle: String?) {
17 | self.titleLabel.text = title
18 | self.subTitleLabel.text = subTitle
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/MapView/LandmarkAnnotationView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LandmarkAnnotationView.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/03.
6 | //
7 |
8 | import CoreLocation
9 | import Foundation
10 | import MapKit
11 |
12 | final class LandmarkAnnotationView: MKAnnotationView {
13 | static let identifier = "LandmarkAnnotationView"
14 |
15 | override var annotation: MKAnnotation? { didSet { configureDetailView() } }
16 |
17 | override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
18 | super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
19 | self.configure()
20 | }
21 |
22 | required init?(coder aDecoder: NSCoder) {
23 | super.init(coder: aDecoder)
24 | self.configure()
25 | }
26 |
27 | func configure() {
28 | self.canShowCallout = true
29 | self.image = UIImage(named: "LandmarkPin")
30 | self.frame.size = CGSize(width: 40, height: 40)
31 | self.configureDetailView()
32 | }
33 |
34 | func configureDetailView() {
35 | guard let annotation = annotation as? LandmarkAnnotation else { return }
36 |
37 | let rect = CGRect(origin: .zero, size: CGSize(width: 300, height: 200))
38 |
39 | let detailView = UIView()
40 | let imageView = UIImageView(frame: rect)
41 | detailView.translatesAutoresizingMaskIntoConstraints = false
42 | _ = imageView.setNetworkImage(with: annotation.image)
43 | imageView.contentMode = .scaleAspectFill
44 | imageView.clipsToBounds = true
45 | detailView.addSubview(imageView)
46 |
47 | self.detailCalloutAccessoryView = detailView
48 | NSLayoutConstraint.activate([
49 | detailView.widthAnchor.constraint(equalToConstant: rect.width),
50 | detailView.heightAnchor.constraint(equalToConstant: rect.height)
51 | ])
52 | }
53 | }
54 |
55 | class LandmarkAnnotation: NSObject, MKAnnotation {
56 | var coordinate: CLLocationCoordinate2D
57 | var title: String?
58 | var subtitle: String?
59 | var image: URL?
60 |
61 | init(landmark: Landmark) {
62 | self.coordinate = CLLocationCoordinate2D(
63 | latitude: landmark.latitude ?? 0,
64 | longitude: landmark.longitude ?? 0
65 | )
66 | self.image = landmark.image
67 | self.title = landmark.title
68 | self.subtitle = nil
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/MapView/RecordAnnotationView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecordAnnotationView.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/03.
6 | //
7 |
8 | import CoreLocation
9 | import Foundation
10 | import MapKit
11 |
12 | final class RecordAnnotationView: MKAnnotationView {
13 | static let identifier = "RecordAnnotationView"
14 |
15 | override var annotation: MKAnnotation? { didSet { configureDetailView() } }
16 |
17 | override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
18 | super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
19 | self.configure()
20 | }
21 |
22 | required init?(coder aDecoder: NSCoder) {
23 | super.init(coder: aDecoder)
24 | self.configure()
25 | }
26 |
27 | func configure() {
28 | self.canShowCallout = true
29 | self.image = UIImage(named: "RecordPin")
30 | self.frame.size = CGSize(width: 40, height: 40)
31 | self.configureDetailView()
32 | }
33 |
34 | func configureDetailView() {
35 | guard let annotation = annotation as? RecordAnnotation else { return }
36 |
37 | let rect = CGRect(origin: .zero, size: CGSize(width: 300, height: 200))
38 |
39 | let detailView = UIView()
40 | let imageView = UIImageView(frame: rect)
41 | detailView.translatesAutoresizingMaskIntoConstraints = false
42 | imageView.setLocalImage(with: annotation.image)
43 | imageView.contentMode = .scaleAspectFill
44 | imageView.clipsToBounds = true
45 | detailView.addSubview(imageView)
46 |
47 | self.detailCalloutAccessoryView = detailView
48 | NSLayoutConstraint.activate([
49 | detailView.widthAnchor.constraint(equalToConstant: rect.width),
50 | detailView.heightAnchor.constraint(equalToConstant: rect.height)
51 | ])
52 | }
53 | }
54 |
55 | class RecordAnnotation: NSObject, MKAnnotation {
56 | var coordinate: CLLocationCoordinate2D
57 | var title: String?
58 | var subtitle: String?
59 | var image: URL?
60 |
61 | init(record: Record) {
62 | self.coordinate = CLLocationCoordinate2D(
63 | latitude: record.latitude ?? 0,
64 | longitude: record.longitude ?? 0
65 | )
66 | guard let photos = record.photoIDs else { return }
67 | let photoURLs = photos.map { ImageFileManager.shared.imageInDocuemtDirectory(image: $0) }
68 | self.image = photoURLs.first ?? nil
69 | self.title = record.title
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/NextButton.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NextButton.swift
3 | // OwnMyWay
4 | //
5 | // Created by 이청수 on 2021/11/04.
6 | //
7 |
8 | import UIKit
9 |
10 | final class NextButton: UIButton {
11 |
12 | func setAvailability(to isEnable: Bool) {
13 | self.isEnabled = isEnable
14 | self.backgroundColor = isEnable ? UIColor(named: "IdentityBlue") ?? .blue : .gray
15 | }
16 |
17 | }
18 |
19 | extension NextButton {
20 | func setBackgroundColor(_ color: UIColor, for state: UIControl.State) {
21 | UIGraphicsBeginImageContext(CGSize(width: 1.0, height: 1.0))
22 | guard let context = UIGraphicsGetCurrentContext() else { return }
23 | context.setFillColor(color.cgColor)
24 | context.fill(CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0))
25 | let backgroundImage = UIGraphicsGetImageFromCurrentImageContext()
26 | UIGraphicsEndImageContext()
27 | self.setBackgroundImage(backgroundImage, for: state)
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/OMWCalendar/CalendarCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CalendarCell.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/25.
6 | //
7 |
8 | import UIKit
9 |
10 | class CalendarCell: UICollectionViewCell {
11 |
12 | static let identifier = "CalendarCell"
13 |
14 | @IBOutlet private weak var dateLabel: UILabel!
15 | @IBOutlet private weak var backgroundCellView: UIView!
16 | private let dates = ["일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"]
17 |
18 | func configure(item: CalendarDataSource.CalendarItem) {
19 | self.dateLabel.text = item.isDummy ? "" : "\(item.date.dayNumber)"
20 | let weekday = item.date.weekday
21 | switch weekday {
22 | case 1:
23 | self.dateLabel.textColor = .red
24 | case 7:
25 | self.dateLabel.textColor = .blue
26 | default:
27 | self.dateLabel.textColor = .label
28 | }
29 | if item.date.isToday() && !item.isDummy {
30 | self.backgroundCellView.backgroundColor = UIColor(named: "WJGreen")
31 | self.dateLabel.textColor = .white
32 | } else {
33 | self.backgroundCellView.backgroundColor = .clear
34 | }
35 |
36 | let hint = "\(item.date.dayNumber)일 \(dates[item.date.weekday - 1])입니다. 눌러서 여행 기간을 선택해주세요."
37 | self.isAccessibilityElement = !item.isDummy
38 | self.accessibilityHint = hint
39 | self.backgroundCellView.layer.cornerRadius = self.backgroundCellView.frame.size.height / 2
40 | self.layoutIfNeeded()
41 | }
42 |
43 | func didSelect() {
44 | self.backgroundCellView.backgroundColor = UIColor(named: "IdentityBlue") ?? .blue
45 | self.dateLabel.textColor = .white
46 | }
47 |
48 | }
49 |
50 | fileprivate extension Date {
51 |
52 | func isToday() -> Bool {
53 | let today = Date()
54 | return today.year == self.year &&
55 | today.month == self.month &&
56 | today.dayNumber == self.dayNumber
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Common/OMWCalendar/CalendarDataSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CalendarDataSource.swift
3 | // OwnMyWay
4 | //
5 | // Created by 이청수 on 2021/11/25.
6 | //
7 |
8 | import UIKit
9 |
10 | class CalendarDataSource: NSObject, UICollectionViewDataSource {
11 |
12 | struct CalendarItem: Hashable {
13 | var isDummy: Bool
14 | var date: Date
15 | }
16 |
17 | var date: Date {
18 | didSet {
19 | var items = [CalendarItem]()
20 | for index in 0..<7 - date.firstWeekDayCount {
21 | items.append(
22 | CalendarItem(isDummy: true, date: date.addingTimeInterval(Double(index)))
23 | )
24 | }
25 | for index in 0.. Int {
46 | return self.items.count
47 | }
48 |
49 | func collectionView(
50 | _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath
51 | ) -> UICollectionViewCell {
52 | guard let cell = collectionView.dequeueReusableCell(
53 | withReuseIdentifier: CalendarCell.identifier, for: indexPath
54 | ) as? CalendarCell
55 | else { return UICollectionViewCell() }
56 |
57 | let item = self.items[indexPath.item]
58 | cell.configure(item: item)
59 | if let startDate = self.startDate,
60 | let endDate = self.endDate,
61 | startDate <= item.date && endDate >= item.date,
62 | !item.isDummy {
63 | cell.didSelect()
64 | }
65 | return cell
66 | }
67 |
68 | func configureDate(from startDate: Date?, to endDate: Date?) {
69 | self.startDate = startDate
70 | self.endDate = endDate
71 | }
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/CompleteScene/CompleteCreation/Coordinator/CompleteCreationCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompleteCreationCoordinator.swift
3 | // OwnMyWay
4 | //
5 | // Created by 이청수 on 2021/11/09.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol CompleteCreationCoordinatingDelegate: AnyObject {
11 | func popToHome()
12 | }
13 |
14 | final class CompleteCreationCoordinator: Coordinator, CompleteCreationCoordinatingDelegate {
15 |
16 | var childCoordinators: [Coordinator]
17 | var navigationController: UINavigationController
18 | private var travel: Travel
19 |
20 | init(navigationController: UINavigationController, travel: Travel) {
21 | self.childCoordinators = []
22 | self.navigationController = navigationController
23 | self.travel = travel
24 | }
25 |
26 | func start() {
27 | guard let appDelegate = UIApplication.shared.delegate as? AppDelegate
28 | else { return }
29 | let repository = CoreDataTravelRepository(contextFetcher: appDelegate)
30 | let usecase = DefaultCompleteCreationUsecase(repository: repository)
31 | let completeCreationVM = DefaultCompleteCreationViewModel(
32 | usecase: usecase, coordinatingDelegate: self, travel: self.travel
33 | )
34 | let completeCreationVC = CompleteCreationViewController.instantiate(
35 | storyboardName: "CompleteCreation"
36 | )
37 | completeCreationVC.bind(viewModel: completeCreationVM)
38 | self.navigationController.pushViewController(completeCreationVC, animated: true)
39 | }
40 |
41 | func popToHome() {
42 | guard let homeVC = self
43 | .navigationController
44 | .viewControllers
45 | .first as? TravelFetchable
46 | else { return }
47 | homeVC.fetchTravel()
48 | self.navigationController.popToRootViewController(animated: true)
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/CompleteScene/CompleteCreation/Interface/Usecase/CompleteCreationUsecase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompleteCreationUsecase.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/30.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol CompleteCreationUsecase {
11 | func executeCreation(travel: Travel, completion: @escaping (Result) -> Void)
12 | }
13 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/CompleteScene/CompleteCreation/View/CompleteCreationViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompleteCreationViewController.swift
3 | // OwnMyWay
4 | //
5 | // Created by 이청수 on 2021/11/09.
6 | //
7 |
8 | import Combine
9 | import UIKit
10 |
11 | final class CompleteCreationViewController: UIViewController, Instantiable {
12 |
13 | @IBOutlet private weak var nextButtonHeightConstraint: NSLayoutConstraint!
14 |
15 | private var viewModel: CompleteCreationViewModel?
16 | private var cancellables: Set = []
17 |
18 | override func viewDidLoad() {
19 | super.viewDidLoad()
20 | self.configureCancellables()
21 | }
22 |
23 | override func viewWillAppear(_ animated: Bool) {
24 | super.viewWillAppear(animated)
25 | self.configureNavigationController()
26 | }
27 |
28 | override func viewWillLayoutSubviews() {
29 | super.viewWillLayoutSubviews()
30 | self.configureButtonConstraint()
31 | }
32 |
33 | func bind(viewModel: CompleteCreationViewModel) {
34 | self.viewModel = viewModel
35 | }
36 |
37 | private func configureNavigationController() {
38 | self.navigationController?.navigationBar.topItem?.title = ""
39 | self.navigationItem.title = "새로운 여행"
40 | }
41 |
42 | private func configureCancellables() {
43 | self.viewModel?.errorPublisher
44 | .receive(on: DispatchQueue.main)
45 | .sink { [weak self] optionalError in
46 | guard let error = optionalError else { return }
47 | ErrorManager.showToast(with: error, to: self)
48 | }
49 | .store(in: &self.cancellables)
50 | }
51 |
52 | private func configureButtonConstraint() {
53 | let bottomPadding = self.view.safeAreaInsets.bottom
54 | self.nextButtonHeightConstraint.constant = 60 + bottomPadding
55 | }
56 |
57 | @IBAction func didTouchCompleteButton(_ sender: UIButton) {
58 | self.viewModel?.didTouchCompleteButton()
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/CompleteScene/CompleteCreation/ViewModel/CompleteCreationViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompleteCreationViewModel.swift
3 | // OwnMyWay
4 | //
5 | // Created by 이청수 on 2021/11/09.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol CompleteCreationViewModel {
11 | var errorPublisher: Published.Publisher { get }
12 |
13 | func didTouchCompleteButton()
14 | }
15 |
16 | final class DefaultCompleteCreationViewModel: CompleteCreationViewModel {
17 | var errorPublisher: Published.Publisher { $error }
18 | private let usecase: CompleteCreationUsecase
19 | private weak var coordinatingDelegate: CompleteCreationCoordinatingDelegate?
20 | private let travel: Travel
21 | @Published private var error: Error?
22 |
23 | init(
24 | usecase: CompleteCreationUsecase,
25 | coordinatingDelegate: CompleteCreationCoordinatingDelegate,
26 | travel: Travel
27 | ) {
28 | self.usecase = usecase
29 | self.coordinatingDelegate = coordinatingDelegate
30 | self.travel = travel
31 | }
32 |
33 | func didTouchCompleteButton() {
34 | self.usecase.executeCreation(travel: travel) { [weak self] result in
35 | switch result {
36 | case .success:
37 | self?.coordinatingDelegate?.popToHome()
38 | case .failure(let error):
39 | self?.error = error
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/CompleteScene/CompleteEditing/Coordinator/CompleteEditingCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompleteEditingCoordinator.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/17.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol CompleteEditingCoordinatingDelegate: AnyObject {
11 | func popToTravelViewController(travel: Travel)
12 | }
13 |
14 | final class CompleteEditingCoordinator: Coordinator, CompleteEditingCoordinatingDelegate {
15 |
16 | var childCoordinators: [Coordinator]
17 | var navigationController: UINavigationController
18 | private var travel: Travel
19 |
20 | init(navigationController: UINavigationController, travel: Travel) {
21 | self.childCoordinators = []
22 | self.navigationController = navigationController
23 | self.travel = travel
24 | }
25 |
26 | func start() {
27 | guard let appDelegate = UIApplication.shared.delegate as? AppDelegate
28 | else { return }
29 | let repository = CoreDataTravelRepository(contextFetcher: appDelegate)
30 | let usecase = DefaultCompleteEditingUsecase(repository: repository)
31 | let completeEditingVM = DefaultCompleteEditingViewModel(
32 | usecase: usecase, coordinatingDelegate: self, travel: self.travel
33 | )
34 | let completeEditingVC = CompleteEditingViewController.instantiate(
35 | storyboardName: "CompleteEditing"
36 | )
37 | completeEditingVC.bind(viewModel: completeEditingVM)
38 | self.navigationController.pushViewController(completeEditingVC, animated: true)
39 | }
40 |
41 | func popToTravelViewController(travel: Travel) {
42 | let viewControllers = self.navigationController.viewControllers
43 | let travelVC = viewControllers.first {
44 | $0 is TravelEditable
45 | }
46 | guard let travelVC = travelVC as? UIViewController & TravelEditable
47 | else { return }
48 | travelVC.didEditTravel(to: travel)
49 | self.navigationController.popToViewController(travelVC, animated: true)
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/CompleteScene/CompleteEditing/Interface/Usecase/CompleteEditingUsecase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompleteEditingUsecase.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/30.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol CompleteEditingUsecase {
11 | func executeUpdate(travel: Travel, completion: @escaping (Result) -> Void)
12 | }
13 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/CompleteScene/CompleteEditing/View/CompleteEditingViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompleteEditingViewController.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/17.
6 | //
7 |
8 | import Combine
9 | import UIKit
10 |
11 | final class CompleteEditingViewController: UIViewController, Instantiable {
12 |
13 | @IBOutlet private weak var nextButtonHeightConstraint: NSLayoutConstraint!
14 |
15 | private var viewModel: CompleteEditingViewModel?
16 | private var cancellables: Set = []
17 |
18 | override func viewDidLoad() {
19 | super.viewDidLoad()
20 | self.configureCancellables()
21 | }
22 |
23 | override func viewWillAppear(_ animated: Bool) {
24 | super.viewWillAppear(animated)
25 | self.configureNavigationController()
26 | }
27 |
28 | override func viewWillLayoutSubviews() {
29 | super.viewWillLayoutSubviews()
30 | self.configureButtonConstraint()
31 | }
32 |
33 | func bind(viewModel: CompleteEditingViewModel) {
34 | self.viewModel = viewModel
35 | }
36 |
37 | private func configureCancellables() {
38 | self.viewModel?.errorPublisher
39 | .receive(on: DispatchQueue.main)
40 | .sink { [weak self] optionalError in
41 | guard let error = optionalError else { return }
42 | ErrorManager.showToast(with: error, to: self)
43 | }
44 | .store(in: &self.cancellables)
45 | }
46 |
47 | private func configureNavigationController() {
48 | self.navigationController?.navigationBar.topItem?.title = ""
49 | self.navigationItem.title = "여행 편집하기"
50 | }
51 |
52 | private func configureButtonConstraint() {
53 | let bottomPadding = self.view.safeAreaInsets.bottom
54 | self.nextButtonHeightConstraint.constant = 60 + bottomPadding
55 | }
56 |
57 | @IBAction func didTouchCompleteButton(_ sender: UIButton) {
58 | self.viewModel?.didTouchCompleteButton()
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/CompleteScene/CompleteEditing/ViewModel/CompleteEditingViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CompleteEditingViewModel.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/17.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol CompleteEditingViewModel {
11 | var errorPublisher: Published.Publisher { get }
12 |
13 | func didTouchCompleteButton()
14 | }
15 |
16 | final class DefaultCompleteEditingViewModel: CompleteEditingViewModel {
17 | var errorPublisher: Published.Publisher { $error }
18 | private let usecase: CompleteEditingUsecase
19 | private weak var coordinatingDelegate: CompleteEditingCoordinatingDelegate?
20 | private let travel: Travel
21 | @Published private var error: Error?
22 |
23 | init(
24 | usecase: CompleteEditingUsecase,
25 | coordinatingDelegate: CompleteEditingCoordinatingDelegate,
26 | travel: Travel
27 | ) {
28 | self.usecase = usecase
29 | self.coordinatingDelegate = coordinatingDelegate
30 | self.travel = travel
31 | }
32 |
33 | func didTouchCompleteButton() {
34 | self.usecase.executeUpdate(travel: travel) { [weak self] result in
35 | switch result {
36 | case .success(let travel):
37 | self?.coordinatingDelegate?.popToTravelViewController(travel: travel)
38 | case .failure(let error):
39 | self?.error = error
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/DetailImageScene/Coordinator/DetailImageCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DetailImageCoordinator.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/23.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol DetailImageCoordinatingDelegate: AnyObject {
11 | func dismissToImageDetail()
12 | }
13 |
14 | final class DetailImageCoordinator: Coordinator, DetailImageCoordinatingDelegate {
15 |
16 | var childCoordinators: [Coordinator]
17 | var navigationController: UINavigationController
18 | var imageIDs: [String]
19 | var selectedIndex: Int
20 |
21 | init(navigationController: UINavigationController, images: [String], index: Int) {
22 | self.navigationController = navigationController
23 | self.childCoordinators = []
24 | self.imageIDs = images
25 | self.selectedIndex = index
26 | }
27 |
28 | func start() {
29 | let imageVM = DefaultDetailImageViewModel(
30 | coordinatingDelegate: self,
31 | images: self.imageIDs,
32 | index: selectedIndex
33 | )
34 | let imageVC = DetailImageViewController.instantiate(storyboardName: "DetailImage")
35 | imageVC.bind(viewModel: imageVM)
36 | imageVC.modalPresentationStyle = .fullScreen
37 | imageVC.modalTransitionStyle = .coverVertical
38 | self.navigationController.viewControllers.last?.present(
39 | imageVC, animated: true
40 | )
41 | }
42 |
43 | func dismissToImageDetail() {
44 | self.navigationController.viewControllers.last?.dismiss(animated: true)
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/DetailImageScene/ViewModel/DetailImageViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DetailImageViewModel.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/23.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol DetailImageViewModel {
11 | var imageIDs: [String] { get }
12 | var selectedIndex: Int { get }
13 | func didTouchBackButton()
14 | }
15 |
16 | final class DefaultDetailImageViewModel: DetailImageViewModel {
17 | var imageIDs: [String]
18 | var selectedIndex: Int
19 | private weak var coordinatingDelegate: DetailImageCoordinatingDelegate?
20 |
21 | init(coordinatingDelegate: DetailImageCoordinatingDelegate, images: [String], index: Int) {
22 | self.coordinatingDelegate = coordinatingDelegate
23 | self.imageIDs = images
24 | self.selectedIndex = index
25 | }
26 |
27 | func didTouchBackButton() {
28 | self.coordinatingDelegate?.dismissToImageDetail()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/DetailRecordScene/Coordinator/DetailRecordCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DetailRecordCoordinator.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/10.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol DetailRecordCoordinatingDelegate: AnyObject {
11 | func pushToAddRecord(record: Record)
12 | func popToParent(with travel: Travel, isPopable: Bool)
13 | func presentDetailImage(images: [String], index: Int)
14 | }
15 |
16 |
17 | final class DetailRecordCoordinator: Coordinator, DetailRecordCoordinatingDelegate {
18 |
19 | var childCoordinators: [Coordinator]
20 | var navigationController: UINavigationController
21 | private var record: Record
22 | private var travel: Travel
23 |
24 | init(navigationController: UINavigationController, record: Record, travel: Travel) {
25 | self.childCoordinators = []
26 | self.navigationController = navigationController
27 | self.record = record
28 | self.travel = travel
29 | }
30 |
31 | func start() {
32 | guard let appDelegate = UIApplication.shared.delegate as? AppDelegate
33 | else { return }
34 | let repository = CoreDataTravelRepository(contextFetcher: appDelegate)
35 | let usecase = DefaultDetailRecordUsecase(repository: repository)
36 | let detailRecordVM = DefaultDetailRecordViewModel(
37 | record: self.record, travel: self.travel, usecase: usecase, coordinatingDelegate: self
38 | )
39 | let detailRecordVC = DetailRecordViewController.instantiate(storyboardName: "DetailRecord")
40 | detailRecordVC.bind(viewModel: detailRecordVM)
41 | self.navigationController.pushViewController(detailRecordVC, animated: true)
42 | }
43 |
44 | func popToParent(with travel: Travel, isPopable: Bool) {
45 | if isPopable {
46 | self.navigationController.popViewController(animated: true)
47 | }
48 | guard let parentVC = navigationController
49 | .viewControllers
50 | .last as? TravelUpdatable & UIViewController
51 | else { return }
52 | parentVC.didUpdateTravel(to: travel)
53 | }
54 |
55 | func pushToAddRecord(record: Record) {
56 | let addRecordCoordinator = AddRecordCoordinator(
57 | navigationController: self.navigationController, record: record, isEditingMode: true
58 | )
59 | self.childCoordinators.append(addRecordCoordinator)
60 | addRecordCoordinator.start()
61 | }
62 |
63 | func presentDetailImage(images: [String], index: Int) {
64 | let detailImageCoordinator = DetailImageCoordinator(
65 | navigationController: self.navigationController, images: images, index: index
66 | )
67 | self.childCoordinators.append(detailImageCoordinator)
68 | detailImageCoordinator.start()
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/DetailRecordScene/Interface/Usecase/DetailRecordUsecase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DetailRecordUsecase.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/30.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol DetailRecordUsecase {
11 | func executeRecordUpdate(record: Record, completion: @escaping (Result) -> Void)
12 | func executeRecordDeletion(
13 | at record: Record, completion: @escaping (Result) -> Void
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/DetailRecordScene/ViewModel/DetailRecordViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DetailRecordViewModel.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/10.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol DetailRecordViewModel {
11 | var record: Record { get }
12 | var recordPublisher: Published.Publisher { get }
13 | var errorPublisher: Published.Publisher { get }
14 |
15 | func didTouchBackButton()
16 | func didTouchDeleteButton()
17 | func didTouchEditButton()
18 | func didUpdateRecord(record: Record)
19 | func didTouchImageView(index: Int)
20 | }
21 |
22 | final class DefaultDetailRecordViewModel: DetailRecordViewModel {
23 |
24 | var recordPublisher: Published.Publisher { $record }
25 | var errorPublisher: Published.Publisher { $error }
26 |
27 | @Published private(set) var record: Record
28 | @Published private var error: Error?
29 | private var travel: Travel
30 |
31 | private let usecase: DetailRecordUsecase
32 | private weak var coordinatingDelegate: DetailRecordCoordinatingDelegate?
33 |
34 | init(
35 | record: Record,
36 | travel: Travel,
37 | usecase: DetailRecordUsecase,
38 | coordinatingDelegate: DetailRecordCoordinatingDelegate
39 | ) {
40 | self.record = record
41 | self.travel = travel
42 | self.usecase = usecase
43 | self.coordinatingDelegate = coordinatingDelegate
44 | }
45 |
46 | func didTouchBackButton() {
47 | self.coordinatingDelegate?.popToParent(with: self.travel, isPopable: false)
48 | }
49 |
50 | func didTouchEditButton() {
51 | self.coordinatingDelegate?.pushToAddRecord(record: self.record)
52 | }
53 |
54 | func didUpdateRecord(record: Record) {
55 | self.record = record
56 | self.usecase.executeRecordUpdate(record: record) { [weak self] result in
57 | switch result {
58 | case .success:
59 | guard self?.travel.records.firstIndex(where: { $0.uuid == record.uuid }) != nil
60 | else {
61 | self?.error = RepositoryError.uuidError
62 | return
63 | }
64 | case .failure(let error):
65 | self?.error = error
66 | }
67 | }
68 | }
69 |
70 | func didTouchDeleteButton() {
71 | self.usecase.executeRecordDeletion(at: self.record) { [weak self] result in
72 | switch result {
73 | case .success:
74 | guard let index = self?.travel.records.firstIndex(where: { $0 == self?.record })
75 | else {
76 | self?.error = ModelError.recordError
77 | return
78 | }
79 | self?.travel.records.remove(at: index)
80 | guard let travel = self?.travel else { return }
81 | self?.coordinatingDelegate?.popToParent(with: travel, isPopable: true)
82 | case .failure(let error):
83 | self?.error = error
84 | }
85 | }
86 | }
87 |
88 | func didTouchImageView(index: Int) {
89 | guard let imageIDs = self.record.photoIDs else { return }
90 | self.coordinatingDelegate?.presentDetailImage(images: imageIDs, index: index)
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/EnterDateScene/Coordinator/EnterDateCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EnterDate.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/23.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol EnterDateCoordinatingDelegate: AnyObject {
11 | func pushToAddLandmark(travel: Travel, isEditingMode: Bool)
12 | func popToCreateTravel(travel: Travel)
13 | }
14 |
15 | final class EnterDateCoordinator: Coordinator, EnterDateCoordinatingDelegate {
16 |
17 | var childCoordinators: [Coordinator]
18 | var navigationController: UINavigationController
19 | private var travel: Travel
20 | private var isEditingMode: Bool
21 |
22 | init(navigationController: UINavigationController, travel: Travel, isEditingMode: Bool) {
23 | self.childCoordinators = []
24 | self.navigationController = navigationController
25 | self.travel = travel
26 | self.isEditingMode = isEditingMode
27 | }
28 |
29 | func start() {
30 | let usecase = DefaultEnterDateUsecase()
31 | let enterDateVM = DefaultEnterDateViewModel(
32 | usecase: usecase,
33 | coordinatingDelegate: self,
34 | travel: self.travel,
35 | isEditingMode: self.isEditingMode
36 | )
37 | let enterDateVC = EnterDateViewController.instantiate(storyboardName: "EnterDate")
38 | enterDateVC.bind(viewModel: enterDateVM)
39 | self.navigationController.pushViewController(enterDateVC, animated: true)
40 | }
41 |
42 | func pushToAddLandmark(travel: Travel, isEditingMode: Bool) {
43 | let addLandmarkCoordinator = AddLandmarkCoordinator(
44 | navigationController: self.navigationController,
45 | travel: travel,
46 | isEditingMode: isEditingMode
47 | )
48 | self.childCoordinators.append(addLandmarkCoordinator)
49 | addLandmarkCoordinator.start()
50 | }
51 |
52 | func popToCreateTravel(travel: Travel) {
53 | guard let createTravelVC = self.navigationController.children.last
54 | as? EnterTitleViewController else { return }
55 |
56 | createTravelVC.travelDidChanged(to: travel)
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/EnterDateScene/Interface/Usecase/EnterDateUsecase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EnterDateUsecase.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/30.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol EnterDateUsecase {
11 | func executeEnteringDate(firstDate: Date, secondDate: Date, completion: ([Date]) -> Void)
12 | }
13 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/EnterDateScene/ViewModel/EnterDateViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EnterDateViewModel.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/23.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol EnterDateViewModel {
11 | var travel: Travel { get }
12 | var isEditingMode: Bool { get }
13 | var calendarStatePublisher: Published.Publisher { get }
14 | func viewDidLoad(completion: (Date?, Date?) -> Void)
15 | func travelDidChanged(to travel: Travel)
16 | func didEnterDate(at date: Date)
17 | func didTouchNoButton()
18 | func didTouchNextButton()
19 | func didTouchBackButton()
20 | }
21 |
22 | enum CalendarState {
23 | case fulfilled, firstDateEntered, empty, datesExisted
24 | }
25 |
26 | final class DefaultEnterDateViewModel: EnterDateViewModel, ObservableObject {
27 | var calendarStatePublisher: Published.Publisher { $calendarState }
28 | @Published private var calendarState: CalendarState = .datesExisted
29 |
30 | private let usecase: EnterDateUsecase
31 | private weak var coordinatingDelegate: EnterDateCoordinatingDelegate?
32 | private(set) var travel: Travel
33 | private(set) var isEditingMode: Bool
34 | private var firstDate: Date?
35 |
36 | init(
37 | usecase: EnterDateUsecase,
38 | coordinatingDelegate: EnterDateCoordinatingDelegate,
39 | travel: Travel,
40 | isEditingMode: Bool
41 | ) {
42 | self.usecase = usecase
43 | self.coordinatingDelegate = coordinatingDelegate
44 | self.isEditingMode = isEditingMode
45 | self.travel = travel
46 | if travel.startDate != nil, travel.endDate != nil {
47 | calendarState = .datesExisted
48 | } else {
49 | calendarState = .empty
50 | }
51 | }
52 |
53 | func viewDidLoad(completion: (Date?, Date?) -> Void) {
54 | completion(self.travel.startDate, self.travel.endDate)
55 | }
56 |
57 | func travelDidChanged(to travel: Travel) {
58 | self.travel = travel
59 | }
60 |
61 | func didEnterDate(at date: Date) {
62 | switch self.calendarState {
63 | case .fulfilled, .datesExisted:
64 | self.travel.startDate = nil
65 | self.travel.endDate = nil
66 | self.calendarState = .empty
67 | case .firstDateEntered:
68 | guard let firstDate = firstDate else { return }
69 | self.usecase.executeEnteringDate(
70 | firstDate: firstDate,
71 | secondDate: date
72 | ) { dates in
73 | self.firstDate = nil
74 | self.travel.startDate = dates.first
75 | self.travel.endDate = dates.last
76 | self.calendarState = .fulfilled
77 | }
78 | case .empty:
79 | self.firstDate = date
80 | self.calendarState = .firstDateEntered
81 | }
82 | }
83 |
84 | func didTouchNextButton() {
85 | self.coordinatingDelegate?.pushToAddLandmark(
86 | travel: self.travel, isEditingMode: self.isEditingMode
87 | )
88 | }
89 |
90 | func didTouchNoButton() {
91 | self.firstDate = nil
92 | self.travel.startDate = nil
93 | self.travel.endDate = nil
94 | self.calendarState = .empty
95 | }
96 |
97 | func didTouchBackButton() {
98 | self.coordinatingDelegate?.popToCreateTravel(travel: self.travel)
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/EnterTitleScene/Coordinator/EnterTitleCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CreateTravelCoordinator.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/08.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol EnterTitleCoordinatingDelegate: AnyObject {
11 | func pushToEnterDate(travel: Travel, isEditingMode: Bool)
12 | }
13 |
14 | final class EnterTitleCoordinator: Coordinator, EnterTitleCoordinatingDelegate {
15 |
16 | var childCoordinators: [Coordinator]
17 | var navigationController: UINavigationController
18 | private var travel: Travel?
19 |
20 | init(navigationController: UINavigationController, travel: Travel?) {
21 | self.childCoordinators = []
22 | self.navigationController = navigationController
23 | self.travel = travel
24 | }
25 |
26 | func start() {
27 | let usecase = DefaultEnterTitleUsecase()
28 | let enterTitleVM = DefaultEnterTitleViewModel(
29 | usecase: usecase,
30 | coordinatingDelegate: self,
31 | travel: self.travel
32 | )
33 | let enterTitleVC = EnterTitleViewController.instantiate(storyboardName: "EnterTitle")
34 | enterTitleVC.bind(viewModel: enterTitleVM)
35 | self.navigationController.pushViewController(enterTitleVC, animated: true)
36 | }
37 |
38 | func pushToEnterDate(travel: Travel, isEditingMode: Bool) {
39 | let enterDateCoordinator = EnterDateCoordinator(
40 | navigationController: self.navigationController,
41 | travel: travel,
42 | isEditingMode: isEditingMode
43 | )
44 | self.childCoordinators.append(enterDateCoordinator)
45 | enterDateCoordinator.start()
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/EnterTitleScene/Interface/Usecase/EnterTitleUsecase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EnterTitleUsecase.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/30.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol EnterTitleUsecase {
11 | func executeTitleValidation(
12 | with title: String,
13 | completion: @escaping (Result) -> Void
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/EnterTitleScene/View/EnterTitleViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CreateTravelViewController.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/02.
6 | //
7 |
8 | import Combine
9 | import UIKit
10 |
11 | final class EnterTitleViewController: UIViewController, Instantiable {
12 |
13 | @IBOutlet private weak var travelTitleField: UITextField!
14 | @IBOutlet private weak var nextButton: NextButton!
15 | @IBOutlet private weak var nextButtonHeightConstraint: NSLayoutConstraint!
16 |
17 | private var viewModel: EnterTitleViewModel?
18 | private var cancellables: Set = []
19 |
20 | override func viewDidLoad() {
21 | super.viewDidLoad()
22 | self.configureCancellable()
23 | self.configureGestureRecognizer()
24 | self.travelTitleField.delegate = self
25 | self.configureLabels()
26 | }
27 | override func viewWillAppear(_ animated: Bool) {
28 | super.viewWillAppear(animated)
29 | self.configureNavigationController()
30 | }
31 |
32 | override func viewWillLayoutSubviews() {
33 | super.viewWillLayoutSubviews()
34 | self.configureButtonConstraint()
35 | }
36 |
37 | func bind(viewModel: EnterTitleViewModel) {
38 | self.viewModel = viewModel
39 | }
40 |
41 | func travelDidChanged(to travel: Travel) {
42 | self.viewModel?.travelDidChanged(to: travel)
43 | }
44 |
45 | private func configureNavigationController() {
46 | self.navigationController?.navigationBar.topItem?.title = ""
47 | guard let isEditingMode = self.viewModel?.isEditingMode else { return }
48 | self.navigationItem.title = isEditingMode ? "여행 편집하기" : "새로운 여행"
49 | }
50 |
51 | private func configureButtonConstraint() {
52 | let bottomPadding = self.view.safeAreaInsets.bottom
53 | self.nextButtonHeightConstraint.constant = 60 + bottomPadding
54 | }
55 |
56 | private func configureCancellable() {
57 | self.viewModel?.validatePublisher
58 | .receive(on: DispatchQueue.main)
59 | .sink { [weak self] isValid in
60 | self?.nextButton.setAvailability(to: isValid ?? false)
61 | }
62 | .store(in: &cancellables)
63 | }
64 |
65 | private func configureLabels() {
66 | self.viewModel?.viewDidLoad { [weak self] title in
67 | if let title = title {
68 | self?.navigationItem.title = "여행 편집하기"
69 | self?.travelTitleField.text = title
70 | } else {
71 | self?.navigationItem.title = "새로운 여행"
72 | }
73 | }
74 | }
75 |
76 | private func configureGestureRecognizer() {
77 | let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapAction(_:)))
78 | tapGesture.cancelsTouchesInView = false
79 | self.view.addGestureRecognizer(tapGesture)
80 | }
81 |
82 | @objc private func tapAction(_ gesture: UITapGestureRecognizer) {
83 | self.view.endEditing(true)
84 | }
85 |
86 | @IBAction func didChangeTitle(_ sender: UITextField) {
87 | self.viewModel?.didChangeTitle(text: sender.text)
88 | }
89 |
90 | @IBAction func didTouchNextButton(_ sender: UIButton) {
91 | self.viewModel?.didTouchNextButton()
92 | }
93 |
94 | }
95 |
96 | extension EnterTitleViewController: UITextFieldDelegate {
97 |
98 | func textFieldShouldReturn(_ textField: UITextField) -> Bool {
99 | self.travelTitleField.resignFirstResponder()
100 | return true
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/EnterTitleScene/ViewModel/EnterTitleViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CreateTravelViewModel.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/02.
6 | //
7 |
8 | import Combine
9 | import Foundation
10 |
11 | protocol EnterTitleViewModel {
12 | var validatePublisher: Published.Publisher { get }
13 | var isEditingMode: Bool { get }
14 | func viewDidLoad(completion: (String?) -> Void)
15 | func travelDidChanged(to travel: Travel)
16 | func didChangeTitle(text: String?)
17 | func didTouchNextButton()
18 | }
19 |
20 | final class DefaultEnterTitleViewModel: EnterTitleViewModel, ObservableObject {
21 |
22 | var validatePublisher: Published.Publisher { $isValidTitle }
23 |
24 | private let usecase: EnterTitleUsecase
25 | private weak var coordinatingDelegate: EnterTitleCoordinatingDelegate?
26 |
27 | @Published private var isValidTitle: Bool?
28 | private var travel: Travel
29 | private var travelTitle: String?
30 | private var travelStartDate: Date?
31 | private var travelEndDate: Date?
32 | private(set) var isEditingMode: Bool
33 |
34 | init(
35 | usecase: EnterTitleUsecase,
36 | coordinatingDelegate: EnterTitleCoordinatingDelegate,
37 | travel: Travel?
38 | ) {
39 | self.usecase = usecase
40 | self.coordinatingDelegate = coordinatingDelegate
41 | self.isEditingMode = travel == nil ? false : true
42 | self.travel = travel ?? Travel.dummy(section: .reserved)
43 | self.didChangeTitle(text: travel?.title)
44 | }
45 |
46 | func viewDidLoad(completion: (String?) -> Void) {
47 | completion(self.travelTitle)
48 | }
49 |
50 | func travelDidChanged(to travel: Travel) {
51 | self.travel = travel
52 | }
53 |
54 | func didChangeTitle(text: String?) {
55 | guard let text = text else { return }
56 | self.usecase.executeTitleValidation(with: text) { [weak self] result in
57 | switch result {
58 | case .success(let title):
59 | self?.travelTitle = title
60 | self?.isValidTitle = true
61 | case .failure:
62 | self?.isValidTitle = false
63 | }
64 | }
65 | }
66 |
67 | func didTouchNextButton() {
68 | self.travel.title = self.travelTitle
69 | self.coordinatingDelegate?.pushToEnterDate(
70 | travel: self.travel, isEditingMode: self.isEditingMode
71 | )
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/HomeScene/Coordinator/HomeCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeCoordinator.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/08.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol HomeCoordinatingDelegate: AnyObject {
11 | func pushToCreateTravel()
12 | func pushToReservedTravel(travel: Travel)
13 | func pushToOngoingTravel(travel: Travel)
14 | func pushToOutdatedTravel(travel: Travel)
15 | }
16 |
17 | final class HomeCoordinator: Coordinator, HomeCoordinatingDelegate {
18 |
19 | var childCoordinators: [Coordinator]
20 | var navigationController: UINavigationController
21 |
22 | init(navigationController: UINavigationController) {
23 | self.childCoordinators = []
24 | self.navigationController = navigationController
25 | }
26 |
27 | func start() {
28 | guard let appDelegate = UIApplication.shared.delegate as? AppDelegate
29 | else { return }
30 | let repository = CoreDataTravelRepository(contextFetcher: appDelegate)
31 | let usecase = DefaultHomeUsecase(repository: repository)
32 | let homeVM = DefaultHomeViewModel(usecase: usecase, coordinatingDelegate: self)
33 | let homeVC = HomeViewController.instantiate(storyboardName: "Home")
34 | homeVC.bind(viewModel: homeVM)
35 | self.navigationController.pushViewController(homeVC, animated: false)
36 | }
37 |
38 | func pushToCreateTravel() {
39 | let createTravelCoordinator = EnterTitleCoordinator(
40 | navigationController: self.navigationController,
41 | travel: nil
42 | )
43 | self.childCoordinators.append(createTravelCoordinator)
44 | createTravelCoordinator.start()
45 | }
46 |
47 | func pushToReservedTravel(travel: Travel) {
48 | let reservedTravelCoordinator = ReservedTravelCoordinator(
49 | navigationController: self.navigationController,
50 | travel: travel
51 | )
52 | self.childCoordinators.append(reservedTravelCoordinator)
53 | reservedTravelCoordinator.start()
54 | }
55 |
56 | func pushToOngoingTravel(travel: Travel) {
57 | let ongoingCoordinator = OngoingTravelCoordinator(
58 | navigationController: self.navigationController, travel: travel
59 | )
60 | self.childCoordinators.append(ongoingCoordinator)
61 | ongoingCoordinator.start()
62 | }
63 |
64 | func pushToOutdatedTravel(travel: Travel) {
65 | let outdatedTravelCoordinator = OutdatedTravelCoordinator(
66 | navigationController: self.navigationController, travel: travel
67 | )
68 | self.childCoordinators.append(outdatedTravelCoordinator)
69 | outdatedTravelCoordinator.start()
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/HomeScene/Interface/Usecase/HomeUsecase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeUsecase.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/30.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol HomeUsecase {
11 | func executeFetch(completion: @escaping (Result<[Travel], Error>) -> Void)
12 | }
13 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/LandmarkCartScene/Coordinator/LandmarkCartCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LandmarkCartCoordinator.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/08.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol LandmarkCartCoordinatingDelegate: AnyObject {
11 | func presentSearchLandmarkModally()
12 | }
13 |
14 | final class LandmarkCartCoordinator: Coordinator, LandmarkCartCoordinatingDelegate {
15 |
16 | var childCoordinators: [Coordinator]
17 | var navigationController: UINavigationController
18 | private var travel: Travel
19 |
20 | init(navigationController: UINavigationController, travel: Travel) {
21 | self.childCoordinators = []
22 | self.navigationController = navigationController
23 | self.travel = travel
24 | }
25 |
26 | func start() {
27 | fatalError("잘못된 접근입니다.")
28 | }
29 |
30 | func pass(from superVC: SuperVC) -> LandmarkCartViewController {
31 | let cartVC = LandmarkCartViewController.instantiate(storyboardName: "LandmarkCart")
32 | let viewModel = DefaultLandmarkCartViewModel(
33 | coordinatingDelegate: self,
34 | travel: travel,
35 | superVC: superVC
36 | )
37 | cartVC.bind(viewModel: viewModel)
38 | return cartVC
39 | }
40 |
41 | func presentSearchLandmarkModally() {
42 | let searchLandmarkCoordinator = SearchLandmarkCoordinator(
43 | navigationController: self.navigationController
44 | )
45 | self.childCoordinators.append(searchLandmarkCoordinator)
46 | searchLandmarkCoordinator.start()
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/LandmarkCartScene/ViewModel/LandmarkCartViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LandmarkCartViewModel.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/02.
6 | //
7 |
8 | import Combine
9 | import Foundation
10 |
11 | enum SuperVC {
12 | case create
13 | case reserved
14 | }
15 |
16 | protocol LandmarkCartViewModel: TravelUpdatable {
17 | var travel: Travel { get }
18 | var travelPublisher: Published.Publisher { get }
19 | var superVC: SuperVC { get }
20 | func didAddLandmark(with landmark: Landmark)
21 | func didDeleteLandmark(at index: Int) -> Landmark
22 | func didTouchPlusButton()
23 | func findLandmark(at index: Int) -> Landmark
24 | }
25 |
26 | final class DefaultLandmarkCartViewModel: LandmarkCartViewModel,
27 | ObservableObject {
28 |
29 | @Published private(set) var travel: Travel
30 | var travelPublisher: Published.Publisher { $travel }
31 | var superVC: SuperVC
32 |
33 | private weak var coordinatingDelegate: LandmarkCartCoordinatingDelegate?
34 |
35 | init(
36 | coordinatingDelegate: LandmarkCartCoordinatingDelegate,
37 | travel: Travel,
38 | superVC: SuperVC
39 | ) {
40 | self.coordinatingDelegate = coordinatingDelegate
41 | self.travel = travel
42 | self.superVC = superVC
43 | }
44 |
45 | func didAddLandmark(with landmark: Landmark) {
46 | self.travel.landmarks.append(landmark)
47 | }
48 |
49 | func didTouchPlusButton() {
50 | self.coordinatingDelegate?.presentSearchLandmarkModally()
51 | }
52 |
53 | func didDeleteLandmark(at index: Int) -> Landmark {
54 | return self.travel.landmarks.remove(at: index)
55 | }
56 |
57 | func findLandmark(at index: Int) -> Landmark {
58 | return self.travel.landmarks[index]
59 | }
60 |
61 | func didUpdateTravel(to travel: Travel) {
62 | self.travel = travel
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/SearchLandmarkScene/Coordinator/SearchLandmarkCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchLandmarkCoordinator.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/08.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol SearchLandmarkCoordinatingDelegate: AnyObject {
11 | func dismissToAddLandmark(landmark: Landmark)
12 | }
13 |
14 |
15 | final class SearchLandmarkCoordinator: Coordinator, SearchLandmarkCoordinatingDelegate {
16 |
17 | var childCoordinators: [Coordinator]
18 | var navigationController: UINavigationController
19 |
20 | init(navigationController: UINavigationController) {
21 | self.navigationController = navigationController
22 | self.childCoordinators = []
23 | }
24 |
25 | func start() {
26 | let repository = LocalJSONLandmarkRepository()
27 | let usecase = DefaultSearchLandmarkUsecase(repository: repository)
28 | let viewModel = DefaultSearchLandmarkViewModel(
29 | usecase: usecase,
30 | coordinatingDelegate: self
31 | )
32 | let searchLandmarkVC = SearchLandmarkViewController.instantiate(
33 | storyboardName: "SearchLandmark"
34 | )
35 | searchLandmarkVC.bind(viewModel: viewModel)
36 | self.navigationController.viewControllers.last?.present(
37 | searchLandmarkVC,
38 | animated: true
39 | )
40 | }
41 |
42 | func dismissToAddLandmark(landmark: Landmark) {
43 | guard let upperVC = navigationController
44 | .viewControllers
45 | .last as? TravelUpdatable & UIViewController,
46 | let cartVC = upperVC.children.first as? LandmarkCartViewController
47 | else { return }
48 |
49 | upperVC.dismiss(animated: true) {
50 | guard let viewModel = cartVC.viewModel else { return }
51 | viewModel.didAddLandmark(with: landmark)
52 | upperVC.didUpdateTravel(to: viewModel.travel)
53 | }
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/SearchLandmarkScene/Interface/Usecase/SearchLandmarkUsecase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchLandmarkUsecase.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/30.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol SearchLandmarkUsecase {
11 | func executeFetch(completion: @escaping (Result<[Landmark], Error>) -> Void)
12 | func executeSearch(by text: String, completion: @escaping (Result<[Landmark], Error>) -> Void)
13 | }
14 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/SearchLandmarkScene/ViewModel/SearchLandmarkViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchLandmarkViewModel.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/03.
6 | //
7 |
8 | import Combine
9 | import Foundation
10 |
11 | protocol SearchLandmarkViewModel {
12 | var landmarksPublisher: Published<[Landmark]>.Publisher { get }
13 | var errorPublisher: Published.Publisher { get }
14 |
15 | func viewDidLoad()
16 | func didChangeSearchText(with text: String)
17 | func didTouchLandmarkCard(at index: Int)
18 | }
19 |
20 | final class DefaultSearchLandmarkViewModel: SearchLandmarkViewModel, ObservableObject {
21 |
22 | var landmarksPublisher: Published<[Landmark]>.Publisher { $landmarks }
23 | var errorPublisher: Published.Publisher { $error }
24 |
25 | private let usecase: SearchLandmarkUsecase
26 | private weak var coordinatingDelegate: SearchLandmarkCoordinatingDelegate?
27 |
28 | @Published private var landmarks: [Landmark]
29 | @Published private var searchText: String
30 | @Published private var error: Error?
31 | private var cancellables: Set
32 |
33 | init(
34 | usecase: SearchLandmarkUsecase,
35 | coordinatingDelegate: SearchLandmarkCoordinatingDelegate
36 | ) {
37 | self.usecase = usecase
38 | self.coordinatingDelegate = coordinatingDelegate
39 | self.landmarks = []
40 | self.searchText = ""
41 | self.cancellables = []
42 | self.bind()
43 | }
44 |
45 | func viewDidLoad() {
46 | self.usecase.executeFetch { [weak self] result in
47 | switch result {
48 | case .success(let landmarks):
49 | self?.landmarks = landmarks
50 | case .failure(let error):
51 | self?.error = error
52 | }
53 | }
54 | }
55 |
56 | func didChangeSearchText(with text: String) {
57 | self.searchText = text
58 | }
59 |
60 | func didTouchLandmarkCard(at index: Int) {
61 | guard landmarks.startIndex..) -> Void)
12 | func executeLandmarkAddition(
13 | of travel: Travel,
14 | completion: @escaping (Result) -> Void
15 | )
16 | func executeLandmarkDeletion(
17 | at landmark: Landmark,
18 | completion: @escaping (Result) -> Void
19 | )
20 | func executeFlagUpdate(
21 | of travel: Travel,
22 | completion: @escaping (Result) -> Void
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/TravelScene/StartedTravel/Interface/Usecase/StartedTravelUsecase.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StartedTravelUsecase.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/30.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol StartedTravelUsecase {
11 | func executeFetch(of travel: Travel, completion: @escaping (Result) -> Void)
12 | func executeFlagUpdate(of travel: Travel, completion: @escaping (Result) -> Void)
13 | func executeDeletion(of travel: Travel, completion: @escaping (Result) -> Void)
14 | func executeLocationUpdate(
15 | of travel: Travel, latitude: Double, longitude: Double,
16 | completion: @escaping (Result) -> Void
17 | )
18 | func executeRecordAddition(
19 | to travel: Travel, with record: Record,
20 | completion: @escaping (Result) -> Void
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/TravelScene/StartedTravel/OngoingTravel/Coordinator/OngoingTravelCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OngoingCoordinator.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/10.
6 | //
7 |
8 | import UIKit
9 |
10 | final class OngoingTravelCoordinator: Coordinator, StartedTravelCoordinatingDelegate {
11 |
12 | var childCoordinators: [Coordinator]
13 | var navigationController: UINavigationController
14 | private var travel: Travel
15 |
16 | init(navigationController: UINavigationController, travel: Travel) {
17 | self.childCoordinators = []
18 | self.navigationController = navigationController
19 | self.travel = travel
20 | }
21 |
22 | func start() {
23 | guard let appDelegate = UIApplication.shared.delegate as? AppDelegate
24 | else { return }
25 | let repository = CoreDataTravelRepository(contextFetcher: appDelegate)
26 | let usecase = DefaultStartedTravelUsecase(repository: repository)
27 | let ongoingVM = DefaultStartedTravelViewModel(
28 | travel: self.travel, usecase: usecase, coordinatingDelegate: self
29 | )
30 | let ongoingVC = OngoingTravelViewController.instantiate(storyboardName: "OngoingTravel")
31 | ongoingVC.bind(viewModel: ongoingVM)
32 | self.navigationController.pushViewController(ongoingVC, animated: true)
33 | }
34 |
35 | func popToHome() {
36 | guard let homeVC = self
37 | .navigationController
38 | .viewControllers
39 | .first as? TravelFetchable
40 | else { return }
41 | homeVC.fetchTravel()
42 | self.navigationController.popToRootViewController(animated: true)
43 | }
44 |
45 | func pushToAddRecord(record: Record?) {
46 | let addRecordCoordinator = AddRecordCoordinator(
47 | navigationController: self.navigationController, record: record, isEditingMode: false
48 | )
49 | self.childCoordinators.append(addRecordCoordinator)
50 | addRecordCoordinator.start()
51 | }
52 |
53 | func pushToEditTravel(travel: Travel) {
54 | let createTravelCoordinator = EnterTitleCoordinator(
55 | navigationController: self.navigationController,
56 | travel: travel
57 | )
58 | self.childCoordinators.append(createTravelCoordinator)
59 | createTravelCoordinator.start()
60 | }
61 |
62 | func pushToDetailRecord(record: Record, travel: Travel) {
63 | let detailRecordCoordinator = DetailRecordCoordinator(
64 | navigationController: self.navigationController, record: record, travel: travel
65 | )
66 | self.childCoordinators.append(detailRecordCoordinator)
67 | detailRecordCoordinator.start()
68 | }
69 |
70 | func moveToOutdated(travel: Travel) {
71 | self.navigationController.popToRootViewController(animated: true)
72 | let outdatedTravelCoordinator = OutdatedTravelCoordinator(
73 | navigationController: self.navigationController,
74 | travel: travel
75 | )
76 | self.childCoordinators.append(outdatedTravelCoordinator)
77 | outdatedTravelCoordinator.start()
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/TravelScene/StartedTravel/OngoingTravel/ViewModel/OngoingTravelViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OngoingViewModel.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/10.
6 | //
7 |
8 | import Combine
9 | import Foundation
10 |
11 | protocol OngoingTravelViewModel {
12 | var travel: Travel { get }
13 | var travelPublisher: Published.Publisher { get }
14 | var errorPublisher: Published.Publisher { get }
15 |
16 | func viewWillAppear()
17 | func didUpdateTravel(to travel: Travel)
18 | func didTouchAddRecordButton()
19 | func didTouchRecordCell(at record: Record)
20 | func didTouchBackButton()
21 | func didTouchEditButton()
22 | func didTouchFinishButton()
23 | func didUpdateCoordinate(latitude: Double, longitude: Double)
24 | func didUpdateRecord(record: Record)
25 | }
26 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/TravelScene/StartedTravel/OutdatedTravel/Coordinator/OutdatedTravelCoordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OutdatedTravelCoordinator.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/10.
6 | //
7 |
8 | import UIKit
9 |
10 | final class OutdatedTravelCoordinator: Coordinator, StartedTravelCoordinatingDelegate {
11 |
12 | var childCoordinators: [Coordinator]
13 | var navigationController: UINavigationController
14 | private var travel: Travel
15 |
16 | init(navigationController: UINavigationController, travel: Travel) {
17 | self.childCoordinators = []
18 | self.navigationController = navigationController
19 | self.travel = travel
20 | }
21 |
22 | func start() {
23 | guard let appDelegate = UIApplication.shared.delegate as? AppDelegate
24 | else { return }
25 | let repository = CoreDataTravelRepository(contextFetcher: appDelegate)
26 | let usecase = DefaultStartedTravelUsecase(repository: repository)
27 | let outdatedVM = DefaultStartedTravelViewModel(
28 | travel: self.travel, usecase: usecase, coordinatingDelegate: self
29 | )
30 | let outdatedVC = OutdatedTravelViewController.instantiate(storyboardName: "OutdatedTravel")
31 | outdatedVC.bind(viewModel: outdatedVM)
32 | self.navigationController.pushViewController(outdatedVC, animated: true)
33 | }
34 |
35 | func popToHome() {
36 | guard let homeVC = self
37 | .navigationController
38 | .viewControllers
39 | .first as? TravelFetchable
40 | else { return }
41 | homeVC.fetchTravel()
42 | self.navigationController.popToRootViewController(animated: true)
43 | }
44 |
45 | func pushToAddRecord(record: Record?) {
46 | let addRecordCoordinator = AddRecordCoordinator(
47 | navigationController: self.navigationController, record: record, isEditingMode: false
48 | )
49 | self.childCoordinators.append(addRecordCoordinator)
50 | addRecordCoordinator.start()
51 | }
52 |
53 | func pushToEditTravel(travel: Travel) {
54 | let createTravelCoordinator = EnterTitleCoordinator(
55 | navigationController: self.navigationController,
56 | travel: travel
57 | )
58 | self.childCoordinators.append(createTravelCoordinator)
59 | createTravelCoordinator.start()
60 | }
61 |
62 | func moveToOutdated(travel: Travel) { return }
63 |
64 | func pushToDetailRecord(record: Record, travel: Travel) {
65 | let detailRecordCoordinator = DetailRecordCoordinator(
66 | navigationController: self.navigationController, record: record, travel: travel
67 | )
68 | self.childCoordinators.append(detailRecordCoordinator)
69 | detailRecordCoordinator.start()
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/TravelScene/StartedTravel/OutdatedTravel/ViewModel/OutdatedTravelViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OutdatedTravelViewModel.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/10.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol OutdatedTravelViewModel {
11 | var travel: Travel { get }
12 | var travelPublisher: Published.Publisher { get }
13 | var errorPublisher: Published.Publisher { get }
14 |
15 | func didUpdateTravel(to travel: Travel)
16 | func didDeleteTravel()
17 | func didTouchAddRecordButton()
18 | func didTouchRecordCell(at record: Record)
19 | func didTouchBackButton()
20 | func didTouchEditButton()
21 | func didUpdateRecord(record: Record)
22 | }
23 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/TravelScene/StartedTravel/StartedTravelCoordinatingDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StartedTravelCoordinatingDelegate.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/30.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol StartedTravelCoordinatingDelegate: AnyObject {
11 | func popToHome()
12 | func pushToAddRecord(record: Record?)
13 | func pushToEditTravel(travel: Travel)
14 | func moveToOutdated(travel: Travel)
15 | func pushToDetailRecord(record: Record, travel: Travel)
16 | }
17 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Utils/Coordinator.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Coordinator.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/08.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol Coordinator {
11 | var childCoordinators: [Coordinator] { get set }
12 | var navigationController: UINavigationController { get set }
13 |
14 | func start()
15 | }
16 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Utils/Extension/UIButton+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIButton+.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/22.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIButton {
11 | func configureTrackingButton() {
12 | self.layer.cornerRadius = 10
13 | let image = UIImage(systemName: "circle.fill")
14 | let normalImage = image?.withTintColor(.systemGray, renderingMode: .alwaysOriginal)
15 | let selectedImage = image?.withTintColor(.red, renderingMode: .alwaysOriginal)
16 | self.setImage(normalImage, for: .normal)
17 | self.setImage(selectedImage, for: .selected)
18 | self.setTitleColor(.systemGray, for: .normal)
19 | self.setTitleColor(.red, for: .selected)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Utils/Extension/UIImageView+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIImageView+.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/10.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIImageView {
11 | func setLocalImage(with url: URL?) {
12 | self.image = UIImage(contentsOfFile: url?.path ?? "")
13 | }
14 |
15 | func setNetworkImage(with url: URL?) -> Cancellable? {
16 | guard let url = url else { return nil }
17 | let request = URLRequest(url: url)
18 |
19 | if let cachedData = ImageFileManager.shared.cachedData(request: request) {
20 | DispatchQueue.main.async {
21 | self.image = UIImage(data: cachedData)
22 | }
23 | return nil
24 | }
25 |
26 | let dataTask = URLSession.shared.dataTask(with: request) { data, response, error in
27 | guard error == nil else { return }
28 | guard let response = response,
29 | let data = data
30 | else { return }
31 | DispatchQueue.main.async {
32 | self.image = UIImage(data: data)
33 | }
34 | ImageFileManager.shared.saveToCache(request: request, response: response, data: data)
35 | }
36 | dataTask.resumeFetch()
37 | return dataTask
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Utils/Extension/UIView+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView+.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/17.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIView {
11 | func makeShadow() {
12 | self.layer.borderWidth = 1
13 | self.layer.borderColor = UIColor.systemGray6.cgColor
14 | self.layer.shadowOpacity = 1
15 | self.layer.shadowColor = UIColor.systemGray5.cgColor
16 | self.layer.shadowOffset = CGSize(width: 3, height: 3)
17 | self.layer.shadowRadius = 3
18 | self.layer.masksToBounds = false
19 | }
20 |
21 | func makePolaroid(with record: Record, at index: Int) {
22 | let photoImageView = UIImageView()
23 | photoImageView.contentMode = .scaleAspectFill
24 | photoImageView.clipsToBounds = true
25 | photoImageView.setLocalImage(
26 | with: ImageFileManager.shared.imageInDocuemtDirectory(
27 | image: record.photoIDs?[index] ?? ""
28 | )
29 | )
30 |
31 | let titleLabel = UILabel()
32 | titleLabel.text = record.title
33 | titleLabel.font = UIFont(name: "NanumMiNiSonGeurSsi", size: 23)
34 | titleLabel.textColor = .black
35 |
36 | photoImageView.translatesAutoresizingMaskIntoConstraints = false
37 | titleLabel.translatesAutoresizingMaskIntoConstraints = false
38 |
39 | self.addSubview(photoImageView)
40 | self.addSubview(titleLabel)
41 |
42 | NSLayoutConstraint.activate([
43 | photoImageView.topAnchor.constraint(
44 | equalTo: self.topAnchor,
45 | constant: 20
46 | ),
47 | photoImageView.leadingAnchor.constraint(
48 | equalTo: self.leadingAnchor,
49 | constant: 20
50 | ),
51 | photoImageView.trailingAnchor.constraint(
52 | equalTo: self.trailingAnchor,
53 | constant: -20
54 | ),
55 | photoImageView.heightAnchor.constraint(
56 | equalTo: photoImageView.widthAnchor,
57 | multiplier: 0.75
58 | ),
59 | titleLabel.topAnchor.constraint(
60 | equalTo: photoImageView.bottomAnchor,
61 | constant: 15
62 | ),
63 | titleLabel.bottomAnchor.constraint(
64 | equalTo: self.bottomAnchor,
65 | constant: -15
66 | ),
67 | titleLabel.leadingAnchor.constraint(
68 | equalTo: self.leadingAnchor,
69 | constant: 24
70 | ),
71 | titleLabel.trailingAnchor.constraint(
72 | equalTo: self.trailingAnchor,
73 | constant: -24
74 | )
75 | ])
76 | self.layoutIfNeeded()
77 | }
78 | }
79 |
80 | extension UIView {
81 | var firstResponder: UIView? {
82 | guard !self.isFirstResponder else { return self }
83 | for subview in subviews {
84 | if let firstResponder = subview.firstResponder { return firstResponder }
85 | }
86 | return nil
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Utils/Extension/UIViewController+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIViewController+.swift
3 | // OwnMyWay
4 | //
5 | // Created by 이청수 on 2021/11/23.
6 | //
7 |
8 | import UIKit
9 |
10 | extension UIViewController {
11 |
12 | func showToast(message: String) {
13 | let toastLabel = self.createToastLabel()
14 | toastLabel.backgroundColor = UIColor.black.withAlphaComponent(0.6)
15 | toastLabel.textColor = UIColor.white
16 | toastLabel.font = UIFont(
17 | name: "Apple SD 산돌고딕 Neo",
18 | size: 15
19 | ) ?? UIFont.systemFont(ofSize: 15)
20 | toastLabel.textAlignment = .center
21 | toastLabel.text = message
22 | toastLabel.numberOfLines = 0
23 | toastLabel.alpha = 1.0
24 | toastLabel.layer.cornerRadius = 10
25 | toastLabel.clipsToBounds = true
26 | self.view.addSubview(toastLabel)
27 | UIView.animate(withDuration: 1.0, delay: 1.0, options: [], animations: {
28 | toastLabel.alpha = 0.0
29 | }, completion: { _ in
30 | toastLabel.removeFromSuperview()
31 | })
32 | }
33 |
34 | private func createToastLabel() -> UILabel {
35 | return UILabel(
36 | frame: CGRect(
37 | x: self.view.frame.size.width / 2 - 75,
38 | y: self.view.frame.size.height - 100,
39 | width: 150,
40 | height: 35
41 | )
42 | )
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Utils/Extension/URLSessionDataTask+.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLSessionDataTask+.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/25.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol Cancellable {
11 | func resumeFetch()
12 | func cancelFetch()
13 | }
14 |
15 | extension URLSessionDataTask: Cancellable {
16 | func resumeFetch() {
17 | self.resume()
18 | }
19 |
20 | func cancelFetch() {
21 | self.cancel()
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Utils/Protocol/Instantiable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Instantiable.swift
3 | // OwnMyWay
4 | //
5 | // Created by 김우재 on 2021/11/03.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol Instantiable {
11 | static func instantiate(storyboardName: String) -> Self
12 | }
13 |
14 | extension Instantiable where Self: UIViewController {
15 | static func instantiate(storyboardName: String) -> Self {
16 | let name = String(describing: self)
17 | let storyboard = UIStoryboard(name: storyboardName, bundle: nil)
18 | guard let vcInstance = storyboard.instantiateViewController(withIdentifier: name) as? Self
19 | else {
20 | return Self()
21 | }
22 | return vcInstance
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Utils/Protocol/LandmarkDeletable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LandmarkDeletable.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/15.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol LandmarkDeletable {
11 | func didDeleteLandmark(at landmark: Landmark)
12 | }
13 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Utils/Protocol/MapAvailable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MapAvailable.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/04.
6 | //
7 |
8 | import MapKit
9 | import Foundation
10 |
11 | protocol MapAvailable {
12 | func configureMapView(with mapView: MKMapView)
13 | func moveRegion(mapView: MKMapView, annotations: [MKAnnotation], animated: Bool)
14 | func drawRecordAnnotations(mapView: MKMapView, annotations: [MKAnnotation])
15 | func drawLandmarkAnnotations(mapView: MKMapView, annotations: [MKAnnotation])
16 | func drawLocationPath(mapView: MKMapView, locations: [Location])
17 | }
18 |
19 | extension MapAvailable {
20 | func configureMapView(with mapView: MKMapView) {
21 | mapView.register(
22 | LandmarkAnnotationView.self,
23 | forAnnotationViewWithReuseIdentifier: LandmarkAnnotationView.identifier
24 | )
25 | mapView.register(
26 | RecordAnnotationView.self,
27 | forAnnotationViewWithReuseIdentifier: RecordAnnotationView.identifier
28 | )
29 | }
30 |
31 | func moveRegion(mapView: MKMapView, annotations: [MKAnnotation], animated: Bool) {
32 | guard !annotations.isEmpty else { return }
33 |
34 | var zoomRect = MKMapRect.null
35 | annotations.forEach { annotation in
36 | let annotationPoint = MKMapPoint(annotation.coordinate)
37 | let pointRect = MKMapRect(
38 | x: annotationPoint.x - 5000,
39 | y: annotationPoint.y - 5000,
40 | width: 10000,
41 | height: 10000
42 | )
43 | zoomRect = zoomRect.union(pointRect)
44 | }
45 |
46 | mapView.setVisibleMapRect(
47 | zoomRect,
48 | edgePadding: UIEdgeInsets(top: 50, left: 50, bottom: 50, right: 50),
49 | animated: true
50 | )
51 | }
52 |
53 | func drawRecordAnnotations(mapView: MKMapView, annotations: [MKAnnotation]) {
54 | let deleteSet = mapView.annotations.filter({ $0 is RecordAnnotation })
55 | mapView.removeAnnotations(deleteSet)
56 | mapView.addAnnotations(annotations)
57 | }
58 |
59 | func drawLandmarkAnnotations(mapView: MKMapView, annotations: [MKAnnotation]) {
60 | let deleteSet = mapView.annotations.filter({ $0 is LandmarkAnnotation })
61 | mapView.removeAnnotations(deleteSet)
62 | mapView.addAnnotations(annotations)
63 | }
64 |
65 | func drawLocationPath(mapView: MKMapView, locations: [Location]) {
66 | let overlays = mapView.overlays
67 | mapView.removeOverlays(overlays)
68 | let coordinates = locations.map {
69 | CLLocationCoordinate2D(
70 | latitude: $0.latitude ?? 0, longitude: $0.longitude ?? 0
71 | )
72 | }
73 | let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count)
74 | mapView.addOverlay(polyline, level: .aboveRoads)
75 | }
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Utils/Protocol/RecordUpdatable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecordUpdatable.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/16.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol RecordUpdatable {
11 | func didUpdateRecord(record: Record)
12 | }
13 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Utils/Protocol/TravelEditable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TravelEditable.swift
3 | // OwnMyWay
4 | //
5 | // Created by 이청수 on 2021/11/18.
6 | //
7 |
8 | import UIKit
9 |
10 | protocol TravelEditable {
11 | func didEditTravel(to travel: Travel)
12 | }
13 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Utils/Protocol/TravelFetchable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TravelFetchable.swift
3 | // OwnMyWay
4 | //
5 | // Created by 유한준 on 2021/11/08.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol TravelFetchable {
11 | func fetchTravel()
12 | }
13 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Presentation/Utils/Protocol/TravelUpdatable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TravelUpdatable.swift
3 | // OwnMyWay
4 | //
5 | // Created by 강현준 on 2021/11/08.
6 | //
7 |
8 | import Foundation
9 |
10 | protocol TravelUpdatable {
11 | func didUpdateTravel(to travel: Travel)
12 | }
13 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/100.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/1024.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/114.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/120.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/144.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/152.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/167.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/167.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/180.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/20.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/29.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/40.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/50.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/57.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/58.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/58.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/60.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/72.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/76.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/80.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/80.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/87.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/87.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"}]}
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Color/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Color/HomeBackground.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.957",
9 | "green" : "0.957",
10 | "red" : "0.957"
11 | }
12 | },
13 | "idiom" : "universal"
14 | },
15 | {
16 | "appearances" : [
17 | {
18 | "appearance" : "luminosity",
19 | "value" : "dark"
20 | }
21 | ],
22 | "color" : {
23 | "color-space" : "srgb",
24 | "components" : {
25 | "alpha" : "1.000",
26 | "blue" : "0.000",
27 | "green" : "0.000",
28 | "red" : "0.000"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Color/HomeContent.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.251",
27 | "green" : "0.251",
28 | "red" : "0.249"
29 | }
30 | },
31 | "idiom" : "universal"
32 | }
33 | ],
34 | "info" : {
35 | "author" : "xcode",
36 | "version" : 1
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Color/IdentityBlue.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.878",
9 | "green" : "0.667",
10 | "red" : "0.180"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Color/PlusCard.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "0.400",
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" : "0.400",
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 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Color/WJGreen.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.345",
9 | "green" : "0.349",
10 | "red" : "0.145"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/AddButton.imageset/AddButton-1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/AddButton.imageset/AddButton-1x.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/AddButton.imageset/AddButton-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/AddButton.imageset/AddButton-2x.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/AddButton.imageset/AddButton-3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/AddButton.imageset/AddButton-3x.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/AddButton.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "AddButton-1x.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "AddButton-2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "AddButton-3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/AppName.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "OwnMyWay.svg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/LandmarkPin.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "LandmarkPin.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/LandmarkPin.imageset/LandmarkPin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/LandmarkPin.imageset/LandmarkPin.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/MainIcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "mainIcon.svg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | },
12 | "properties" : {
13 | "template-rendering-intent" : "template"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/MainIcon.imageset/mainIcon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/RecordPin.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "RecordPin.svg",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/RecordPin.imageset/RecordPin.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/Setting.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "setting.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "filename" : "setting-2x.png",
10 | "idiom" : "universal",
11 | "scale" : "2x"
12 | },
13 | {
14 | "filename" : "setting-3x.png",
15 | "idiom" : "universal",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "author" : "xcode",
21 | "version" : 1
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/Setting.imageset/setting-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/Setting.imageset/setting-2x.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/Setting.imageset/setting-3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/Setting.imageset/setting-3x.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/Setting.imageset/setting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/Setting.imageset/setting.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/airplane.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "airplane.jpeg",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
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 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/airplane.imageset/airplane.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/Assets.xcassets/Image/airplane.imageset/airplane.jpeg
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/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 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/BundleFont/DancingScript-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/BundleFont/DancingScript-Bold.ttf
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/BundleFont/DancingScript-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/BundleFont/DancingScript-Regular.ttf
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/BundleFont/nanum_handwriting.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/BundleFont/nanum_handwriting.ttf
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/BundleImage/addImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/boostcampwm-2021/iOS04-OwnMyWay/d63640c6bfece8786ae4f9e7fb70bfb2cc379593/OwnMyWay/OwnMyWay/Resource/BundleImage/addImage.png
--------------------------------------------------------------------------------
/OwnMyWay/OwnMyWay/Resource/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | ko_KR
7 | UIAppFonts
8 |
9 | DancingScript-Bold.ttf
10 | DancingScript-Regular.ttf
11 |
12 | UIApplicationSceneManifest
13 |
14 | UIApplicationSupportsMultipleScenes
15 |
16 | UISceneConfigurations
17 |
18 | UIWindowSceneSessionRoleApplication
19 |
20 |
21 | UISceneConfigurationName
22 | Default Configuration
23 | UISceneDelegateClassName
24 | $(PRODUCT_MODULE_NAME).SceneDelegate
25 | UISceneStoryboardFile
26 | Home
27 |
28 |
29 |
30 |
31 | UIBackgroundModes
32 |
33 | location
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/OwnMyWay/UsecaseTest/AddRecordUsecaseTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UsecaseTest.swift
3 | // UsecaseTest
4 | //
5 | // Created by 강현준 on 2021/11/30.
6 | //
7 |
8 | import XCTest
9 | import CoreData
10 |
11 | class AddRecordUsecaseTest: XCTestCase {
12 | class MokContextFetcher: ContextAccessable {
13 | func fetchContext() -> NSManagedObjectContext {
14 | return NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
15 | }
16 | }
17 |
18 | var usecase: AddRecordUsecase!
19 |
20 | override func setUp() {
21 | super.setUp()
22 | self.usecase = DefaultAddRecordUsecase(
23 | repository: CoreDataTravelRepository(contextFetcher: MokContextFetcher()),
24 | imageFileManager: ImageFileManager.shared
25 | )
26 | }
27 |
28 | override func tearDown() {
29 | self.usecase = nil
30 | super.tearDown()
31 | }
32 |
33 | func test_타이틀_검증() {
34 | let testTitle1 = "Test입니다."
35 | let testTitle2 = ""
36 | let testTitle3 = "Test입니다.Test입니다.Test입니다.Test입니다."
37 | XCTAssertTrue(self.usecase.executeValidationTitle(with: testTitle1))
38 | XCTAssertFalse(self.usecase.executeValidationTitle(with: testTitle2))
39 | XCTAssertFalse(self.usecase.executeValidationTitle(with: testTitle3))
40 | }
41 |
42 | func test_날짜_검증() {
43 | let date = Date()
44 | let nilDate: Date? = nil
45 | XCTAssertTrue(self.usecase.executeValidationDate(with: date))
46 | XCTAssertFalse(self.usecase.executeValidationDate(with: nilDate))
47 | }
48 |
49 | func test_위치_검증() {
50 | let location1 = Location(latitude: -90, longitude: -180)
51 | let location2 = Location(latitude: 90, longitude: 180)
52 | let location3 = Location(latitude: -91, longitude: -180)
53 | let location4 = Location(latitude: 90, longitude: 181)
54 | XCTAssertTrue(self.usecase.executeValidationCoordinate(with: location1))
55 | XCTAssertTrue(self.usecase.executeValidationCoordinate(with: location2))
56 | XCTAssertFalse(self.usecase.executeValidationCoordinate(with: location3))
57 | XCTAssertFalse(self.usecase.executeValidationCoordinate(with: location4))
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/OwnMyWay/ViewModelTest/AddLandmarkViewModelTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AddLandmarkViewModelTest.swift
3 | // ViewModelTest
4 | //
5 | // Created by 김우재 on 2021/11/30.
6 | //
7 |
8 | import Combine
9 | import XCTest
10 |
11 | class AddLandmarkCoordinatorMock: AddLandmarkCoordinatingDelegate {
12 | func pushToCompleteCreation(travel: Travel) { return }
13 | func pushToCompleteEditing(travel: Travel) { return }
14 | func popToEnterDate(travel: Travel) { return }
15 | }
16 |
17 | class AddLandmarkViewModelTest: XCTestCase {
18 |
19 | var viewModel: AddLandmarkViewModel!
20 | var coordinator: AddLandmarkCoordinatingDelegate!
21 |
22 | override func setUp() {
23 | super.setUp()
24 | self.coordinator = AddLandmarkCoordinatorMock()
25 | self.viewModel = DefaultAddLandmarkViewModel(travel: Travel.dummy(section: .dummy), coordinatingDelegate: self.coordinator, isEditingMode: true)
26 |
27 | }
28 |
29 | override func tearDown() {
30 | self.viewModel = nil
31 | self.coordinator = nil
32 | super.tearDown()
33 | }
34 |
35 | func test_여행_업데이트_성공() {
36 | let testTravel = Travel(
37 | uuid: UUID(), flag: 0, title: "테스트", startDate: Date(), endDate: Date(), landmarks: [], records: [], locations: []
38 | )
39 |
40 | self.viewModel.didUpdateTravel(to: testTravel)
41 |
42 | XCTAssertEqual(self.viewModel.travel, testTravel, "여행이 일치하지 않습니다.")
43 | }
44 |
45 | func test_관광명소_삭제_성공() {
46 | let testLandmark = Landmark(uuid: UUID(), image: URL(string: ""), latitude: 120, longitude: 120, title: "테스트 장소")
47 | let testTravel = Travel(uuid: UUID(), flag: 0, title: "테스트 여행", startDate: Date(), endDate: Date(), landmarks: [testLandmark], records: [], locations: [])
48 |
49 | self.viewModel.didUpdateTravel(to: testTravel)
50 |
51 | self.viewModel.didDeleteLandmark(at: testLandmark)
52 |
53 | XCTAssertEqual(self.viewModel.travel.landmarks, [] , "관광명소가 삭제되지 않았습니다.")
54 | }
55 |
56 | func test_관광명소_삭제_실패() {
57 | let insertLandmark = Landmark(uuid: UUID(), image: URL(string: ""), latitude: 120, longitude: 120, title: "삽입할 장소")
58 | let deleteLandmark = Landmark(uuid: UUID(), image: URL(string: ""), latitude: 30, longitude: 30, title: "제거할 장소")
59 | let testTravel = Travel(uuid: UUID(), flag: 0, title: "테스트 여행", startDate: Date(), endDate: Date(), landmarks: [insertLandmark], records: [], locations: [])
60 |
61 | self.viewModel.didUpdateTravel(to: testTravel)
62 |
63 | self.viewModel.didDeleteLandmark(at: deleteLandmark)
64 |
65 | XCTAssertNotEqual(self.viewModel.travel.landmarks, [], "관광명소가 삭제되면 안되는데 삭제되었습니다.ㅌ")
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/OwnMyWay/ViewModelTest/EnterTitleViewModelTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EnterTitleViewModelTest.swift
3 | // ViewModelTest
4 | //
5 | // Created by 김우재 on 2021/11/30.
6 | //
7 |
8 | import Combine
9 | import XCTest
10 |
11 | class EnterTitleViewModelTest: XCTestCase {
12 |
13 | var viewModel: EnterTitleViewModel!
14 | var coordinator: MockCoordinator!
15 | var cancellables: Set!
16 |
17 | class MockCoordinator: EnterTitleCoordinatingDelegate {
18 | var travel: Travel?
19 |
20 | func pushToEnterDate(travel: Travel, isEditingMode: Bool) {
21 | self.travel = travel
22 | return
23 | }
24 | }
25 |
26 | override func setUp() {
27 | super.setUp()
28 | coordinator = MockCoordinator()
29 | viewModel = DefaultEnterTitleViewModel(
30 | usecase: DefaultEnterTitleUsecase(),
31 | coordinatingDelegate: self.coordinator,
32 | travel: nil
33 | )
34 | cancellables = []
35 | }
36 |
37 | override func tearDown() {
38 | coordinator = nil
39 | viewModel = nil
40 | cancellables = nil
41 | super.tearDown()
42 | }
43 |
44 | func test_텍스트_입력() {
45 | let expectation = XCTestExpectation()
46 |
47 | self.viewModel.validatePublisher
48 | .sink { result in
49 | if result == nil { return }
50 |
51 | if result == true {
52 | expectation.fulfill()
53 | } else {
54 | XCTFail()
55 | }
56 | }
57 | .store(in: &self.cancellables)
58 |
59 | self.viewModel.didChangeTitle(text: "테스트입니다.")
60 | wait(for: [expectation], timeout: 1)
61 | }
62 |
63 | func test_텍스트_입력_실패() {
64 | let expectation = XCTestExpectation()
65 |
66 | self.viewModel.validatePublisher
67 | .sink { result in
68 | if result == nil { return }
69 |
70 | if result == true {
71 | XCTFail()
72 | } else {
73 | expectation.fulfill()
74 | }
75 | }
76 | .store(in: &self.cancellables)
77 |
78 | self.viewModel.didChangeTitle(text: "")
79 | wait(for: [expectation], timeout: 1)
80 | }
81 |
82 | func test_다음_버튼_클릭() {
83 | let expectedTitle = "테스트입니다."
84 | self.viewModel.didChangeTitle(text: expectedTitle)
85 | self.viewModel.didTouchNextButton()
86 | XCTAssertNotNil(self.coordinator.travel)
87 | XCTAssertEqual(self.coordinator.travel?.title, expectedTitle)
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/OwnMyWay/ViewModelTest/HomeViewModelTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeViewModelTest.swift
3 | // ViewModelTest
4 | //
5 | // Created by 김우재 on 2021/11/30.
6 | //
7 |
8 | import XCTest
9 | import Combine
10 |
11 | class HomeViewModelTest: XCTestCase {
12 |
13 | var viewModel: HomeViewModel!
14 | var cancellables: Set!
15 |
16 | override func setUp() {
17 | super.setUp()
18 | viewModel = DefaultHomeViewModel(
19 | usecase: MockUsecase(),
20 | coordinatingDelegate: MockDelegate()
21 | )
22 | cancellables = []
23 | }
24 |
25 | override func tearDown() {
26 | super.tearDown()
27 | viewModel = nil
28 | cancellables = nil
29 | }
30 |
31 | func test_예정된_여행_불러오기() {
32 | let expectation = XCTestExpectation()
33 | let expectedValue = ["test1", "test2"]
34 | var loadedValue: [String] = []
35 | self.viewModel.reservedTravelPublisher.sink { result in
36 | loadedValue = result.compactMap { $0.title }
37 | expectation.fulfill()
38 | }.store(in: &cancellables)
39 | viewModel.viewDidLoad()
40 | wait(for: [expectation], timeout: 1)
41 | XCTAssertEqual(expectedValue, loadedValue)
42 | }
43 |
44 | func test_진행중인_여행_불러오기() {
45 | let expectation = XCTestExpectation()
46 | let expectedValue = ["test3", "test4"]
47 | var loadedValue: [String] = []
48 | self.viewModel.ongoingTravelPublisher.sink { result in
49 | loadedValue = result.compactMap { $0.title }
50 | expectation.fulfill()
51 | }.store(in: &cancellables)
52 | viewModel.viewDidLoad()
53 | wait(for: [expectation], timeout: 1)
54 | XCTAssertEqual(expectedValue, loadedValue)
55 | }
56 |
57 | func test_지난_여행_불러오기() {
58 | let expectation = XCTestExpectation()
59 | let expectedValue = ["test5", "test6"]
60 | var loadedValue: [String] = []
61 | self.viewModel.outdatedTravelPublisher.sink { result in
62 | loadedValue = result.compactMap { $0.title }
63 | expectation.fulfill()
64 | }.store(in: &cancellables)
65 | viewModel.viewDidLoad()
66 | wait(for: [expectation], timeout: 1)
67 | XCTAssertEqual(expectedValue, loadedValue)
68 | }
69 |
70 | }
71 |
72 | class MockUsecase: HomeUsecase {
73 | func executeFetch(completion: @escaping (Result<[Travel], Error>) -> Void) {
74 | completion(.success([
75 | Travel(uuid: UUID(), flag: 0, title: "test1", startDate: Date(), endDate: Date(), landmarks: [], records: [], locations: []),
76 | Travel(uuid: UUID(), flag: 0, title: "test2", startDate: Date(), endDate: Date(), landmarks: [], records: [], locations: []),
77 | Travel(uuid: UUID(), flag: 1, title: "test3", startDate: Date(), endDate: Date(), landmarks: [], records: [], locations: []),
78 | Travel(uuid: UUID(), flag: 1, title: "test4", startDate: Date(), endDate: Date(), landmarks: [], records: [], locations: []),
79 | Travel(uuid: UUID(), flag: 2, title: "test5", startDate: Date(), endDate: Date(), landmarks: [], records: [], locations: []),
80 | Travel(uuid: UUID(), flag: 2, title: "test6", startDate: Date(), endDate: Date(), landmarks: [], records: [], locations: [])
81 | ]))
82 | }
83 | }
84 |
85 | class MockDelegate: HomeCoordinatingDelegate {
86 | func pushToCreateTravel() { }
87 | func pushToReservedTravel(travel: Travel) { }
88 | func pushToOngoingTravel(travel: Travel) { }
89 | func pushToOutdatedTravel(travel: Travel) { }
90 | }
91 |
--------------------------------------------------------------------------------
/OwnMyWay/ViewModelTest/ReservedTravelViewModelTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReservedTravelViewModelTest.swift
3 | // ViewModelTest
4 | //
5 | // Created by 김우재 on 2021/11/30.
6 | //
7 |
8 | import XCTest
9 | import Combine
10 |
11 | class ReservedTravelViewModelTest: XCTestCase {
12 |
13 | var viewModel: ReservedTravelViewModel!
14 | var cancellables: Set!
15 |
16 | override func setUp() {
17 | super.setUp()
18 | cancellables = []
19 | }
20 |
21 | override func tearDown() {
22 | super.tearDown()
23 | viewModel = nil
24 | cancellables = nil
25 | }
26 |
27 | func test_시작불가능한_여행일때() {
28 | viewModel = DefaultReservedTravelViewModel(
29 | usecase: ReservedTravelUsecaseMock(),
30 | travel:
31 | Travel(uuid: UUID(), flag: 0, title: "invalid travel", startDate: Date().addingTimeInterval(86400), endDate: Date().addingTimeInterval(86400), landmarks: [], records: [], locations: []),
32 | coordinatingDelegate: ReservedTravelDelegateMock()
33 | )
34 | XCTAssertEqual(self.viewModel.isPossibleStart, false)
35 | }
36 |
37 | func test_여행_수정되었을때() {
38 | var travel = Travel(uuid: UUID(), flag: 0, title: "before", startDate: Date(), endDate: Date().addingTimeInterval(86400), landmarks: [], records: [], locations: [])
39 | viewModel = DefaultReservedTravelViewModel(
40 | usecase: ReservedTravelUsecaseMock(),
41 | travel: travel,
42 | coordinatingDelegate: ReservedTravelDelegateMock()
43 | )
44 | travel.title = "after"
45 | self.viewModel.didUpdateTravel(to: travel)
46 | XCTAssertEqual(self.viewModel.travel.title, "after")
47 | }
48 |
49 | func test_관광명소_삭제했을때() {
50 | let landmark = Landmark(uuid: UUID(), image: .init(string: "test"), latitude: 0.0, longitude: 0.0, title: "title")
51 | viewModel = DefaultReservedTravelViewModel(
52 | usecase: ReservedTravelUsecaseMock(),
53 | travel:
54 | Travel(uuid: UUID(), flag: 0, title: "invalid travel", startDate: Date(), endDate: Date().addingTimeInterval(86400), landmarks: [landmark], records: [], locations: []),
55 | coordinatingDelegate: ReservedTravelDelegateMock()
56 | )
57 | self.viewModel.didDeleteLandmark(at: landmark)
58 | XCTAssertEqual(self.viewModel.travel.landmarks.count, 0)
59 | }
60 | }
61 |
62 | class ReservedTravelUsecaseMock: ReservedTravelUsecase {
63 | func executeDeletion(of travel: Travel, completion: @escaping (Result) -> Void) {}
64 | func executeLandmarkAddition(of travel: Travel, completion: @escaping (Result) -> Void) {}
65 | func executeLandmarkDeletion(at landmark: Landmark, completion: @escaping (Result) -> Void) {}
66 | func executeFlagUpdate(of travel: Travel, completion: @escaping (Result) -> Void) {}
67 | }
68 |
69 | class ReservedTravelDelegateMock: ReservedTravelCoordinatingDelegate {
70 | func popToHome() {}
71 | func moveToOngoing(travel: Travel) {}
72 | func pushToEditTravel(travel: Travel) {}
73 | }
74 |
--------------------------------------------------------------------------------
/OwnMyWay/ViewModelTest/StartedTravelViewModelTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StartedTravelViewModelTest.swift
3 | // ViewModelTest
4 | //
5 | // Created by 김우재 on 2021/11/30.
6 | //
7 |
8 | import XCTest
9 | import Combine
10 |
11 | class StartedTravelViewModelTest: XCTestCase {
12 |
13 | var viewModel: StartedTravelViewModel!
14 | var cancellables: Set!
15 |
16 | override func setUp() {
17 | super.setUp()
18 | viewModel = DefaultStartedTravelViewModel(
19 | travel: Travel.dummy(section: .dummy),
20 | usecase: StartedTravelUsecaseMock(),
21 | coordinatingDelegate: StartedTravelDelegateMock())
22 | cancellables = []
23 | }
24 |
25 | override func tearDown() {
26 | super.tearDown()
27 | viewModel = nil
28 | cancellables = nil
29 | }
30 |
31 | func test_여행_수정했을때() {
32 | let newTravel = Travel(uuid: UUID(), flag: 1, title: "newTravel", startDate: Date(), endDate: Date(), landmarks: [], records: [], locations: [])
33 | self.viewModel.didUpdateTravel(to: newTravel)
34 | XCTAssertEqual(self.viewModel.travel.title, "newTravel")
35 | }
36 |
37 | func test_좌표_추가했을때() {
38 | let beforeLocationCount = self.viewModel.travel.locations.count
39 | self.viewModel.didUpdateCoordinate(latitude: 0.0, longitude: 0.0)
40 | let afterLocationCount = self.viewModel.travel.locations.count
41 | XCTAssertEqual(beforeLocationCount + 1, afterLocationCount)
42 | }
43 | }
44 |
45 | class StartedTravelUsecaseMock: StartedTravelUsecase {
46 | func executeFetch(of travel: Travel, completion: @escaping (Result) -> Void) {}
47 | func executeLocationUpdate(of travel: Travel, latitude: Double, longitude: Double, completion: @escaping (Result) -> Void) {}
48 | func executeRecordAddition(to travel: Travel, with record: Record, completion: @escaping (Result) -> Void) {}
49 | func executeFlagUpdate(of travel: Travel, completion: @escaping (Result) -> Void) {}
50 | func executeDeletion(of travel: Travel, completion: @escaping (Result) -> Void) {}
51 | }
52 |
53 | class StartedTravelDelegateMock: StartedTravelCoordinatingDelegate {
54 | func popToHome() {}
55 | func pushToEditTravel(travel: Travel) {}
56 | func pushToAddRecord(record: Record?) {}
57 | func moveToOutdated(travel: Travel) {}
58 | func pushToDetailRecord(record: Record, travel: Travel) {}
59 | }
60 |
--------------------------------------------------------------------------------