Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- 리팩토링
- ribs
- Refactoring
- map
- swiftUI
- ios
- clean architecture
- swift documentation
- Xcode
- uiscrollview
- Human interface guide
- rxswift
- 애니메이션
- MVVM
- Observable
- collectionview
- HIG
- Clean Code
- Protocol
- uitableview
- combine
- SWIFT
- 리펙토링
- tableView
- 리펙터링
- RxCocoa
- 클린 코드
- 스위프트
- UITextView
- UICollectionView
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] UIGestureRecognizer, UIViewPropertyAnimator - 응용(swipe하여 뷰 넘기기) 본문
UI 컴포넌트 (swift)
[iOS - swift] UIGestureRecognizer, UIViewPropertyAnimator - 응용(swipe하여 뷰 넘기기)
jake-kim 2022. 5. 11. 22:30
구현 아이디어
- UIGestureRecognizer를 사용하여 제스쳐 이벤트를 감지
- 제스쳐 이벤트가 발생하면, 애니메이션 실행
- 애니메이션은 UIViewPropertyAnimator를 사용하여 구현
- 애니메이션은 다음 뷰가 점점 보여지도록 설정
- UIViewPropertyAnimator를 사용하면 현재 진행되고 있는 진행률에 따라 애니메이션을 진행시킬 수 있고, 다시 reversed 시키는 기능이 편하므로 사용
- 드래그하면 뷰가 따라서 이동되게끔 하는 구현 아이디어
- animator 정의 (transform 사용하여 뷰가 왼쪽으로 이동되는 애니메이션)
- gesture의 began상태 - animator를 생성
- gesture의 changed 상태 - 현재까지 swipe된 x좌표와, width를 구하고 비율을 구하여 UIViewPropertyAnimator의 fractionComplete에 적용
- gesture의 ended or cancelled 상태 - animator의 isReversed를 사용하여 특정 임계값만큰 넘지 않거나 canclled상태인 경우 다시 뷰가 닫히도록 적용
* UIViewPropertyAnimator 개념은 이전 포스팅 글 참고
gesture 정의
- right에서 left로 제스쳐 이벤트만 감지하도록 UIPanGestureRecognizer를 서브클래싱
- 사용하는쪽에서 gesture의 state 프로퍼티를 가지고 상태를 확인
- 대표적으로 .began, .changed, .ended, .cancelled 상태가 존재
- gesture의 manitude와 velocity를 확인하여 좌에서 우로 swipe되는 경우가 아니면 failed 상태가 되도록 설정
import UIKit
final class RightToLeftSwipeGestureRecognizer: UIPanGestureRecognizer {
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesMoved(touches, with: event)
guard let view = self.view, self.state == .began else { return }
// x가 더 크면 좌우로 스와이프, y가 더 크면 위 아래로 스와이프
if velocity(in: view).x.magnitude > velocity(in: view).y.magnitude {
velocity(in: view).x < 0 ? print("우->좌") : print("좌->우")
} else {
velocity(in: view).y < 0 ? print("하->상") : print("상->하")
}
guard
velocity(in: view).x.magnitude > velocity(in: view).y.magnitude, // 수평 스와이프
velocity(in: view).x < 0 // 우 -> 좌 스와이프
else {
self.state = .failed
return
}
}
}
샘플 데이터 정의
- 하나의 ViewController에서 views들을 가지고 이 뷰들을 swipe하면서 테스트해야하므로, views들을 준비
import UIKit
private var randomColor: UIColor {
UIColor(red: CGFloat(drand48()), green: CGFloat(drand48()), blue: CGFloat(drand48()), alpha: 1.0)
}
enum Mock {
static func getViews() -> [UIView] {
(0...30).map {
let view = UIView()
// label
let label = UILabel()
label.text = "\($0) 번째 뷰"
label.textColor = .black
label.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(label)
NSLayoutConstraint.activate([
view.centerYAnchor.constraint(equalTo: label.centerYAnchor),
view.centerXAnchor.constraint(equalTo: label.centerXAnchor),
])
view.backgroundColor = randomColor
return view
}
}
}
구현
- ViewController 클래스 준비
// ViewController.swift
import UIKit
class ViewController: UIViewController {
}
- gesture인스턴스와 animator 인스턴스 준비
private let gesture = RightToLeftSwipeGestureRecognizer()
private var animator: UIViewPropertyAnimator?
private var views = Mock.getViews()
- viewDidLoad에서 테스트 데이터 view 세팅
- 테스트 view들을 self.view에 addSubview()
override func viewDidLoad() {
super.viewDidLoad()
self.views.forEach {
self.view.addSubview($0)
$0.alpha = 0
$0.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
$0.leftAnchor.constraint(equalTo: self.view.leftAnchor),
$0.rightAnchor.constraint(equalTo: self.view.rightAnchor),
$0.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
$0.topAnchor.constraint(equalTo: self.view.topAnchor),
])
}
self.views.first?.alpha = 1
self.view.addGestureRecognizer(self.gesture)
self.gesture.addTarget(self, action: #selector(handleGesture(_:)))
}
@objc private func handleGesture(_ gesture: UIPanGestureRecognizer) {
}
- handleGesture에서는 UIPanGestureRecognizer의 상태를 구분하여 처리
@objc private func handleGesture(_ gesture: UIPanGestureRecognizer) {
switch gesture.state {
case .began:
case .changed:
case
.ended,
.cancelled
:
default:
break
}
}
- gesture의 began상태 - animator를 생성
case .began:
self.view.isUserInteractionEnabled = false
self.animator = self.getNextAnimator() // 해당 메소드는 아래에서 정의
- gesture의 changed 상태 - 현재까지 swipe된 x좌표와, width를 구하고 비율을 구하여 UIViewPropertyAnimator의 fractionComplete에 적용
case .changed:
let translationX = -gesture.translation(in: self.view).x / self.view.bounds.width
let fractionComplete = translationX.clamped(to: 0...1) // 0보다 작으면 0, 1보다 크면1로 되게끔하는 메소드 따로 extension한것
guard self.animator?.fractionComplete != fractionComplete else { break }
self.animator?.fractionComplete = fractionComplete
- gesture의 ended or cancelled 상태 - animator의 isReversed를 사용하여 특정 임계값만큰 넘지 않거나 canclled상태인 경우 다시 뷰가 닫히도록 적용
case
.ended,
.cancelled
:
self.animator?.isReversed = gesture.velocity(in: self.view).x > -200 || gesture.state == .cancelled
self.animator?.startAnimation() // isReversed를 사용하면 다시 active해야하므로
- getNextAnimator() 메소드에서 애니메이션 정의
- 해당 메소드에서 지금 진행중인 애니메이션이 있으면 종료
- views 데이터에서 획득
private func getNextAnimator() -> UIViewPropertyAnimator? {
self.animator?.stopAnimation(false) // true인 경우 completionHandler호출없이 inactive상태, false인 경우 애니메이션이 멈춘 Stopped상태 (finishAnimation과 같이 사용)
self.animator?.finishAnimation(at: .end) // completion handler 호출, inactive 상태로 전환
guard self.views.count > 1 else { return nil }
let currentView = self.views[0]
let nextView = self.views[1]
- UIViewPropertyAnimation 정의 3가지 (Init, Animation, Completion)
init) duration값은 panGesture시 뷰가 따라오는 시간이므로 왠만하면 짧게 설정
// Init
let animator = UIViewPropertyAnimator(
duration: 0.5,
timingParameters: UICubicTimingParameters(animationCurve: .easeInOut)
)
Animation) currentView의 transform에 x좌표를 화면의 왼쪽으로 이동되게 설정 + 등장할 뷰의 alpha값도 1로 설정
// Animation
animator.addAnimations {
UIView.animate(withDuration: 1) {
currentView.transform = .init(translationX: -UIScreen.main.bounds.width, y: 0)
nextView.alpha = 1
}
}
Completion) 애니메이션이 종료되면 뷰를 제거하고 animator를 deinit
// Completion
animator.addCompletion { [weak self, weak currentView] position in
self?.view.isUserInteractionEnabled = true
guard
position == .end && animator.isReversed == false,
let currentView = currentView,
self?.views.isEmpty == false
else { return }
currentView.removeFromSuperview()
self?.views.removeFirst()
guard self?.animator === animator else { return }
self?.animator = nil
}
return animator
* 전체 코드: https://github.com/JK0369/ExGestures
* 참고
'UI 컴포넌트 (swift)' 카테고리의 다른 글
Comments