본문 바로가기

학습 노트/iOS (2021)

145 ~ 153. Gesture Recognizers

Gesture Recognizer

  • Tap Gesture
    흔히 '터치'라고 부른다.
    목록에서 원하는 항목을 선택하거나 버튼을 터치하는 등의 제스쳐를 의미한다.
  • Pan Gesture
    흔히 'Drag'라고 부른다.
    손가락으로 화면을 누른 상태로 이동한다.
  • Pinch Gesture
    손가락 두개로 터치한 다음 사이의 간격을 넓히거나 좁힌다.
  • Rotation Gesture
    손가락 두개로 터치한 다음 간격을 유지하며 회전시킨다.
  • Swipe Gesture
    화면을 특정 방향으로 빠르고 터치하는 동작이다.
  • Long Press Gesture
    화면을 0.5초 이상 누른다.

사람마다 Gesture를 구사하는 방식이 조금씩 다르기 때문에 이를 정확히 인식하도록 직접 구현하는 것은 매우 어렵다.
iOS에서는 이미 구현되어있는 UIGestureRecognizer를 통해 사용할 수 있다.
또한 위에서 설명한 각각의 Gesture들은 해당 클래스를 상속한 클래스로 구현되어있다.

화면에서 Touch Sequence가 발생하면 Window 객체에 전송된다.
Window는 이벤트가 발생한 위치에 어떤 View가 있는지 확인한 다음 연관된 Gesture Recognizer가 있는지 확인한다.
존재한다면 해당 Recognizer로 Sequence를 전달한다.
Gesture Recognizer는 전달된 Sequence를 파악하고, 자신이 인식할 수 있는 Gesture라면 직접 처리한다.
불가한 경우 연결되어있는 View로 Sequence를 전달한다.

Gseture를 처리하는 코드는 크게 3단계로 구분한다.

  • Gesture Recognizer 생성
    Interface Builder에서 생성해도 좋고, 코드를 사용해 직접 생성해도 좋다.
  • View와 Gesture Recognizer 연결
    Gesture Recognizer는 반드시 한 개의 View와 연결되어야 한다.
    하지만 View는 두 개 이상의 Gesture Recognizer와 연결할 수 있다.
  • Gesture가 인식됐을 때 처리할 코드를 작성, Gesture Recognizer와 연결

모든 Gesture는 두 가지 카테고리로 분류한다.

Discrete Gesture Continuous Gesture
단일 동작으로 구성된 Gesture 여러 동작을 구성된 Gesture
Sequence가 완료된 다음 하나의 Action을 전달한다. Sequence가 진행되는 동안 Action을 반복적으로 전달한다.

Gesture Recognizer가 전달하는 Action에는 현재 상태를 나타내는 값이 저장되어있다.

  • Possible
    모든 Gesture는 해당 상태에서 시작한다.
    아직 Gesture가 인식되지 않았음을 의미한다.
  • Began
    Continuous Gesture로 인식되면 해당 상태로 변경된다.
  • Changed
    Gesture Sequence가 계속되면 해당 상태로 전환된다.
  • Ended
    Gesture Sequence가 종료되면 해당 상태로 전환되고 Possible 상태로 돌아간다.
  • Cancelled
    Gesture Sequence가 취소되면 해당 상태로 전환되고 Possible 상태로 돌아간다.
  • Failed
    Gesture를 인식할 수 없다면 해당 상태로 전환되고 Possible 상태로 돌아간다.
    Custom Gesture를 구현하는 경우를 제외하면 사용되지 않는다.
  • Recognized
    Multitouch Sequence로 인식한 경우 해당 상태로 전환되고 Possible 상태로 돌아간다.

Discrete Gesture는 Possible 상태에서 Gesture가 인식되면 Ended 상태로 전환되고 Action이 전달된다.
Gesture를 인식할 수 없다면 Faild 상태로 전환한다.
둘 모두 Possible 상태로 돌아간다.

Continuous Gesture는 Possible 상태에서 Began 상태로 전환된다.
Sequence가 진행되는 동안 Changed로 전환되고 Action이 연속해서 전달된다.
정상적으로 종료되면 Ended 상태로 전환되고, 아니라면 Cancelled 상태로 전환된다.

 

Tap Gesture

Interface Builder로 구현하기

//
//  TapViewController.swift
//  Gesture Recognizer Practice
//
//  Created by Martin.Q on 2021/11/29.
//

import UIKit

class TapViewController: UIViewController {
	
	var count = 0
	
	@IBOutlet weak var countLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
		
		countLabel.text = "\(count)"
    }
    
}

사용할 Scene과 코드는 위와 같다.
Tap Gesture를 실행하는 만큼 Label에 숫자를 표시하도록 구현한다.

라이브러리에서 Tap Gesture Recognizer를 찾아 드래그 해 추가한다.
이때 Root View가 아닌 Label에 연결되도록 해야 한다.
이렇게 되면 Label 내에서 발생하는 Tap Gesture만을 처리하게 된다.

