├── .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 | 2 | 3 | 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 | 5 | 9 | 12 | 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 | --------------------------------------------------------------------------------