├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── issue-template.md ├── auto_assign.yml └── pull_request_template.md ├── .gitignore ├── README.md └── Tars ├── InfoPlist.xcstrings ├── Tars.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved ├── xcshareddata │ └── xcschemes │ │ └── Tars.xcscheme └── xcuserdata │ └── lena.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist └── Tars ├── .swiftlint.yml ├── Global ├── Constants │ ├── AudioConstants.swift │ ├── PlanetConstants.swift │ └── ResourceConstants.swift ├── Extensions │ ├── CALayer+Extension.swift │ ├── CGPoint+Extension.swift │ ├── Color+Extension.swift │ ├── CommonVariables+Extension.swift │ ├── DashedCircle+Extension.swift │ ├── Float+Extension.swift │ ├── Label+Extension.swift │ ├── SCNVector3+Extension.swift │ ├── String+Extension.swift │ ├── UserDefaults+Extension.swift │ └── View+Extension.swift ├── Localization │ ├── Localizable.xcstrings │ └── LocalizableKeys.swift ├── Resource │ ├── 3dPlanets │ │ ├── Jupiter.usdz │ │ ├── Mars.usdz │ │ ├── Mercury.usdz │ │ ├── Moon.usdz │ │ ├── Neptune.usdz │ │ ├── Saturn.usdz │ │ ├── Sun.usdz │ │ ├── Uranus.usdz │ │ └── Venus.usdz │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ ├── 1024.png │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── LaunchScreen │ │ │ ├── Contents.json │ │ │ └── airpods.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── airpods.png │ │ ├── PlanetsImage │ │ │ ├── Contents.json │ │ │ ├── Jupiter.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── jupiter.png │ │ │ ├── Mars.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── mars.png │ │ │ ├── Mercury.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── mercury.png │ │ │ ├── Moon.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── moon.png │ │ │ ├── Neptune.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── neptune.png │ │ │ ├── Saturn.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── saturn.png │ │ │ ├── Sun.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── sun.png │ │ │ ├── Uranus.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── uranus.png │ │ │ └── Venus.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── venus.png │ │ ├── PlanetsMap │ │ │ ├── Contents.json │ │ │ ├── Jupiter_Map.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── jpegPIA07782.width-1600.jpg │ │ │ ├── Mars_Map.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── mars.jpg │ │ │ ├── Mercury_Map.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── mercury.jpg │ │ │ ├── Moon_Map.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── moon-diffuse.jpg │ │ │ ├── Neptune_Map.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── NEP0VTT1-CC-10x5k.jpg │ │ │ ├── Saturn_Map.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── saturn.jpg │ │ │ ├── Sun_Map.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── sun-diffuse.jpg │ │ │ ├── Uranus_Map.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── uranusmap-10x5k-CC.jpg │ │ │ └── Venus_Map.imageset │ │ │ │ ├── 2k_venus_surface.png │ │ │ │ └── Contents.json │ │ ├── SelectPlanetCollectionView │ │ │ ├── BackgroundImage.imageset │ │ │ │ ├── Contents.json │ │ │ │ └── backgroundImage.pdf │ │ │ └── Contents.json │ │ └── jupiter.imageset │ │ │ ├── Contents.json │ │ │ └── jupiter2.png │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── SoundAssets │ │ ├── Detecting_planet.wav │ │ ├── SoundMono │ │ │ ├── Searching_Jupiter.mp3 │ │ │ ├── Searching_Mars.mp3 │ │ │ ├── Searching_Mercury.mp3 │ │ │ ├── Searching_Moon.mp3 │ │ │ ├── Searching_Neptune.mp3 │ │ │ ├── Searching_Saturn.mp3 │ │ │ ├── Searching_Sun.mp3 │ │ │ ├── Searching_Uranus.mp3 │ │ │ └── Searching_Venus.mp3 │ │ └── SoundSpatial │ │ │ ├── Detail_Jupiter.mp3 │ │ │ ├── Detail_Mars.mp3 │ │ │ ├── Detail_Mercury.mp3 │ │ │ ├── Detail_Moon.mp3 │ │ │ ├── Detail_Neptune.mp3 │ │ │ ├── Detail_Saturn.mp3 │ │ │ ├── Detail_Sun.mp3 │ │ │ ├── Detail_Uranus.mp3 │ │ │ └── Detail_Venus.mp3 │ └── UniverseBackground.scn ├── Supports │ ├── AppDelegate.swift │ ├── Info.plist │ └── SceneDelegate.swift └── UIComponent │ └── CustomViews │ ├── CustomArrowView.swift │ ├── CustomCircleView.swift │ └── CustomSquareView.swift ├── Manager ├── HapticManager │ └── HapticManager.swift ├── LocationManager │ ├── LocationManager.swift │ └── LocationManagerDelegate.swift ├── PlanetManager.swift └── SoundManager │ └── SoundManager.swift ├── Model ├── Cardinal.swift ├── Mode.swift └── PlanetInfo.swift ├── Network ├── APIService │ ├── APIManager.swift │ ├── BodiesPositionsResponse.swift │ ├── Endpoint.swift │ └── NetworkService.swift ├── Base │ └── URLConstants.swift └── Model │ ├── .gitkeep │ └── Body.swift ├── SceneKitAudioPlayer.swift ├── View ├── InfoView │ ├── CustomPlanetInfoView.swift │ └── InfoViewController.swift ├── LauchScreen │ ├── View │ │ ├── CustomBackgroundOverlayView.swift │ │ └── CustomOnboardingOverlayView.swift │ └── ViewController │ │ ├── LaunchScreenViewController.swift │ │ └── OnboardingView.swift └── UniverseSearch │ ├── Cells │ ├── SelectPlanetCollectionViewCell.swift │ └── SelectPlanetMainCollectionViewCell.swift │ ├── Collection │ ├── UniverseMainViewController+DataSource.swift │ ├── UniverseMainViewController+FlowLayoutDelegate.swift │ └── UniverseSearch │ │ ├── UniverseSearchViewController+DataSource.swift │ │ └── UniverseSearchViewController+FlowLayoutDelegate.swift │ └── ViewController │ ├── UniverseMainViewController.swift │ └── UniverseSearchViewController.swift └── ViewModel ├── UniverseLocationViewModel.swift └── UniverseModeViewModel.swift /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: 버그 템플릿 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | --- 11 | name: Bug report 12 | about: File a bug report 13 | title: "[BUG]: " 14 | labels: 'bug' 15 | assignees: 'Lia316' 16 | --- 17 | 18 | ## 문제 상황 19 | 20 | ## 영상 21 | 22 | 23 | 24 | ## 시도해본 방법 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue template 3 | about: 이슈 템플릿 4 | title: 'feat' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | ## 💡 Issue 12 | 13 | 14 | ## 📝 todo 15 | - [ ] todo ! 16 | 17 | 18 | 19 | ## 📸 Screenshot 20 | 21 | -------------------------------------------------------------------------------- /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | # Set to true to add reviewers to pull requests 2 | addReviewers: true 3 | 4 | # Set to true to add assignees to pull requests 5 | addAssignees: false 6 | 7 | # A list of reviewers to be added to pull requests (GitHub user name) 8 | reviewers: 9 | - glitterer 10 | - Genesis2010 11 | - lenamin 12 | - DoAY9 13 | - HeejiSohn 14 | - YoonyoungL 15 | # A list of keywords to be skipped the process that add reviewers if pull requests include it 16 | skipKeywords: 17 | - wip 18 | 19 | # A number of reviewers added to the pull request 20 | # Set 0 to add all the reviewers (default: 0) 21 | numberOfReviewers: 0 22 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## 관련 이슈 2 | 🔒 Closes 3 | 4 | 5 | 6 | ## 작업내용 7 | 8 | - [x] 작업 내용 9 | 10 | 11 | |작업 전|작업 후| 12 | |:---:|:---:| 13 | ||| 14 | 15 | 16 | ## 추후 진행할 사항 17 | 18 | 19 | 20 | 21 | ## 리뷰포인트 22 | 23 | 24 | 25 | ## Reference 26 | 27 | 28 | 29 | ## Checklist 30 | - [ ] 빌드를 위해 SceneDelegate 수정한 것 PR로 올리지 않았는지 확인 31 | - [ ] 필요없는 주석, 프린트문 제거했는지 확인 32 | - [ ] 컨벤션 지켰는지 확인 33 | 34 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/swift,xcode,macos,cocoapods 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=swift,xcode,macos,cocoapods 3 | 4 | ### CocoaPods ### 5 | ## CocoaPods GitIgnore Template 6 | 7 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing 8 | # - Also handy if you have a large number of dependant pods 9 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE 10 | Pods/ 11 | 12 | ### macOS ### 13 | # General 14 | .DS_Store 15 | .AppleDouble 16 | .LSOverride 17 | 18 | # Icon must end with two \r 19 | Icon 20 | 21 | 22 | # Thumbnails 23 | ._* 24 | 25 | # Files that might appear in the root of a volume 26 | .DocumentRevisions-V100 27 | .fseventsd 28 | .Spotlight-V100 29 | .TemporaryItems 30 | .Trashes 31 | .VolumeIcon.icns 32 | .com.apple.timemachine.donotpresent 33 | 34 | # Directories potentially created on remote AFP share 35 | .AppleDB 36 | .AppleDesktop 37 | Network Trash Folder 38 | Temporary Items 39 | .apdisk 40 | 41 | ### macOS Patch ### 42 | # iCloud generated files 43 | *.icloud 44 | 45 | ### Swift ### 46 | # Xcode 47 | # 48 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 49 | 50 | ## User settings 51 | xcuserdata/ 52 | 53 | 54 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 55 | *.xcscmblueprint 56 | *.xccheckout 57 | 58 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 59 | build/ 60 | DerivedData/ 61 | *.moved-aside 62 | *.pbxuser 63 | !default.pbxuser 64 | *.mode1v3 65 | !default.mode1v3 66 | *.mode2v3 67 | !default.mode2v3 68 | *.perspectivev3 69 | !default.perspectivev3 70 | 71 | ## Obj-C/Swift specific 72 | *.hmap 73 | 74 | ## App packaging 75 | *.ipa 76 | *.dSYM.zip 77 | *.dSYM 78 | 79 | ## Playgrounds 80 | timeline.xctimeline 81 | playground.xcworkspace 82 | 83 | # Swift Package Manager 84 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 85 | # Packages/ 86 | # Package.pins 87 | # Package.resolved 88 | # *.xcodeproj 89 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 90 | # hence it is not needed unless you have added a package configuration file to your project 91 | # .swiftpm 92 | 93 | .build/ 94 | 95 | # CocoaPods 96 | # We recommend against adding the Pods directory to your .gitignore. However 97 | # you should judge for yourself, the pros and cons are mentioned at: 98 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 99 | # Pods/ 100 | # Add this line if you want to avoid checking in source code from the Xcode workspace 101 | # *.xcworkspace 102 | 103 | # Carthage 104 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 105 | # Carthage/Checkouts 106 | 107 | Carthage/Build/ 108 | 109 | # Accio dependency management 110 | Dependencies/ 111 | .accio/ 112 | 113 | # fastlane 114 | # It is recommended to not store the screenshots in the git repo. 115 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 116 | # For more information about the recommended setup visit: 117 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 118 | 119 | fastlane/report.xml 120 | fastlane/Preview.html 121 | fastlane/screenshots/**/*.png 122 | fastlane/test_output 123 | 124 | # Code Injection 125 | # After new code Injection tools there's a generated folder /iOSInjectionProject 126 | # https://github.com/johnno1962/injectionforxcode 127 | 128 | iOSInjectionProject/ 129 | 130 | ### Xcode ### 131 | 132 | ## Xcode 8 and earlier 133 | 134 | ### Xcode Patch ### 135 | *.xcodeproj/* 136 | !*.xcodeproj/project.pbxproj 137 | !*.xcodeproj/xcshareddata/ 138 | !*.xcodeproj/project.xcworkspace/ 139 | !*.xcworkspace/contents.xcworkspacedata 140 | /*.gcno 141 | **/xcshareddata/WorkspaceSettings.xcsettings 142 | *.xcuserstate 143 | 144 | # End of https://www.toptal.com/developers/gitignore/api/swift,xcode,macos,cocoapods 145 | *.xcuserstate 146 | Tars/Tars/Network/Base/Keys.swift 147 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | # SpaceOver 6 | Apple Developer Academy @ POSTECH 1기 Spotlight 프로젝트 7 | > **“Hear the Universe”**

8 | > 시각장애인 유저들이 스스로 우주를 탐색하고 태양계의 천체들을 찾을 수 있는 천문학적 경험을 제공합니다.
9 | > SpaceOver provides an astronomical experience for BVI(blind and visually impaired) users
to independently navigate through space and locate the planets of the solar system. 10 | 11 |
12 |
13 | 14 | 15 | 16 |     17 |
18 |
19 | 20 | 21 | ### 💡Features 22 | - **천체의 실시간 위치** : 사용자의 위치를 기준으로 천체의 실시간 위치 제공 23 | - **천체 검색** : 찾고싶은 천체를 선택해 음악 및 화살표를 따라 천체 검색 가능 24 | - **천체 여행** : 찾은 천체를 선택하여 천체 여행. 아름다운 3D 천체 애니메이션 및 천체 고유의 음악과 함께 천체 여행 가이드 제공 25 | - **공간음향** : 음악이 들리는 방향으로 천체의 위치를 찾을 수 있도록 안내. 에어팟 등 공간음향을 지원하는 이어폰이라면 사용 가능 26 | - **보이스오버** : 아이폰 설정 > 손쉬운 사용 > VoiceOver 를 활성화하여, 앱 내의 컨텐츠를 음성으로 안내 27 | - **Localization** : 영어 & 한국어 지원 28 |

29 | 30 | ### 📱 Screenshots 31 | | Name | Screenshot | Detail | 32 | |:---:|:---:|---| 33 | |**Launchscreen**||- 앱 실행 시 에어팟 착용을 권장
- 가독성을 위해 bold text 사용
- 보이스오버 : 앱이름과 텍스트 읽음 | 34 | |**Explore**||- AR 권한을 허용하면 주변을 둘러보도록 안내
- 사용자의 위치를 기반하여 실시간 천체의 위치를 API를 통해 받아옴
- 천체가 감지되는 경우 알림음 및 진동으로 알려줌
- 공간음향: 모든 천체의 음악이 하나의 음악을 이뤄 배경음악으로 들림 | 35 | |**Search**||- 찾고싶은 천체를 아래 표에서 선택
- 선택한 천체의 음악이 단독으로 들림
- 천체의 방향에 따라 음악이 들리는 방향이 달라짐
- 해당 천체가 있는 방향을 화살표로 알려줌
- 보이스오버: 이동해야 하는 방향 및 찾게된 경우 이를 알려줌
- 찾은 천체를 탭하면 해당 천체로 여행을 떠남| 36 | |**Travel**||- 상호작용 가능한 3D 이미지 제공 및 천체의 특성을 고려한 천체 여행 가이드 제공
- 해당 천체 음악 감상 가능
- 시각적 요소 없이 천체를 묘사하도록 컨텐츠 제작| 37 | 38 |

39 | 40 | ### :sparkles: Skills & Tech Stack 41 | |구분|항목| 42 | |:---:|---| 43 | |**Environment**|iOS 15.0+, Xcode 14.0| 44 | |**Framework**|UIKit, Codebase, ARKit, SceneKit, CoreLocation, Haptics| 45 | |**Version Control**|Git, GitHub| 46 | |**Design**|Figma, Illustration| 47 | |**Communication**|Notion, Slack, Gather, Miro| 48 | 49 |

50 | 51 | ### 🫂 Developers 52 | 53 | |박준혁|석혜민|오세익|조은우|손희지|김소현|이윤영| 54 | |:-:|:-:|:-:|:-:|:-:|:-:|:-:| 55 | |||||||| 56 | |[Niro](https://github.com/Genesis2010)|[Lena](https://github.com/lenamin)|[Oz](https://github.com/glitterer)|[Ayden](https://github.com/DoAY9)|[Sohni](https://github.com/HeejiSohn)|[Colli](https://github.com/SohyeonKim-dev)|[Jerry](https://github.com/YoonyoungL)| 57 | |

- ARKit 관련 기능
- 행성 배치 구현
- 오디오 재생 기능 구현|

- 오디오 기능 및 음원 제작
- InfoView 구현
- VoiceOver 기능
- 배포 및 유지보수|

- Localization
- VoiceOver 기능
- LaunchScreen 구현|

- UI/UX 디자인 총괄
- 앱 로고 및 스크린샷 제작
- InfoView 구현
- 3D animation 구현|

- 햅틱 기능 구현
- VoiceOver 기능
- 천체 컨텐츠 제작|

- SearchView 구현
- MainView 구현
- tap gesture 기능
- 데이터 관련 구현|