Gesture Recognizer의 Connection Pannel에서 연결된 View를 확인할 수 있다.

Attribute에는 Gesture Recognizer의 속성이 보인다.
Recognize의 Taps는 인식할 탭의 수를 의미한다. 1은 싱글 탭, 2는 더블 탭을 의미한다.
Touches는 인식에 필요한 Touch의 수를 의미한다. 1은 싱글 터치, 2는 두 손가락으로 터치해야 인식함을 의미한다.

Label의 Connection Pannel에도 연결된 Gesture Recognizer가 표시된다.

이제 Action을 연결하고 필요한 코드를 구현하는 것 만이 남았다.

@IBAction func tapAction(_ sender: UITapGestureRecognizer) {
}

다른 View들을 Action으로 연결했던 것과 동일하게 드래그해 연결한다.
이때 Type을 UITapGestureRecognizer로 변경해야 한다.
Gesture Action을 구현하는 경우 상태 값에 따라 필요한 코드를 구현하기 때문에 대부분 Type을 GestureRecognizer로 설정한다.

@IBAction func tapAction(_ sender: UITapGestureRecognizer) {
	count += 1
	countLabel.text = "\(count)"
}

값을 1 증가시킨 후 Label을 업데이트한다.

이후 실행해 결과를 확인해 보면 별다른 변화가 없다.

이는 Label 자체에서 User Interaction Enabled가 비활성화돼 반응하지 않도록 설정되어있기 때문이다.
Image View나 Label View같이 데이터를 표시하는 정적인 View들은 기본값이 비활성화돼있다.
이를 활성화하고 결과를 확인해 보면

정상적으로 반응하는 것을 확인할 수 있다.
지금은 Label에 Gesture Recognizer를 연결했기 때문에 Label을 제외한 영역에서는 반응하지 않는다.
Gesture Recognizer는 자신과 연결된 View에 대한 이벤트를 처리한다.

@IBAction func tapAction(_ sender: UITapGestureRecognizer) {
	if sender.state == .ended {
		count += 1
		countLabel.text = "\(count)"
	}
}

Gesture에는 여러 상태가 존재하고, 이러한 상태를 고려하지 않으면 논리적인 오류가 발생할 수 있다.
Tap Gesture는 Discrete Gesture로 Possible, Ended, Faild 세 가지의 상태를 가질 수 있다.
이 중 Gesture가 정상적으로 종료된 Ended 상태에서만 데이터를 반영하도록 수정해 논리적 오류를 방지할 수 있다.

코드로 구현하기

//
//  TapWithCodeViewController.swift
//  Gesture Recognizer Practice
//
//  Created by Martin.Q on 2021/11/29.
//

import UIKit

class TapWithCodeViewController: UIViewController {
	
	var count = 0
	
	@IBOutlet weak var countLabel: UILabel!
	
    override func viewDidLoad() {
        super.viewDidLoad()

		countLabel.text = "\(count)"
    }

}

사용할 Scene과 연결된 코드는 이전과 동일하다.
이번에는 Gesture 생성, View 연결, Action 메서드 연결을 코드로 구현해 본다.

//
//  TapWithCodeViewController.swift
//  Gesture Recognizer Practice
//
//  Created by Martin.Q on 2021/11/29.
//

import UIKit

class TapWithCodeViewController: UIViewController {
	
	var count = 0
	
	@IBOutlet weak var countLabel: UILabel!
	
	var tapGesture: UITapGestureRecognizer?
	
	@objc func tapAction(_ tap: UITapGestureRecognizer) {
		
	}
	
    override func viewDidLoad() {
        super.viewDidLoad()

		countLabel.text = "\(count)"
    }

}

Gesture Recognizer를 저장할 속성을 생성한 뒤 action 메서드를 구현한다.
이때 @objc 태그를 사용해야 한다.

@objc func tapAction(_ tap: UITapGestureRecognizer) {
	if tap.state == .ended {
		count += 1
		countLabel.text = "\(count)"
	}
}

메서드의 구현 자체는 동일하다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	countLabel.text = "\(count)"
	
	tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapAction(_:)))
}

나머지는 viewDidLoad에서 진행한다.
UITapGestureRecognizer를 생성하고, 파라미터에 각각 적당한 값을 전달한다.
첫 번째 파라미터는 Action 메서드가 구현되어있는 인스턴스를 전달한다. 지금은 self를 그대로 전달한다.
두 번째 파라미터는 Acition 메서드를 selector로 전달한다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	countLabel.text = "\(count)"
	
	tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapAction(_:)))
	tapGesture?.numberOfTapsRequired = 1
	tapGesture?.numberOfTouchesRequired = 1
}

