2022. 11. 21. 12:30ใProgramming/Swift
[ ๊ณต์ ๋ฌธ์ ์ฝ๊ธฐ ]
https://developer.apple.com/documentation/uikit/uiview/1622496-addgesturerecognizer
https://developer.apple.com/documentation/uikit/uitapgesturerecognizer
//
// UniverseSearchViewController.swift
// Tars
//
// Created by ๊น์ํ on 2022/11/02.
//
import UIKit
import SceneKit
import ARKit
enum Mode {
case explore
case search(planet: String)
var titleText: String {
switch self {
case .explore:
return "์ฐ์ฃผ ๋๋ฌ๋ณด๊ธฐ"
case .search(planet: let name):
return "\(planetNameDict[name] ?? name) ์ฐพ๋ ์ค"
}
}
}
class UniverseSearchViewController: UIViewController, ARSCNViewDelegate, LocationManagerDelegate, UIGestureRecognizerDelegate {
private var guideCircleView = CustomCircleView()
private var selectedSquareView = CustomSquareView()
private var guideArrowView = CustomArrowView()
let contentsViewController = ContentsViewController()
// TapGesture ์ ์ธ
let selectedSquareViewTap = UITapGestureRecognizer()
var mode: Mode = .explore {
didSet {
setModeChangedLayout()
}
}
var planetObjectList: [String: SCNNode] = [:]
var circleCenter: CGPoint = .zero
let searchGuideLabel: UILabel = {
let label: UILabel = UILabel()
label.text = "๋น ๋ฅด๊ฒ ์ฒ์ฒด ์ฐพ๊ธฐ"
label.textColor = .white
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 20, weight: .semibold)
return label
}()
public let selectPlanetCollectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = screenWidth * 0.05
layout.minimumInteritemSpacing = CGFloat(UInt16.max)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.register(SelectPlanetCollectionViewCell.self,
forCellWithReuseIdentifier: SelectPlanetCollectionViewCell.identifier)
collectionView.contentInset = UIEdgeInsets(top: 0, left: screenWidth * 0.09, bottom: 0, right: screenWidth * 0.09)
collectionView.showsHorizontalScrollIndicator = true
collectionView.backgroundColor = .black
return collectionView
}()
/// ARKit ์ ์ฌ์ฉํ๊ธฐ ์ํ view ์ ์ธ
lazy var sceneView: ARSCNView = {
let sceneView = ARSCNView()
sceneView.delegate = self
return sceneView
}()
override func viewDidLoad() {
super.viewDidLoad()
selectPlanetCollectionView.delegate = self
selectPlanetCollectionView.dataSource = self
// TapGesture์ View ์ฐ๊ฒฐ
selectedSquareViewTap.delegate = self
selectedSquareViewTap.addTarget(self, action: #selector(squareViewTapped))
selectedSquareView.addGestureRecognizer(selectedSquareViewTap)
[guideCircleView, guideArrowView, selectedSquareView].forEach { sceneView.addSubview($0) }
[sceneView, selectPlanetCollectionView, searchGuideLabel].forEach { view.addSubview($0) }
configureConstraints()
selectedSquareView.isHidden = true
guideArrowView.isHidden = true
let locationManager = LocationManager.shared
locationManager.delegate = self
locationManager.updateLocation()
// navigation title ์ค์
self.navigationController?.isNavigationBarHidden = false
self.navigationController?.topViewController?.title = "์ฐ์ฃผ ๋๋ฌ๋ณด๊ธฐ"
self.navigationController?.navigationBar.titleTextAttributes = [ NSAttributedString.Key.foregroundColor: UIColor.white]
self.navigationController?.navigationBar.backgroundColor = .black
// settingButton navigationItem
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "gearshape.fill"), style: .plain, target: self, action: #selector(settingButtonTapped))
self.navigationItem.rightBarButtonItem?.tintColor = .white
self.navigationItem.hidesBackButton = true
}
// TapGesture ํ๋ฉด ์ ํ ๋์
@objc func squareViewTapped() {
print(self.selectedSquareView.planetLabel.text ?? "nil")
contentsViewController.planet.planetName = self.selectedSquareView.planetLabel.text ?? "Sun"
self.navigationController?.pushViewController(contentsViewController, animated: true)
}
@objc func settingButtonTapped() {
self.navigationController?.pushViewController(SettingViewController(), animated: false)
}
/// ํ์ฑ์ ๋ฐฐ์นํ๊ธฐ ์ํ ํจ์
private func setPlanetPosition(to scene: SCNScene?, planets: [Body]) {
for planet in planets {
if planet.name == "Earth" || planet.name == "Pluto" {
continue
} else {
let sphere = SCNSphere(radius: 0.2)
sphere.firstMaterial?.diffuse.contents = UIImage(named: planet.name + "_Map")
let sphereNode = SCNNode(geometry: sphere)
sphereNode.position = SCNVector3(planet.coordinate.x, planet.coordinate.y, planet.coordinate.z)
sphereNode.name = planet.name
scene?.rootNode.addChildNode(sphereNode)
// print(planet.name)
planetObjectList[planet.name] = sphereNode
let audioSource: SCNAudioSource = {
let source = SCNAudioSource(fileNamed: "\(planet.name).mp3")!
source.loops = true
source.load()
return source
}()
sphereNode.removeAllAudioPlayers()
sphereNode.addAudioPlayer(SCNAudioPlayer(source: audioSource))
}
}
}
private func configureConstraints() {
sceneView.anchor(top: view.topAnchor, leading: view.leadingAnchor, bottom: view.bottomAnchor, trailing: view.trailingAnchor, paddingTop: screenHeight * 0.1)
guideCircleView.centerX(inView: view)
guideCircleView.anchor(top: view.topAnchor, paddingTop: screenHeight * 0.23)
searchGuideLabel.centerX(inView: view)
searchGuideLabel.anchor(top: view.topAnchor, paddingTop: screenHeight * 0.7)
selectPlanetCollectionView.anchor(top: view.topAnchor, paddingTop: screenHeight * 0.68)
selectPlanetCollectionView.setHeight(height: screenHeight * 0.35)
selectPlanetCollectionView.setWidth(width: screenWidth)
selectPlanetCollectionView.centerX(inView: view)
selectedSquareView.frame = CGRect(x: 0, y: 0, width: screenWidth / 5.65, height: (screenWidth / 5.65) + (screenHeight / 26.375))
guideArrowView.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARWorldTrackingConfiguration()
configuration.worldAlignment = .gravityAndHeading
sceneView.session.run(configuration)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
circleCenter = guideCircleView.center
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
sceneView.session.pause()
}
// MARK: - LocationManagerDelegate
func didUpdateUserLocation() {
Task {
let bodies = try await AstronomyAPIManager().requestBodies()
setPlanetPosition(to: sceneView.scene, planets: bodies)
}
}
// MARK: - ARSCNViewDelegate
func renderer(_ renderer: SCNSceneRenderer, willRenderScene scene: SCNScene, atTime time: TimeInterval) {
switch mode {
case .explore:
explore()
case .search(planet: let name):
search(for: name)
}
}
}
// MARK: - ๋ ์ด์์ ์ค์ ํจ์
extension UniverseSearchViewController {
// ๋ชจ๋ ๋ณ๊ฒฝ์ ๋ฐ๋ฅธ ๋ ์ด์์ ์ค์
public func setModeChangedLayout() {
self.navigationController?.topViewController?.title = mode.titleText
switch mode {
case .explore:
setArrowHidden()
case .search(planet: _):
break
}
}
// ํ์ฑ์ด ํ์ง๋์ง ์์์ ๋ ๋ ์ด์์ ์ค์
private func setNotDetectedLayout() {
DispatchQueue.main.async {
self.guideCircleView.isHidden = false
self.selectedSquareView.isHidden = true
}
}
// ํ์ฑ์ด ํ์ง๋์์ ๋ ๋ ์ด์์ ์ค์
private func setDetectedLayout(name: String, point: CGPoint) {
DispatchQueue.main.async {
self.selectedSquareView.frame.origin = point
self.selectedSquareView.setLabel(planetNameDict[name] ?? name)
self.guideCircleView.isHidden = true
self.selectedSquareView.isHidden = false
}
}
// ๊ฒ์ ์ ํ์ดํ ๋ ์ด์์ ์ค์
private func setArrowLayout(point: CGPoint, distance: CGFloat, locatedBehind: Bool = false) {
let dx = screenWidth / 3 * (point.x - circleCenter.x) / distance
let dy = screenWidth / 3 * (circleCenter.y - point.y) / distance
let angle = locatedBehind ? atan2(point.y - circleCenter.y, point.x - circleCenter.x) + .pi : atan2(point.y - circleCenter.y, point.x - circleCenter.x)
let arrowPosition = locatedBehind ? CGPoint(x: circleCenter.x - dx, y: circleCenter.y + dy) : CGPoint(x: circleCenter.x + dx, y: circleCenter.y - dy)
DispatchQueue.main.async {
self.guideArrowView.transform = CGAffineTransform(rotationAngle: angle)
self.guideArrowView.layer.position = arrowPosition
self.guideArrowView.isHidden = false
}
}
// ํ์ดํ ์จ๊น ์ค์
private func setArrowHidden() {
DispatchQueue.main.async {
self.guideArrowView.isHidden = true
}
}
}
extension UniverseSearchViewController {
// MARK: - ํ์ ๋ชจ๋ ๊ธฐ๋ฅ
private func explore() {
var detectNode: SCNNode?
var nodeCenter: CGPoint = .zero
var minDistance: CGFloat = screenHeight
guard let pointOfView = sceneView.pointOfView else { return }
let detectNodes = sceneView.nodesInsideFrustum(of: pointOfView) // ํ๋ฉด์ ๋ค์ด์จ ๋
ธ๋ ๋ฆฌ์คํธ
for node in detectNodes {
let nodePosition = sceneView.projectPoint(node.position)
let nodeScreenPos = nodePosition.toCGPoint()
let distance = circleCenter.distanceTo(nodeScreenPos)
// ์ ์์ ๋ค์ด์จ ๊ฐ์ฅ ์งง์ ๊ฑฐ๋ฆฌ, ๋
ธ๋, ํ๋ฉด์์ ์์น ์ ์ฅ
if distance < screenWidth / 3 && distance < minDistance {
detectNode = node
nodeCenter = nodeScreenPos
minDistance = distance
}
}
if let detectNode = detectNode {
// ์ ์์ ๋ค์ด์จ ๋
ธ๋ ์กด์ฌํ์ ๋
guard let planetName = detectNode.name else { return }
guard let name = planetNameDict[planetName] else { return }
let nodeOrigin = CGPoint(x: nodeCenter.x - screenWidth / 11.3, y: nodeCenter.y - screenWidth / 11.3)
setDetectedLayout(name: name, point: nodeOrigin)
} else {
// ํ์ง๋ ๋
ธ๋๊ฐ ์์ ๋
setNotDetectedLayout()
}
}
// MARK: - ๊ฒ์ ๋ชจ๋ ๊ธฐ๋ฅ
private func search(for name: String) {
guard let node = planetObjectList[name] else {return}
let nodePosition = sceneView.projectPoint(node.position)
let nodeScreenPos = nodePosition.toCGPoint()
let distanceToCenter = circleCenter.distanceTo(nodeScreenPos)
if nodePosition.z >= 1 {
// ์ฐพ๋ ๋
ธ๋๊ฐ ๋ค์ ์์ ๋
setNotDetectedLayout()
setArrowLayout(point: nodeScreenPos, distance: distanceToCenter, locatedBehind: true)
} else if distanceToCenter >= (screenWidth / 3) {
// ์ฐพ๋ ๋
ธ๋๊ฐ ์์ ๋ฐ๊นฅ์ ์์ ๋
setNotDetectedLayout()
setArrowLayout(point: nodeScreenPos, distance: distanceToCenter)
} else {
// ์ฐพ๋ ๋
ธ๋๊ฐ ์ ์์ ์์ ๋
let nodeOrigin = CGPoint(x: nodeScreenPos.x - screenWidth / 11.3, y: nodeScreenPos.y - screenWidth / 11.3)
setArrowHidden()
setDetectedLayout(name: name, point: nodeOrigin)
}
}
}
class UniverseSearchViewController: UIViewController, ARSCNViewDelegate, LocationManagerDelegate, UIGestureRecognizerDelegate {
// TapGesture ์ ์ธ
let selectedSquareViewTap = UITapGestureRecognizer()
override func viewDidLoad() {
super.viewDidLoad()
// TapGesture์ View ์ฐ๊ฒฐ
selectedSquareViewTap.delegate = self
selectedSquareViewTap.addTarget(self, action: #selector(squareViewTapped))
selectedSquareView.addGestureRecognizer(selectedSquareViewTap)
}
// TapGesture ํ๋ฉด ์ ํ ๋์
@objc func squareViewTapped() {
print(self.selectedSquareView.planetLabel.text ?? "nil")
contentsViewController.planet.planetName = self.selectedSquareView.planetLabel.text ?? "Sun"
self.navigationController?.pushViewController(contentsViewController, animated: true)
}
}
- planet model์ ๋ณ์๋ค์ var๋ก ์ ํ or ๊ตฌ์กฐ ๋ฐ๊พธ๋ ๊ฒ ๊ณ ๋ฏผํด๋ณด๊ธฐ
- UIView, CustomSquareView, selectedSquareView ์ค ๋ฌด์์ target์ผ๋ก ์ ํด์ผ ํ๋์ง ๊ณ ๋ฏผ + ๊ฐ์ฅ ์ด๋ ค์ ๋ค
- selectedSquareView ๋ก ํต์ผ
- tap์ addTarget๋, view์ addGesture๋ selectedSquareView
- selectedSquareViewTap.delegate = self๊ฐ ์์ด๋ ์ ๋์ํจ
- selectedSquareView ๋ก ํต์ผ
- ์ค๋์ ๊ตํ: tap์๋ addTarget, view์๋ addGesture ๊ผญ ํด์ฃผ์ (๋์ผํ targetView๋ก)
- ๊ธฐ์กด ์์์์๋ view์ addGestureํ๋ ๊ณผ์ ์ด ์๋ค.
- Programmatically -> tap ์ ์ธ ์ addTarget์ ํด๋ ๋๊ณ , tap ์ ์ธ ์ดํ add ํ ์๋ ์๋ค.
- ๊ทผ๋ฐ ์ ์ธ ์ ์ด๊ธฐํ ์ฝ๋์ (target: selectedSquareView, action: #selector(squareViewTapped())) ์ผ๋ก selectedSquareView๋ฅผ ์ฃผ๋ ค๊ณ ํ๋ฉด ์๋ฌ๋ฉ์ธ์ง ๋ฐ์ 'Cannot use instance member 'selectedSquareView' within property initializer; property initializers run before 'self' is available'
- viewDidLoad์์ addTarget์ ์ ์ธํ๋ฉด ์ ๋์๊ฐ๋ค.
'Programming > Swift' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[Swift] SnapKit vs AutoLayout & Pros and cons of using 3rd-Party (0) | 2023.01.26 |
---|---|
[Swift] CocoaPod vs Swift Package Manager (0) | 2023.01.26 |
[UI-Kit] CollectionView Cell Selection / Deselection (0) | 2022.11.15 |
[Swift] CollectionView Cell _ Lingo Feedback (0) | 2022.11.01 |
[Swift] ๋ค๋ฅธ file (viewController) ๊ฐ์ ์กฐ์ (0) | 2022.08.31 |