티스토리 뷰

이제 멀치터치 이벤트 내용입니다.

 

제스처


앞서 UIView에서 그리는 방법을 알아봤습니다. 터치는 어떻게 알수 있을까요?

Touch Down, Move, Up등에 대한 모든 터치 이벤트를 알아낼 수 있습니다.(저수준/구현량이 많음)

아니면 특정 제스터에 동작도 인식이 가능합니다.(스와이프, 확대/축소 등)(고수준/구현량이 적음)

 

UIGestureRecognizer 인스터스를 통해 제스쳐을 인식할 수 잇습니다.

추상 클래스이며, 모든 제스처를 인식 할 수는 없습니다.

하지만 많은 서브 클래스들을 통하여 다양한 제스처를 인식할 수 있습니다.

크게 2가지 동작이 필요합니다.

1. 제스처 인식기를 UIView에 추가합니다.(UIView에게 특정 제스처를 인식하라고 알려줍니다.)

2. 제스처가 인식되었을대 처리할 메소드를 제공해야합니다.

 

주로 첫번째는 컨트롤러가 많이 사용합니다.(뷰에게 특정 제스처를 인식 요청)

스토리 보드에서도 추가가 가능합니다.

때로는 뷰가 내부적으로  추가할 수 있습니다.(스크롤, 확대/축소등)

하지만 대부분은 컨트롤러가 추가합니다.

 

두번째는 제스처를 처리하는 것입니다.

뷰가 처리하거나 컨트롤러가 처리할 수 있습니다.

모델에 영향을 받는다면 컨트롤러에서 처리할 것이고

뷰가 보이는 방식에만 영향을 주는것이라면 뷰에서 처리할 것입니다.

 

UIView에 제스처 추가하기

주로 아웃렛 설정자의 didSet에서 추가를 합니다.

pan 제스처를 등록하고 싶다면 아래와 같은 방식으로 추가합니다.