이제는 GestureRecognizer를 설정해야 한다.
Taps와 Touches를 설정하기 위해 numerOfTapsRequired와 numberOfTouchesRequired를 사용할 수 있다.
이번엔 기본값을 그대로 사용한다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	countLabel.text = "\(count)"
	
	tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapAction(_:)))
	tapGesture?.numberOfTapsRequired = 1
	tapGesture?.numberOfTouchesRequired = 1
	
	countLabel.addGestureRecognizer(tapGesture!)
	countLabel.isUserInteractionEnabled = true
}

이번에는 View와 Gesture Recognizer를 연결한다.
정확히는 Gesture를 처리할 View에 Gesture Recognizer를 추가하는 방식이다.
대상 View인 Label에 addGestureRecognizer 메서드를 사용해 Gesture Recognizer를 추가하고,
UserInteractionEnabled를 활성화한다.

정상적으로 동작한다.

Gesture를 추가하는 것은 Interface Builder가 상대적으로 간편하다.
단, 가끔가다 Storyboard에서 추가할 수 없는 경우가 있기 때문에 두 가지 방법 모두 알아두는 것이 좋다.

 

Pinch Gesture

//
//  PinchViewController.swift
//  Gesture Recognizer Practice
//
//  Created by Martin.Q on 2021/11/29.
//

import UIKit

class PinchViewController: UIViewController {
	
	@IBOutlet weak var imageView: UIImageView!
	
	
	@IBAction func reset(_ sender: Any) {
	}
	
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

사용할 Scene과 연결된 코드는 위와 같다.

imageView에 이미지가 표시되어있고,
RightBarButton으로 Reset 버튼이 추가되어있다.

이번에는 Pinch Gesture Recognizer를 추가해 이미지를 확대하거나 축소해 본다.

라이브러리에서 Pinch Gesture Recognizer를 찾아 드래그해 연결한다.
ImageView에 연결되었는지 Connection Pannel을 총해 확인할 수 있다.

Gesture Recognizer의 Attribute에는 여러 속성이 존재한다.
Scale은 기본값이 1이고, 해당 값은 원래 크기에 대한 상대적인 비율이다.
즉, 1보다 크면 확대, 1보다 작으면 축소가 된다.

//
//  PinchViewController.swift
//  Gesture Recognizer Practice
//
//  Created by Martin.Q on 2021/11/29.
//

import UIKit

class PinchViewController: UIViewController {
	
	@IBOutlet weak var imageView: UIImageView!
	
	@IBAction func pinchAction(_ sender: UIPinchGestureRecognizer) {
	}
	
	@IBAction func reset(_ sender: Any) {
	}
	
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

GestureRecognizer를 Action으로 연결한다.
이때 Type을 UIPinchGestureRecognizer로 변경해야 함에 주의하자.

Pinch Gesture는 Countinuous Gesture에 해당한다.
따라서 Image View에서 손가락을 움직일 때마다 Action이 반복적으로 전달된다.

@IBAction func pinchAction(_ sender: UIPinchGestureRecognizer) {
	guard let target = sender.view else {
		return
	}
	
	target.transform = target.transform.scaledBy(x: sender.scale, y: sender.scale)
}

우선 연결된 View를 상수에 저장한다.
Scale 속성을 통해 이미지의 크기를 변경한다.
확대와 축소를 구현할 때는 Frame을 변경하지 않고 transform 속성을 통해 수행한다.

Image View 또한 정적인 View이기 때문에 Interaction 관련 설정이 꺼져있다.
해당 설정을 활성화해야 결과를 확인할 수 있다.

하지만 너무 민감하게 반응해 제대로 사용하기 어렵다.

@IBAction func pinchAction(_ sender: UIPinchGestureRecognizer) {
	guard let target = sender.view else {
		return
	}
	
	target.transform = target.transform.scaledBy(x: sender.scale, y: sender.scale)
	
	print(sender.scale)
}

변수로 사용되고 있는 scale값을 콘솔에서 확인해 보면

값 자체는 문제가 없다.
그럼에도 이러한 문제가 생기는 이유는 Scale 값이 누적되기 때문이다.
즉 1.01배 확대된 다음 처음 크기에서 1.04배 확대되는 것이 아닌,
1.01배가 확대된 다음 해당 크기에서 1.04배가 다시 확대되는 식이다.
따라서 변경한 다음 Scale 값을 초기화해 줘야 한다.

@IBAction func pinchAction(_ sender: UIPinchGestureRecognizer) {
	guard let target = sender.view else {
		return
	}
	
	target.transform = target.transform.scaledBy(x: sender.scale, y: sender.scale)
	sender.scale = 1
}

이제는 원래의 문제가 사라졌다.

마지막으로 Reset 버튼을 누르면 원래의 비율로 돌아가도록 구현한다.

@IBAction func reset(_ sender: Any) {
	UIView.animate(withDuration: 0.3) {
		self.imageView.transform = CGAffineTransform.identity
	}
}

UIView의 animate를 사용해 애니메이션 효과를 추가하고,
transform 속성에서 값을 초기화하면 원래대로의 모습으로 돌아온다.

의도한 대로 동작한다.

연습

@IBAction func pinchAction(_ sender: UIPinchGestureRecognizer) {
	guard let target = sender.view else {
		return
	}
	
	target.transform = target.transform.scaledBy(x: sender.scale, y: sender.scale)
	sender.scale = 1
	
	if sender.state == .ended {
		UIView.animate(withDuration: 0.3) {
			self.imageView.transform = CGAffineTransform.identity
		}
	}
}

이전과 마찬가지로 state를 사용하면 Gesture가 끝났을 때 자동으로 원래의 비율로 돌아오게 구현할 수도 있다.

 

Rotation Gesture

//
//  RotationViewController.swift
//  Gesture Recognizer Practice
//
//  Created by Martin.Q on 2021/11/29.
//

import UIKit

class RotationViewController: UIViewController {
	
