좋아, 슬라이드로 다시 돌아가서 제스처에 대해서 알아보자
그리고 돌아와서 실제로 우리 얼굴에 제스처를 넣어볼꺼야
우리는 마지막 시간에 뷰에서 어떻게 그리는지 배웠지
우리는 입이랑, 눈이랑, 눈썹 같은 모든 걸 그려봤고
이제 어떻게 하는지 감이 꽤 잡힐꺼야
우리는 drawRect에 UIBezierPath를 사용했어, 어렵지 않지
하지만 제스처는 어떨까?
제스처라는 것은 화면을 통해 전달된 사용자의 입력이야
우린 화면 위에 올려진 모든 손가락의 위치와 운동을 알 수 있지
이건 가능해
이걸 해주는 API도 있지만 절대로 사용하지는 않지
왜 사용하지 않을까?
그 이유는 사용자는 너희가 만든 UI와 소통하는 방법이
스와이프(Swipe)나 핀치(Pinch)처럼 제스처로 하기 때문이야
아니면 팬(Pan)한다던지
이것들은 모두 사용자가 하는 제스처야
iOS에는 사용자의 입력을 제스처로 인식하는 추상 레이어가 있어
그래서 제스처를 미리 정의해서 무언가를 하게 하는거야
그 제스처가 실제로 발생하면 말이지
미리 정의된 제스처인거지
이 방법의 좋은 점은 스와이프(Swipe, 쓸어내기)는
모든 앱에서 같은 것으로 인식된다는 것이야
예를 들어 스와이프하는 속도라든가
얼마나 길게 스와이프 하는지 등은 모든 앱에서 똑같이 인식 돼
왜냐하면 모든 앱들이 이 제스처를 사용하기 때문이야
만약 너희가 스와이프를 직접 구현해야 했다면
사용자가 화면을 넘길만큼 스와이프를 충분히 빠르게 했는지
아니면 이게 팬을 한건 아닌지 일일이 확인해야겠지
이 모든 로직을 직접 구현해야 할 뿐더러
직접 구현하는 것은 귀찮고 앱 사이에서 일정하지도 못할거야
제스처는 iOS에서 UIGestureRecognizer라는
클래스의 인스턴스에 의해 인식이 돼
좋은 이름이지만 UIGestureRecognizer 자체는 추상적이야
UIGestureRecognizer를 절대 생성할 일은 없지
대신 UIGestureRecognizer를 상속받는 클래스들은 여럿 있어
원하는 제스처 종류를 쓰려고 그 서브클래스들을 생성하는거야
팬를 인식하는 PanGestureRecognizer,
핀치를 인식하는 PinchGestureRecognizer,
탭과 스와이프를 인식하는 TapGestureRecognizer,
SwipeGestureRecognizer 등등이 있지
이런 Recognizer 클래스를 쓰고 싶다면 2가지 파트가 있어
하나는 어떤 UIView객체가 이 Recognizer를 받아서
해당하는 제스처를 인식할 수 있도록 해야 해
그래서 제스처를 인식하는 파트가 있어
GestureRecognizer를 생성하면 인식하게 되는데
View에게 이 GestureRecognizer를 사용하라고 요청하는거야
오직 View만 제스처를 인식할 수 있고 Controller는 불가능해
Controller는 제스처를 인식할 수 없고 오로지 View만이야
따라서 첫번째 파트는 원하는 GestureRecognizer를 생성하고
원하는대로 설정한 뒤에
어떤 View에게 지금부터 제스처를 인식해달라고 부탁하는거야
두번째 파트는 그 Recognizer가 제스처를 인식했다면?
그렇다면 어떻게든 그걸 처리해야하고
처리하는 건 GestureHandler가 담당해
그래서 GestureRecognizer와 GestureHandler가 있는거야
GestureHandler는 GestureRecognizer가
해당하는 제스처를 인식하는 '상태 기계'으로 갈 때 호출 돼
'상태 기계'(State Machine)가 어떻게 생겼는지는 곧 볼거야
GestureHandler는 계속 반복적으로 호출될거야
GestureHandler는 제스처가 인식되는 과정부터
특정한 것을 움직이거나 액션을 취할 때 반복적으로 호출될꺼야
이런 과정에서 첫번째로
GestureRecognizer를 생성하고 어떤 View에 추가하는건
일반적으로 Controller가 처리해
반드시 Controller가 처리할 필요는 없지만
Controller가 특정한 View에
제스처를 인식하도록 하는건 좋은 방법이야
View들은 어차피 Controller의 미니언(부하)이니까
Controller는 자신의 미니언들을 관리하고 싶어하기 때문에
일반적으로 Controller가 GestureRecognizer를
View에 추가하려고 하겠지만
어떤 View들은 제스처 인식 기능이 매우 핵심적이어서
View 자체적으로 GestureRecognizer를 추가하지
예를 들어, scrollView를 생각해보자
scrollView를 위 아래로 팬(Pan)해서
스크롤할 수 없다면 무슨 의미가 있을까?
쓸모가 없겠지
더이상 scrollView도 아닐꺼야
따라서 scrollView는 자체적으로 PanGestureRecognizer와
PinchGestureRecognizer를 자기 자신에 추가해
아마도 생성자에 넣어둘꺼야
하지만 대부분은 Controller가GestureRecognizer를 추가해줘
다시 말하지만, Controller는 제스처를 인식할 수 없어
오로지 GestureRecognizer를 가지고 있는 View만 할 수 있지
Controller는 GestureRecognizer를 View에 추가할 수만 있어
소속된 View에 GestureRecognizer를 생성해서 추가하는거야
두번째로 처리를 담당하는 Handler는
이건 Controller에서 처리할 수도 있고, View에서 처리할 수도 있어
아니면 아예 다른 사람이 처리할 수도 있지
UI와 독립된 구조이기 때문에 모델에서 처리할 일은 절대로 없어
하지만 View나 Controller가 처리할 수 있는거지
일반적으로, 만약 제스처가 하는 일이 유일하게
View가 어떻게 보이는지 만을 바꾸는 거라면
FaceView에서처럼 View의 비율이나 색상을 바꾸는 정도라면
View가 자체적으로 그 제스처를 처리할거야
View는 Recognizer로 인식을 한 다음에 그걸 처리할거야
하지만 만약 제스처가 모델을 직접적으로 변경한다면
그럴 때는 당연히 Controller가 제스처를 처리하게 될거야
왜 그런건지 이해하겠니?
View는 Model을 직접 볼 수 없기 때문이야
하지만 Controller는 볼 수 있지
따라서 Controller는 모델에 영향을 주는
제스처의 Recognizer로 자기 자신을 설정할거야
우리 데모에서도 보게 될거야
그럼 어떻게 GestureRecognizer를 UIView에 추가할까?
예시로 팬(Pan)하는 제스처를 인식하는 뷰가 필요하다고 해보자
팬(Pan)이란 화면 위에 손가락을 올려놓고
떼지 않은채로 움직이는 걸 말해
손가락을 때버리면 그때 팬 제스처는 끝나버리는 거야
이게 팬(Pan)이야
화면 위를 손가락으로 옮겨다니는 거야
팬 제스처를 인식하는 View를 원한다면 어떻게 해야 할까?
우선 Controller 어딘가에 PanGestureRecognizer를
추가하는 코드를 넣어야 해
팬 제스처가 이뤄질 View에 추가하는거야
이 코드를 넣기 제일 좋은 곳은 Outlet의 didSet 구문 안이야
팬(Pan)을 하고 싶은 View의 포인터가 생기자마자
그곳에서 바로 PanGestureRecognizer를 추가하는거야
여기 didSet은 전에 했던 데모에서 본 적이 있을텐데
어떤 Outlet에 didset을 해주면 오직 한번만 호출되고
iOS가 처음 저 View를 연결시킬 때 호출이 될거야
GestureRecognizer를 넣기에 완벽한 타이밍인거지
이걸 해볼건데
우선 PanGestureRecognizer를 생성할거야
여기에 UIGestureRecognizer의
구체화된 서브클래스인 PanGestureRecognizer가 있어
생성자 메소드에 인자 2개를 받고 있는데
첫번째 인자는 제스처를 인식을 하면 누가 처리를 할지야
따라서 이 PanGestureRecognizer가 하는 말은
팬를 인식하면 누가 날 위해 이것을 처리해줄거니? 하는 거야
답은 self이고 다시 말해 Controller야
이건 Controller에 있는 Outlet이고
target에 self를 넣는다는 의미는
Controller가 자체적으로 팬 제스처를 처리한다는 뜻이지
여기 두번째 인자는
제스처가 인식되면 self 안에 어떤 메소드를 호출할 건지야
여기는 좀 특이한 코드 구문이 쓰이는데
여기에는 Objective-C 런타임 호환 selector가 쓰여야 해
이건 Objective-C 런타임에 보이는 메소드인 selector를 말해
Objective-C 런타임에 보이는 메소드가 되려면
이 메소드는 NSObject를 상속받는 클래스의 메소드여야만 해
예전에 NSObject를 설명하면서 가끔 이게 필요하다고 했었지?
지금이 필요한 때야
여기에는 일반적으로 신경을 쓰지 않아도 돼
거의 항상 NSObject를 상속받은 UIViewController나
NSObject를 상속받는 UIView가 제스처를 처리하기 때문이야
따라서 일반적으로 신경쓰지 않아도 돼
NSObject를 상속받지 않는 스위프트 객체에
이 처리를 보낼 일은 거의 없을꺼야
하지만 여기 있는 이 구문의 뜻은
Objective-C에 호환되는 selector를 생성하라는 뜻이야
'#selector'는 메소드를 판별하는데 필요한 구문이야
여기에는 클래스 이름 다음에 마침표
그리고 메소드 이름과 함께 인자 이름을 넣어줘야 해
인자 이름을 넣는 걸 잊지마
여기 있는 pan( ) 메소드는 다음 화면에서 보여줄건데
하나의 인자를 가지고 있어
그 인자가 PanGestureRecognizer야
Target-Action을 설정할 때
자기 자신을 보내는 sender를 가진 버튼을 만든 것처럼
여기도 똑같아
PanGestureRecognizer가 제스처를 인식하기 시작하면
이 pan( ) 메소드를 ViewController에게 보낼 때
자기 자신을 인자로 넣고 보내 주지
여기에 왜 언더바가 들어가는지 궁금할 수도 있어
왜 여기에 첫번째 인자 이름이 들어가지 않냐고 물어볼 수도 있지
왜냐하면 그 이름이 뭔지 상관없거든
pan 메소드 중에 하나의 인자를 가진 pan 메소드를
원한다는 것만 분명히 하면 돼
만약 pan 메소드가 여러개 있는데 인자 이름이 다르다면
여기에 인자 이름까지 명시해주어야겠지
하지만 나는 여기에 언더바 '_' 를 넣을 수 있어
스위프트에서 언더바는 그게 뭐든간에 신경쓰지 않는다는 뜻이지
이게 뭔지 정말 상관이 없다는 말이야
'신경쓰지 않는다' 를 대신 표현한 거야
그래서 여긴 정말 신경쓰지 않아
여기에 언더바를 쓸 수 있다는 걸 알려주고 싶었어
하지만 여기에 콜론과 언더바를 넣어 주지 않으면
인자가 없는 pan 메소드를 호출하려고 할거야
그러니까 주의해야 해
GestureRecognizer가 인자로 있는 pan 메소드를 호출하려면
인자가 있다고 거기에 명시해줘야 해
여기까지가 selector를 명시해주는 거였고
그리고 이 recognizer를 활성화시켜야 하는데
UIView인 pannableView라는 view에
GestureRecognizer로 생성한 recognizer를 추가해주면 돼
저걸 추가하자마자 이 뷰는 팬 제스처를 인식하기 시작할꺼야
인식이 되면 여기 이 메소드를 Controller인 self에게 보낼꺼야
이제 어떻게 Handler를 구현할건지 알아보자
여기 있는 이 pan( ) 메소드 말이야
이 메소드를 구현할려면 어떻게 해야할까?
이런 메소드를 어떻게 구현할지 이해하려면
먼저 UIGestureRecognizer의
구체화된 서브클래스들을 좀더 이해해봐야 할거야
팬 제스처를 살펴보자
UIPanGestureRecognizer에는
특히 팬 제스처를 위한 몇가지 메소드들이 있어
에를 들어 UIPanGestureRecognizer에는
translationInView라는 메소드가 있어
이 메소드는 UIView를 인자로 받고
View의 좌표에서 팬이 얼마나 움직였는지를 알려줘
이건 팬에 꼭 필요한 기능이지
왜냐하면 얼마나 멀리 움직였는지 알아야 하기때문이야
UIPanGestureRecognizer는 velocityInView도 있는데
이건 팬이 얼마나 빨리 진행되는지를 알려줘
엄청 빨리 팬을 하거나 매우 천천히 팬을 하는지 확인할 수 있어
그림을 그리는 앱이라면 천천히 매우 조심스럽게 팬을 하겠지
반대로 빠르게 팬을 하면 크게 곡선을 그려지거나 하겠지
여기에는 심지어 translation(변환) 값을 지정해주는
setTranslation이라는 메소드도 있어
이걸 왜 설정해야 할까?
만약 이걸 설정해주지 않는다면
이 translationInView는 누적한 translationInView가 될거야
팬 제스처가 처음 시작되었을 때부터
쭉 누적된 이동 경로가 되는거야
이렇게 누적된 값 대신에
마지막에 알려준 값에서 얼마나 값이 변했는지를 알고 싶다면
이 값을 0으로 계속 다시 설정해주면 돼
만약 0으로 계속 다시 설정하고
다음 번에 translationInView를 호출하면
증가된 값만 구할 수 있을꺼야
마지막 움직임에서 변화한 매우 작은 값이지
데모에서 이걸 0으로 매번 다시 설정해서
그때 그때 팬의 변화값을 확인할게 될거야
UIPanGesture의 추상화된 슈퍼클래스는
매우 중요한 변수인 'state'를 가지고 있어
내가 말했듯이 GestureRecognizer는 '상태 기계'를 거치는데
핸들러에서 제스처가 어떤 상태에 있는지 확인할 수 있어
모든 GestureRecognizer는 .Possible 상태에서 시작해
UIGestureRecognizerState.Possible 이야
그리고 스와이프(Swipe)같은 끊어지는 제스처에는
일단 스와이프를 감지하면 바로 .Recognized로 상태가 바뀌어
스와이프를 인식(recognized)한거야
그럼 핸들러가 호출되고
상태(state)도 .Recognized 로 바뀔거야
팬이나 핀치같이 지속되는 제스처는
팬이 눌리는 순간부터 바로 .Began이라는 상태가 되고
팬이 움직이는 순간부터 .Changed라는 상태로 바뀌어
핸들러도 .Changed의 상태가 될 때마다 반복적으로 호출될거야
매번 이 translation은 바뀌고 있는거야
그러다가 손가락이 화면에서 떼지면 .Ended 상태로 가게 돼
.Failed(실패한)와 .Cancelled(취소된)같은 다른 상태들도 있어
예를 들어 팬을 하는 도중에 전화가 걸려온다면
제스처는 .Cancelled될꺼야
그럴 때, 그걸 처리해야 할 수도 있고 안 해도 될 수도 있어
만약 팬 움직임이 그때 그때 바뀌는 것만 다루고 있다면
인터럽트(*실행중인 프로그램을 일시중단하고 새 요청 처리)가
들어오든 말든 누가 신경쓰겠어
팬이 그때 그때 움직이는 것만 보고 있었으니까
만약 팬이 끝날 때 뭔가 큰 걸 처리해야 한다면
팬이 끝났을 때 처리 할 일을 여기에 넣어주는 게 좋을거야
하지만 대부분은 이걸 신경 쓸 필요가 없어
왜냐하면 모든 일은 .Change 상태에서 처리하기 때문이야
따라서 .Cancelled 와 .Failed는 .Ended과 같지만
손가락을 떼는 건 알아채지 못해
별건 아니야
이제 팬 제스처에 대한 정보를 다 알아봤어
이제 어떻게 핸들러가 팬 제스처를 처리하도록 만들어줄까?
이게 그 코드야
이 코드는 Controller 안에 들어가게 될거야
Controller가 팬 제스처를 처리한다고 말 했었지
첫번째로 상태(state)를 볼건데
pan(_ :) 라고 되어 있는 메소드 보이지?
이 pan( ) 메소드는 한 개의 인자를 받아
저기 언더바 대신에 'gesture'라는 인자를 넣어줄수도 있었어
왜냐하면 여기선 gesture라고 불리기 때문이지
하지만 난 그 이름이 뭐든 상관이 없어
여기선 인자 하나만 받지
인자가 없는 pan 메소드가 있을 수 있겠지만 저기에 그런건 없고
그런 메소드는 팬 제스처를 인자로 넘기지 않을꺼야
하지만 여기선 팬 제스처가 필요해
팬이 얼마나 움직였는지 알아야 하기 때문이지
다음으로 팬 제스처가 들어 있는
상태(gesture.state)값에 switch구문을 써서
만약 제스처의 상태가 .Changed나 .Ended 라면
Controller에 있는 무언가를 업데이트 해줄거야
두가지 경우가 아니라면 그냥 무시해버릴거야
만약 .Cancelled나 .Failed나 .Began 상태라면
신경쓰지 않을꺼야
제스처가 움직일 때만 관심이 있거든
그래서 .Changed나 .Ended 상태만 보는거야
여기서 switch문의 신기한 기능을 보여줄건데
너희 과제에도 이걸 활용해봤으면 해
여기서 'fallthrough'(다음 case로 넘어감)라는게 있는데
스위프트 switch문의 case는
C처럼 다음 case로 fallthrough 하지 않는다는 걸 기억해
물론 'fallthrough' 명령어로 강제로 fallthrough하게 할 순 있어
아마 코드를 이렇게 적는 것보다
case .Changed, .Ended 라고 하는게 더 나았을 거야
그게 더 간단하지만 fallthrough를 보여주려고 이렇게 해봤어
.Changed 거나 .Ended인 경우에는 이 안에서
pannableView의 translation을 구할거야
pannableView라는 outlet 기억하지
translation을 gesture에서 구할거야
그리곤 팬이 있는 장소에 의존적인 뭔가를 업데이트 할거야
팬이 어디서부터 얼마나 움직였는지 아니까
이 정보가 필요한 무언가를 업데이트 할거고
그리고 translation을 다시 0으로 설정해줄꺼야
원하면 CGPoint.0 으로 해도 돼
어쨌든 0으로 다시 설정해줄꺼야
따라서 핸들러가 호출되는 다음 번엔
이 translation은 나한테 보낸 마지막 값에서
증가한 translation이 될거야
모두들 이해 됐니?
이게 다야
여기까지가 핸들러를 구현하는 방법이야
꽤 단순하고 직관적이야
데모에서 보게 될텐데 그렇게 어렵지 않아
팬 말고도 다른 구체적인 제스처에 대해서 간단히 이야기해보자
핀치(Pinch)라는 것도 있어
핀치는 이렇게 손가락 두개로 오므렸다 폈다 하는거야
핀치에서는 translation 말고 scale(비율)을 가져올 수 있어
따라서 손가락을 여기서 시작해서 2배만큼 키우면
scale 값은 2가 되겠지
절반으로 줄이면 scale은 0.5가 될거고
그래서 화면에 뭐가 있든 그것의 비율을 조정하고
팬과 마찬가지로 velocity(속도)를 구할 수 있어
얼마나 빠른 속도로 핀치하는지를 말해주지
UIRotationGestureRecognizer에서 Rotation(회전)은
두 손가락을 돌리는 제스처를 말해
문고리를 돌리는 것처럼 말이야
몇 라디안만큼 회전했는지를 알려줄꺼야
여기도 얼마나 빠르게 회전하는지를 나타내는 velocity가 있어
UISwipeGestureRecognizer
Swipe(쓸어내기) 제스처는 보통 사용 전에 설정을 해줘야 해
쓸어내는 방향을 왼쪽, 오른쪽, 위, 아래 중에서
어디로 할건지 먼저 말해줘야 해
또 두 손가락인지, 한 손가락인지 아니면 세 손가락인지도 필요해
이 설정들을 모두 마치고나서
GestureRecognizer를 추가하게 되는데
미리 설정해 둔 형태의 제스처만 인식하게 될꺼야
예를 들어 손가락 두개로 위 방향으로 스와이프한다고 했으면
손가락 두개로 위로 스와이프를 해야 하고
그렇지 않으면 인식하지 못할거야
이게 스와이프야
탭(UITapGesture~)은 얼핏 보기에는 끊어지는 제스처 같지만
끊어지는 불연속적인 제스처가 아니야
탭(두드리기)을 몇 번 해야 하는지도 설정할 수 있어
탭을 한번 했는지 아니면 두번 했는지가 다른거야
또 두 손가락으로 했는지 한 손가락으로 했는지도 마찬가지이지
그리고 여기에선 탭이 언제 발생했는지를 알려면
.Ended 상태를 찾아야 해
왜냐하면 탭은 사실 중간 과정이 있을 수도 있거든
더블 탭은 첫번째 탭을 한 뒤 두번째 탭을 하잖아
그 사이에 핸들러를 호출하게 되는 상태 변화가 일어날 수 있지만
더블 탭이 완전히 끝난건 아니지
탭에서는 .Recognized 상태를 확인할 수 없는게
탭은 불연속적인 제스처가 아니기 때문이야
중간 단계를 거치니까 그래
탭에서는 .Ended 상태로
설정해 둔 제스처가 모두 끝났는지를 확인하게 될거야
'연습 > swift개발' 카테고리의 다른 글
[swift 입문] Dynamic Table View - Data Source (0) | 2022.01.20 |
---|---|
4.11 얼굴에 제스처를 적용해보자 (0) | 2022.01.20 |
4.9 얼굴앱 - 모델과 컨트롤러 분리 (0) | 2022.01.20 |
4.8 다양한 얼굴 표정 (0) | 2022.01.20 |
4.7 얼굴앱 - 눈 넣기 (0) | 2022.01.20 |