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