	@IBOutlet weak var imageView: UIImageView!
	
	@IBAction func reset(_ sender: Any) {
	}
	
    override func viewDidLoad() {
        super.viewDidLoad()
		
    }

}

사용할 Scene과 코드는 위와 같다.
기본적으로 Pinch Gesture에서 사용했던 것과 같은 구성이다.

라이브러리에서 Rotation Gesture Recognizer를 검색해 드래그하여 추가하고,
Attribute를 확인해 보면 여러 속성이 존재한다.

Rotation 속성은 회전 값의 라디안 형식이다.

//
//  RotationViewController.swift
//  Gesture Recognizer Practice
//
//  Created by Martin.Q on 2021/11/29.
//

import UIKit

class RotationViewController: UIViewController {
	
	@IBOutlet weak var imageView: UIImageView!
	
	@IBAction func rotationAction(_ sender: UIRotationGestureRecognizer) {
	}
	
	@IBAction func reset(_ sender: Any) {
	}
	
    override func viewDidLoad() {
        super.viewDidLoad()
		
    }

}

드래그해 코드에 Action으로 연결한다.
이때 Type을 UIRotationGestureRecognizer로 설정해야 한다.

@IBAction func rotationAction(_ sender: UIRotationGestureRecognizer) {
	guard let target = sender.view else {
		return
	}
	target.transform = target.transform.rotated(by: sender.rotation)
	
	sender.rotation = 0
}

View를 회전시킬 때에는 Transform 속성을 사용해 수행한다.
Gesture Recognizer와 연결된 View를 상수에 저장하고, 값만큼 회전시킨다.
이때 Pinch와 마찬가지로 값을 변경한 다음 초기화해 줘야 누적되어 적용되는 것을 방지할 수 있다.

//
//  RotationViewController.swift
//  Gesture Recognizer Practice
//
//  Created by Martin.Q on 2021/11/29.
//

import UIKit

class RotationViewController: UIViewController {
	
	@IBOutlet weak var imageView: UIImageView!
	
	@IBAction func rotationAction(_ sender: UIRotationGestureRecognizer) {
		guard let target = sender.view else {
			return
		}
		target.transform = target.transform.rotated(by: sender.rotation)
		
		sender.rotation = 0
	}
	
	@IBAction func reset(_ sender: Any) {
	}
	
    override func viewDidLoad() {
        super.viewDidLoad()
		
		imageView.isUserInteractionEnabled = true
		
    }

}

이후 viewDidLoad에서 User Interaction을 활성화한다.

의도한 대로 이미지가 회전된다.

@IBAction func reset(_ sender: Any) {
	UIView.animate(withDuration: 0.3) {
		self.imageView.transform = CGAffineTransform.identity
	}
}

reset 버튼에도 switch 때와 같은 동작을 수행하도록 구현하면 

원상 복구된다.

 

Swipe Gesture

Swipe Gesture는 Page 전환과 같은 기능을 구현할 때 사용한다.
화면을 터치한 상태에서 지정된 방향으로 일정 거리 이상 움직인 경우에 Swipe로 인식되는데, 속도가 중요하다.
속도가 느리다면 적은 거리를 움직여도 Swipe로 인식이 되고, 이때는 방향이 정확해야 한다.
반대로 빠르다면 방향이 부정확해도 괜찮지만 움직여야 하는 거리는 늘어난다.

사용할 Scene들은 위와 같고, 둘은 화면 중앙의 버튼을 통해 Full Screen 방식 Modal로 연결되어있다.
두 번째 화면에서 손가락을 사용해 왼쪽으로 Swipe시 화면을 닫도록 구현한다.

Swipe Gesture Recognizer를 추가하고, Attribute를 확인하면 여러 속성을 볼 수 있다.
Swpie는 제스처의 방향을 의미하고,
Touches는 필요한 Touch Interaction의 수를 의미한다.
둘을 Left과 1로 수정한다.

Swipe Gesture Recognizer는 기본적으로 한 가지의 방향만 인식하기 때문에 여러 방향을 인식하고자 한다면
여러 개의 Swipe Gesture Recognizer가 필요하다.

//
//  ModalViewController.swift
//  Gesture Recognizer Practice
//
//  Created by Martin.Q on 2021/11/29.
//

import UIKit

class ModalViewController: UIViewController {
	