- 실시간 천체 위치 API 구현
- 네트워크 모델링
- 사용자 위치 모델
- 행성 탐색 로직
- VoiceOver 기능| 58 | -------------------------------------------------------------------------------- /Tars/InfoPlist.xcstrings: -------------------------------------------------------------------------------- 1 | { 2 | "sourceLanguage" : "en", 3 | "strings" : { 4 | "CFBundleName" : { 5 | "comment" : "Bundle name", 6 | "extractionState" : "extracted_with_value", 7 | "localizations" : { 8 | "en" : { 9 | "stringUnit" : { 10 | "state" : "new", 11 | "value" : "SpaceOver" 12 | } 13 | }, 14 | "ko" : { 15 | "stringUnit" : { 16 | "state" : "translated", 17 | "value" : "SpaceOver" 18 | } 19 | } 20 | } 21 | }, 22 | "NSCameraUsageDescription" : { 23 | "comment" : "InfoPlist.strings\n Tars\n\n Created by Lena on 2022/11/28.", 24 | "localizations" : { 25 | "en" : { 26 | "stringUnit" : { 27 | "state" : "translated", 28 | "value" : "The app use camera for AR experience" 29 | } 30 | }, 31 | "ko" : { 32 | "stringUnit" : { 33 | "state" : "translated", 34 | "value" : "증강현실 기능을 사용하기 위해, 카메라 기능을 사용합니다." 35 | } 36 | } 37 | } 38 | }, 39 | "NSLocationWhenInUseUsageDescription" : { 40 | "comment" : "Privacy - Location When In Use Usage Description", 41 | "localizations" : { 42 | "en" : { 43 | "stringUnit" : { 44 | "state" : "translated", 45 | "value" : "Turning on location service allows us to show where the planet is based on location" 46 | } 47 | }, 48 | "ko" : { 49 | "stringUnit" : { 50 | "state" : "translated", 51 | "value" : "행성 위치를 실시간으로 표시하기 위해, 위치 서비스를 켜주세요." 52 | } 53 | } 54 | } 55 | } 56 | }, 57 | "version" : "1.0" 58 | } -------------------------------------------------------------------------------- /Tars/Tars.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tars/Tars.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Tars/Tars.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "0cd11a4ea344b95c25e88fb39b351003de5b41040e76927e86f0f8b8a4edc7a3", 3 | "pins" : [ 4 | { 5 | "identity" : "snapkit", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/SnapKit/SnapKit.git", 8 | "state" : { 9 | "revision" : "2842e6e84e82eb9a8dac0100ca90d9444b0307f4", 10 | "version" : "5.7.1" 11 | } 12 | }, 13 | { 14 | "identity" : "then", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/devxoul/Then.git", 17 | "state" : { 18 | "revision" : "d41ef523faef0f911369f79c0b96815d9dbb6d7a", 19 | "version" : "3.0.0" 20 | } 21 | } 22 | ], 23 | "version" : 3 24 | } 25 | -------------------------------------------------------------------------------- /Tars/Tars.xcodeproj/xcshareddata/xcschemes/Tars.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Tars/Tars.xcodeproj/xcuserdata/lena.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SnapKitPlayground (Playground) 1.xcscheme 8 | 9 | isShown 10 | 11 | orderHint 12 | 2 13 | 14 | SnapKitPlayground (Playground) 2.xcscheme 15 | 16 | isShown 17 | 18 | orderHint 19 | 3 20 | 21 | SnapKitPlayground (Playground).xcscheme 22 | 23 | isShown 24 | 25 | orderHint 26 | 1 27 | 28 | Tars.xcscheme_^#shared#^_ 29 | 30 | orderHint 31 | 0 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Tars/Tars/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | # swiftlint 규칙파일 2 | 3 | disabled_rules: # rule identifiers to exclude from running 4 | - variable_name 5 | - nesting 6 | - function_parameter_count 7 | - line_length 8 | - trailing_whitespace 9 | 10 | included: # paths to include during linting. `--path` is ignored if present. 11 | - Project 12 | - ProjectTests 13 | - ProjectUITests 14 | 15 | excluded: # paths to ignore during linting. Takes precedence over `included`. 16 | - Pods 17 | - Project/R.generated.swift 18 | 19 | # configurable rules can be customized from this configuration file 20 | # binary rules can set their severity level 21 | force_cast: warning # implicitly. Give warning only for force casting 22 | 23 | force_try: 24 | severity: warning # explicitly. Give warning only for force try 25 | 26 | # or they can set both explicitly 27 | file_length: 28 | warning: 500 29 | 30 | # naming rules can set warnings/errors for min_length and max_length 31 | # additionally they can set excluded names 32 | type_name: 33 | min_length: 4 # only warning 34 | max_length: # warning and error 35 | warning: 30 36 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Constants/AudioConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AudioConstants.swift 3 | // Tars 4 | // 5 | // Created by Lena on 2024/7/13. 6 | // 7 | 8 | import Foundation 9 | 10 | enum AudioMode: String, CaseIterable { 11 | case search 12 | case detail 13 | case detected 14 | 15 | var prefix: String { 16 | switch self { 17 | case .search: 18 | return "Searching_" 19 | case .detail: 20 | return "Detail_" 21 | case .detected: 22 | return "Detecting_" 23 | } 24 | } 25 | } 26 | 27 | enum AudioVolume: CaseIterable { 28 | case max 29 | case half 30 | case third 31 | case tenth 32 | case mute 33 | 34 | var volume: Float { 35 | switch self { 36 | case .max: 37 | return 1.0 38 | case .half: 39 | return 0.5 40 | case .third: 41 | return 0.3 42 | case .tenth: 43 | return 0.1 44 | case .mute: 45 | return 0.0 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Constants/PlanetConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanetConstants.swift 3 | // Tars 4 | // 5 | // Created by Lena on 2024/7/11. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Planet: String, CaseIterable { 11 | case sun 12 | case moon 13 | case mercury 14 | case venus 15 | case mars 16 | case jupiter 17 | case saturn 18 | case uranus 19 | case neptune 20 | } 21 | 22 | // MARK: - Localization 23 | extension Planet { 24 | /// 문자열을 열거형으로 변환 25 | init?(from string: String) { 26 | self.init(rawValue: string) 27 | } 28 | 29 | /// 시스템 언어에 따라 localized 된 행성이름 30 | var planetName: String { 31 | return self.rawValue.localized() 32 | } 33 | 34 | /// 영어로 localized된 행성이름 35 | var nameEnglish: String { 36 | return self.rawValue.localized(for: .english) 37 | } 38 | 39 | /// 한글로 localized된 행성이름 40 | var nameKorean: String { 41 | return self.rawValue.localized(for: .korean) 42 | } 43 | 44 | /// String 값을 받아 이를 Planet 중 해당 행성의 case 를 찾아, 이를 다시 localized된 행성 이름 45 | static func localizedName(for name: String) -> String { 46 | if let planet = Planet(from: name) { 47 | return planet.planetName 48 | } 49 | return name 50 | } 51 | 52 | /// 모든 행성들의 배열 (.en, .ko, systemLanguage) 53 | static func allPlanetNames(in language: Language? = nil) -> [String] { 54 | return self.allCases.map { planet in 55 | switch language { 56 | case .english: 57 | return planet.nameEnglish 58 | case .korean: 59 | return planet.nameKorean 60 | default: 61 | return planet.planetName 62 | } 63 | } 64 | } 65 | } 66 | 67 | // MARK: - Planet Contents 68 | extension Planet { 69 | /// LocalizableKeys와 매핑하여 해당 key를 가진 프로퍼티 70 | private var chapterKeys: [Chapter] { 71 | return ["One", "Two", "Three"].map { 72 | Chapter(titleKey: "\(self.rawValue)Chapter\($0)Title", 73 | contentKey: "\(self.rawValue)Chapter\($0)Content") 74 | } 75 | } 76 | 77 | /// 매핑 후 실제 value를 가진 프로퍼티 78 | var titlesAndContents: [(String, String)] { 79 | return chapterKeys.map { chapter in 80 | let titleKey = chapter.titleKey 81 | let contentKey = chapter.contentKey 82 | return (titleKey, contentKey) 83 | } 84 | } 85 | } 86 | 87 | struct Chapter { 88 | let titleKey: String 89 | let contentKey: String 90 | } 91 | 92 | struct PlanetConstants { 93 | static let planetsKo = Planet.allPlanetNames(in: .korean) 94 | static let planetsEn = Planet.allPlanetNames(in: .english) 95 | static let planetsSystem = Planet.allPlanetNames() 96 | } 97 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Constants/ResourceConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResourceConstants.swift 3 | // Tars 4 | // 5 | // Created by Lena on 2024/7/13. 6 | // 7 | 8 | import Foundation 9 | 10 | enum ResourceConstants: String, CaseIterable { 11 | case mp3 12 | case wav 13 | case aif 14 | case usdz 15 | case Cube_002 16 | case map = "_Map" 17 | 18 | var name: String { 19 | return "\(rawValue)" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Extensions/CALayer+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CALayer+Extension.swift 3 | // Tars 4 | // 5 | // Created by Lena on 2024/7/29. 6 | // 7 | 8 | import UIKit 9 | 10 | extension CALayer { 11 | public func configureGradientBackground(_ colors: CGColor...) { 12 | let gradient = CAGradientLayer() 13 | let maxWidth = max(self.bounds.size.height, self.bounds.size.width) 14 | let squareFrame = CGRect(origin: self.bounds.origin, size: CGSize(width: maxWidth, height: maxWidth)) 15 | gradient.frame = squareFrame 16 | gradient.colors = colors 17 | self.insertSublayer(gradient, at: 0) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Extensions/CGPoint+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CGPoint+Extension.swift 3 | // Tars 4 | // 5 | // Created by 이윤영 on 2022/11/15. 6 | // 7 | 8 | import Foundation 9 | 10 | extension CGPoint { 11 | func distanceTo(_ to: CGPoint) -> CGFloat { 12 | return sqrt((self.x - to.x) * (self.x - to.x) + (self.y - to.y) * (self.y - to.y)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Extensions/Color+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color_Extension.swift 3 | // Tars 4 | // 5 | // Created by Lena on 2022/10/18. 6 | // 7 | 8 | import UIKit.UIColor 9 | 10 | extension UIColor { 11 | convenience init(red: Int, green: Int, blue: Int) { 12 | assert(red >= 0 && red <= 255, "Invalid red component") 13 | assert(green >= 0 && green <= 255, "Invalid green component") 14 | assert(blue >= 0 && blue <= 255, "Invalid blue component") 15 | 16 | self.init(red: CGFloat(red) / 255.0, green: CGFloat(green) / 255.0, blue: CGFloat(blue) / 255.0, alpha: 1.0) 17 | } 18 | 19 | convenience init(rgb: Int) { 20 | self.init( 21 | red: (rgb >> 16) & 0xFF, 22 | green: (rgb >> 8) & 0xFF, 23 | blue: rgb & 0xFF 24 | ) 25 | } 26 | } 27 | 28 | extension UIColor { 29 | static var customYellow: UIColor { 30 | return UIColor(rgb: 0xFFD426) 31 | } 32 | static var customGradientPurple: UIColor { 33 | return UIColor(rgb: 0x19063A) 34 | } 35 | static var customGradientBlue: UIColor { 36 | return UIColor(rgb: 0x315EB6) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Extensions/CommonVariables+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommonVariables.swift 3 | // Tars 4 | // 5 | // Created by Lena on 2022/10/22. 6 | // 7 | 8 | import UIKit 9 | 10 | let screenWidth = UIScreen.main.bounds.width 11 | let screenHeight = UIScreen.main.bounds.height 12 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Extensions/DashedCircle+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GuideCircleView.swift 3 | // Tars 4 | // 5 | // Created by Lena on 2022/10/22. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIView { 11 | public func addDashedCircle() { 12 | let circleLayer = CAShapeLayer() 13 | circleLayer.path = UIBezierPath(ovalIn: bounds).cgPath 14 | circleLayer.lineWidth = 2.0 15 | circleLayer.strokeColor = UIColor.customYellow.cgColor 16 | circleLayer.fillColor = UIColor.clear.cgColor 17 | circleLayer.lineJoin = .round 18 | circleLayer.lineDashPattern = [15, 3] 19 | layer.addSublayer(circleLayer) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Extensions/Float+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Float+Extension.swift 3 | // Tars 4 | // 5 | // Created by 이윤영 on 2022/10/26. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Float { 11 | var degreeToRadian: Self { self * .pi / 180 } 12 | } 13 | 14 | extension FloatingPoint { 15 | var degreeToRadians: Self { self * .pi / 180 } 16 | var radiansToDegree: Self { self * 180 / .pi } 17 | } 18 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Extensions/Label+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Label+Extension.swift 3 | // Tars 4 | // 5 | // Created by Ayden on 2022/10/25. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UILabel { 11 | func setLineSpacing(spacing: CGFloat) { 12 | guard let text = text else { return } 13 | 14 | let attributeString = NSMutableAttributedString(string: text) 15 | let style = NSMutableParagraphStyle() 16 | style.lineSpacing = spacing 17 | attributeString.addAttribute(.paragraphStyle, 18 | value: style, 19 | range: NSRange(location: 0, length: attributeString.length)) 20 | attributedText = attributeString 21 | } 22 | } 23 | 24 | extension UILabel { 25 | 26 | /// font size의 하드코딩 입력 없이, textStyle에 bold 효과를 주기 위한 메서드 27 | func setBoldFont(forTextStyle textStyle: UIFont.TextStyle) { 28 | if let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: textStyle).withSymbolicTraits(.traitBold) { 29 | self.font = UIFont(descriptor: descriptor, size: 0) 30 | } else { 31 | self.font = UIFont.preferredFont(forTextStyle: textStyle) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Extensions/SCNVector3+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SCNVector3+Extension.swift 3 | // Tars 4 | // 5 | // Created by 이윤영 on 2022/11/15. 6 | // 7 | 8 | import SceneKit 9 | 10 | extension SCNVector3 { 11 | func toCGPoint() -> CGPoint { 12 | CGPoint(x: CGFloat(self.x), y: CGFloat(self.y)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Extensions/String+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extension.swift 3 | // Tars 4 | // 5 | // Created by 이윤영 on 2022/10/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | func toBase64() -> String { 12 | return Data(self.utf8).base64EncodedString() 13 | } 14 | 15 | func extractCoord() -> (String, String) { 16 | if self.range(of: "$") != nil { 17 | let firstS = self.firstIndex(of: "$")! 18 | let lastS = self.lastIndex(of: "$")! 19 | let firstExtraction = self[firstS.. String { 38 | let languageCode: String 39 | 40 | if let language = language { 41 | languageCode = language.rawValue 42 | } else { 43 | let preferredLanguage = Locale.preferredLanguages.first ?? "en" 44 | 45 | languageCode = preferredLanguage.components(separatedBy: "-").first ?? "en" 46 | } 47 | 48 | guard let path = Bundle.main.path(forResource: languageCode, ofType: "lproj"), 49 | let bundle = Bundle(path: path) else { 50 | return NSLocalizedString(self, comment: "") 51 | } 52 | 53 | return NSLocalizedString(self, tableName: nil, bundle: bundle, value: "", comment: "") 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Extensions/UserDefaults+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaults+Extension.swift 3 | // Tars 4 | // 5 | // Created by Seik Oh on 15/11/2022. 6 | // 7 | 8 | import Foundation 9 | 10 | enum TextLiteral { 11 | static var checkedOnboarding: String { return "checkedOnboarding"} 12 | } 13 | 14 | extension UserDefaults { 15 | var checkedOnboarding: Bool? { 16 | get { 17 | let count = UserDefaults.standard.bool(forKey: TextLiteral.checkedOnboarding) 18 | return count 19 | } 20 | set { 21 | UserDefaults.standard.set(newValue, forKey: TextLiteral.checkedOnboarding) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Extensions/View+Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View_Extension.swift 3 | // Tars 4 | // 5 | // Created by Lena on 2022/10/18. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UIView { 11 | func addSubviews(_ views: UIView...) { 12 | views.forEach { 13 | self.addSubview($0) 14 | $0.translatesAutoresizingMaskIntoConstraints = false 15 | } 16 | } 17 | 18 | func anchor(top: NSLayoutYAxisAnchor? = nil, 19 | leading: NSLayoutXAxisAnchor? = nil, 20 | bottom: NSLayoutYAxisAnchor? = nil, 21 | trailing: NSLayoutXAxisAnchor? = nil, 22 | paddingTop: CGFloat = 0, 23 | paddingLeading: CGFloat = 0, 24 | paddingBottom: CGFloat = 0, 25 | paddingTrailing: CGFloat = 0, 26 | width: CGFloat? = nil, 27 | height: CGFloat? = nil) { 28 | 29 | translatesAutoresizingMaskIntoConstraints = false 30 | 31 | if let top = top { 32 | topAnchor.constraint(equalTo: top, constant: paddingTop).isActive = true 33 | } 34 | 35 | if let leading = leading { 36 | leadingAnchor.constraint(equalTo: leading, constant: paddingLeading).isActive = true 37 | } 38 | 39 | if let bottom = bottom { 40 | bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom).isActive = true 41 | } 42 | 43 | if let trailing = trailing { 44 | trailingAnchor.constraint(equalTo: trailing, constant: -paddingTrailing).isActive = true 45 | } 46 | 47 | if let width = width { 48 | widthAnchor.constraint(equalToConstant: width).isActive = true 49 | } 50 | 51 | if let height = height { 52 | heightAnchor.constraint(equalToConstant: height).isActive = true 53 | } 54 | } 55 | 56 | func centerX(inView view: UIView) { 57 | translatesAutoresizingMaskIntoConstraints = false 58 | centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true 59 | } 60 | 61 | func centerY(inView view: UIView, leadingAnchor: NSLayoutXAxisAnchor? = nil, 62 | paddingLeading: CGFloat = 0, constant: CGFloat = 0) { 63 | 64 | translatesAutoresizingMaskIntoConstraints = false 65 | centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: constant).isActive = true 66 | 67 | if let leading = leadingAnchor { 68 | anchor(leading: leading, paddingLeading: paddingLeading) 69 | } 70 | } 71 | 72 | func setDimensions(height: CGFloat, width: CGFloat) { 73 | translatesAutoresizingMaskIntoConstraints = false 74 | heightAnchor.constraint(equalToConstant: height).isActive = true 75 | widthAnchor.constraint(equalToConstant: width).isActive = true 76 | } 77 | 78 | func setHeight(height: CGFloat) { 79 | translatesAutoresizingMaskIntoConstraints = false 80 | heightAnchor.constraint(equalToConstant: height).isActive = true 81 | } 82 | 83 | func setWidth(width: CGFloat) { 84 | translatesAutoresizingMaskIntoConstraints = false 85 | widthAnchor.constraint(equalToConstant: width).isActive = true 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Localization/LocalizableKeys.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocalizableKeys.swift 3 | // Tars 4 | // 5 | // Created by Lena on 12/07/2024. 6 | // 7 | 8 | import Foundation 9 | 10 | enum LocalizableKeys: String { 11 | case sun, moon, mercury, venus, mars, jupiter, saturn, uranus, neptune 12 | case airPodsInstructionstring, onboardingInstructionstring, onboardingInstructionTitle 13 | case collectionViewTitle, collectionViewContent 14 | 15 | case exploreUniverseNavigationTitle, searchingNavigationTitle 16 | case directionUp, directionUpRight, directionRight, directionDownRight, directionDown, directionDownLeft, directionLeft, directionUpLeft 17 | case locationUsageMessage, locationAuthRequest, defaultAction, cancel 18 | 19 | case image 20 | 21 | case chapterOneHint, chapterTwoHint, chapterThreeHint 22 | 23 | case sunChapterOneTitle, sunChapterOneContent, sunChapterTwoTitle, sunChapterTwoContent, sunChapterThreeTitle, sunChapterThreeContent 24 | case moonChapterOneTitle, moonChapterOneContent, moonChapterTwoTitle, moonChapterTwoContent, moonChapterThreeTitle, moonChapterThreeContent 25 | case mercuryChapterOneTitle, mercuryChapterOneContent, mercuryChapterTwoTitle, mercuryChapterTwoContent, mercuryChapterThreeTitle, mercuryChapterThreeContent 26 | case venusChapterOneTitle, venusChapterOneContent, venusChapterTwoTitle, venusChapterTwoContent, venusChapterThreeTitle, venusChapterThreeContent 27 | case marsChapterOneTitle, marsChapterOneContent, marsChapterTwoTitle, marsChapterTwoContent, marsChapterThreeTitle, marsChapterThreeContent 28 | case jupiterChapterOneTitle, jupiterChapterOneContent, jupiterChapterTwoTitle, jupiterChapterTwoContent, jupiterChapterThreeTitle, jupiterChapterThreeContent 29 | case saturnChapterOneTitle, saturnChapterOneContent, saturnChapterTwoTitle, saturnChapterTwoContent, saturnChapterThreeTitle, saturnChapterThreeContent 30 | case uranusChapterOneTitle, uranusChapterOneContent, uranusChapterTwoTitle, uranusChapterTwoContent, uranusChapterThreeTitle, uranusChapterThreeContent 31 | case neptuneChapterOneTitle, neptuneChapterOneContent, neptuneChapterTwoTitle, neptuneChapterTwoContent, neptuneChapterThreeTitle, neptuneChapterThreeContent 32 | 33 | /// 문자 형태의 string을 LocalizableKeys의 열거형으로 변환 34 | init?(from string: String) { 35 | self.init(rawValue: string) 36 | } 37 | 38 | /// LocalizableKeys의 값을 시스템 언어로 localize 39 | var localized: String { 40 | return self.rawValue.localized() 41 | } 42 | 43 | /// 원하는 언어로 localize 44 | /// - Parameter language: .english / .korean 중 선택 45 | func localized(for language: Language) -> String { 46 | return self.rawValue.localized(for: language) 47 | } 48 | } 49 | 50 | enum Language: String { 51 | case korean = "ko" 52 | case english = "en" 53 | 54 | var locale: Locale { 55 | return Locale(identifier: self.rawValue) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/3dPlanets/Jupiter.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/3dPlanets/Jupiter.usdz -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/3dPlanets/Mars.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/3dPlanets/Mars.usdz -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/3dPlanets/Mercury.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/3dPlanets/Mercury.usdz -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/3dPlanets/Moon.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/3dPlanets/Moon.usdz -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/3dPlanets/Neptune.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/3dPlanets/Neptune.usdz -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/3dPlanets/Saturn.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/3dPlanets/Saturn.usdz -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/3dPlanets/Sun.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/3dPlanets/Sun.usdz -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/3dPlanets/Uranus.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/3dPlanets/Uranus.usdz -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/3dPlanets/Venus.usdz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/3dPlanets/Venus.usdz -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/LaunchScreen/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/LaunchScreen/airpods.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "airpods.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/LaunchScreen/airpods.imageset/airpods.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/LaunchScreen/airpods.imageset/airpods.png -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Jupiter.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "jupiter.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Jupiter.imageset/jupiter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Jupiter.imageset/jupiter.png -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Mars.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "mars.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Mars.imageset/mars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Mars.imageset/mars.png -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Mercury.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "mercury.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Mercury.imageset/mercury.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Mercury.imageset/mercury.png -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Moon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "moon.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Moon.imageset/moon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Moon.imageset/moon.png -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Neptune.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "neptune.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Neptune.imageset/neptune.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Neptune.imageset/neptune.png -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Saturn.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "saturn.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Saturn.imageset/saturn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Saturn.imageset/saturn.png -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Sun.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "sun.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Sun.imageset/sun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Sun.imageset/sun.png -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Uranus.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "uranus.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Uranus.imageset/uranus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Uranus.imageset/uranus.png -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Venus.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "venus.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Venus.imageset/venus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/PlanetsImage/Venus.imageset/venus.png -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Jupiter_Map.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "jpegPIA07782.width-1600.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Jupiter_Map.imageset/jpegPIA07782.width-1600.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Jupiter_Map.imageset/jpegPIA07782.width-1600.jpg -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Mars_Map.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "mars.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Mars_Map.imageset/mars.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Mars_Map.imageset/mars.jpg -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Mercury_Map.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "mercury.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Mercury_Map.imageset/mercury.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Mercury_Map.imageset/mercury.jpg -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Moon_Map.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "moon-diffuse.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Moon_Map.imageset/moon-diffuse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Moon_Map.imageset/moon-diffuse.jpg -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Neptune_Map.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "NEP0VTT1-CC-10x5k.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Neptune_Map.imageset/NEP0VTT1-CC-10x5k.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Neptune_Map.imageset/NEP0VTT1-CC-10x5k.jpg -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Saturn_Map.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "saturn.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Saturn_Map.imageset/saturn.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Saturn_Map.imageset/saturn.jpg -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Sun_Map.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "sun-diffuse.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Sun_Map.imageset/sun-diffuse.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Sun_Map.imageset/sun-diffuse.jpg -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Uranus_Map.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "uranusmap-10x5k-CC.jpg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Uranus_Map.imageset/uranusmap-10x5k-CC.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Uranus_Map.imageset/uranusmap-10x5k-CC.jpg -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Venus_Map.imageset/2k_venus_surface.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Venus_Map.imageset/2k_venus_surface.png -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/PlanetsMap/Venus_Map.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "2k_venus_surface.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/SelectPlanetCollectionView/BackgroundImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "backgroundImage.pdf", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/SelectPlanetCollectionView/BackgroundImage.imageset/backgroundImage.pdf: -------------------------------------------------------------------------------- 1 | %PDF-1.7 2 | 3 | 1 0 obj 4 | << >> 5 | endobj 6 | 7 | 2 0 obj 8 | << /Length 3 0 R >> 9 | stream 10 | /DeviceRGB CS 11 | /DeviceRGB cs 12 | q 13 | 1.000000 0.000000 -0.000000 1.000000 0.000000 36.000000 cm 14 | 1.000000 0.831373 0.149020 scn 15 | 109.000000 54.500000 m 16 | 109.000000 24.400482 84.599518 0.000000 54.500000 0.000000 c 17 | 24.400480 0.000000 0.000000 24.400482 0.000000 54.500000 c 18 | 0.000000 84.599518 24.400480 109.000000 54.500000 109.000000 c 19 | 84.599518 109.000000 109.000000 84.599518 109.000000 54.500000 c 20 | h 21 | f 22 | n 23 | Q 24 | q 25 | 1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm 26 | 1.000000 0.831373 0.149020 scn 27 | 0.000000 88.000000 m 28 | 0.000000 94.627419 5.372583 100.000000 12.000000 100.000000 c 29 | 97.000000 100.000000 l 30 | 103.627419 100.000000 109.000000 94.627419 109.000000 88.000000 c 31 | 109.000000 12.000000 l 32 | 109.000000 5.372581 103.627419 0.000000 97.000000 0.000000 c 33 | 12.000000 0.000000 l 34 | 5.372583 0.000000 0.000000 5.372581 0.000000 12.000000 c 35 | 0.000000 88.000000 l 36 | h 37 | f 38 | n 39 | Q 40 | 41 | endstream 42 | endobj 43 | 44 | 3 0 obj 45 | 853 46 | endobj 47 | 48 | 4 0 obj 49 | << /Annots [] 50 | /Type /Page 51 | /MediaBox [ 0.000000 0.000000 109.000000 145.000000 ] 52 | /Resources 1 0 R 53 | /Contents 2 0 R 54 | /Parent 5 0 R 55 | >> 56 | endobj 57 | 58 | 5 0 obj 59 | << /Kids [ 4 0 R ] 60 | /Count 1 61 | /Type /Pages 62 | >> 63 | endobj 64 | 65 | 6 0 obj 66 | << /Pages 5 0 R 67 | /Type /Catalog 68 | >> 69 | endobj 70 | 71 | xref 72 | 0 7 73 | 0000000000 65535 f 74 | 0000000010 00000 n 75 | 0000000034 00000 n 76 | 0000000943 00000 n 77 | 0000000965 00000 n 78 | 0000001140 00000 n 79 | 0000001214 00000 n 80 | trailer 81 | << /ID [ (some) (id) ] 82 | /Root 6 0 R 83 | /Size 7 84 | >> 85 | startxref 86 | 1273 87 | %%EOF -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/SelectPlanetCollectionView/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/jupiter.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "jupiter2.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/Assets.xcassets/jupiter.imageset/jupiter2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/Assets.xcassets/jupiter.imageset/jupiter2.png -------------------------------------------------------------------------------- /Tars/Tars/Global/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 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/SoundAssets/Detecting_planet.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/SoundAssets/Detecting_planet.wav -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/SoundAssets/SoundMono/Searching_Jupiter.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/SoundAssets/SoundMono/Searching_Jupiter.mp3 -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/SoundAssets/SoundMono/Searching_Mars.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/SoundAssets/SoundMono/Searching_Mars.mp3 -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/SoundAssets/SoundMono/Searching_Mercury.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/SoundAssets/SoundMono/Searching_Mercury.mp3 -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/SoundAssets/SoundMono/Searching_Moon.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/SoundAssets/SoundMono/Searching_Moon.mp3 -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/SoundAssets/SoundMono/Searching_Neptune.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/SoundAssets/SoundMono/Searching_Neptune.mp3 -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/SoundAssets/SoundMono/Searching_Saturn.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/SoundAssets/SoundMono/Searching_Saturn.mp3 -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/SoundAssets/SoundMono/Searching_Sun.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/SoundAssets/SoundMono/Searching_Sun.mp3 -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/SoundAssets/SoundMono/Searching_Uranus.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/SoundAssets/SoundMono/Searching_Uranus.mp3 -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/SoundAssets/SoundMono/Searching_Venus.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/SoundAssets/SoundMono/Searching_Venus.mp3 -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/SoundAssets/SoundSpatial/Detail_Jupiter.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/SoundAssets/SoundSpatial/Detail_Jupiter.mp3 -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/SoundAssets/SoundSpatial/Detail_Mars.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/SoundAssets/SoundSpatial/Detail_Mars.mp3 -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/SoundAssets/SoundSpatial/Detail_Mercury.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/SoundAssets/SoundSpatial/Detail_Mercury.mp3 -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/SoundAssets/SoundSpatial/Detail_Moon.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/SoundAssets/SoundSpatial/Detail_Moon.mp3 -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/SoundAssets/SoundSpatial/Detail_Neptune.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/SoundAssets/SoundSpatial/Detail_Neptune.mp3 -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/SoundAssets/SoundSpatial/Detail_Saturn.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/SoundAssets/SoundSpatial/Detail_Saturn.mp3 -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/SoundAssets/SoundSpatial/Detail_Sun.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/SoundAssets/SoundSpatial/Detail_Sun.mp3 -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/SoundAssets/SoundSpatial/Detail_Uranus.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/SoundAssets/SoundSpatial/Detail_Uranus.mp3 -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/SoundAssets/SoundSpatial/Detail_Venus.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/SoundAssets/SoundSpatial/Detail_Venus.mp3 -------------------------------------------------------------------------------- /Tars/Tars/Global/Resource/UniverseBackground.scn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeveloperAcademy-POSTECH/MacC-Team-TARS/02636737514b4e6c9137ba8fe3b8e95cae7b631d/Tars/Tars/Global/Resource/UniverseBackground.scn -------------------------------------------------------------------------------- /Tars/Tars/Global/Supports/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Tars 4 | // 5 | // Created by Lena on 2022/10/18. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 14 | // Override point for customization after application launch. 15 | return true 16 | } 17 | 18 | // MARK: UISceneSession Lifecycle 19 | 20 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 21 | // Called when a new scene session is being created. 22 | // Use this method to select a configuration to create the new scene with. 23 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 24 | } 25 | 26 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 27 | // Called when the user discards a scene session. 28 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 29 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Supports/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Tars/Tars/Global/Supports/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Tars 4 | // 5 | // Created by Lena on 2022/10/18. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 15 | guard let windowScene = (scene as? UIWindowScene) else { return } 16 | window = UIWindow(frame: windowScene.coordinateSpace.bounds) 17 | window?.windowScene = windowScene 18 | window?.makeKeyAndVisible() 19 | window?.rootViewController = UINavigationController(rootViewController: LaunchScreenViewController()) 20 | } 21 | 22 | func sceneDidDisconnect(_ scene: UIScene) { 23 | // Called as the scene is being released by the system. 24 | // This occurs shortly after the scene enters the background, or when its session is discarded. 25 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 26 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 27 | } 28 | 29 | func sceneDidBecomeActive(_ scene: UIScene) { 30 | // Called when the scene has moved from an inactive state to an active state. 31 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 32 | } 33 | 34 | func sceneWillResignActive(_ scene: UIScene) { 35 | // Called when the scene will move from an active state to an inactive state. 36 | // This may occur due to temporary interruptions (ex. an incoming phone call). 37 | } 38 | 39 | func sceneWillEnterForeground(_ scene: UIScene) { 40 | // Called as the scene transitions from the background to the foreground. 41 | // Use this method to undo the changes made on entering the background. 42 | } 43 | 44 | func sceneDidEnterBackground(_ scene: UIScene) { 45 | // Called as the scene transitions from the foreground to the background. 46 | // Use this method to save data, release shared resources, and store enough scene-specific state information 47 | // to restore the scene back to its current state. 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tars/Tars/Global/UIComponent/CustomViews/CustomArrowView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomArrowView.swift 3 | // Tars 4 | // 5 | // Created by 이윤영 on 2022/11/15. 6 | // 7 | 8 | import UIKit 9 | 10 | class CustomArrowView: UIView { 11 | override init(frame: CGRect) { 12 | super.init(frame: .zero) 13 | self.backgroundColor = .clear 14 | } 15 | 16 | required init?(coder: NSCoder) { 17 | super.init(coder: coder) 18 | } 19 | override func draw(_ rect: CGRect) { 20 | let triangle = UIBezierPath() 21 | triangle.lineJoinStyle = .round 22 | triangle.lineWidth = 5.0 23 | triangle.move(to: CGPoint(x: 12, y: 12)) 24 | triangle.addLine(to: CGPoint(x: bounds.width - 12, y: bounds.height / 2)) 25 | triangle.addLine(to: CGPoint(x: 12, y: bounds.height-12)) 26 | 27 | UIColor.customYellow.set() 28 | triangle.close() 29 | triangle.stroke() 30 | triangle.fill() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tars/Tars/Global/UIComponent/CustomViews/CustomCircleView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GuideCircleView.swift 3 | // Tars 4 | // 5 | // Created by Lena on 2022/10/22. 6 | // 7 | 8 | import UIKit 9 | 10 | class CustomCircleView: UIView { 11 | 12 | lazy var guideCircleView: UIView = { 13 | let circleView = UIView() 14 | circleView.backgroundColor = .clear 15 | circleView.frame = CGRect(x: 0, y: 0, width: screenWidth / 1.5, height: screenWidth / 1.5) 16 | circleView.layer.cornerRadius = self.frame.width / 2 17 | circleView.clipsToBounds = true 18 | circleView.layer.masksToBounds = true 19 | circleView.addDashedCircle() 20 | circleView.translatesAutoresizingMaskIntoConstraints = false 21 | return circleView 22 | 23 | }() 24 | 25 | override init(frame: CGRect) { 26 | super.init(frame: .zero) 27 | configureCircle() 28 | } 29 | 30 | required init?(coder: NSCoder) { 31 | super.init(coder: coder) 32 | } 33 | 34 | private func configureCircle() { 35 | 36 | addSubview(guideCircleView) 37 | 38 | NSLayoutConstraint.activate([ 39 | guideCircleView.leadingAnchor.constraint(equalTo: self.leadingAnchor), 40 | guideCircleView.trailingAnchor.constraint(equalTo: self.trailingAnchor), 41 | guideCircleView.topAnchor.constraint(equalTo: self.topAnchor), 42 | guideCircleView.bottomAnchor.constraint(equalTo: self.bottomAnchor), 43 | guideCircleView.widthAnchor.constraint(equalToConstant: screenWidth / 1.5), 44 | guideCircleView.heightAnchor.constraint(equalToConstant: screenWidth / 1.5) 45 | ]) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tars/Tars/Global/UIComponent/CustomViews/CustomSquareView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomSquareView.swift 3 | // Tars 4 | // 5 | // Created by Lena on 2022/10/24. 6 | // 7 | 8 | import UIKit 9 | 10 | class CustomSquareView: UIView { 11 | 12 | lazy var squareView: UIView = { 13 | let squareView = UIView(frame: CGRect(x: 0, y: 0, width: screenWidth / 5.65, height: screenWidth / 5.65)) 14 | squareView.backgroundColor = .clear 15 | squareView.layer.borderWidth = 2 16 | squareView.layer.borderColor = UIColor.customYellow.cgColor 17 | squareView.translatesAutoresizingMaskIntoConstraints = false 18 | return squareView 19 | }() 20 | 21 | lazy var planetLabel: UILabel = { 22 | let planetLabel = UILabel() 23 | planetLabel.frame = CGRect(x: 0, y: 0, width: screenWidth / 5.65, height: screenHeight / 26.375) 24 | planetLabel.textAlignment = .center 25 | planetLabel.font = .systemFont(ofSize: 20) 26 | planetLabel.adjustsFontSizeToFitWidth = true 27 | planetLabel.textColor = .black 28 | planetLabel.backgroundColor = .customYellow 29 | planetLabel.translatesAutoresizingMaskIntoConstraints = false 30 | return planetLabel 31 | }() 32 | 33 | override init(frame: CGRect) { 34 | super.init(frame: .zero) 35 | configureSquare() 36 | } 37 | 38 | required init?(coder: NSCoder) { 39 | super.init(coder: coder) 40 | } 41 | 42 | private func configureSquare() { 43 | 44 | [squareView, planetLabel].forEach { addSubview($0) } 45 | 46 | squareView.anchor(top: self.topAnchor, 47 | leading: self.leadingAnchor, 48 | bottom: planetLabel.topAnchor, 49 | trailing: self.trailingAnchor, 50 | width: screenWidth / 5.65, 51 | height: screenWidth / 5.65) 52 | 53 | planetLabel.anchor(top: squareView.bottomAnchor, 54 | leading: self.leadingAnchor, 55 | bottom: self.bottomAnchor, 56 | trailing: self.trailingAnchor, 57 | width: screenWidth / 5.65, 58 | height: screenHeight / 26.375) 59 | } 60 | 61 | func setLabel(_ name: String) { 62 | self.planetLabel.text = name 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tars/Tars/Manager/HapticManager/HapticManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HapticManager.swift 3 | // Tars 4 | // 5 | // Created by Heeji Sohn on 2022/11/23. 6 | // 7 | 8 | import UIKit 9 | 10 | class HapticManager { 11 | static let instance = HapticManager() 12 | 13 | /// type에 따른 haptic feedback 14 | func hapticNotification(type: UINotificationFeedbackGenerator.FeedbackType) { 15 | let generator = UINotificationFeedbackGenerator() 16 | generator.notificationOccurred(type) 17 | } 18 | 19 | /// style에 따른 haptic feedback 20 | func hapticImpact(style: UIImpactFeedbackGenerator.FeedbackStyle) { 21 | let generator = UIImpactFeedbackGenerator(style: style) 22 | generator.impactOccurred() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Tars/Tars/Manager/LocationManager/LocationManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationManager.swift 3 | // Tars 4 | // 5 | // Created by 이윤영 on 2022/10/24. 6 | // 7 | 8 | /* 9 | import Foundation 10 | import CoreLocation 11 | 12 | 13 | enum LocationError: Error { 14 | case currentLocationFailure 15 | } 16 | 17 | class LocationManager: NSObject, CLLocationManagerDelegate { 18 | static let shared = LocationManager() 19 | weak var delegate: LocationManagerDelegate? 20 | 21 | private let locationManager = CLLocationManager() 22 | private var location: CLLocation? 23 | private var isLocationUpdated: Bool = false 24 | 25 | override init() { 26 | super.init() 27 | locationManager.delegate = self 28 | } 29 | 30 | // MARK: - CLLocationManagerDelegate 31 | 32 | func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 33 | if !isLocationUpdated { 34 | isLocationUpdated = true 35 | 36 | manager.stopUpdatingLocation() 37 | let location = locations[locations.count - 1] 38 | self.location = location 39 | 40 | didUpdateUserLocation() 41 | } 42 | } 43 | 44 | func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { 45 | print("\(error): \(error.localizedDescription)") 46 | } 47 | 48 | func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { 49 | switch manager.authorizationStatus { 50 | case .authorizedWhenInUse: 51 | manager.startUpdatingLocation() 52 | case .restricted, .denied: 53 | openSetting() 54 | case .notDetermined: 55 | manager.requestWhenInUseAuthorization() 56 | default: 57 | break 58 | } 59 | } 60 | 61 | // MARK: - LocationManager method 62 | 63 | func updateLocation() { 64 | if locationManager.authorizationStatus == .authorizedWhenInUse { 65 | locationManager.startUpdatingLocation() 66 | } else { 67 | locationManager.requestWhenInUseAuthorization() 68 | } 69 | } 70 | 71 | func getCurrentLocation() -> (Double, Double, Double)? { 72 | guard let location = location else { return nil } 73 | let latitude = location.coordinate.latitude 74 | let longtitude = location.coordinate.longitude 75 | let altitude = location.altitude 76 | return (latitude, longtitude, altitude) 77 | } 78 | 79 | private func didUpdateUserLocation() { 80 | guard let delegate = delegate else { return } 81 | delegate.didUpdateUserLocation() 82 | } 83 | 84 | private func openSetting() { 85 | guard let delegate = delegate else { return } 86 | delegate.openSetting() 87 | 88 | } 89 | } 90 | */ 91 | 92 | 93 | /* 94 | import Foundation 95 | import CoreLocation 96 | import Combine 97 | 98 | enum LocationError: Error { 99 | case currentLocationFailure 100 | } 101 | 102 | class LocationManagerPublisher: NSObject, CLLocationManagerDelegate { 103 | static let shared = LocationManagerPublisher() 104 | weak var delegate: LocationManagerDelegate? 105 | 106 | private let locationManager = CLLocationManager() 107 | private var location: CLLocation? 108 | private var isLocationUpdated: Bool = false 109 | 110 | // Publihsers 111 | var locationPublisher = PassthroughSubject 112 | var authorizationStatusPublisher = PassthroughSubject 113 | 114 | override init() { 115 | super.init() 116 | locationManager.delegate = self 117 | } 118 | 119 | // MARK: - CLLocationManagerDelegate 120 | 121 | func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 122 | if !isLocationUpdated { 123 | isLocationUpdated = true 124 | 125 | manager.stopUpdatingLocation() 126 | let location = locations[locations.count - 1] 127 | self.location = location 128 | 129 | didUpdateUserLocation() 130 | } 131 | } 132 | 133 | func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { 134 | print("\(error): \(error.localizedDescription)") 135 | } 136 | 137 | func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { 138 | switch manager.authorizationStatus { 139 | case .authorizedWhenInUse: 140 | manager.startUpdatingLocation() 141 | case .restricted, .denied: 142 | openSetting() 143 | case .notDetermined: 144 | manager.requestWhenInUseAuthorization() 145 | default: 146 | break 147 | } 148 | } 149 | 150 | // MARK: - LocationManager method 151 | 152 | func updateLocation() { 153 | if locationManager.authorizationStatus == .authorizedWhenInUse { 154 | locationManager.startUpdatingLocation() 155 | } else { 156 | locationManager.requestWhenInUseAuthorization() 157 | } 158 | } 159 | 160 | func getCurrentLocation() -> (Double, Double, Double)? { 161 | guard let location = location else { return nil } 162 | let latitude = location.coordinate.latitude 163 | let longtitude = location.coordinate.longitude 164 | let altitude = location.altitude 165 | return (latitude, longtitude, altitude) 166 | } 167 | 168 | private func didUpdateUserLocation() { 169 | guard let delegate = delegate else { return } 170 | delegate.didUpdateUserLocation() 171 | } 172 | 173 | private func openSetting() { 174 | guard let delegate = delegate else { return } 175 | delegate.openSetting() 176 | 177 | } 178 | } 179 | */ 180 | 181 | import Foundation 182 | import CoreLocation 183 | import Combine 184 | 185 | enum LocationError: Error { 186 | case currentLocationFailure 187 | } 188 | 189 | class LocationManager: NSObject { 190 | static let shared = LocationManager() 191 | @Published var location: CLLocation? 192 | @Published var isLocationUpdated: Bool = false 193 | @Published var needsSettingAlert: Bool = false 194 | 195 | private let locationManager = CLLocationManager() 196 | private var cancellable = Set() 197 | 198 | override init() { 199 | super.init() 200 | locationManager.delegate = self 201 | } 202 | 203 | func updateLocation() { 204 | if locationManager.authorizationStatus == .authorizedWhenInUse { 205 | locationManager.startUpdatingLocation() 206 | } else { 207 | locationManager.requestWhenInUseAuthorization() 208 | } 209 | } 210 | 211 | func getCurrentLocation() -> (Double, Double, Double)? { 212 | guard let location = location else { return nil } 213 | let latitude = location.coordinate.latitude 214 | let longtitude = location.coordinate.longitude 215 | let altitude = location.altitude 216 | return (latitude, longtitude, altitude) 217 | } 218 | } 219 | 220 | extension LocationManager: CLLocationManagerDelegate { 221 | func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { 222 | if !isLocationUpdated { 223 | isLocationUpdated = true 224 | manager.stopUpdatingLocation() 225 | let location = locations.last 226 | self.location = location 227 | } 228 | } 229 | 230 | func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) { 231 | print("(didFailWithError : \(error.localizedDescription)") 232 | } 233 | 234 | func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { 235 | switch manager.authorizationStatus { 236 | case .authorizedWhenInUse: 237 | manager.startUpdatingLocation() 238 | case .restricted, .denied: 239 | needsSettingAlert = true 240 | case .notDetermined: 241 | manager.requestWhenInUseAuthorization() 242 | default: 243 | break 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /Tars/Tars/Manager/LocationManager/LocationManagerDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationManagerDelegate.swift 3 | // Tars 4 | // 5 | // Created by 이윤영 on 2022/11/02. 6 | // 7 | 8 | 9 | import UIKit 10 | 11 | protocol LocationManagerDelegate: AnyObject, UIViewController { 12 | // func didUpdateUserLocation() 13 | } 14 | 15 | extension LocationManagerDelegate { 16 | /* 17 | func openSetting() { 18 | let alert = UIAlertController(title: LocalizableKeys.locationUsageMessage.localized, 19 | message: LocalizableKeys.locationAuthRequest.localized, 20 | preferredStyle: .alert) 21 | let defaultAction = UIAlertAction(title: LocalizableKeys.defaultAction.localized, style: .default, handler: { _ in 22 | guard let url = URL(string: UIApplication.openSettingsURLString) else { return } 23 | DispatchQueue.main.async { 24 | UIApplication.shared.open(url) 25 | } 26 | }) 27 | let destructiveAction = UIAlertAction(title: LocalizableKeys.cancel.localized, style: .destructive, handler: nil) 28 | 29 | alert.addAction(destructiveAction) 30 | alert.addAction(defaultAction) 31 | present(alert, animated: true, completion: nil) 32 | } 33 | */ 34 | } 35 | -------------------------------------------------------------------------------- /Tars/Tars/Manager/PlanetManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanetManager.swift 3 | // Tars 4 | // 5 | // Created by Lena on 2024/7/13. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | class PlanetManager { 12 | static let shared = PlanetManager() 13 | 14 | @Published var currentPlanet: Planet? 15 | 16 | private init() { } 17 | } 18 | -------------------------------------------------------------------------------- /Tars/Tars/Manager/SoundManager/SoundManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SoundManager.swift 3 | // Tars 4 | // 5 | // Created by Lena on 2022/11/26. 6 | // 7 | 8 | import Foundation 9 | import AVFoundation 10 | 11 | class AudioManager { 12 | static let shared = AudioManager() 13 | var audioPlayer: AVAudioPlayer? 14 | 15 | init() {} 16 | 17 | public func playAudio(pre: String = AudioMode.search.prefix, 18 | fileName: String, 19 | audioExtension: String, 20 | audioVolume: Float, 21 | isLoop: Bool = true) { 22 | 23 | guard let url = Bundle.main.url(forResource: "\(pre)\(fileName)", withExtension: "\(audioExtension)") else { return } 24 | 25 | do { 26 | try audioPlayer = AVAudioPlayer(contentsOf: url) 27 | 28 | audioPlayer?.prepareToPlay() 29 | audioPlayer?.volume = audioVolume 30 | audioPlayer?.play() 31 | if isLoop { 32 | audioPlayer?.numberOfLoops = -1 33 | } 34 | 35 | } catch { 36 | print(error.localizedDescription) 37 | } 38 | } 39 | 40 | public func playDetectingAudio(fileName: String) { 41 | guard let url = Bundle.main.url(forResource: "\(fileName)", withExtension: "wav") else { return } 42 | 43 | do { 44 | try audioPlayer = AVAudioPlayer(contentsOf: url) 45 | 46 | audioPlayer?.prepareToPlay() 47 | audioPlayer?.volume = 0.3 48 | audioPlayer?.play() 49 | 50 | } catch { 51 | print(error.localizedDescription) 52 | } 53 | } 54 | 55 | public func pauseAudio() { 56 | audioPlayer?.pause() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tars/Tars/Model/Cardinal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Cardinal.swift 3 | // Tars 4 | // 5 | // Created by ParkJunHyuk on 8/9/24. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Cardinal: Int { 11 | case N = 0 12 | case NE = 1 13 | case E = 2 14 | case SE = 3 15 | case S = 4 16 | case SW = 5 17 | case W = 6 18 | case NW = 7 19 | case None 20 | 21 | func isNear(new: Cardinal) -> Bool { 22 | if new == .None { 23 | return true 24 | } else if self == .None { 25 | return false 26 | } else { 27 | let difference = abs(self.rawValue - new.rawValue) % 7 28 | return difference <= 1 29 | } 30 | } 31 | 32 | var directionText: String { 33 | switch self { 34 | case .N: 35 | return LocalizableKeys.directionUp.localized 36 | case .NE: 37 | return LocalizableKeys.directionUpRight.localized 38 | case .E: 39 | return LocalizableKeys.directionRight.localized 40 | case .SE: 41 | return LocalizableKeys.directionDownRight.localized 42 | case .S: 43 | return LocalizableKeys.directionDown.localized 44 | case .SW: 45 | return LocalizableKeys.directionDownLeft.localized 46 | case .W: 47 | return LocalizableKeys.directionLeft.localized 48 | case .NW: 49 | return LocalizableKeys.directionUpLeft.localized 50 | default: 51 | return "" 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Tars/Tars/Model/Mode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Mode.swift 3 | // Tars 4 | // 5 | // Created by ParkJunHyuk on 8/9/24. 6 | // 7 | 8 | import Foundation 9 | 10 | enum Mode { 11 | case explore 12 | case search(planet: String) 13 | 14 | var titleText: String { 15 | switch self { 16 | case .explore: 17 | return LocalizableKeys.exploreUniverseNavigationTitle.localized 18 | case .search(planet: let name): 19 | return LocalizableKeys.searchingNavigationTitle.localized 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tars/Tars/Model/PlanetInfo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlanetInfo.swift 3 | // Tars 4 | // 5 | // Created by ParkJunHyuk on 8/2/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct PlanetInfo { 11 | let planetIdName: String 12 | let planetName: String 13 | let planetImage: String 14 | var isSelected: StateCell 15 | } 16 | -------------------------------------------------------------------------------- /Tars/Tars/Network/APIService/APIManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIManager.swift 3 | // Tars 4 | // 5 | // Created by 이윤영 on 2022/10/24. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Astronomy API 11 | class AstronomyAPIManager: NetworkService { 12 | func requestBodies() async throws -> [Body] { 13 | let parameters = try getBodiesPositionsParameters() 14 | let url = AstronomyURL.bodiesPosition 15 | let endpoint = url.getEndpoint(with: parameters) 16 | guard let request = endpoint.getURLRequest() else { throw NetworkError.invalidURL} 17 | let data: BodiesPositionsResponse = try await getRequest(request) 18 | 19 | return data.bodiesData 20 | } 21 | } 22 | 23 | extension AstronomyAPIManager { 24 | func getBodiesPositionsParameters() throws -> [String: String] { 25 | // Get current Date 26 | var parameters: [String: String] = [:] 27 | let formatter = DateFormatter() 28 | formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 29 | let currentDate = formatter.string(from: Date()).split(separator: " ") 30 | let (currentDay, currentTime) = (currentDate[0], currentDate[1]) 31 | parameters["from_date"] = String(currentDay) 32 | parameters["to_date"] = String(currentDay) 33 | parameters["time"] = String(currentTime) 34 | 35 | // Get current Location 36 | let locationManager = LocationManager.shared 37 | guard let (latitude, longtitude, elevation) = locationManager.getCurrentLocation() else { throw LocationError.currentLocationFailure} 38 | parameters["latitude"] = String(latitude) 39 | parameters["longitude"] = String(longtitude) 40 | parameters["elevation"] = String(elevation) 41 | 42 | return parameters 43 | } 44 | } 45 | 46 | // MARK: - Horizons API 47 | class HorizonsAPIManager: NetworkService { 48 | func requestBodies() async throws -> [Body] { 49 | var bodiesData: [Body] = [] 50 | let siteCoord = try getSiteCoordParameter() 51 | let (startTime, stopTime) = try getTimeParameter() 52 | let majorBodies: [MajorBody] = MajorBody.allCases 53 | for body in majorBodies { 54 | let parameters = try getBodiesPositionsParameters(command: body.command, siteCoord: siteCoord, startTime: startTime, stopTime: stopTime) 55 | let url = HorizonURL.horizonAPI 56 | let endpoint = url.getEndpoint(with: parameters) 57 | guard let request = endpoint.getURLRequest() else { throw NetworkError.invalidURL} 58 | let data: HorizonResponse = try await getRequest(request) 59 | let (azimuth, altitude) = data.result.extractCoord() 60 | bodiesData.append(Body(id: body.id, name: body.name, altitude: altitude, azimuth: azimuth)) 61 | } 62 | 63 | return bodiesData 64 | } 65 | } 66 | extension HorizonsAPIManager { 67 | func getSiteCoordParameter() throws -> String { 68 | let locationManager = LocationManager.shared 69 | guard let (latitude, longtitude, elevation) = locationManager.getCurrentLocation() else { throw LocationError.currentLocationFailure} 70 | let site_coord = String("'\(longtitude),\(latitude),\(elevation/1000)") 71 | 72 | return site_coord 73 | } 74 | 75 | func getTimeParameter() throws -> (startTime: String, stopTime: String) { 76 | // Get time now and 1 minute later 77 | 78 | let formatter = DateFormatter() 79 | formatter.timeZone = TimeZone(identifier: "Europe/London") 80 | formatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 81 | let nowTime = Date() 82 | let afterTime = Date(timeInterval: 60, since: nowTime) 83 | let startTime = formatter.string(from: nowTime) 84 | let stopTime = formatter.string(from: afterTime) 85 | 86 | return (String("'\(startTime)'"), String("'\(stopTime)'")) 87 | } 88 | 89 | func getBodiesPositionsParameters(command: String, siteCoord: String, startTime: String, stopTime: String) throws -> [String: String] { 90 | var parameters: [String: String] = [:] 91 | 92 | parameters["format"] = String("json") 93 | parameters["COMMAND"] = String(command) 94 | parameters["APPARENT"] = String("REFRACTED") 95 | parameters["CENTER"] = String("coord") 96 | parameters["SITE_COORD"] = String(siteCoord) 97 | parameters["START_TIME"] = String(startTime) 98 | parameters["STOP_TIME"] = String(stopTime) 99 | parameters["STEP_SIZE"] = String("1d") 100 | parameters["QUANTITIES"] = String("4") 101 | 102 | return parameters 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Tars/Tars/Network/APIService/BodiesPositionsResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BodiesPositionsResponse.swift 3 | // Tars 4 | // 5 | // Created by 이윤영 on 2022/10/24. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - Astronomy API 11 | struct BodiesPositionsResponse: Decodable { 12 | var bodiesData: [Body] 13 | 14 | enum CodingKeys: String, CodingKey { 15 | case data, table, rows 16 | } 17 | } 18 | 19 | extension BodiesPositionsResponse { 20 | init(from decoder: Decoder) throws { 21 | let container = try decoder.container(keyedBy: CodingKeys.self) 22 | let data = try container.nestedContainer(keyedBy: CodingKeys.self, forKey: .data) 23 | let table = try data.nestedContainer(keyedBy: CodingKeys.self, forKey: .table) 24 | bodiesData = try table.decode([Body].self, forKey: .rows) 25 | } 26 | } 27 | 28 | // MARK: - Horizons API 29 | struct HorizonResponse: Decodable { 30 | var signature: Signature 31 | var result: String 32 | } 33 | 34 | struct Signature: Decodable { 35 | var source: String 36 | var version: String 37 | } 38 | -------------------------------------------------------------------------------- /Tars/Tars/Network/APIService/Endpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoint.swift 3 | // Tars 4 | // 5 | // Created by 이윤영 on 2022/10/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Endpoint { 11 | var url: String 12 | var method: String 13 | var headers: [String: String]? 14 | var queryParameters: [String: String]? 15 | 16 | init(url: String, method: String, headers: [String: String]? = nil, queryParameters: [String: String]? = nil) { 17 | self.url = url 18 | self.method = method 19 | self.headers = headers 20 | self.queryParameters = queryParameters 21 | } 22 | 23 | func getURLRequest() -> URLRequest? { 24 | guard var urlComponents = URLComponents(string: url) else { return nil } 25 | if let queryParameters = queryParameters { 26 | urlComponents.queryItems = queryParameters.map({ key, value in 27 | URLQueryItem(name: key, value: value) 28 | }) 29 | } 30 | 31 | guard let url = urlComponents.url else { return nil } 32 | var urlRequest = URLRequest(url: url) 33 | urlRequest.httpMethod = method 34 | if let headers = headers { 35 | urlRequest.allHTTPHeaderFields = headers 36 | } 37 | 38 | return urlRequest 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tars/Tars/Network/APIService/NetworkService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkService.swift 3 | // Tars 4 | // 5 | // Created by 이윤영 on 2022/10/24. 6 | // 7 | 8 | import Foundation 9 | 10 | enum NetworkError: Error { 11 | case invalidResponse 12 | case invalidURL 13 | case decodeError 14 | } 15 | 16 | protocol NetworkService {} 17 | extension NetworkService { 18 | func getRequest(_ request: URLRequest) async throws -> D { 19 | let (data, response) = try await URLSession.shared.data(for: request) 20 | guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { 21 | throw NetworkError.invalidResponse 22 | } 23 | let decodedData = try JSONDecoder().decode(D.self, from: data) 24 | return decodedData 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Tars/Tars/Network/Base/URLConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLConstants.swift 3 | // Tars 4 | // 5 | // Created by 이윤영 on 2022/10/24. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol URLConstants { 11 | var url: String { get } 12 | var httpMethod: String { get } 13 | func getEndpoint(with parameters: [String: String]?) -> Endpoint 14 | } 15 | 16 | // MARK: - Astronomy API 17 | // https://astronomyapi.com/ 18 | enum AstronomyURL: String, URLConstants { 19 | case bodies = "api/v2/bodies" // Get available bodies 20 | case bodiesPosition = "api/v2/bodies/positions" // Get all bodies positions 21 | case bodiesPositionBody = "/api/v2/bodies/positions/:body" // Get body positions 22 | 23 | var url: String { 24 | "https://api.astronomyapi.com/\(self.rawValue)" 25 | } 26 | 27 | var httpMethod: String { 28 | switch self { 29 | case .bodies, .bodiesPosition, .bodiesPositionBody: 30 | return "GET" 31 | } 32 | } 33 | 34 | /// getEndpoint 35 | /// - Parameter parameters: query parameters for urlrequest 36 | /// - Returns: Endpoint with query parameters and authorization header 37 | func getEndpoint(with parameters: [String: String]?) -> Endpoint { 38 | // Get API key and create hash for authorization 39 | let hash = "\(Keys.applicationId):\(Keys.applicationSecret)" 40 | let headers = ["Authorization": "Basic \(hash.toBase64())"] 41 | return Endpoint(url: self.url, method: self.httpMethod, headers: headers, queryParameters: parameters) 42 | } 43 | } 44 | 45 | // MARK: - Horizon API 46 | // https://ssd-api.jpl.nasa.gov/doc/horizons.html 47 | enum HorizonURL: String, URLConstants { 48 | case horizonAPI = "api/horizons.api" 49 | 50 | var url: String { 51 | "https://ssd.jpl.nasa.gov/\(self.rawValue)" 52 | } 53 | 54 | var httpMethod: String { 55 | switch self { 56 | case .horizonAPI: 57 | return "GET" 58 | } 59 | } 60 | 61 | func getEndpoint(with parameters: [String: String]?) -> Endpoint { 62 | return Endpoint(url: self.url, method: self.httpMethod, queryParameters: parameters) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tars/Tars/Network/Model/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Tars/Tars/Network/Model/Body.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Body.swift 3 | // Tars 4 | // 5 | // Created by 이윤영 on 2022/10/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct Body: Decodable { 11 | var id: String 12 | var name: String 13 | var distanceFromEarth: String 14 | var altitude: String 15 | var azimuth: String 16 | var coordinate: (x: Float, y: Float, z: Float) 17 | 18 | enum CodingKeys: String, CodingKey { 19 | case cells, id, name, distance, position 20 | case fromEarth, km 21 | case horizonal, altitude, azimuth, degrees 22 | } 23 | } 24 | 25 | extension Body { 26 | // TODO: - API 임시 변경 관련 코드 해결 27 | init(id: String, name: String, altitude: String, azimuth: String) { 28 | self.id = id 29 | self.name = name 30 | self.distanceFromEarth = "0" 31 | self.altitude = altitude 32 | self.azimuth = azimuth 33 | self.coordinate = Self.horizontalCoordinateToXYZ(azimuth: self.azimuth, altitude: self.altitude) 34 | } 35 | 36 | init(from decoder: Decoder) throws { 37 | let container = try decoder.container(keyedBy: CodingKeys.self) 38 | var array = try container.nestedUnkeyedContainer(forKey: .cells) 39 | let cell = try array.nestedContainer(keyedBy: CodingKeys.self) 40 | let distance = try cell.nestedContainer(keyedBy: CodingKeys.self, forKey: .distance) 41 | let fromEarth = try distance.nestedContainer(keyedBy: CodingKeys.self, forKey: .fromEarth) 42 | let position = try cell.nestedContainer(keyedBy: CodingKeys.self, forKey: .position) 43 | let horizonal = try position.nestedContainer(keyedBy: CodingKeys.self, forKey: .horizonal) 44 | let altitude = try horizonal.nestedContainer(keyedBy: CodingKeys.self, forKey: .altitude) 45 | let azimuth = try horizonal.nestedContainer(keyedBy: CodingKeys.self, forKey: .azimuth) 46 | 47 | self.id = try cell.decode(String.self, forKey: .id) 48 | self.name = try cell.decode(String.self, forKey: .name) 49 | self.distanceFromEarth = try fromEarth.decode(String.self, forKey: .km) 50 | self.altitude = try altitude.decode(String.self, forKey: .degrees) 51 | self.azimuth = try azimuth.decode(String.self, forKey: .degrees) 52 | self.coordinate = Self.horizontalCoordinateToXYZ(azimuth: self.azimuth, altitude: self.altitude) 53 | } 54 | 55 | private static func horizontalCoordinateToXYZ(azimuth: String, altitude: String) -> (x: Float, y: Float, z: Float) { 56 | let azimuth = (azimuth as NSString).floatValue.degreeToRadian 57 | let altitude = (altitude as NSString).floatValue.degreeToRadian 58 | 59 | let x = 5 * cos(altitude) * sin(azimuth) 60 | let y = 5 * sin(altitude) 61 | let z = 5 * -(cos(altitude) * cos(azimuth)) 62 | 63 | return (x, y, z) 64 | } 65 | } 66 | 67 | enum MajorBody: String, CaseIterable { 68 | case sun 69 | case moon 70 | case mercury 71 | case venus 72 | case mars 73 | case jupiter 74 | case saturn 75 | case uranus 76 | case neptune 77 | 78 | var id: String { 79 | return "\(rawValue)" 80 | } 81 | 82 | var name: String { 83 | return "\(rawValue)".capitalized 84 | } 85 | 86 | var command: String { 87 | switch self { 88 | case .sun: 89 | return "10" 90 | case .moon: 91 | return "301" 92 | case .mercury: 93 | return "199" 94 | case .venus: 95 | return "299" 96 | case .mars: 97 | return "499" 98 | case .jupiter: 99 | return "599" 100 | case .saturn: 101 | return "699" 102 | case .uranus: 103 | return "799" 104 | case .neptune: 105 | return "899" 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Tars/Tars/SceneKitAudioPlayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneKitAudioPlayer.swift 3 | // Tars 4 | // 5 | // Created by ParkJunHyuk on 8/11/24. 6 | // 7 | 8 | import Foundation 9 | import SceneKit 10 | import AVFAudio 11 | 12 | protocol SceneKitAudioVolumeProtocol { 13 | func setSoundPlayer(_ soundPlayer: [String: SCNAudioPlayer]) 14 | func setVolumeForAll(volume: Float) 15 | func setVolume(selectedName: String, selectedVolume: Float, otherVolume: Float) 16 | } 17 | 18 | class SceneKitAudioVolumeManager: SceneKitAudioVolumeProtocol { 19 | private var soundPlayer: [String: SCNAudioPlayer] = [:] 20 | 21 | // soundPlayer를 설정하는 메서드 22 | func setSoundPlayer(_ soundPlayer: [String: SCNAudioPlayer]) { 23 | for (key, value) in soundPlayer { 24 | self.soundPlayer[key] = value 25 | } 26 | } 27 | 28 | // 모든 행성에 대한 볼륨 조절 메서드 29 | func setVolumeForAll(volume: Float) { 30 | for audioPlayer in soundPlayer.values { 31 | if let avNode = audioPlayer.audioNode as? AVAudioMixing { 32 | avNode.volume = volume 33 | } 34 | } 35 | } 36 | 37 | // 각 행성에 맞게 볼륨 조절 메서드 38 | func setVolume(selectedName: String, selectedVolume: Float, otherVolume: Float) { 39 | for (name, audioPlayer) in soundPlayer { 40 | guard let avNode = audioPlayer.audioNode as? AVAudioMixing else { continue } 41 | avNode.volume = (name == selectedName) ? selectedVolume : otherVolume 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tars/Tars/View/InfoView/CustomPlanetInfoView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomPlanetInfoView.swift 3 | // Tars 4 | // 5 | // Created by Ayden on 2022/11/23. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | import Then 12 | 13 | class CustomPlanetInfoView: UIView { 14 | 15 | /* 16 | lazy var chapter: UILabel = { 17 | let chapter = UILabel() 18 | chapter.font = .preferredFont(forTextStyle: .title2) 19 | chapter.textColor = .white 20 | chapter.adjustsFontForContentSizeCategory = true 21 | return chapter 22 | }() 23 | 24 | lazy var planetInfoTitle: UILabel = { 25 | let planetInfoTitle = UILabel() 26 | if let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title1).withSymbolicTraits(.traitBold) { 27 | planetInfoTitle.font = .init(descriptor: descriptor, size: 0) 28 | } 29 | planetInfoTitle.textColor = .white 30 | planetInfoTitle.textAlignment = .left 31 | planetInfoTitle.numberOfLines = 0 32 | planetInfoTitle.adjustsFontForContentSizeCategory = true 33 | return planetInfoTitle 34 | }() 35 | 36 | lazy var planetInfoContents: UILabel = { 37 | let label = UILabel() 38 | let attributedString = NSMutableAttributedString(string: String()) 39 | let paragraphStyle = NSMutableParagraphStyle() 40 | 41 | label.font = .preferredFont(forTextStyle: .title2) 42 | label.numberOfLines = 0 43 | label.attributedText = attributedString 44 | 45 | label.textAlignment = .justified 46 | label.lineBreakMode = .byCharWrapping 47 | paragraphStyle.hyphenationFactor = 1 48 | 49 | paragraphStyle.lineSpacing = 18 50 | attributedString.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: attributedString.length)) 51 | label.textColor = .white 52 | label.adjustsFontForContentSizeCategory = true 53 | return label 54 | }() 55 | 56 | override init(frame: CGRect) { 57 | super.init(frame: .zero) 58 | configurePlanetInfoContents() 59 | } 60 | 61 | required init?(coder: NSCoder) { 62 | super.init(coder: coder) 63 | } 64 | 65 | public func setInfoContents(chapter: String, title: String, contents: String) { 66 | self.chapter.text = chapter 67 | self.planetInfoTitle.text = title 68 | self.planetInfoContents.text = contents 69 | 70 | self.chapter.accessibilityLabel = chapter 71 | self.planetInfoTitle.accessibilityLabel = title 72 | self.planetInfoContents.accessibilityLabel = contents 73 | } 74 | 75 | public func setContentsIndex(planet: Planet, chapterIndex: Int) { 76 | let chapterNumber = "Chapter \(chapterIndex)" 77 | let titlesAndContents = planet.titlesAndContents 78 | 79 | guard chapterIndex > 0 && chapterIndex <= titlesAndContents.count else { 80 | print("Invalid chapter index") 81 | return 82 | } 83 | 84 | let (title, content) = (titlesAndContents[chapterIndex - 1].0, titlesAndContents[chapterIndex - 1].1) 85 | let localizedTitle = LocalizableKeys(from: title)?.localized ?? String() 86 | let localizedContent = LocalizableKeys(from: content)?.localized ?? String() 87 | 88 | setInfoContents(chapter: chapterNumber, title: localizedTitle, contents: localizedContent) 89 | } 90 | 91 | private func configurePlanetInfoContents() { 92 | [chapter, planetInfoTitle, planetInfoContents].forEach { addSubview($0) } 93 | 94 | self.isAccessibilityElement = false 95 | self.accessibilityElements = [chapter, planetInfoTitle, planetInfoContents] 96 | 97 | chapter.anchor(top: self.topAnchor, 98 | leading: self.leadingAnchor, 99 | trailing: self.trailingAnchor, 100 | paddingLeading: screenWidth / 12.18, 101 | width: screenWidth / 1.19) 102 | planetInfoTitle.anchor(top: chapter.bottomAnchor, 103 | leading: self.leadingAnchor, 104 | trailing: self.trailingAnchor, 105 | paddingLeading: screenWidth / 12.18, 106 | width: screenWidth / 1.19) 107 | planetInfoContents.anchor(top: planetInfoTitle.bottomAnchor, 108 | leading: self.leadingAnchor, 109 | bottom: self.bottomAnchor, 110 | trailing: self.trailingAnchor, 111 | paddingTop: screenHeight / 52.75, 112 | paddingLeading: screenWidth / 12.18, 113 | paddingBottom: screenHeight / 21.1, 114 | width: screenWidth / 1.19) 115 | } 116 | */ 117 | 118 | lazy var chapter = UILabel().then { 119 | $0.font = .preferredFont(forTextStyle: .title2) 120 | $0.textColor = .white 121 | $0.adjustsFontForContentSizeCategory = true 122 | } 123 | 124 | lazy var planetInfoTitle = UILabel().then { 125 | if let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title1).withSymbolicTraits(.traitBold) { 126 | $0.font = .init(descriptor: descriptor, size: 0) 127 | } 128 | $0.textColor = .white 129 | $0.textAlignment = .left 130 | $0.numberOfLines = 0 131 | $0.adjustsFontForContentSizeCategory = true 132 | } 133 | 134 | lazy var planetInfoContents = UILabel().then { 135 | let attributedString = NSMutableAttributedString(string: String()) 136 | let paragraphStyle = NSMutableParagraphStyle().then { 137 | $0.hyphenationFactor = 1 138 | $0.lineSpacing = 18 139 | } 140 | attributedString.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: attributedString.length)) 141 | 142 | $0.font = .preferredFont(forTextStyle: .title2) 143 | $0.numberOfLines = 0 144 | $0.attributedText = attributedString 145 | $0.textAlignment = .justified 146 | $0.lineBreakMode = .byCharWrapping 147 | $0.textColor = .white 148 | $0.adjustsFontForContentSizeCategory = true 149 | } 150 | 151 | override init(frame: CGRect) { 152 | super.init(frame: .zero) 153 | configurePlanetInfoContents() 154 | } 155 | 156 | required init?(coder: NSCoder) { 157 | super.init(coder: coder) 158 | } 159 | 160 | public func setInfoContents(chapter: String, title: String, contents: String) { 161 | self.chapter.text = chapter 162 | self.planetInfoTitle.text = title 163 | self.planetInfoContents.text = contents 164 | 165 | setAccessibilityLabels() 166 | } 167 | 168 | public func setContentsIndex(planet: Planet, chapterIndex: Int) { 169 | let chapterNumber = "Chapter \(chapterIndex)" 170 | let titlesAndContents = planet.titlesAndContents 171 | 172 | guard chapterIndex > 0 && chapterIndex <= titlesAndContents.count else { 173 | print("Invalid chapter index") 174 | return 175 | } 176 | 177 | let (title, content) = titlesAndContents[chapterIndex - 1] 178 | let localizedTitle = LocalizableKeys(from: title)?.localized ?? String() 179 | let localizedContent = LocalizableKeys(from: content)?.localized ?? String() 180 | 181 | setInfoContents(chapter: chapterNumber, title: localizedTitle, contents: localizedContent) 182 | } 183 | 184 | private func configurePlanetInfoContents() { 185 | [chapter, planetInfoTitle, planetInfoContents].forEach { addSubview($0) } 186 | 187 | self.isAccessibilityElement = false 188 | self.accessibilityElements = [chapter, planetInfoTitle, planetInfoContents] 189 | 190 | let chapterPadding = screenWidth / 12.18 191 | let width = screenWidth / 1.19 192 | 193 | chapter.snp.makeConstraints { 194 | $0.top.equalToSuperview() 195 | $0.leading.trailing.equalToSuperview().inset(chapterPadding) 196 | $0.width.equalTo(width) 197 | } 198 | 199 | planetInfoTitle.snp.makeConstraints { 200 | $0.top.equalTo(chapter.snp.bottom) 201 | $0.leading.trailing.equalToSuperview().inset(chapterPadding) 202 | $0.width.equalTo(width) 203 | } 204 | 205 | planetInfoContents.snp.makeConstraints { 206 | $0.top.equalTo(planetInfoTitle.snp.bottom).offset(screenHeight / 52.75) 207 | $0.leading.trailing.equalToSuperview().inset(chapterPadding) 208 | $0.bottom.equalToSuperview().inset(screenHeight / 21.1) 209 | $0.width.equalTo(width) 210 | } 211 | } 212 | 213 | private func setAccessibilityLabels() { 214 | self.chapter.accessibilityLabel = self.chapter.text 215 | self.planetInfoTitle.accessibilityLabel = self.planetInfoTitle.text 216 | self.planetInfoContents.accessibilityLabel = self.planetInfoContents.text 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /Tars/Tars/View/InfoView/InfoViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfoViewController.swift 3 | // Tars 4 | // 5 | // Created by Ayden on 2022/10/24. 6 | // 7 | 8 | import UIKit 9 | import Combine 10 | import SceneKit.ModelIO 11 | 12 | import SnapKit 13 | import Then 14 | 15 | class InfoViewController: UIViewController { 16 | 17 | var cancellables = Set() 18 | var currentPlanet: Planet? { 19 | didSet { 20 | if let planet = currentPlanet { 21 | let planetString = planet.rawValue 22 | } 23 | } 24 | } 25 | 26 | private var customPlanetInfoChapters: [CustomPlanetInfoView] = [CustomPlanetInfoView(), CustomPlanetInfoView(), CustomPlanetInfoView()] 27 | private var audioManager = AudioManager() 28 | 29 | private lazy var sceneView = SCNView().then { 30 | configureSceneView($0) 31 | } 32 | 33 | private lazy var customInfoScrollView = UIScrollView().then { 34 | $0.backgroundColor = .clear 35 | $0.accessibilityScroll(.down) 36 | } 37 | 38 | private lazy var customInfoStackView = UIStackView(arrangedSubviews: customPlanetInfoChapters).then { 39 | $0.distribution = .fillProportionally 40 | $0.axis = .vertical 41 | $0.alignment = .leading 42 | } 43 | 44 | // MARK: - Life Cycle 45 | override func viewDidLoad() { 46 | super.viewDidLoad() 47 | view.backgroundColor = .black 48 | 49 | bindCurrentPlanet() 50 | 51 | [sceneView, customInfoScrollView].forEach { view.addSubview($0) } 52 | customInfoScrollView.addSubview(customInfoStackView) 53 | 54 | configureChapterAccessibilityHints() 55 | configureConstraints() 56 | 57 | navigationItem.title = currentPlanet?.planetName 58 | 59 | playPlanetAudio() 60 | } 61 | 62 | override func viewDidDisappear(_ animated: Bool) { 63 | super.viewDidDisappear(animated) 64 | audioManager.pauseAudio() 65 | audioManager.audioPlayer?.prepareToPlay() 66 | } 67 | } 68 | 69 | private extension InfoViewController { 70 | 71 | func bindCurrentPlanet() { 72 | PlanetManager.shared.$currentPlanet 73 | .assign(to: \.currentPlanet, on: self) 74 | .store(in: &cancellables) 75 | } 76 | 77 | func configureSceneView(_ sceneView: SCNView) { 78 | let path = Bundle.main.path(forResource: currentPlanet?.nameEnglish, ofType: ResourceConstants.usdz.name, inDirectory: "3dPlanets") ?? "" 79 | guard let url = URL(string: path) else { return } 80 | 81 | let mdlAsset = MDLAsset(url: url) 82 | mdlAsset.loadTextures() 83 | let scene = SCNScene(mdlAsset: mdlAsset) 84 | 85 | let planetNode = scene.rootNode.childNode(withName: ResourceConstants.Cube_002.name, recursively: true) 86 | planetNode?.runAction(SCNAction.repeatForever(SCNAction.rotateBy(x: 0, y: CGFloat(GLKMathDegreesToRadians(-360)), z: 0, duration: 30))) 87 | 88 | if currentPlanet == .saturn { 89 | scene.rootNode.eulerAngles = SCNVector3(0.1, 0, 0) 90 | } 91 | 92 | sceneView.allowsCameraControl = true 93 | sceneView.backgroundColor = .clear 94 | sceneView.cameraControlConfiguration.allowsTranslation = false 95 | sceneView.cameraControlConfiguration.autoSwitchToFreeCamera = true 96 | disableUnwantedGestures(in: sceneView) 97 | 98 | sceneView.autoenablesDefaultLighting = true 99 | sceneView.scene = scene 100 | 101 | sceneView.isAccessibilityElement = true 102 | sceneView.accessibilityLabel = "\(String(describing: currentPlanet?.planetName)) \(LocalizableKeys.image)" 103 | } 104 | 105 | func disableUnwantedGestures(in sceneView: SCNView) { 106 | sceneView.gestureRecognizers?.forEach { reco in 107 | if let panReco = reco as? UIPanGestureRecognizer { 108 | panReco.maximumNumberOfTouches = 0 109 | } 110 | 111 | if let pinchReco = reco as? UIPinchGestureRecognizer { 112 | pinchReco.isEnabled = false 113 | } 114 | } 115 | } 116 | 117 | func configureChapterAccessibilityHints() { 118 | let hints = [LocalizableKeys.chapterOneHint, LocalizableKeys.chapterTwoHint, LocalizableKeys.chapterThreeHint] 119 | for (index, chapter) in customPlanetInfoChapters.enumerated() { 120 | chapter.setContentsIndex(planet: currentPlanet ?? .mars, chapterIndex: index + 1) 121 | chapter.chapter.accessibilityHint = hints[index].localized 122 | } 123 | } 124 | 125 | func playPlanetAudio() { 126 | self.audioManager.playAudio(pre: AudioMode.detail.prefix, 127 | fileName: currentPlanet?.nameEnglish ?? "", 128 | audioExtension: ResourceConstants.mp3.name, 129 | audioVolume: AudioVolume.third.volume, 130 | isLoop: false) 131 | } 132 | 133 | func configureConstraints() { 134 | 135 | let topPadding: CGFloat = currentPlanet == .saturn ? screenHeight / 21.6 : screenHeight / 52.75 136 | let heightMultiplier: CGFloat = currentPlanet == .saturn ? 1.56 : 1.2 137 | 138 | sceneView.snp.makeConstraints { 139 | $0.centerX.equalTo(view) 140 | $0.top.equalTo(view.safeAreaLayoutGuide.snp.top).offset(topPadding) 141 | $0.width.equalToSuperview() 142 | $0.height.equalTo(sceneView.snp.width).dividedBy(heightMultiplier) 143 | } 144 | 145 | customInfoScrollView.snp.makeConstraints { 146 | $0.top.equalTo(sceneView.snp.bottom).offset(screenHeight / 21.1) 147 | $0.leading.trailing.bottom.equalToSuperview() 148 | } 149 | 150 | customInfoStackView.snp.makeConstraints { 151 | $0.edges.equalToSuperview() 152 | $0.width.equalToSuperview() 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Tars/Tars/View/LauchScreen/View/CustomBackgroundOverlayView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomBackgroundOverlayView.swift 3 | // Tars 4 | // 5 | // Created by Seik Oh on 19/11/2022. 6 | // 7 | 8 | /* 9 | import UIKit 10 | 11 | class CustomBackgroundOverlayView: UIView { 12 | 13 | lazy var coachingOnboardingBackground: UIView = { 14 | let background = UIView() 15 | background.backgroundColor = UIColor.black.withAlphaComponent(0.6) 16 | background.translatesAutoresizingMaskIntoConstraints = false 17 | return background 18 | }() 19 | 20 | override init(frame: CGRect) { 21 | super.init(frame: .zero) 22 | configureBackgroundOverlay() 23 | } 24 | 25 | required init?(coder: NSCoder) { 26 | super.init(coder: coder) 27 | } 28 | 29 | private func configureBackgroundOverlay() { 30 | 31 | [coachingOnboardingBackground].forEach { addSubview($0) } 32 | 33 | coachingOnboardingBackground.anchor(width: screenWidth, height: screenHeight) 34 | } 35 | } 36 | */ 37 | -------------------------------------------------------------------------------- /Tars/Tars/View/LauchScreen/View/CustomOnboardingOverlayView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CustomOnboardingOverlayView.swift 3 | // Tars 4 | // 5 | // Created by Seik Oh on 19/11/2022. 6 | // 7 | 8 | /* 9 | 10 | import UIKit 11 | 12 | class CustomOnboardingOverlayView: UIView { 13 | 14 | lazy var coachingOnboardingLabel: UILabel = { 15 | let coachingOverlay = UILabel() 16 | coachingOverlay.text = LocalizableKeys.onboardingInstructionTitle.localized 17 | coachingOverlay.font = .preferredFont(forTextStyle: .largeTitle) 18 | coachingOverlay.textAlignment = .center 19 | coachingOverlay.numberOfLines = 0 20 | coachingOverlay.adjustsFontSizeToFitWidth = true 21 | if let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitBold) { 22 | coachingOverlay.font = .init(descriptor: descriptor, size: 0) 23 | } 24 | coachingOverlay.textColor = .white 25 | coachingOverlay.adjustsFontForContentSizeCategory = true 26 | coachingOverlay.translatesAutoresizingMaskIntoConstraints = false 27 | return coachingOverlay 28 | }() 29 | 30 | override init(frame: CGRect) { 31 | super.init(frame: .zero) 32 | configureCoachingOverlay() 33 | } 34 | 35 | required init?(coder: NSCoder) { 36 | super.init(coder: coder) 37 | } 38 | 39 | private func configureCoachingOverlay() { 40 | 41 | [coachingOnboardingLabel].forEach { addSubview($0) } 42 | 43 | NSLayoutConstraint.activate([ 44 | coachingOnboardingLabel.leadingAnchor.constraint(equalTo: self.leadingAnchor), 45 | coachingOnboardingLabel.trailingAnchor.constraint(equalTo: self.trailingAnchor), 46 | coachingOnboardingLabel.topAnchor.constraint(equalTo: self.topAnchor), 47 | coachingOnboardingLabel.bottomAnchor.constraint(equalTo: self.bottomAnchor), 48 | coachingOnboardingLabel.widthAnchor.constraint(equalToConstant: screenWidth / 1.5), 49 | coachingOnboardingLabel.heightAnchor.constraint(equalToConstant: screenWidth / 1.5) 50 | ]) 51 | } 52 | } 53 | */ 54 | -------------------------------------------------------------------------------- /Tars/Tars/View/LauchScreen/ViewController/LaunchScreenViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LaunchScreenViewController.swift 3 | // Tars 4 | // 5 | // Created by Seik Oh on 15/11/2022. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | import Then 12 | 13 | final class LaunchScreenViewController: UIViewController { 14 | 15 | // MARK: - Properties 16 | 17 | private var airPodsImage = UIImageView() 18 | private var airPodsInstruction = UILabel() 19 | private var appName = UILabel() 20 | 21 | // MARK: - Life Cycles 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | makeGradientBackground() 27 | configureStyle() 28 | configureHierarchy() 29 | configureConstraints() 30 | configureAccessibility() 31 | navigateToUniverseVCWithDelay() 32 | } 33 | } 34 | 35 | // MARK: - Configure View Layout 36 | 37 | private extension LaunchScreenViewController { 38 | 39 | func makeGradientBackground() { 40 | self.view.layer.configureGradientBackground(UIColor.customGradientPurple.cgColor, UIColor.customGradientBlue.cgColor) 41 | } 42 | 43 | func configureHierarchy() { 44 | view.addSubviews(airPodsImage, airPodsInstruction, appName) 45 | } 46 | 47 | func configureStyle() { 48 | airPodsImage.do { 49 | $0.image = UIImage(resource: .airpods) 50 | $0.contentMode = .scaleAspectFit 51 | } 52 | 53 | airPodsInstruction.do { 54 | let attributedString = NSMutableAttributedString(string: LocalizableKeys.airPodsInstructionstring.localized) 55 | $0.attributedText = attributedString 56 | $0.setBoldFont(forTextStyle: .title1) 57 | $0.numberOfLines = 0 58 | $0.textAlignment = .center 59 | $0.textColor = .white 60 | } 61 | 62 | appName.do { 63 | let attributedString = NSMutableAttributedString(string: "SpaceOver") 64 | $0.attributedText = attributedString 65 | $0.font = .preferredFont(forTextStyle: .title2) 66 | $0.textAlignment = .center 67 | $0.textColor = .white 68 | } 69 | } 70 | } 71 | 72 | // MARK: - Auto Layout 설정 73 | 74 | private extension LaunchScreenViewController { 75 | func configureConstraints() { 76 | airPodsImage.snp.makeConstraints { 77 | $0.centerX.equalToSuperview() 78 | $0.centerY.equalToSuperview().offset(-32) 79 | $0.width.height.equalTo(screenWidth * 0.6) 80 | } 81 | 82 | airPodsInstruction.snp.makeConstraints { 83 | $0.centerX.equalToSuperview() 84 | $0.width.equalTo(screenWidth * 0.6) 85 | $0.top.equalTo(airPodsImage.snp.bottom).offset(16) 86 | } 87 | 88 | appName.snp.makeConstraints { 89 | $0.centerX.equalToSuperview() 90 | $0.bottom.equalTo(view.safeAreaLayoutGuide) 91 | } 92 | } 93 | } 94 | 95 | // MARK: - accessibility 및 내비게이션 설정 96 | 97 | private extension LaunchScreenViewController { 98 | 99 | /// accessibility 설정 100 | private func configureAccessibility() { 101 | airPodsInstruction.isAccessibilityElement = true 102 | airPodsInstruction.accessibilityLabel = LocalizableKeys.airPodsInstructionstring.localized 103 | } 104 | 105 | /// 화면 이동 106 | private func navigateToUniverseVCWithDelay() { 107 | DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { 108 | self.airPodsInstruction.isAccessibilityElement = false 109 | 110 | let universeViewController = UniverseMainViewController() 111 | self.navigationController?.pushViewController(universeViewController, animated: false) 112 | self.navigationController?.isNavigationBarHidden = true 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /Tars/Tars/View/LauchScreen/ViewController/OnboardingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OnboardingViewController.swift 3 | // Tars 4 | // 5 | // Created by Lena on 2024/7/29. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | import Then 12 | 13 | class OnboardingView: UIView { 14 | 15 | private var coachingOnboardingLabel = UILabel() 16 | private var onboardingBackground = UIView() 17 | 18 | override init(frame: CGRect) { 19 | super.init(frame: .zero) 20 | configureSubviews() 21 | configureBackground() 22 | configureLabel() 23 | configureLayout() 24 | } 25 | 26 | required init?(coder: NSCoder) { 27 | super.init(coder: coder) 28 | } 29 | } 30 | 31 | private extension OnboardingView { 32 | 33 | func configureLabel() { 34 | coachingOnboardingLabel.do { 35 | $0.text = LocalizableKeys.onboardingInstructionTitle.localized 36 | $0.font = .preferredFont(forTextStyle: .largeTitle) 37 | $0.textAlignment = .center 38 | $0.numberOfLines = 0 39 | $0.adjustsFontSizeToFitWidth = true 40 | if let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitBold) { 41 | $0.font = .init(descriptor: descriptor, size: 0) 42 | } 43 | $0.textColor = .white 44 | $0.adjustsFontForContentSizeCategory = true 45 | $0.translatesAutoresizingMaskIntoConstraints = false 46 | } 47 | } 48 | 49 | func configureBackground() { 50 | onboardingBackground.do { 51 | $0.backgroundColor = .black.withAlphaComponent(0.6) 52 | } 53 | } 54 | 55 | func configureSubviews() { 56 | self.addSubview(onboardingBackground) 57 | onboardingBackground.addSubview(coachingOnboardingLabel) 58 | } 59 | 60 | func configureLayout() { 61 | onboardingBackground.snp.makeConstraints { 62 | $0.edges.equalToSuperview() 63 | } 64 | 65 | coachingOnboardingLabel.snp.makeConstraints { 66 | $0.centerX.equalToSuperview() 67 | $0.top.equalToSuperview().inset(screenHeight * 0.34) 68 | $0.width.height.lessThanOrEqualToSuperview().multipliedBy(0.8) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Tars/Tars/View/UniverseSearch/Cells/SelectPlanetCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectPlanetCollectionViewCell.swift 3 | // Tars 4 | // 5 | // Created by 김소현 on 2022/10/24. 6 | // 7 | 8 | import UIKit 9 | 10 | class SelectPlanetCollectionViewCell: UICollectionViewCell { 11 | static let identifier: String = "SelectPlanetCollectionViewCell" 12 | 13 | let planetBackgroundView: UIImageView = { 14 | let imageView: UIImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: screenWidth * 0.27, height: screenWidth * 0.17)) 15 | imageView.image = UIImage(resource: .background) 16 | return imageView 17 | }() 18 | 19 | let planetImageView: UIImageView = { 20 | let imageView: UIImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: screenWidth * 0.24, height: screenWidth * 0.24)) 21 | imageView.isUserInteractionEnabled = true 22 | imageView.sizeToFit() 23 | return imageView 24 | }() 25 | 26 | let planetNameLabel: UILabel = { 27 | let label: UILabel = UILabel() 28 | label.textColor = .white 29 | label.textAlignment = .center 30 | label.font = UIFont.systemFont(ofSize: 24, weight: .semibold) 31 | return label 32 | }() 33 | 34 | override func layoutSubviews() { 35 | 36 | [planetImageView, planetNameLabel].forEach { 37 | self.contentView.addSubview($0) 38 | } 39 | 40 | planetImageView.anchor(top: self.topAnchor, paddingTop: screenHeight * 0.005) 41 | planetImageView.centerX(inView: self) 42 | planetImageView.setWidth(width: screenWidth * 0.34) 43 | planetImageView.setHeight(height: screenHeight * 0.12) 44 | 45 | planetNameLabel.anchor(top: planetImageView.bottomAnchor) 46 | planetNameLabel.centerX(inView: planetImageView) 47 | } 48 | 49 | override init(frame: CGRect) { 50 | super.init(frame: frame) 51 | } 52 | 53 | required init?(coder: NSCoder) { 54 | fatalError("init(coder:) has not been implemented") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Tars/Tars/View/UniverseSearch/Cells/SelectPlanetMainCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SelectPlanetMainCollectionViewCell.swift 3 | // Tars 4 | // 5 | // Created by ParkJunHyuk on 7/29/24. 6 | // 7 | 8 | import UIKit 9 | 10 | import SnapKit 11 | import Then 12 | 13 | enum StateCell { 14 | case select 15 | case notSelect 16 | } 17 | 18 | class SelectPlanetMainCollectionViewCell: UICollectionViewCell { 19 | static let identifier: String = "SelectPlanetMainCollectionViewCell" 20 | 21 | private var selectCell: StateCell = .notSelect { 22 | didSet { 23 | self.modifyLayoutCell() 24 | } 25 | } 26 | 27 | // MARK: - UI Properties 28 | 29 | private let planetBackgroundView = UIImageView(frame: CGRect(x: 0, y: 0, width: screenWidth * 0.27, height: screenWidth * 0.17)) 30 | private let planetImageView = UIImageView(frame: CGRect(x: 0, y: 0, width: screenWidth * 0.24, height: screenWidth * 0.24)) 31 | private let planetNameLabel = UILabel() 32 | 33 | // MARK: - Life Cycles 34 | 35 | override init(frame: CGRect) { 36 | super.init(frame: frame) 37 | 38 | configureStyle() 39 | configureHierarchy() 40 | configureLayout() 41 | } 42 | 43 | required init?(coder: NSCoder) { 44 | fatalError("init(coder:) has not been implemented") 45 | } 46 | } 47 | 48 | // MARK: - Extension 49 | 50 | extension SelectPlanetMainCollectionViewCell { 51 | func selectCellType(_ isSelected: Bool) { 52 | selectCell = isSelected ? .select : .notSelect 53 | } 54 | 55 | func configureCell(text: String, image: UIImage, selectCell: StateCell) { 56 | planetNameLabel.text = text 57 | planetNameLabel.textColor = .white 58 | planetImageView.image = image 59 | self.selectCell = selectCell 60 | } 61 | 62 | func accessibilityValueNameLabel() -> String { 63 | return planetNameLabel.text ?? "" 64 | } 65 | } 66 | 67 | // MARK: - Configure View Layout Private Extension 68 | 69 | private extension SelectPlanetMainCollectionViewCell { 70 | func configureStyle() { 71 | planetBackgroundView.do { 72 | $0.image = UIImage(resource: .background) 73 | } 74 | 75 | planetImageView.do { 76 | $0.isUserInteractionEnabled = true 77 | $0.sizeToFit() 78 | } 79 | 80 | planetNameLabel.do { 81 | $0.textColor = .white 82 | $0.textAlignment = .center 83 | $0.font = UIFont.systemFont(ofSize: 24, weight: .semibold) 84 | } 85 | } 86 | 87 | func configureHierarchy() { 88 | self.addSubviews(planetImageView, planetNameLabel) 89 | } 90 | 91 | func configureLayout() { 92 | planetImageView.snp.makeConstraints { 93 | $0.top.equalToSuperview().offset(screenHeight * 0.005) 94 | $0.centerX.equalToSuperview() 95 | $0.width.equalTo(screenWidth * 0.34) 96 | $0.height.equalTo(screenHeight * 0.12) 97 | } 98 | 99 | planetNameLabel.snp.makeConstraints { 100 | $0.top.equalTo(planetImageView.snp.bottom) 101 | $0.centerX.equalTo(planetImageView) 102 | } 103 | } 104 | 105 | func modifyLayoutCell() { 106 | switch selectCell { 107 | case .notSelect: 108 | planetNameLabel.textColor = .white 109 | self.backgroundView = nil 110 | 111 | case .select: 112 | planetNameLabel.textColor = .black 113 | self.backgroundView = planetBackgroundView 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Tars/Tars/View/UniverseSearch/Collection/UniverseMainViewController+DataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UniverseMainViewController+DataSource.swift 3 | // Tars 4 | // 5 | // Created by ParkJunHyuk on 7/29/24. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UniverseMainViewController: UICollectionViewDataSource { 11 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 12 | return planetListData.count 13 | } 14 | 15 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 16 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SelectPlanetMainCollectionViewCell.identifier, for: indexPath) as? SelectPlanetMainCollectionViewCell 17 | else { return UICollectionViewCell() } 18 | 19 | if let image = UIImage(named: planetListData[indexPath.row].planetImage) { 20 | cell.configureCell(text: planetListData[indexPath.row].planetName, 21 | image: image, 22 | selectCell: planetListData[indexPath.row].isSelected) 23 | } 24 | 25 | // VoiceOver 처리 26 | cell.isAccessibilityElement = true 27 | cell.accessibilityValue = cell.accessibilityValueNameLabel() 28 | 29 | return cell 30 | } 31 | 32 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 33 | guard let cell = collectionView.cellForItem(at: indexPath) as? SelectPlanetMainCollectionViewCell else { return } 34 | 35 | /// 이전 선택한 행성과 새로 선택한 행성이 다를 경우 36 | if let previouseIndexPath = selectedIndexPath, previouseIndexPath != indexPath { 37 | planetListData[previouseIndexPath.row].isSelected = .notSelect 38 | collectionView.deselectItem(at: previouseIndexPath, animated: true) 39 | 40 | universeModeViewModel.changeMode(newMode: .search(planet: planetListData[indexPath.row].planetIdName)) 41 | cell.selectCellType(cell.isSelected) 42 | } 43 | 44 | /// 같은 행성을 선택 시 선택 취소 45 | if selectedIndexPath == indexPath { 46 | selectedIndexPath = nil 47 | planetListData[indexPath.row].isSelected = .notSelect 48 | collectionView.deselectItem(at: indexPath, animated: true) 49 | universeModeViewModel.changeMode(newMode: .explore) 50 | 51 | cell.selectCellType(cell.isSelected) 52 | } else { 53 | selectedIndexPath = indexPath 54 | planetListData[indexPath.row].isSelected = .select 55 | universeModeViewModel.changeMode(newMode: .search(planet: planetListData[indexPath.row].planetIdName)) 56 | 57 | cell.selectCellType(cell.isSelected) 58 | } 59 | } 60 | 61 | func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { 62 | guard let cell = collectionView.cellForItem(at: indexPath) as? SelectPlanetMainCollectionViewCell else { return } 63 | planetListData[indexPath.row].isSelected = .notSelect 64 | print("didDeselectItemAt", cell.isSelected, indexPath.row) 65 | 66 | cell.selectCellType(cell.isSelected) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tars/Tars/View/UniverseSearch/Collection/UniverseMainViewController+FlowLayoutDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UniverseMainViewController+FlowLayoutDelegate.swift 3 | // Tars 4 | // 5 | // Created by ParkJunHyuk on 7/29/24. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UniverseMainViewController: UICollectionViewDelegateFlowLayout { 11 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 12 | 13 | let cellWidth = screenWidth * 0.27 14 | let cellHeight = screenHeight * 0.17 15 | 16 | return CGSize(width: cellWidth, height: cellHeight) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tars/Tars/View/UniverseSearch/Collection/UniverseSearch/UniverseSearchViewController+DataSource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UniverseSearchViewController+DataSource.swift 3 | // Tars 4 | // 5 | // Created by 김소현 on 2022/11/04. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UniverseSearchViewController: UICollectionViewDataSource { 11 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 12 | return Planet.allCases.count 13 | } 14 | 15 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 16 | guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: SelectPlanetCollectionViewCell.identifier, for: indexPath) as? SelectPlanetCollectionViewCell 17 | else { return UICollectionViewCell() } 18 | 19 | // reusable cell init 20 | cell.backgroundView = nil 21 | cell.planetNameLabel.textColor = .white 22 | 23 | cell.planetNameLabel.text = PlanetConstants.planetsSystem[indexPath.row] 24 | cell.planetImageView.image = UIImage(named: PlanetConstants.planetsEn[indexPath.row]) 25 | 26 | // VoiceOver 처리 27 | cell.isAccessibilityElement = true 28 | cell.accessibilityValue = cell.planetNameLabel.text 29 | 30 | return cell 31 | } 32 | 33 | func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { 34 | guard let cell = collectionView.cellForItem(at: indexPath) as? SelectPlanetCollectionViewCell else { 35 | return true 36 | } 37 | 38 | if cell.isSelected { 39 | // cell이 재선택 된 경우 40 | cell.planetNameLabel.textColor = .white 41 | cell.backgroundView = nil 42 | self.mode = .explore 43 | 44 | collectionView.deselectItem(at: indexPath, animated: true) 45 | return false 46 | } else { 47 | return true 48 | } 49 | } 50 | 51 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 52 | guard let cell = collectionView.cellForItem(at: indexPath) as? SelectPlanetCollectionViewCell else { return } 53 | 54 | if cell.isSelected { 55 | cell.planetNameLabel.textColor = .black 56 | cell.backgroundView = cell.planetBackgroundView 57 | 58 | self.mode = .search(planet: PlanetConstants.planetsEn[indexPath.row]) 59 | } 60 | } 61 | 62 | func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { 63 | if let cell = collectionView.cellForItem(at: indexPath) as? SelectPlanetCollectionViewCell { 64 | cell.planetNameLabel.textColor = .white 65 | cell.backgroundView = nil 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tars/Tars/View/UniverseSearch/Collection/UniverseSearch/UniverseSearchViewController+FlowLayoutDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UniverseSearchViewController+FlowLayoutDelegate.swift 3 | // Tars 4 | // 5 | // Created by 김소현 on 2022/11/04. 6 | // 7 | 8 | import UIKit 9 | 10 | extension UniverseSearchViewController: UICollectionViewDelegateFlowLayout { 11 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 12 | 13 | let cellWidth = screenWidth * 0.27 14 | let cellHeight = screenHeight * 0.17 15 | 16 | return CGSize(width: cellWidth, height: cellHeight) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tars/Tars/View/UniverseSearch/ViewController/UniverseMainViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UniverseMainViewController.swift 3 | // Tars 4 | // 5 | // Created by ParkJunHyuk on 7/16/24. 6 | // 7 | 8 | import UIKit 9 | import Combine 10 | import SceneKit 11 | import ARKit 12 | 13 | import SnapKit 14 | import Then 15 | 16 | final class UniverseMainViewController: UIViewController { 17 | 18 | // MARK: - Properties 19 | 20 | private var planetObjectList: [String: SCNNode] = [:] 21 | private var circleCenter: CGPoint = .zero 22 | 23 | private var universeLocationViewModel = UniverseLocationViewModel() 24 | var universeModeViewModel = UniverseModeViewModel(sceneKitAudioVolumeManager: SceneKitAudioVolumeManager()) 25 | 26 | private var cancellables = Set() 27 | 28 | private let planetCollectionViewFlowLayout = UICollectionViewFlowLayout() 29 | 30 | var planetListData = [PlanetInfo]() 31 | var selectedIndexPath: IndexPath? 32 | 33 | private var audioManager = AudioManager.shared 34 | 35 | // MARK: - UI Properties 36 | 37 | private lazy var arSceneView: ARSCNView = ARSCNView() 38 | private let searchGuideLabel: UILabel = UILabel() 39 | private let selectPlanetCollectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) 40 | 41 | // MARK: - Custom UI Properties 42 | 43 | private var guideCircleView = CustomCircleView() 44 | private var selectedSquareView = CustomSquareView() 45 | private var guideArrowView = CustomArrowView() 46 | private var onboardingView = OnboardingView() 47 | 48 | // MARK: - Life Cycle 49 | 50 | override func viewDidLoad() { 51 | super.viewDidLoad() 52 | 53 | configureDelegate() 54 | configureStyle() 55 | configureHierarchy() 56 | configureLayout() 57 | configureTapGesture() 58 | configureVoiceOver() 59 | configureNavigationTitle() 60 | 61 | Planet.allCases.forEach { 62 | planetListData.append( 63 | PlanetInfo( 64 | planetIdName: $0.nameEnglish, 65 | planetName: $0.planetName, 66 | planetImage: $0.nameEnglish, 67 | isSelected: .notSelect 68 | ) 69 | ) 70 | } 71 | 72 | setUpNetworkState() 73 | setUpShowSettingBindidng() 74 | setUpBodiesBinding() 75 | configureModeBinding() 76 | 77 | selectedSquareView.isHidden = true 78 | guideArrowView.isHidden = true 79 | } 80 | 81 | override func viewWillAppear(_ animated: Bool) { 82 | super.viewWillAppear(animated) 83 | 84 | let configuration = ARWorldTrackingConfiguration() 85 | configuration.worldAlignment = .gravityAndHeading 86 | arSceneView.session.run(configuration) 87 | } 88 | 89 | override func viewDidAppear(_ animated: Bool) { 90 | super.viewDidAppear(animated) 91 | view.layoutIfNeeded() 92 | circleCenter = guideCircleView.center 93 | } 94 | 95 | override func viewWillDisappear(_ animated: Bool) { 96 | super.viewWillDisappear(animated) 97 | 98 | arSceneView.session.pause() 99 | } 100 | 101 | override func viewDidDisappear(_ animated: Bool) { 102 | super.viewDidDisappear(animated) 103 | 104 | universeModeViewModel.muteAllNode() 105 | } 106 | } 107 | 108 | // MARK: - objc Extension 109 | 110 | private extension UniverseMainViewController { 111 | /// square 를 눌렀을 때 화면 전환을 하는 메서드 112 | @objc func squareViewTapped() { 113 | let infoViewController = InfoViewController() 114 | self.navigationController?.pushViewController(infoViewController, animated: true) 115 | } 116 | } 117 | 118 | // MARK: - Configure View Layout 119 | 120 | private extension UniverseMainViewController { 121 | 122 | /// Delegate 를 할당하는 메서드 123 | func configureDelegate() { 124 | arSceneView.delegate = self 125 | selectPlanetCollectionView.delegate = self 126 | selectPlanetCollectionView.dataSource = self 127 | } 128 | 129 | /// UIView 의 Layout 을 할당하는 메서드 130 | func configureStyle() { 131 | onboardingView.do { 132 | $0.layer.zPosition = 1 133 | } 134 | 135 | planetCollectionViewFlowLayout.do { 136 | $0.scrollDirection = .horizontal 137 | $0.minimumLineSpacing = screenWidth * 0.05 138 | $0.minimumInteritemSpacing = CGFloat(UInt16.max) 139 | } 140 | 141 | searchGuideLabel.do { 142 | $0.text = LocalizableKeys.collectionViewTitle.localized 143 | $0.accessibilityHint = LocalizableKeys.collectionViewContent.localized 144 | $0.textColor = .white 145 | $0.textAlignment = .center 146 | $0.font = .systemFont(ofSize: 20, weight: .semibold) 147 | } 148 | 149 | selectPlanetCollectionView.do { 150 | let layout = UICollectionViewFlowLayout().then { 151 | $0.scrollDirection = .horizontal 152 | $0.minimumLineSpacing = screenWidth * 0.05 153 | $0.minimumInteritemSpacing = CGFloat(UInt16.max) 154 | } 155 | 156 | $0.register(SelectPlanetMainCollectionViewCell.self, forCellWithReuseIdentifier: SelectPlanetMainCollectionViewCell.identifier) 157 | $0.backgroundColor = .black 158 | $0.showsHorizontalScrollIndicator = true 159 | $0.contentInset = UIEdgeInsets(top: 0, left: screenWidth * 0.09, bottom: 0, right: screenWidth * 0.09) 160 | $0.allowsMultipleSelection = false 161 | $0.collectionViewLayout = layout 162 | } 163 | } 164 | 165 | /// VC 에 출력할 요소를 할당하는 메서드 166 | func configureHierarchy() { 167 | view.addSubviews(onboardingView, arSceneView, selectPlanetCollectionView, searchGuideLabel) 168 | 169 | arSceneView.addSubviews(guideCircleView, guideArrowView, selectedSquareView) 170 | } 171 | 172 | /// Snapkit 을 이용해 AutoLayout 을 설계하는 메서드 173 | func configureLayout() { 174 | onboardingView.snp.makeConstraints { 175 | $0.edges.equalToSuperview() 176 | } 177 | 178 | searchGuideLabel.snp.makeConstraints { 179 | $0.top.equalToSuperview().offset(screenHeight * 0.7) 180 | $0.centerX.equalToSuperview() 181 | } 182 | 183 | selectPlanetCollectionView.snp.makeConstraints { 184 | $0.height.equalTo(screenHeight * 0.35) 185 | $0.horizontalEdges.equalToSuperview() 186 | $0.bottom.equalToSuperview() 187 | } 188 | 189 | arSceneView.snp.makeConstraints { 190 | $0.top.equalTo(view.safeAreaLayoutGuide) 191 | $0.leading.trailing.bottom.equalToSuperview() 192 | } 193 | 194 | guideCircleView.snp.makeConstraints { 195 | $0.top.equalToSuperview().offset(screenHeight * 0.24 / 2) 196 | $0.centerX.equalToSuperview() 197 | } 198 | 199 | selectedSquareView.frame = CGRect(x: 0, y: 0, width: screenWidth / 5.65, height: (screenWidth / 5.65) + (screenHeight / 26.375)) 200 | 201 | guideArrowView.frame = CGRect(x: 0, y: 0, width: 50, height: 50) 202 | } 203 | 204 | /// 제스처를 할당하기 위한 메서드 205 | func configureTapGesture() { 206 | let selectedSquareViewTap = UITapGestureRecognizer(target: self, action: #selector(squareViewTapped)) 207 | selectedSquareView.addGestureRecognizer(selectedSquareViewTap) 208 | } 209 | 210 | /// VoiceOver 를 구성하기 위한 메서드 211 | func configureVoiceOver() { 212 | onboardingView.isAccessibilityElement = true 213 | onboardingView.accessibilityLabel = LocalizableKeys.onboardingInstructionstring.localized 214 | UIAccessibility.post(notification: .layoutChanged, argument: onboardingView) 215 | 216 | self.accessibilityElements = [selectPlanetCollectionView] 217 | } 218 | 219 | /// VC 의 NavigationTitle 을 설정하는 메서드 220 | func configureNavigationTitle() { 221 | self.navigationController?.navigationBar.layer.zPosition = 0 222 | self.navigationController?.isNavigationBarHidden = false 223 | self.navigationController?.title = LocalizableKeys.exploreUniverseNavigationTitle.localized 224 | self.navigationController?.navigationBar.titleTextAttributes = [ NSAttributedString.Key.foregroundColor: UIColor.white] 225 | self.navigationController?.navigationBar.backgroundColor = .black 226 | self.navigationItem.rightBarButtonItem?.tintColor = .white 227 | self.navigationItem.hidesBackButton = true 228 | self.navigationController?.navigationBar.layer.zPosition = -1 229 | } 230 | } 231 | 232 | // MARK: - ARSCNViewDelegate 233 | 234 | extension UniverseMainViewController: ARSCNViewDelegate { 235 | func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) { 236 | switch universeModeViewModel.modeStateSubject.value { 237 | case .explore: 238 | explore() 239 | case .search(planet: let name): 240 | search(for: name) 241 | } 242 | } 243 | } 244 | 245 | // MARK: - LocationManagerDelegate 246 | /* 247 | private extension UniverseMainViewController { 248 | 249 | func updateUserLocation() { 250 | Task { 251 | // let bodies = try await HorizonsAPIManager().requestBodies() 252 | 253 | } 254 | } 255 | } 256 | */ 257 | 258 | // MARK: - 탐색 / 검색 모드 기능 259 | 260 | extension UniverseMainViewController { 261 | // MARK: - 탐색 모드 기능 262 | private func explore() { 263 | var detectNode: SCNNode? 264 | var nodeCenter: CGPoint = .zero 265 | var minDistance: CGFloat = screenHeight 266 | 267 | guard let pointOfView = arSceneView.pointOfView else { return } 268 | let detectNodes = arSceneView.nodesInsideFrustum(of: pointOfView) // 화면에 들어온 노드 리스트 269 | 270 | for node in detectNodes { 271 | let nodePosition = arSceneView.projectPoint(node.position) 272 | let nodeScreenPos = nodePosition.toCGPoint() 273 | let distance = circleCenter.distanceTo(nodeScreenPos) 274 | 275 | // 원 안에 들어온 가장 짧은 거리, 노드, 화면상의 위치 저장 276 | if distance < screenWidth / 3 && distance < minDistance { 277 | detectNode = node 278 | nodeCenter = nodeScreenPos 279 | minDistance = distance 280 | } 281 | } 282 | 283 | if let detectNode = detectNode { 284 | // 원 안에 들어온 노드 존재했을 때 285 | guard let detectedPlanet = detectNode.name else { return } 286 | 287 | let nodeOrigin = CGPoint(x: nodeCenter.x - screenWidth / 11.3, y: nodeCenter.y - screenWidth / 11.3) 288 | setDetectedLayout(name: detectedPlanet, point: nodeOrigin) 289 | 290 | universeModeViewModel.selectedNodeExploreMode(selectPlanetName: detectedPlanet) 291 | universeModeViewModel.updateDetectedNodeName(detectedPlanet) 292 | } else { 293 | // 탐지된 노드가 없을 때 294 | setNotDetectedLayout() 295 | universeModeViewModel.exploreMode() 296 | } 297 | } 298 | 299 | // MARK: - 검색 모드 기능 300 | private func search(for name: String) { 301 | guard let node = planetObjectList[name] else {return} 302 | let nodePosition = arSceneView.projectPoint(node.position) 303 | let nodeScreenPos = nodePosition.toCGPoint() 304 | let distanceToCenter = circleCenter.distanceTo(nodeScreenPos) 305 | 306 | universeModeViewModel.searchMode(selectPlanetName: name) 307 | 308 | if nodePosition.z >= 1 { 309 | // 찾는 노드가 뒤에 있을 때 310 | setNotDetectedLayout() 311 | setArrowLayout(point: nodeScreenPos, locatedBehind: true) 312 | } else if distanceToCenter >= (screenWidth / 3) { 313 | // 찾는 노드가 원의 바깥에 있을 때 314 | setNotDetectedLayout() 315 | setArrowLayout(point: nodeScreenPos) 316 | } else { 317 | // 찾는 노드가 원 안에 있을 때 318 | let nodeOrigin = CGPoint(x: nodeScreenPos.x - screenWidth / 11.3, y: nodeScreenPos.y - screenWidth / 11.3) 319 | setArrowHidden() 320 | setDetectedLayout(name: name, point: nodeOrigin) 321 | 322 | universeModeViewModel.updateAnnounceCardinal(.None) 323 | universeModeViewModel.updateDetectedNodeName(name) 324 | } 325 | } 326 | } 327 | 328 | // MARK: - ARKit 관련 Planet Sphere, Node 메서드 329 | 330 | private extension UniverseMainViewController { 331 | /// 행성의 데이터를 통해 구를 만드는 메서드 332 | /// - Parameters: 333 | /// - plaaents : API 를 통해 받은 Planet 데이터를 갖고 있는 배열 334 | /// - Returns: SCNSphere 으로 만들어진 Planet 구체의 데이터를 담고 있는 배열 335 | func makePlanetSphere(planets: [Body]) -> [SCNSphere] { 336 | return planets.map { 337 | let sphere = SCNSphere(radius: 0.2) 338 | sphere.firstMaterial?.diffuse.contents = UIImage(named: $0.name + "_Map") 339 | 340 | return sphere 341 | } 342 | } 343 | 344 | /// Planet Sphere 를 통해 Node ( 위치 정보를 담고 있는 ) 를 만드는 메서드 345 | /// - Parameters: 346 | /// - plaaents : API 를 통해 받은 Planet 데이터를 갖고 있는 배열 347 | /// - planetSpheres : makePlanetSphere 메서드 에서 만든 각 행성에 대한 SCNSphere 348 | /// - Returns: planetSpheres 의 데이터를 통해 만든 SCNNode 배열 349 | func makePlanetNode(planets: [Body], planetSpheres: [SCNSphere]) -> [SCNNode] { 350 | return planets.enumerated().map { index, planet in 351 | let sphereNode = SCNNode(geometry: planetSpheres[index]) 352 | sphereNode.position = SCNVector3(planet.coordinate.x, planet.coordinate.y, planet.coordinate.z) 353 | sphereNode.name = planet.name 354 | 355 | planetObjectList[planet.name] = sphereNode 356 | 357 | return sphereNode 358 | } 359 | } 360 | 361 | /// SNCScene 에 PlanetNode 를 배치하는 메서드 362 | /// - Parameters: 363 | /// - scene : SceneKit 으로 만든 객체를 해당 scene 에 렌더링하기 위한 View 364 | /// - planetNodes : Planet 데이터를 통해 만든 SCNNode 365 | func setupPlanetPosition(to scene: SCNScene?, planetNodes: [SCNNode]) { 366 | planetNodes.forEach { 367 | scene?.rootNode.addChildNode($0) 368 | } 369 | } 370 | 371 | /// PlanetNode 에 Audio 소스를 추가하기 위한 메서드 372 | /// - Parameters: 373 | /// - plaaents : API 를 통해 받은 Planet 데이터를 갖고 있는 배열 374 | /// - planetNodes : makePlanetNode 메서드 에서 만든 각 행성에 대한 SCNNode 375 | /// - Returns: audioSource 의 정보를 담고 있는 SCNNode 배열 376 | func addAudioToPlanetNode(planets: [Body], planetNodes: [SCNNode]) -> [SCNNode] { 377 | return planets.enumerated().map { index, planet in 378 | let audioSource = SCNAudioSource(fileNamed: "\(AudioMode.search.prefix)\(planet.name).\(ResourceConstants.mp3.name)")! 379 | 380 | // 노드와 해당 위치에와 소스의 볼륨, 반향 및 거리에 따라 자동으로 변경 381 | audioSource.isPositional = true 382 | audioSource.volume = AudioVolume.half.volume 383 | audioSource.loops = true 384 | audioSource.load() 385 | 386 | let scnPlayer = SCNAudioPlayer(source: audioSource) 387 | 388 | planetNodes[index].removeAllAudioPlayers() 389 | planetNodes[index].addAudioPlayer(scnPlayer) 390 | 391 | var planetObjectSound: [String: SCNAudioPlayer] = [:] 392 | planetObjectSound[planet.name] = scnPlayer 393 | universeModeViewModel.sceneKitAudioVolumeManager.setSoundPlayer(planetObjectSound) 394 | 395 | return planetNodes[index] 396 | } 397 | } 398 | } 399 | 400 | // TODO: - 오류가 나지 않기 위해 실행을 위한 메서드 (VM 으로 바꾸어야 합니다.) 401 | extension UniverseMainViewController { 402 | private func setArrowHidden() { 403 | DispatchQueue.main.async { 404 | self.guideArrowView.isHidden = true 405 | } 406 | } 407 | 408 | // 검색 시 화살표 레이아웃 설정 409 | private func setArrowLayout(point: CGPoint, locatedBehind: Bool = false) { 410 | var radian = locatedBehind ? atan2(circleCenter.y - point.y, point.x - circleCenter.x) 411 | + .pi : atan2(circleCenter.y - point.y, point.x - circleCenter.x) 412 | var degree = radian.radiansToDegree 413 | 414 | if locatedBehind { 415 | if degree > 45 && degree <= 90 { 416 | degree = 45 417 | } else if degree > 90 && degree < 135 { 418 | degree = 135 419 | } else if degree > 225 && degree < 270 { 420 | degree = 225 421 | } else if degree < 315 && degree >= 270 { 422 | degree = 315 423 | } 424 | radian = degree.degreeToRadians 425 | } 426 | 427 | let dx = screenWidth / 3 * cos(radian) 428 | let dy = screenWidth / 3 * sin(radian) 429 | let arrowPosition = CGPoint(x: circleCenter.x + dx, y: circleCenter.y - dy) 430 | 431 | universeModeViewModel.updateArrowCardinal(universeModeViewModel.getCardinal(angle: degree)) 432 | 433 | DispatchQueue.main.async { 434 | self.guideArrowView.transform = CGAffineTransform(rotationAngle: -radian) 435 | self.guideArrowView.layer.position = arrowPosition 436 | self.guideArrowView.isHidden = false 437 | } 438 | } 439 | 440 | private func setModeChangedLayout(newMode: Mode) { 441 | self.navigationController?.topViewController?.title = newMode.titleText 442 | switch newMode { 443 | case .explore: 444 | setArrowHidden() 445 | universeModeViewModel.updateAnnounceCardinal(.None) 446 | case .search(planet: _): 447 | universeModeViewModel.updateAnnounceCardinal(.None) 448 | } 449 | } 450 | 451 | // 행성이 탐지되지 않았을 때 레이아웃 설정 452 | private func setNotDetectedLayout() { 453 | universeModeViewModel.updateDetectedNodeName("") 454 | DispatchQueue.main.async { 455 | self.guideCircleView.isHidden = false 456 | self.selectedSquareView.isHidden = true 457 | } 458 | } 459 | 460 | // 행성이 탐지되었을 때 레이아웃 설정 461 | private func setDetectedLayout(name: String, point: CGPoint) { 462 | DispatchQueue.main.async { [self] in 463 | 464 | let localizedDetectedNode = Planet(from: name.lowercased())?.planetName 465 | 466 | self.selectedSquareView.frame.origin = point 467 | self.selectedSquareView.planetLabel.text = localizedDetectedNode 468 | 469 | self.guideCircleView.isHidden = true 470 | self.selectedSquareView.isHidden = false 471 | self.selectedSquareView.isAccessibilityElement = true 472 | 473 | // 추후 사용예정 주석 474 | // self.selectedSquareView.accessibilityLabel = planetNameDict[name] ?? name 475 | } 476 | } 477 | 478 | // 행성 detect되었을 때 announce 479 | private func guideDetectedAnnounce(name: String) { 480 | UIAccessibility.post(notification: .layoutChanged, argument: selectedSquareView) 481 | UIAccessibility.post(notification: .announcement, 482 | argument: Planet(from: name.lowercased())?.planetName) 483 | HapticManager.instance.hapticImpact(style: .soft) 484 | PlanetManager.shared.currentPlanet = Planet(from: name.lowercased()) 485 | 486 | self.audioManager.playDetectingAudio(fileName: "Detecting_planet") 487 | } 488 | 489 | /// 화살표 변경시 가이드 음성 490 | private func guideAnnounce(_ newCardinal: Cardinal) { 491 | let announcementText = "\(newCardinal.directionText)" 492 | Task { 493 | try await Task.sleep(nanoseconds: 100) 494 | UIAccessibility.post(notification: .announcement, argument: announcementText) 495 | } 496 | } 497 | } 498 | 499 | // MARK: - Binding Methods 500 | 501 | private extension UniverseMainViewController { 502 | 503 | private func setUpBodiesBinding() { 504 | universeLocationViewModel.$bodies 505 | .sink { [weak self] bodies in 506 | self?.setUpPlanetBinding() 507 | } 508 | .store(in: &cancellables) 509 | } 510 | 511 | private func setUpNetworkState() { 512 | universeLocationViewModel.$isSuccess 513 | .sink { [weak self] success in 514 | if let success = success { 515 | if success { 516 | self?.showOnboarding() 517 | } else { 518 | print("인터넷 연결 불가") 519 | } 520 | } 521 | } 522 | .store(in: &cancellables) 523 | } 524 | 525 | func setUpShowSettingBindidng() { 526 | 527 | universeLocationViewModel.$showSettingAlert 528 | .sink { [weak self] showAlert in 529 | if showAlert { 530 | self?.showLocationSettingsAlert() 531 | } 532 | } 533 | .store(in: &cancellables) 534 | universeLocationViewModel.updateLocation() 535 | } 536 | 537 | func showOnboarding() { 538 | Task { 539 | self.onboardingView.isAccessibilityElement = false 540 | self.onboardingView.removeFromSuperview() 541 | self.navigationController?.navigationBar.layer.zPosition = 0 542 | 543 | // UIAccessibility.post(notification: .layoutChanged, argument: self.sceneView) 544 | 545 | self.navigationController?.isNavigationBarHidden = false 546 | self.navigationController?.topViewController?.title = LocalizableKeys.exploreUniverseNavigationTitle.localized 547 | self.navigationController?.navigationBar.titleTextAttributes = [ NSAttributedString.Key.foregroundColor: UIColor.white] 548 | self.navigationController?.navigationBar.backgroundColor = .black 549 | 550 | let backBarButtonItem = UIBarButtonItem(title: self.navigationItem.title, style: .plain, target: self, action: nil) 551 | self.navigationItem.backBarButtonItem = backBarButtonItem 552 | backBarButtonItem.tintColor = .customYellow 553 | 554 | self.navigationItem.rightBarButtonItem?.tintColor = .white 555 | self.navigationItem.hidesBackButton = true 556 | } 557 | } 558 | 559 | func showLocationSettingsAlert() { 560 | let alert = UIAlertController(title: LocalizableKeys.locationUsageMessage.localized, 561 | message: LocalizableKeys.locationAuthRequest.localized, 562 | preferredStyle: .alert) 563 | let defaultAction = UIAlertAction(title: LocalizableKeys.defaultAction.localized, style: .default, handler: { _ in 564 | guard let url = URL(string: UIApplication.openSettingsURLString) else { return } 565 | DispatchQueue.main.async { 566 | UIApplication.shared.open(url) 567 | } 568 | }) 569 | let destructiveAction = UIAlertAction(title: LocalizableKeys.cancel.localized, style: .destructive, handler: nil) 570 | 571 | alert.addAction(destructiveAction) 572 | alert.addAction(defaultAction) 573 | present(alert, animated: true, completion: nil) 574 | } 575 | 576 | func configureModeBinding() { 577 | universeModeViewModel.modeStateSubject 578 | .receive(on: DispatchQueue.main) 579 | .sink { [weak self] newMode in 580 | self?.setModeChangedLayout(newMode: newMode) 581 | } 582 | .store(in: &cancellables) 583 | 584 | universeModeViewModel.detectedNodeSubject 585 | .receive(on: DispatchQueue.main) 586 | .scan(("", "")) { (old, new) in (old.1, new) } 587 | .filter { oldValue, newValue in 588 | oldValue != newValue && !newValue.isEmpty 589 | } 590 | .map { $0.1 } 591 | .sink { [weak self] detectedNodeName in 592 | self?.guideDetectedAnnounce(name: detectedNodeName) 593 | } 594 | .store(in: &cancellables) 595 | 596 | universeModeViewModel.arrowCardinalSubject 597 | .filter { [weak self] newCardinal in 598 | guard let self = self else { return false } 599 | return !self.universeModeViewModel.announceCardinal.isNear(new: newCardinal) 600 | } 601 | .receive(on: DispatchQueue.main) 602 | .sink { [weak self] newCardinal in 603 | self?.universeModeViewModel.announceCardinal = newCardinal 604 | self?.guideAnnounce(newCardinal) 605 | } 606 | .store(in: &cancellables) 607 | } 608 | } 609 | 610 | // MARK: - 행성의 위치 좌표 및 AR 노드 생성 611 | private extension UniverseMainViewController { 612 | 613 | func setUpPlanetBinding() { 614 | universeLocationViewModel.$bodies 615 | .sink { [weak self] planets in 616 | guard let self = self else { return } 617 | let planetSpheres = self.makePlanetSphere(planets: planets) 618 | let planetNodes = self.makePlanetNode(planets: planets, planetSpheres: planetSpheres) 619 | self.setupPlanetPosition(to: self.arSceneView.scene, planetNodes: planetNodes) 620 | self.addAudioToPlanetNode(planets: planets, planetNodes: planetNodes) 621 | } 622 | .store(in: &cancellables) 623 | } 624 | 625 | func updateSceneWithNodes(_ nodes: [SCNNode]) { 626 | arSceneView.scene.rootNode.enumerateChildNodes { (node, _) in 627 | node.removeFromParentNode() 628 | } 629 | for node in nodes { 630 | arSceneView.scene.rootNode.addChildNode(node) 631 | } 632 | } 633 | } 634 | -------------------------------------------------------------------------------- /Tars/Tars/View/UniverseSearch/ViewController/UniverseSearchViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UniverseSearchViewController.swift 3 | // Tars 4 | // 5 | // Created by 김소현 on 2022/11/02. 6 | // 7 | 8 | import UIKit 9 | import SceneKit 10 | import ARKit 11 | 12 | class UniverseSearchViewController: UIViewController, ARSCNViewDelegate, LocationManagerDelegate, UIGestureRecognizerDelegate { 13 | 14 | // MARK: properties 15 | 16 | private var guideCircleView = CustomCircleView() 17 | private var selectedSquareView = CustomSquareView() 18 | private var guideArrowView = CustomArrowView() 19 | private var onboardingView = OnboardingView() 20 | 21 | private var audioManager = AudioManager.shared 22 | 23 | // TapGesture 선언 24 | let selectedSquareViewTap = UITapGestureRecognizer() 25 | 26 | var mode: Mode = .explore { 27 | didSet { 28 | setModeChangedLayout() 29 | } 30 | } 31 | var planetObjectList: [String: SCNNode] = [:] 32 | var planetObjectSound: [String: SCNAudioPlayer] = [:] 33 | var circleCenter: CGPoint = .zero 34 | 35 | var detectedNode: String = "" { 36 | didSet { 37 | if oldValue != detectedNode && detectedNode != "" { 38 | guideDetectedAnnounce(name: detectedNode) 39 | } 40 | } 41 | } 42 | 43 | var announceCardinal: Cardinal = .None 44 | var arrowCardinal: Cardinal = .None { 45 | didSet { 46 | if !announceCardinal.isNear(new: self.arrowCardinal) { 47 | guideAnnounce() 48 | announceCardinal = arrowCardinal 49 | } 50 | } 51 | } 52 | 53 | let searchGuideLabel: UILabel = { 54 | let label: UILabel = UILabel() 55 | label.text = LocalizableKeys.collectionViewTitle.localized 56 | label.accessibilityHint = LocalizableKeys.collectionViewContent.localized 57 | label.textColor = .white 58 | label.textAlignment = .center 59 | label.font = UIFont.systemFont(ofSize: 20, weight: .semibold) 60 | return label 61 | }() 62 | 63 | public let selectPlanetCollectionView: UICollectionView = { 64 | let layout = UICollectionViewFlowLayout() 65 | layout.scrollDirection = .horizontal 66 | layout.minimumLineSpacing = screenWidth * 0.05 67 | layout.minimumInteritemSpacing = CGFloat(UInt16.max) 68 | 69 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) 70 | collectionView.register(SelectPlanetCollectionViewCell.self, 71 | forCellWithReuseIdentifier: SelectPlanetCollectionViewCell.identifier) 72 | collectionView.contentInset = UIEdgeInsets(top: 0, left: screenWidth * 0.09, bottom: 0, right: screenWidth * 0.09) 73 | collectionView.showsHorizontalScrollIndicator = true 74 | collectionView.backgroundColor = .black 75 | 76 | return collectionView 77 | }() 78 | 79 | /// ARKit 을 사용하기 위한 view 선언 80 | lazy var sceneView: ARSCNView = { 81 | let sceneView = ARSCNView() 82 | sceneView.delegate = self 83 | return sceneView 84 | }() 85 | 86 | // MARK: Life cycle 87 | 88 | override func viewDidLoad() { 89 | super.viewDidLoad() 90 | 91 | onboardingView.isAccessibilityElement = true 92 | onboardingView.accessibilityLabel = LocalizableKeys.onboardingInstructionstring.localized 93 | UIAccessibility.post(notification: .layoutChanged, argument: onboardingView) 94 | 95 | [guideCircleView, guideArrowView, selectedSquareView].forEach { sceneView.addSubview($0) } 96 | [onboardingView, sceneView, selectPlanetCollectionView, searchGuideLabel].forEach { view.addSubview($0) } 97 | configureConstraints() 98 | 99 | self.accessibilityElements = [selectPlanetCollectionView] 100 | 101 | DispatchQueue.main.asyncAfter(deadline: .now() + 7.0) { 102 | self.onboardingView.isAccessibilityElement = false 103 | self.onboardingView.removeFromSuperview() 104 | self.navigationController?.navigationBar.layer.zPosition = 0 105 | 106 | // navigation title 설정 107 | self.navigationController?.isNavigationBarHidden = false 108 | self.navigationController?.topViewController?.title = LocalizableKeys.exploreUniverseNavigationTitle.localized 109 | self.navigationController?.navigationBar.titleTextAttributes = [ NSAttributedString.Key.foregroundColor: UIColor.white] 110 | self.navigationController?.navigationBar.backgroundColor = .black 111 | 112 | self.navigationItem.hidesBackButton = true 113 | } 114 | 115 | selectPlanetCollectionView.delegate = self 116 | selectPlanetCollectionView.dataSource = self 117 | 118 | selectedSquareView.isHidden = true 119 | guideArrowView.isHidden = true 120 | 121 | let locationManager = LocationManager.shared 122 | // locationManager.delegate = self 123 | locationManager.updateLocation() 124 | 125 | var result: Bool = checkAuthorization() 126 | 127 | // 권한을 체크해서 허용인 경우에만 overlay뷰가 없어지도록 구현 128 | _ = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in 129 | if result == true { 130 | timer.invalidate() 131 | DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { 132 | self.onboardingView.isAccessibilityElement = false 133 | self.onboardingView.removeFromSuperview() 134 | self.navigationController?.navigationBar.layer.zPosition = 0 135 | 136 | // UIAccessibility.post(notification: .layoutChanged, argument: self.sceneView) 137 | 138 | // navigation title 설정 139 | self.navigationController?.isNavigationBarHidden = false 140 | self.navigationController?.topViewController?.title = LocalizableKeys.exploreUniverseNavigationTitle.localized 141 | self.navigationController?.navigationBar.titleTextAttributes = [ NSAttributedString.Key.foregroundColor: UIColor.white] 142 | self.navigationController?.navigationBar.backgroundColor = .black 143 | 144 | let backBarButtonItem = UIBarButtonItem(title: self.navigationItem.title, style: .plain, target: self, action: nil) 145 | self.navigationItem.backBarButtonItem = backBarButtonItem 146 | backBarButtonItem.tintColor = .customYellow 147 | 148 | self.navigationItem.rightBarButtonItem?.tintColor = .white 149 | self.navigationItem.hidesBackButton = true 150 | 151 | } 152 | } else { 153 | result = self.checkAuthorization() 154 | } 155 | } 156 | 157 | // TapGesture와 View 연결 158 | selectedSquareViewTap.addTarget(self, action: #selector(squareViewTapped)) 159 | selectedSquareView.addGestureRecognizer(selectedSquareViewTap) 160 | } 161 | 162 | /// TapGesture 화면 전환 동작 163 | @objc func squareViewTapped() { 164 | let infoViewController = InfoViewController() 165 | self.navigationController?.pushViewController(infoViewController, animated: true) 166 | } 167 | 168 | /// 행성을 배치하기 위한 함수 169 | private func setPlanetPosition(to scene: SCNScene?, planets: [Body]) { 170 | for planet in planets { 171 | if !PlanetConstants.planetsEn.contains(planet.name) { 172 | continue 173 | } else { 174 | let sphere = SCNSphere(radius: 0.2) 175 | sphere.firstMaterial?.diffuse.contents = UIImage(named: planet.name + ResourceConstants.map.rawValue) 176 | let sphereNode = SCNNode(geometry: sphere) 177 | sphereNode.position = SCNVector3(planet.coordinate.x, planet.coordinate.y, planet.coordinate.z) 178 | sphereNode.name = planet.name 179 | scene?.rootNode.addChildNode(sphereNode) 180 | planetObjectList[planet.name] = sphereNode 181 | 182 | let audioSource: SCNAudioSource = { 183 | let source = SCNAudioSource(fileNamed: "\(AudioMode.search.prefix)\(planet.name).\(ResourceConstants.mp3.name)")! 184 | // TODO: 강제언래핑 제거하기 185 | /// 노드와 해당 위치에와 소스의 볼륨, 반향 및 거리에 따라 자동으로 변경 186 | source.isPositional = true 187 | source.volume = AudioVolume.half.volume 188 | /// 오디오 소스를 반복적으로 재상할지 여부를 결정 189 | source.loops = true 190 | source.load() 191 | return source 192 | }() 193 | 194 | let scnPlayer = SCNAudioPlayer(source: audioSource) 195 | planetObjectSound[planet.name] = scnPlayer 196 | sphereNode.removeAllAudioPlayers() 197 | sphereNode.addAudioPlayer(scnPlayer) 198 | } 199 | } 200 | } 201 | 202 | /// 위치사용 및 카메라 사용권한을 체크하기 위한 함수 (사용권한이 모두 허용된 경우에만 true를 반환) 203 | func checkAuthorization() -> Bool { 204 | 205 | let locationStatus = CLLocationManager.authorizationStatus() 206 | let cameraStatus = AVCaptureDevice.authorizationStatus(for: .video) 207 | 208 | if locationStatus == .authorizedAlways || 209 | locationStatus == .authorizedWhenInUse && 210 | cameraStatus == .authorized { 211 | return true 212 | } else { 213 | return false 214 | } 215 | } 216 | 217 | private func configureConstraints() { 218 | sceneView.anchor(top: view.topAnchor, leading: view.leadingAnchor, bottom: view.bottomAnchor, trailing: view.trailingAnchor, paddingTop: screenHeight * 0.1) 219 | 220 | onboardingView.layer.zPosition = 2 221 | onboardingView.centerX(inView: view) 222 | // onboardingView.anchor(top: view.topAnchor, paddingTop: screenHeight * 0.23) 223 | 224 | onboardingView.layer.zPosition = 1 225 | self.navigationController?.navigationBar.layer.zPosition = -1 226 | 227 | guideCircleView.centerX(inView: view) 228 | guideCircleView.anchor(top: view.topAnchor, paddingTop: screenHeight * 0.23) 229 | 230 | searchGuideLabel.centerX(inView: view) 231 | searchGuideLabel.anchor(top: view.topAnchor, paddingTop: screenHeight * 0.7) 232 | 233 | selectPlanetCollectionView.anchor(top: view.topAnchor, paddingTop: screenHeight * 0.68) 234 | selectPlanetCollectionView.setHeight(height: screenHeight * 0.35) 235 | selectPlanetCollectionView.setWidth(width: screenWidth) 236 | selectPlanetCollectionView.centerX(inView: view) 237 | 238 | selectedSquareView.frame = CGRect(x: 0, y: 0, width: screenWidth / 5.65, height: (screenWidth / 5.65) + (screenHeight / 26.375)) 239 | guideArrowView.frame = CGRect(x: 0, y: 0, width: 50, height: 50) 240 | } 241 | 242 | override func viewWillAppear(_ animated: Bool) { 243 | super.viewWillAppear(animated) 244 | 245 | let configuration = ARWorldTrackingConfiguration() 246 | configuration.worldAlignment = .gravityAndHeading 247 | sceneView.session.run(configuration) 248 | } 249 | 250 | override func viewDidAppear(_ animated: Bool) { 251 | super.viewDidAppear(animated) 252 | circleCenter = guideCircleView.center 253 | } 254 | 255 | override func viewWillDisappear(_ animated: Bool) { 256 | super.viewWillDisappear(animated) 257 | sceneView.session.pause() 258 | } 259 | 260 | override func viewDidDisappear(_ animated: Bool) { 261 | super.viewDidDisappear(animated) 262 | muteExploreSearchModeSound(soundPlayer: planetObjectSound) 263 | } 264 | 265 | // MARK: - LocationManagerDelegate 266 | func didUpdateUserLocation() { 267 | Task { 268 | let bodies = try await HorizonsAPIManager().requestBodies() 269 | // TODO: - API 임시 변경 해결 270 | // let bodies = try await AstronomyAPIManager().requestBodies() 271 | setPlanetPosition(to: sceneView.scene, planets: bodies) 272 | } 273 | } 274 | 275 | // MARK: - ARSCNViewDelegate 276 | func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) { 277 | switch mode { 278 | case .explore: 279 | explore() 280 | case .search(planet: let name): 281 | search(for: name) 282 | } 283 | } 284 | } 285 | 286 | // MARK: - 레이아웃 설정 함수 287 | extension UniverseSearchViewController { 288 | // 모드 변경에 따른 레이아웃 설정 289 | private func setModeChangedLayout() { 290 | self.navigationController?.topViewController?.title = mode.titleText 291 | switch mode { 292 | case .explore: 293 | setArrowHidden() 294 | announceCardinal = .None 295 | case .search(planet: _): 296 | announceCardinal = .None 297 | } 298 | } 299 | 300 | // 행성이 탐지되지 않았을 때 레이아웃 설정 301 | private func setNotDetectedLayout() { 302 | detectedNode = String() 303 | DispatchQueue.main.async { 304 | self.guideCircleView.isHidden = false 305 | self.selectedSquareView.isHidden = true 306 | } 307 | } 308 | 309 | // 행성이 탐지되었을 때 레이아웃 설정 310 | private func setDetectedLayout(name: String, point: CGPoint) { 311 | detectedNode = name 312 | DispatchQueue.main.async { [self] in 313 | 314 | let localizedDetectedNode = Planet(from: detectedNode.lowercased())?.planetName 315 | 316 | self.selectedSquareView.frame.origin = point 317 | self.selectedSquareView.planetLabel.text = localizedDetectedNode 318 | 319 | self.guideCircleView.isHidden = true 320 | self.selectedSquareView.isHidden = false 321 | self.selectedSquareView.isAccessibilityElement = true 322 | 323 | // 추후 사용예정 주석 324 | // self.selectedSquareView.accessibilityLabel = planetNameDict[name] ?? name 325 | } 326 | } 327 | 328 | // 검색 시 화살표 레이아웃 설정 329 | private func setArrowLayout(point: CGPoint, locatedBehind: Bool = false) { 330 | var radian = locatedBehind ? atan2(circleCenter.y - point.y, point.x - circleCenter.x) 331 | + .pi : atan2(circleCenter.y - point.y, point.x - circleCenter.x) 332 | var degree = radian.radiansToDegree 333 | 334 | if locatedBehind { 335 | if degree > 45 && degree <= 90 { 336 | degree = 45 337 | } else if degree > 90 && degree < 135 { 338 | degree = 135 339 | } else if degree > 225 && degree < 270 { 340 | degree = 225 341 | } else if degree < 315 && degree >= 270 { 342 | degree = 315 343 | } 344 | radian = degree.degreeToRadians 345 | } 346 | 347 | let dx = screenWidth / 3 * cos(radian) 348 | let dy = screenWidth / 3 * sin(radian) 349 | let arrowPosition = CGPoint(x: circleCenter.x + dx, y: circleCenter.y - dy) 350 | arrowCardinal = getCardinal(angle: degree) 351 | 352 | DispatchQueue.main.async { 353 | self.guideArrowView.transform = CGAffineTransform(rotationAngle: -radian) 354 | self.guideArrowView.layer.position = arrowPosition 355 | self.guideArrowView.isHidden = false 356 | } 357 | 358 | } 359 | 360 | // 화살표 숨김 설정 361 | private func setArrowHidden() { 362 | DispatchQueue.main.async { 363 | self.guideArrowView.isHidden = true 364 | } 365 | } 366 | 367 | // 행성 detect되었을 때 announce 368 | private func guideDetectedAnnounce(name: String) { 369 | UIAccessibility.post(notification: .layoutChanged, argument: selectedSquareView) 370 | UIAccessibility.post(notification: .announcement, 371 | argument: name) 372 | HapticManager.instance.hapticImpact(style: .soft) 373 | PlanetManager.shared.currentPlanet = Planet(from: name.lowercased()) 374 | 375 | self.audioManager.playAudio(pre: AudioMode.detected.prefix, 376 | fileName: name, 377 | audioExtension: ResourceConstants.wav.name, 378 | audioVolume: AudioVolume.third.volume, 379 | isLoop: false) 380 | } 381 | 382 | /// 화살표 변경시 가이드 음성 383 | private func guideAnnounce() { 384 | let announcementText = "\(arrowCardinal.directionText)" 385 | Task { 386 | try await Task.sleep(nanoseconds: 100) 387 | UIAccessibility.post(notification: .announcement, argument: announcementText) 388 | } 389 | } 390 | } 391 | 392 | // MARK: - enum Mode 393 | extension UniverseSearchViewController { 394 | enum Mode { 395 | case explore 396 | case search(planet: String) 397 | 398 | var titleText: String { 399 | switch self { 400 | case .explore: 401 | return LocalizableKeys.exploreUniverseNavigationTitle.localized 402 | case .search(planet: let name): 403 | return LocalizableKeys.searchingNavigationTitle.localized 404 | } 405 | } 406 | } 407 | 408 | enum Cardinal: Int { 409 | case N = 0 410 | case NE = 1 411 | case E = 2 412 | case SE = 3 413 | case S = 4 414 | case SW = 5 415 | case W = 6 416 | case NW = 7 417 | case None 418 | 419 | func isNear(new: Cardinal) -> Bool { 420 | if new == .None { 421 | return true 422 | } else if self == .None { 423 | return false 424 | } else { 425 | let difference = abs(self.rawValue - new.rawValue) % 7 426 | return difference <= 1 427 | } 428 | } 429 | 430 | var directionText: String { 431 | switch self { 432 | case .N: 433 | return LocalizableKeys.directionUp.localized 434 | case .NE: 435 | return LocalizableKeys.directionUpRight.localized 436 | case .E: 437 | return LocalizableKeys.directionRight.localized 438 | case .SE: 439 | return LocalizableKeys.directionDownRight.localized 440 | case .S: 441 | return LocalizableKeys.directionDown.localized 442 | case .SW: 443 | return LocalizableKeys.directionDownLeft.localized 444 | case .W: 445 | return LocalizableKeys.directionLeft.localized 446 | case .NW: 447 | return LocalizableKeys.directionUpLeft.localized 448 | default: 449 | return "" 450 | } 451 | } 452 | } 453 | 454 | private func getCardinal(angle: CGFloat) -> Cardinal { 455 | let angle = angle < 0 ? angle + 360 : angle 456 | 457 | if angle >= 22.5 && angle < 67.5 { 458 | return Cardinal.NE 459 | } else if angle >= 67.5 && angle < 112.5 { 460 | return Cardinal.N 461 | } else if angle >= 112.5 && angle < 157.5 { 462 | return Cardinal.NW 463 | } else if angle >= 157.5 && angle < 202.5 { 464 | return Cardinal.W 465 | } else if angle >= 202.5 && angle < 247.5 { 466 | return Cardinal.SW 467 | } else if angle >= 247.5 && angle < 292.5 { 468 | return Cardinal.S 469 | } else if angle >= 292.5 && angle < 337.5 { 470 | return Cardinal.SE 471 | } else if (angle >= 337.5 && angle < 360) || (angle >= 0 && angle < 22.5) { 472 | return Cardinal.E 473 | } 474 | 475 | return Cardinal.None 476 | } 477 | } 478 | 479 | // MARK: - 탐색 / 검색 모드 기능 480 | 481 | extension UniverseSearchViewController { 482 | // MARK: - 탐색 모드 기능 483 | private func explore() { 484 | var detectNode: SCNNode? 485 | var nodeCenter: CGPoint = .zero 486 | var minDistance: CGFloat = screenHeight 487 | 488 | guard let pointOfView = sceneView.pointOfView else { return } 489 | let detectNodes = sceneView.nodesInsideFrustum(of: pointOfView) // 화면에 들어온 노드 리스트 490 | 491 | for node in detectNodes { 492 | let nodePosition = sceneView.projectPoint(node.position) 493 | let nodeScreenPos = nodePosition.toCGPoint() 494 | let distance = circleCenter.distanceTo(nodeScreenPos) 495 | 496 | // 원 안에 들어온 가장 짧은 거리, 노드, 화면상의 위치 저장 497 | if distance < screenWidth / 3 && distance < minDistance { 498 | detectNode = node 499 | nodeCenter = nodeScreenPos 500 | minDistance = distance 501 | } 502 | } 503 | 504 | if let detectNode = detectNode { 505 | // 원 안에 들어온 노드 존재했을 때 506 | guard let detectedPlanet = detectNode.name else { return } 507 | 508 | let nodeOrigin = CGPoint(x: nodeCenter.x - screenWidth / 11.3, y: nodeCenter.y - screenWidth / 11.3) 509 | setDetectedLayout(name: detectedPlanet, point: nodeOrigin) 510 | selectedExploreSoundPlay(soundPlayer: planetObjectSound, selectedName: detectedPlanet) 511 | } else { 512 | // 탐지된 노드가 없을 때 513 | setNotDetectedLayout() 514 | exploreModeSoundPlay(soundPlayer: planetObjectSound) 515 | } 516 | } 517 | 518 | // MARK: - 검색 모드 기능 519 | private func search(for name: String) { 520 | guard let node = planetObjectList[name] else {return} 521 | let nodePosition = sceneView.projectPoint(node.position) 522 | let nodeScreenPos = nodePosition.toCGPoint() 523 | let distanceToCenter = circleCenter.distanceTo(nodeScreenPos) 524 | 525 | selectModeSoundPlay(soundPlayer: planetObjectSound, selectedName: name) 526 | 527 | if nodePosition.z >= 1 { 528 | // 찾는 노드가 뒤에 있을 때 529 | setNotDetectedLayout() 530 | setArrowLayout(point: nodeScreenPos, locatedBehind: true) 531 | } else if distanceToCenter >= (screenWidth / 3) { 532 | // 찾는 노드가 원의 바깥에 있을 때 533 | setNotDetectedLayout() 534 | setArrowLayout(point: nodeScreenPos) 535 | } else { 536 | // 찾는 노드가 원 안에 있을 때 537 | let nodeOrigin = CGPoint(x: nodeScreenPos.x - screenWidth / 11.3, y: nodeScreenPos.y - screenWidth / 11.3) 538 | setArrowHidden() 539 | setDetectedLayout(name: name, point: nodeOrigin) 540 | announceCardinal = .None 541 | } 542 | } 543 | } 544 | 545 | // MARK: - 모드에 따른 소리 증감 기능 546 | 547 | extension UniverseSearchViewController { 548 | 549 | // TODO: 반복되는 부분 클래스로 빼서 재사용하기 (볼륨에 따라) 550 | /// 탐색 모드일 때 - 모든 행성의 소리의 음량을 동일하게 551 | private func exploreModeSoundPlay(soundPlayer: [String: SCNAudioPlayer]) { 552 | 553 | for audioPlayer in soundPlayer.values { 554 | guard let avNode = audioPlayer.audioNode as? AVAudioMixing else { return } 555 | 556 | avNode.volume = AudioVolume.half.volume 557 | } 558 | } 559 | 560 | /// 검색 모드일때 - 선택된 이외의 행성의 소리 음소거 561 | private func selectModeSoundPlay(soundPlayer: [String: SCNAudioPlayer], selectedName: String) { 562 | 563 | for name in soundPlayer.keys { 564 | if name == selectedName { 565 | let audioPlayer = soundPlayer[name] 566 | 567 | guard let avNode = audioPlayer?.audioNode as? AVAudioMixing else { return } 568 | 569 | avNode.volume = AudioVolume.half.volume 570 | } else { 571 | let audioPlayer = soundPlayer[name] 572 | 573 | guard let avNode = audioPlayer?.audioNode as? AVAudioMixing else { return } 574 | 575 | avNode.volume = AudioVolume.mute.volume 576 | } 577 | } 578 | } 579 | 580 | /// 탐색된 행성 있을 때 - 탐색된 행성 소리는 증가 다른 행성은 감소 581 | private func selectedExploreSoundPlay(soundPlayer: [String: SCNAudioPlayer], selectedName: String) { 582 | 583 | for name in soundPlayer.keys { 584 | if name == selectedName { 585 | let audioPlayer = soundPlayer[name] 586 | 587 | guard let avNode = audioPlayer?.audioNode as? AVAudioMixing else { return } 588 | 589 | avNode.volume = AudioVolume.max.volume 590 | } else { 591 | let audioPlayer = soundPlayer[name] 592 | 593 | guard let avNode = audioPlayer?.audioNode as? AVAudioMixing else { return } 594 | 595 | avNode.volume = AudioVolume.tenth.volume 596 | } 597 | } 598 | } 599 | 600 | /// 탐색, 검색화면에서의 음소거 601 | private func muteExploreSearchModeSound(soundPlayer: [String: SCNAudioPlayer]) { 602 | 603 | for audioPlayer in soundPlayer.values { 604 | guard let avNode = audioPlayer.audioNode as? AVAudioMixing else { return } 605 | 606 | avNode.volume = AudioVolume.mute.volume 607 | } 608 | } 609 | } 610 | 611 | -------------------------------------------------------------------------------- /Tars/Tars/ViewModel/UniverseLocationViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UniverseLocationViewModel.swift 3 | // Tars 4 | // 5 | // Created by Lena on 2024/8/2. 6 | // 7 | 8 | import Foundation 9 | import AVFoundation 10 | import CoreLocation 11 | import Combine 12 | 13 | class UniverseLocationViewModel: NSObject, ObservableObject { 14 | private let locationManager = LocationManager.shared 15 | @Published var currentLocation: CLLocation? 16 | @Published var isAuthorized: Bool = false 17 | @Published var isSuccess: Bool? 18 | @Published var showSettingAlert: Bool = false 19 | @Published var bodies: [Body] = [] 20 | private var cancellables = Set() 21 | 22 | override init() { 23 | super.init() 24 | bindToSettingAlert() 25 | bindToLocationUpdates() 26 | } 27 | 28 | func bindToLocationUpdates() { 29 | locationManager.$location 30 | .sink { [weak self] location in 31 | self?.currentLocation = location 32 | self?.handleLocationUpdate() 33 | } 34 | .store(in: &cancellables) 35 | } 36 | 37 | func bindToSettingAlert() { 38 | locationManager.$needsSettingAlert 39 | .sink { [weak self] needsAlert in 40 | if needsAlert { 41 | self?.showSettingAlert = true 42 | } 43 | } 44 | .store(in: &cancellables) 45 | } 46 | 47 | func updateLocation() { 48 | locationManager.updateLocation() 49 | } 50 | 51 | func updateAuthorizationStatus() -> Bool { 52 | let locationStatus = CLLocationManager.authorizationStatus() 53 | let cameraStatus = AVCaptureDevice.authorizationStatus(for: .video) 54 | 55 | return (locationStatus == .authorizedAlways || locationStatus == .authorizedWhenInUse) && cameraStatus == .authorized 56 | } 57 | 58 | private func handleLocationUpdate() { 59 | guard let location = currentLocation else { return } 60 | 61 | Task { 62 | do { 63 | let bodies = try await HorizonsAPIManager().requestBodies() 64 | self.bodies = bodies 65 | self.isSuccess = true 66 | } catch { 67 | print("Failed to fetch bodies: \(error.localizedDescription)") 68 | self.isSuccess = false 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tars/Tars/ViewModel/UniverseModeViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UniverseModeViewModel.swift 3 | // Tars 4 | // 5 | // Created by ParkJunHyuk on 8/9/24. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | final class UniverseModeViewModel { 12 | 13 | let sceneKitAudioVolumeManager: SceneKitAudioVolumeProtocol 14 | 15 | /// 현재 방위 정보를 갖고 있는 프로퍼티 16 | @Published var announceCardinal: Cardinal = .None 17 | 18 | /// explore, search 모드에 대한 Publisher 19 | let modeStateSubject = CurrentValueSubject(.explore) 20 | 21 | /// 감지 된 Node 에 대한 정보를 전달하는 Publisher 22 | let detectedNodeSubject = CurrentValueSubject("") 23 | 24 | /// 방위를 나타내는 Publisher 25 | let arrowCardinalSubject = CurrentValueSubject(.None) 26 | 27 | private var cancellables = Set() 28 | 29 | // MARK: - init 30 | 31 | init(sceneKitAudioVolumeManager: SceneKitAudioVolumeProtocol) { 32 | self.sceneKitAudioVolumeManager = sceneKitAudioVolumeManager 33 | } 34 | 35 | func getCardinal(angle: CGFloat) -> Cardinal { 36 | let angle = angle < 0 ? angle + 360 : angle 37 | 38 | if angle >= 22.5 && angle < 67.5 { 39 | return Cardinal.NE 40 | } else if angle >= 67.5 && angle < 112.5 { 41 | return Cardinal.N 42 | } else if angle >= 112.5 && angle < 157.5 { 43 | return Cardinal.NW 44 | } else if angle >= 157.5 && angle < 202.5 { 45 | return Cardinal.W 46 | } else if angle >= 202.5 && angle < 247.5 { 47 | return Cardinal.SW 48 | } else if angle >= 247.5 && angle < 292.5 { 49 | return Cardinal.S 50 | } else if angle >= 292.5 && angle < 337.5 { 51 | return Cardinal.SE 52 | } else if (angle >= 337.5 && angle < 360) || (angle >= 0 && angle < 22.5) { 53 | return Cardinal.E 54 | } 55 | 56 | return Cardinal.None 57 | } 58 | } 59 | 60 | extension UniverseModeViewModel { 61 | /// mode 변경을 위한 메서드 62 | func changeMode(newMode: Mode) { 63 | modeStateSubject.send(newMode) 64 | } 65 | 66 | /// detectedNode 의 값을 바꾸기 위한 메서드 67 | func updateDetectedNodeName(_ newValue: String) { 68 | detectedNodeSubject.send(newValue) 69 | } 70 | 71 | /// ArrowCardinal 의 값을 바꾸기 위한 메서드 72 | func updateArrowCardinal(_ newCardinal: Cardinal) { 73 | arrowCardinalSubject.send(newCardinal) 74 | } 75 | 76 | /// announceCardinal 의 값을 바꾸기 위한 메서드 77 | func updateAnnounceCardinal(_ newAnnouceCardinal: Cardinal) { 78 | announceCardinal = newAnnouceCardinal 79 | } 80 | } 81 | 82 | /// 음향을 조절하는 메서드 83 | extension UniverseModeViewModel { 84 | 85 | /// 탐색 모드 시 음향 조절 86 | func exploreMode() { 87 | sceneKitAudioVolumeManager.setVolumeForAll(volume: AudioVolume.half.volume) 88 | } 89 | 90 | /// 탐색 모드에서 행성을 탐지 했을 때 음향 조절 91 | func selectedNodeExploreMode(selectPlanetName: String) { 92 | sceneKitAudioVolumeManager.setVolume(selectedName: selectPlanetName, selectedVolume: AudioVolume.max.volume, otherVolume: AudioVolume.tenth.volume) 93 | } 94 | 95 | /// 빠르게 천체 찾기 모드 시 음향 조절 96 | func searchMode(selectPlanetName: String) { 97 | sceneKitAudioVolumeManager.setVolume(selectedName: selectPlanetName, selectedVolume: AudioVolume.half.volume, otherVolume: AudioVolume.mute.volume) 98 | } 99 | 100 | /// 탐색, 검색화면에서의 음소거 101 | func muteAllNode() { 102 | sceneKitAudioVolumeManager.setVolumeForAll(volume: AudioVolume.mute.volume) 103 | } 104 | } 105 | --------------------------------------------------------------------------------