@IBOutlet weak var pannableView: UIView {
    didSet {
        let panGestureRecognizer = UIPanGestureRecognizer(target: self,
                                                          action: #selector(ViewController.pan(recognizer:)))
        pannableView.addGestureRecognizer(panGestureRecognizer)
    }
}

대부분의 GestureRecognizer들의 생성자는 두 파라미터를 받습니다.

처음은 처리할 객체(주로 뷰나 컨트롤러입니다.)

두번째는 제스처가 인식될 시 처리할 함수입니다. 

뷰는 여러개의 제스처를 동시에 가질 수 있습니다.(1개든 20개든)

 

각 제스쳐 클래스들은 특정한 정보들을 가지고 있습니다.

UIPanGestureRecognizer에는 아래와 같은 함수들을 제공합니다.

func translation(in: UIView?) -> CGPoint // cumulative since start of recognition
func velocity(in: UIView?) -> CGPoint // how fast the finger is moving (points/s)
func setTranslation(CGPoint, in: UIView?)

 

제스처 최상위 클래스인 UIGestureRecognizer에는 state라는 중요한 변수가 있습니다.

var state: UIGestureRecognizerState { get }

.possible은 터치가 시작되어 Pan 동작이 시작되기전 상태가 됩니다.

그러다 인식(이동)이 시작되면 .began으로 바뀌고

계속 이동 중이라면 .changed로 바뀝니다. 계속해서 이동을 하여도 .changed 상태이며

계속해서 등록된 핸들러 함수가 호출됩니다.

그러다 손가락이 떨어지면 .end 상태로 바뀌게 됩니다.

swipe와 같은 제스처에는 .changed 상태가 없고 바로 .end로 바뀌게 됩니다.

.failed과 .cancelled 상태도 존재합니다.

failed는 여러 제스처 중 하나가 다른 것보다 우위를 점했을 경우에 나타납니다.

탭과 이동 제스처가 등록된 경우에서 탭만 했을시 탭 제스처는 동작되지만 이동 제스처는 취소가 됩니다.

캔슬은 드래그&드랍에서 주로 나타나는데

뭔가를 시작하다가 하다가 드래그&드랍을 하면 취소가 됩니다.

 

팬 제스처를 예를 들어봅시다.

func pan(recognizer: UIPanGestureRecognizer) {
      switch recognizer.state {
          case .changed: fallthrough
          case .ended:
              let translation = recognizer.translation(in: pannableView)
              // update anything that depends on the pan gesture using translation.x and .y
             recognizer.setTranslation(CGPoint.zero, in: pannableView)
        default: break
      }
}

 

fallthrough는 C나 C++에서 case문을 여러개 사용하는것도 동일한 효과를 가집니다.( case. changed, .end: 와 동일 )

translation은 전달한 뷰를 기준으로 좌표값 구하는 함수입니다.

setTranslation의 첫 파라미터가 CGPoint.zero인데 점차적으로 증가하는 pan으로 사용하기 위해서 입니다.

(다음 제스처에서 0부터 시작함???)

 

UIPinchGestureRecognizer

var scale: CGFloat // not read-only (can reset) 2.0은 두배 / 0.5는 절반
var velocity: CGFloat { get } // scale factor per second / 속도

 

UIRotationGestureRecognizer

var rotation: CGFloat // not read-only (can reset); in radians / 회전각도(라디안)
var velocity: CGFloat { get } // radians per second / 속도

 

UISwipeGestureRecognizer

아래 변수들은 추가시 설정해줘야하는 값들입니다.

 

var direction: UISwipeGestureRecoginzerDirection // 인식하길 원하는 방향
var numberOfTouchesRequired: Int // 필요한 손가락 터치수

 

UITapGestureRecognizer

아래 변수들고 설정해줘야합니다.

조건에 해당할 경우 .ended 상태로 변하게 됩니다.

var numberOfTapsRequired: Int // 싱글 터치, 더블 터치
var numberOfTouchesRequired: Int // 터치에 필요한 손가락 수

 

UILongPressRecognizer

아래 변수들고 설정해줘야합니다.

조건에 해당할 경우 .ended 상태로 변하게 됩니다. 특이하게 손가락 이동시 .chaged 상태를 가집니다.

var minimumPressDuration: TimeInterval // 최소 눌러야하는 시간
var numberOfTouchesRequired: Int // 필요한 손가락 개수
var allowableMovement: CGFloat // 얼마나 멀리 손가락이 이동할 수 있는가?(드래그&드랍과 연관되어 중요함 / .cancelled 상태로 변하는 조건)

 

아래는 실습내용입니다.

 

추가할 제스처


스와이프 제스처  : 덱에서 다음 카드 가져오기(모델 영향 -> 컨트롤러에 핸들러 추가)

탭  제스처  : 카드 뒤집기(스토리 보드에서 추가할 예정 -> 모델 영향)

핀치  제스처 : 카드 확대 및 축소( 뷰에만 영향 -> 뷰에 핸들러 추가 )

 

스와이프 제스처 추가하기

먼저 인터페이스 빌더에서 PlaycardView를 outlet으로 연결합니다.

Ctrl+드래그를 통하여 추가합시다.

 

뷰 생성 후 호출되는 didSet에서 제스처를 등록시키도록 합시다.

class ViewController: UIViewController {

    var deck = PlayingCardDeck()
    
    @IBOutlet weak var playingCardView: PlayingCardView! {
        didSet {
            let swipe = UISwipeGestureRecognizer(target: self, action: #selector(nextCard))
            swipe.direction = [.left, .right]
            
            playingCardView.addGestureRecognizer(swipe)
        }
    }
    
    @objc func nextCard() {
        if let card = deck.draw() {
            playingCardView.rank = card.rank.order
            playingCardView.suit = card.suit.rawValue
        }
    }
    
    ...
}

didSet에서 UISwipeGestureRecognizer를 생성하고 뷰에 추가합니다.

UISwipeGestureRecognizer의 파라미터의 첫번째는 이벤트를 받을 target을 전달해주며(self:controller)

두번째 파라미터는 동작 인식시 호출될 함수입니다.

#selector()로 함수를 감싸줘야하며 호출될 함수의 맨앞에는 @objc 키워드를 추가해줘야합니다.

런타임시 swift 함수를 objc로 전달해주는 키워드입니다.

코드를 적용 후 스와이프 해보면 정상적으로 카드가 변환됩니다.

스와이프하는게 안보이네요...

 

탭 제스처 추가하기

스토리보드를 통하여 탭 제스처를 추가해봅시다.

object에서 tabGuesture 검색하여 PlayingCardView에 추가한 뒤

Ctrl+드래그를 통하여 flipCard 이름의 Action를 추가합시다.

동작을 위해 추가한 flipCard 함수 내용을 추가합시다.

@IBAction func flipCard(_ sender: UITapGestureRecognizer) {
    switch sender.state {
    case .ended:
        playingCardView.isFaceUp = !playingCardView.isFaceUp
    default: break
    }
}

state 변수값이 .end인 경우에 갱신하도록 수정했습니다.

빌드 후 실행시 정상적으로 뒤집어지는것을 확인 할 수 있습니다.

 

핀치 제스처 추가하기

PlayingCardView에서 확대 및 축소에 대한 비율값을 저장할 변수를 추가합니다.

그리고 draw(Rect)함수도 부분 수정합니다.

class PlayingCardView: UIView {

    ...
    
    var faceCardScale: CGFloat = SizeRatio.faceCardImageSizeToBoundsSize  { didSet { setNeedsDisplay(); setNeedsLayout() }}
    
    ...
    
    
    override func draw(_ rect: CGRect) {
        let roundRect = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
        roundRect.addClip()
        UIColor.white.setFill()
        roundRect.fill()
        
        if isFaceUp {
            if let faceCardImage = UIImage(named:"\(rankString)\(suit)",
                                           in:Bundle(for:self.classForCoder),
                                           compatibleWith: traitCollection) {
                //faceCardImage.draw(in: bounds.zoom(by: SizeRatio.faceCardImageSizeToBoundsSize))
                faceCardImage.draw(in: bounds.zoom(by: faceCardScale))
            }
            else {
                drawPips()
            }
        }
        else {
            if let cardBackImage = UIImage(named:"cardback",
                                           in:Bundle(for:self.classForCoder),
                                           compatibleWith: traitCollection) {
                cardBackImage.draw(in: bounds)
            }
        }
    }
 }

확대/축소 동작시 처리할 핸들러 함수를 추가합시다.

class PlayingCardView: UIView {
    ...
    
    @objc func adjustFaceCarScale(byHandlingGestureRecognizedBy recognizer: UIPinchGestureRecognizer) {
        switch recognizer.state {
        case .changed, .ended:
            faceCardScale *= recognizer.scale
            recognizer.scale = 1.0
        default: break
        }
    }
    
    ...
}

recognizer.scale값을 1.0으로 초기화하지 않으면 변동폭이 정상적으로 적용되지 않을것입니다.

 

이제 PlayingCardView에 제스처를 추가합시다.

ViewController 파일로 돌아와 아래와 같이 코드를 적용합시다.

class ViewController: UIViewController {

    var deck = PlayingCardDeck()
    
    @IBOutlet weak var playingCardView: PlayingCardView! {
        didSet {
            let swipe = UISwipeGestureRecognizer(target: self, action: #selector(nextCard))
            swipe.direction = [.left, .right]
            playingCardView.addGestureRecognizer(swipe)
            
            let pinch = UIPinchGestureRecognizer(target: playingCardView, action: #selector(playingCardView.adjustFaceCarScale(byHandlingGestureRecognizedBy:)))
            playingCardView.addGestureRecognizer(pinch)
        }
    }
	
    ...
}

위 코드를 적용 후 빌드를 해보면 확대/축소가 적용되는 것을 확인 할 수 있습니다.

그림카드(J,Q,K) 11,12,13에서만 적용이 되니 참고바랍니다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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
글 보관함