	@IBAction func swipeAction(_ sender: UISwipeGestureRecognizer) {
	}
	
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

Gesture Recognizer를 Action으로 연결한다.
마찬가지로 Type을 UISwipeGestureRecognizer로 변경해야 한다.

//
//  ModalViewController.swift
//  Gesture Recognizer Practice
//
//  Created by Martin.Q on 2021/11/29.
//

import UIKit

class ModalViewController: UIViewController {
	
	@IBAction func swipeAction(_ sender: UISwipeGestureRecognizer) {
		if sender.state == .ended {
			dismiss(animated: true, completion: nil)
		}
	}
	
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

Swipe Gesture는 Discrete Gesture로 ended 상태에서 값을 변경해야 한다.

같은 방법으로 Swipe Gesture Scene에서 Gseture를 활용해 다음 화면을 표시할 수도 있다.

Swipe Gesture Recognizer를 추가하고

Attribute에서 원하는 방향을 선택한 다음

//
//  SwipeViewController.swift
//  Gesture Recognizer Practice
//
//  Created by Martin.Q on 2021/11/29.
//

import UIKit

class SwipeViewController: UIViewController {
	
	@IBAction func swipeAction(_ sender: UISwipeGestureRecognizer) {
		if sender.state == .ended {
			performSegue(withIdentifier: "gotoSegue", sender: sender)
		}
	}
	
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

Action으로 연결해 Segue를 호출하도록 코드를 작성한다.

위로 올리면 다음 씬이 표시되고, 왼쪽으로 밀면 사라진다.

 

Pan Gesture

//
//  PanViewController.swift
//  Gesture Recognizer Practice
//
//  Created by Martin.Q on 2021/11/29.
//
 
import UIKit

class PanViewController: UIViewController {
	
	@IBOutlet weak var redView: UIView!
	
    override func viewDidLoad() {
        super.viewDidLoad()
		
		redView.layer.cornerRadius = 50
		redView.clipsToBounds = true
    }

}

이번에 사용할 Scene과 Code는 위와 같다.
화면의 Red View를 터치해 움직이면 따라서 움직이도록 구현한다.

첫 번째로 Gesture Recognizer를 추가해야 한다.

Root View에 추가하는 경우 대상인 red View의 밖에서 Pan 해도 이를 인식하게 된다.
따라서 Red View의 Frame과 위치를 확인한 다음 해당 Frame의 내부에 위치한 경우에만 처리해야 한다.
이 경우 정상 작동하지만 코드가 굉장히 복잡해진다.

Red View에 추가하는 경우 위와 같은 복잡한 과정은 필요 없다.
대신 얼마만큼 이동했는지를 판단해야 하는데 해당 부분은 Gesture Recognizer가 알아서 처리해 준다.
따라서 이동거리를 계산할 View만 알려주면 된다.

즉 Pan Gesture Recognizer는 Root View가 아닌 Red View에 추가해야 한다.

라이브러리에서 Pan Gesture Recognizer를 드래그해 추가한다.

//
//  PanViewController.swift
//  Gesture Recognizer Practice
//
//  Created by Martin.Q on 2021/11/29.
//
 
import UIKit

class PanViewController: UIViewController {
	
	@IBAction func panAxtion(_ sender: UIPanGestureRecognizer) {
	}
	
	
	@IBOutlet weak var redView: UIView!
	
