[Swift] tap gesture

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๊ฐ€ ์—†์–ด๋„ ์ž˜ ๋™์ž‘ํ•จ

 

  •  ์˜ค๋Š˜์˜ ๊ตํ›ˆ: 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์„ ์„ ์–ธํ•˜๋ฉด ์ž˜ ๋Œ์•„๊ฐ„๋‹ค.