    override func viewDidLoad() {
        super.viewDidLoad()
		
		redView.layer.cornerRadius = 50
		redView.clipsToBounds = true
    }

}

또한 해당 Gesture를 Action으로 연결한다.
이때 Type은 UIPanGestureRecognizer로 변경해야 한다.

Pan Geture는 Continuous Gesture에 속한다.
즉 터치가 이동할 때마다 반복적으로 Action을 호출한다.
Gesture가 시작하거나 끝나는 시점을 구분해 처리해야 한다면 Began, Changed, Ended 상태를 개별적으로 처리해야 한다.
지금처럼 이동에만 관련된 간단한 경우라면 이를 무시해도 괜찮다.

@IBAction func panAxtion(_ sender: UIPanGestureRecognizer) {
	guard let target = sender.view else {
		return
	}
	
	let translation = sender.translation(in: view)
}

먼저 Gesture가 발생한 View를 확인하고 상수에 저장한다.
그리고 Gesture가 얼마나 이동했는지를 파악하는 것이 중요하다.
이는 gesture에 요청하면 계산된 값을 받아 올 수 있다.
translation(in:) 메서드에 Root View를 전달하면 Root View 내에서 터치가 얼마나 아동 했는지를 반환한다.

@IBAction func panAxtion(_ sender: UIPanGestureRecognizer) {
	guard let target = sender.view else {
		return
	}
	
	let translation = sender.translation(in: view)
	
	target.center.x += translation.x
	target.center.y += translation.y
    
	sender.setTranslation(.zero, in: view)
}

다른 Gesture들과 같이 수치가 누적되어 적용되기 때문에 이동시킨 다음 이를 초기화해 줘야 한다.
Translation을 초기화할 때는 setTranslatuon(in:) 메서드를 호출하고, 첫 번째 파라미터에 zero를 전달해 초기화한다.

초기화하지 않아 누적되며 이동하던 것과는 다르게 정상적으로 표시된다.

 

Long Press Gesture

//
//  LongPressViewController.swift
//  Gesture Recognizer Practice
//
//  Created by Martin.Q on 2021/11/29.
//

import UIKit

class LongPressViewController: UIViewController {
	
	@IBOutlet weak var blurView: UIVisualEffectView!
	
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

사용할 Scene과 코드이다.
Scene에는 화면 전체를 채우는 Image View가 존재하고,
그 위에 동일한 사이즈의 Visual Effect View가 존재한다.
화면을 길게 터치하면 Visual Effect View를 숨기고, 떼면 다시 표시하도록 구현한다.

라이브러리에서 Long Press Gesture Recognizer를 찾아 추가한 뒤
Attribute를 보면 여러 속성들을 확인할 수 있다.

  • Min Duration
    Gesture를 인식하기 위한 최소 단위 시간이다.
    0.5초간 누르고 있으면 Gesture를 인식한다.
    만약 해당 값이 너무 짧다면 더 큰 값으로 설정할 수 있다.
  • Recognize
    Tap 수와 touch 수를 조절하는데 보통은 기본값인 0과 1을 사용한다.
  • Tolerance
    오차 범위를 Point 단위로 설정한다.
    화면을 10초 이상 누르고 터치의 이동 범위가 10pt보다 작다면 Long Press로 인식한다.
//
//  LongPressViewController.swift
//  Gesture Recognizer Practice
//
//  Created by Martin.Q on 2021/11/29.
//

import UIKit

class LongPressViewController: UIViewController {
	
	@IBOutlet weak var blurView: UIVisualEffectView!
	
	@IBAction func longPressAction(_ sender: UILongPressGestureRecognizer) {
	}
	
	
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

코드에 드래그해 Action으로 연결한다.
이때 Type은 UILongPressGestureRecognizer로 설정한다.

@IBAction func longPressAction(_ sender: UILongPressGestureRecognizer) {
	if sender.state == .began {
		blurView.isHidden = true
	} else if sender.state == .ended {
		blurView.isHidden = false
	}
}

이번 실습에서는 Long Press 하는 동안 Visual Effect를 숨기고,
종료하면 다시 나타나게 하는 것이 목표이다.
따라서 상태에 따라 다르게 동작을 구현해야 한다.

began 상태에서 숨기고 ended 상태에서 다시 보이도록 구현했다.

설정 값에 따른 작동 조건은 다음과 같다.

Min Duration이 0.5 이므로 Long Press Gesture를 인식하는 데에 0.5초가 소모된다.
Recognize의 Tap이 0, touches가 1 이므로 별도의 동작 없이 한 손가락으로 길게 누르는 것으로 인식 가능하다.
Tolerance의 값이 10이므로 Long Press를 시도하는 터치는 인식하는데 필요한 0.5초간 10pt 이상 위치가 움직이면 안 된다.

Long Press Gesture는 겉으로 보이는 작동 방식과는 다르게 Continuous Gesture에 속한다.
Gesture가 인식된 후에 터치가 움직이면 Gesture가 Changed 상태로 전환되고, Action 메서드를 반복적으로 호출할 수 있다.

@IBAction func longPressAction(_ sender: UILongPressGestureRecognizer) {
	if sender.state == .began {
		blurView.isHidden = true
	} else {
		blurView.isHidden = false
	}
}

따라서 위와 같이 Descete Gesture로 판단하여 코드를 작성하면 else 블록이 반복적으로 호출되고.
논리적 문제가 발생할 여지가 생긴다.

@IBAction func longPressAction(_ sender: UILongPressGestureRecognizer) {
	if sender.state == .began {
		blurView.isHidden = true
	} else if sender.state != .changed {
		blurView.isHidden = false
	}
}

따라서 이전처럼 ended 상태를 처리하거나 위와 같이 changed 상태가 아닌 상태를 처리하도록 구현하기도 한다.
이 경우 Gesture가 error로 중지되는 경우에도 블록 내의 코드를 실행하게 되는 장점도 생긴다.

 

Screen Edge Pan Gesture

Screen Edge Pan Gesture는 Pan Gesture이다.
일반적인 Pan Gesture와 동일하지만 Gesture의 시작 위치에서 차이점이 있다.

Screen Edge Pan Gesture는 기기의 가장자리에서 Pan Gesture를 시작해야 인식된다.
단, Screen Edge Pen Gesture Recognizer가 추가되어있지 않다면 일반적인 Pan Gesture로 인식된다.

//
//  ScrennEdgePanViewController.swift
//  Gesture Recognizer Practice
//
//  Created by Martin.Q on 2021/11/29.
//

import UIKit

class ScrennEdgePanViewController: UIViewController {
	
	@IBOutlet weak var containerView: UIView!
	@IBOutlet weak var redView: UIView!
	@IBOutlet weak var blueView: UIView!
	
	override func viewDidLoad() {
        super.viewDidLoad()
		
		redView.isHidden = true
		blueView.isHidden = true
    }
}

사용할 Scene과 연결된 코드는 위와 같다.

Container View 이하에 두 개의 View가 추가되어있고,
둘은 각각 redView, blueView로 연결되어있다.
Container View에 Gesture Recognizer를 추가하고, 이하의 두 View를 효과를 사용해 전환되도록 구현한다.

라이브러리에서 Screen Edge Pan Gesture Recognizer를 찾아 Container View에 추가한다.
Attribute에는 사용할 수 있는 여러 속성이 존재한다.

  • Edges
    선택하는 위치에서 시작하는 Pan Gesture를 Screen Edge Pan Gesture로 인식한다.
    두 개 이상을 선택할 수 있도록 되어있지만 하나 이상을 선택하면 정상적으로 작동하지 않는다.
    따라서 여러 Edge를 사용하고 싶다면 필요한 수만큼 Recognizer를 추가해야 한다.
//
//  ScrennEdgePanViewController.swift
//  Gesture Recognizer Practice
//
//  Created by Martin.Q on 2021/11/29.
//

import UIKit

class ScrennEdgePanViewController: UIViewController {
	
	@IBOutlet weak var containerView: UIView!
	@IBOutlet weak var redView: UIView!
	@IBOutlet weak var blueView: UIView!
	
	@IBAction func screenEdgePanAction(_ sender: UIScreenEdgePanGestureRecognizer) {
	}
	
	
	override func viewDidLoad() {
        super.viewDidLoad()
		
		redView.isHidden = true
		blueView.isHidden = true
    }
}

다른 Gesture와 동일하게 Action으로 코드에 연결한다.
Type은 UIScreenEdgePanGestureRecognizer로 설정한다.

@IBAction func screenEdgePanAction(_ sender: UIScreenEdgePanGestureRecognizer) {
	if sender.state == .ended {
		UIView.transition(with: containerView, duration: 0.3, options: [.transitionFlipFromRight]) {
			self.redView.isHidden = !self.redView.isHidden
			self.blueView.isHidden = !self.blueView.isHidden
		} completion: { _ in
		}
	}
}

Gesture의 상태가 ended로 전환되면 UIView의 Transition 메서드를 사용해 둘의 isHidden 속성을 반전시킨다.

의도한 대로 두 View가 전환된다.

Secreen Edge Pan Gesture는 다른 Gesture 대비 활용도가 낮은데,
이는 System이 해당 Gesture를 이미 사용하고 있기 때문이다.
Notification Center를 표시하거나, Control Center를 표시할 때 Top Edge를 사용하고,
Bottom Edge는 이미 Home Indicator를 위한 Gesture가 사용 중이다.
또한 이전 화면으로 돌아가는 Dismiss 등을 위해 Left Edge를 사용한다.

물론 위에서 설명한 System Gesture를 비활성화하거나 우선순위를 바꾸면 이미 System에서 사용 중인 Gesture도 사용할 수 있다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	redView.isHidden = true
	blueView.isHidden = false
	
	navigationController?.interactivePopGestureRecognizer?.isEnabled = false
}

이렇게 Navigation Controller의 interactivePopGestureRecognizer의 isEnabled 속성을 flase로 바꾸면,
이전으로 돌아가는 Gesture는 인식하지 않는다.

override var preferredScreenEdgesDeferringSystemGestures: UIRectEdge {
	return UIRectEdge.all
}

코드에서 prefferedScreenEdgesDeferringSystemGestures를 오버 라이딩하고,
UIRectEdge 구조체를 반환할 때 all로 설정하면 모든 Edge에서의 System Gesture보다
앱에서 제공하는 Gesture의 우선순위를 높게 설정할 수 있다.

System Gesture가 존재하는 Top과 Bottom에서의 Gesture는 일단은 비활성화되고,
Handle이 표시되거나 Home Indicator를 강조해 두 번 동작해야 System Gesture를 사용할 수 있도록 변경된다.

 

Handling Multiple Gesture

//
//  MultipleGestureViewController.swift
//  Gesture Recognizer Practice
//
//  Created by Martin.Q on 2021/11/29.
//

import UIKit

class MultipleGestureViewController: UIViewController {
	
	@IBOutlet var pinchGesture: UIPinchGestureRecognizer!
	@IBOutlet var rotationGesture: UIRotationGestureRecognizer!
	
	@IBAction func rotationGestureAction(_ sender: UIRotationGestureRecognizer) {
		guard let target = sender.view else { return }
		target.transform = target.transform.rotated(by: sender.rotation)
		sender.rotation = 0
	}
	
	@IBAction func pinchGestureAction(_ sender: UIPinchGestureRecognizer) {
		guard let target = sender.view else { return }
		target.transform = target.transform.scaledBy(x: sender.scale, y: sender.scale)
		sender.scale = 1
	}
	
	override func viewDidLoad() {
        super.viewDidLoad()

    }
}

사용할 Scene과 연결된 Code는 위와 같다.

단일 View에서 복수의 Gesture를 인식해야 할 때는 두 가지만 기억하면 된다.

Gesture가 인식되는 순서가 필요한 경우

실습에선 Rotation Gesture가 실패하는 경우 Pinch Gesture를 실행하도록 구현한다.
이처럼 인식 순서가 고정되어있다면 viewDidLoad에서 구현한다.

override func viewDidLoad() {
	super.viewDidLoad()
	
	pinchGesture.require(toFail: rotationGesture)
}

pinchGesture에서 require(toFail:)메서드를 호출하고 파라미터로 rotationGesture를 전달한다.
이렇게 되면 rotatinGesture가 실패하는 경우 pinchGesture를 인식한다.

인식 순서가 정해져 있지 않다면 delegate 패턴으로 구현한다.

//
//  MultipleGestureViewController.swift
//  Gesture Recognizer Practice
//
//  Created by Martin.Q on 2021/11/29.
//

import UIKit

class MultipleGestureViewController: UIViewController {
	
	@IBOutlet var pinchGesture: UIPinchGestureRecognizer!
	@IBOutlet var rotationGesture: UIRotationGestureRecognizer!
	
	@IBAction func rotationGestureAction(_ sender: UIRotationGestureRecognizer) {
		guard let target = sender.view else { return }
		target.transform = target.transform.rotated(by: sender.rotation)
		sender.rotation = 0
	}
	
	@IBAction func pinchGestureAction(_ sender: UIPinchGestureRecognizer) {
		guard let target = sender.view else { return }
		target.transform = target.transform.scaledBy(x: sender.scale, y: sender.scale)
		sender.scale = 1
	}
	
	override func viewDidLoad() {
        super.viewDidLoad()
		
		pinchGesture.delegate = self
		rotationGesture.delegate = self
    }
}

extension MultipleGestureViewController: UIGestureRecognizerDelegate {
	func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
		if gestureRecognizer == pinchGesture && otherGestureRecognizer == rotationGesture {
			return true
		}
		
		return false
	}
}

사용할 모든 Gesture의 delegate를 View Controller로 지정하고,
UIGestureRecognizerDelegete를 채용하는 extension을 추가한다.

gestureRecognizer(shouldRequireFailureOf:) 메소드를 호출해 파라미터로 gesture를 전달하면
두 번째 파라미터인 otherGestureRecognizer를 먼저 인식하도록 할 수 있다.
이 둘은 동작이 완전히 다르기 때문에 해당 패턴이 필요하지 않다.
실제로 앱을 개발할 때 이러한 패턴을 사용해야 한다면 UX 자체에는 문제가 없는지 검토해봐야 한다.

따라서 이런 패턴도 있다는 것만 알아두고 해당 코드는 주석 처리한다.

지금은 한 번에 한 가지의 Gesture만 처리되고,
이전의 Gesture가 실패해야 다른 Gesture가 인식되기 때문에 전환이 조금 어려운 경우가 생긴다.

모든 Gesture 동시에 처리하기

extension MultipleGestureViewController: UIGestureRecognizerDelegate {
//	func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer) -> Bool {
//		if gestureRecognizer == pinchGesture && otherGestureRecognizer == rotationGesture {
//			return true
//		}
//
//		return false
//	}
	
	func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
		return true
	}
}

extension에 gestureRecognizer(shoulRecognizSimultaneouslyWith:) 메서드를 추가하고 true를 반환하도록 구현한다.
해당 메소드를 구현하지 않으면 false를 반환하는 것과 같고, 한 번에 한 가지의 Gesture만 인식하게 된다.
파라미터를 통해 전달되는 Gesture에 따라 상세조건을 명시할 수도 있지만,
이번엔 true만 반환해 모든 Gesture를 동시에 처리할 수 있도록 구현한다.