概要
- Twitterの画像ビューアを閉じる動作をRxSwiftを使って再現してみる
- どっちかというとLINEの画像閉じる動作に近い
- というかRxSwiftほぼ活用していない
- 加速度を計算して画面を閉じるのではなく、移動した距離を見て閉じるようにする
- サンプルリポジトリ
環境
- Xcode 9.4
- Swift 4.1
- RxSwift 4.2
- RxCocoa 4.2
- Cocoapods 1.5.3
イメージ
前提
- RxSwiftを導入している
構成
├── TwitterImageViewExample
│ ├── resources
│ │ ├── TwitterSwipeImageViewController.xib
│ │ └── ViewController.xib
│ └── sources
│ ├── AppDelegate.swift
│ ├── TwitterSwipeImageViewController.swift
│ └── ViewController.swift
-
ViewController.swift
からTwitterSwipeImageViewController
をpresentしている
コード
呼び出す側
ViewController.swift
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
@IBOutlet weak var openImageViewButton: UIButton!
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
setupViewController()
}
}
extension ViewController {
private func setupViewController() {
openImageViewButton.rx.tap
.subscribe(onNext: { [weak self] in
self?.presentTwitterSwipeImageViewController()
})
.disposed(by: disposeBag)
}
private func presentTwitterSwipeImageViewController() {
let viewController = TwitterSwipeImageViewController()
// 👇overCurrentContextを指定しないと、ViewControllerの背景が透過しない
viewController.modalPresentationStyle = .overCurrentContext
navigationController?.present(viewController, animated: false, completion: nil)
}
}
呼ばれる側
TwitterSwipeImageViewController.swift
import UIKit
import RxSwift
import RxCocoa
class TwitterSwipeImageViewController: UIViewController {
@IBOutlet weak var imageView: UIImageView!
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
setupClosePanGesture()
}
}
extension TwitterSwipeImageViewController {
enum CloseDirection {
case up
case down
}
private func setupClosePanGesture() {
// スワイプ開始時の位置を格納
var startPanPointY: CGFloat = 0.0
// スワイプ開始時の位置とImageViewのCenterの距離を格納
var distanceY: CGFloat = 0.0
// 画面を閉じるラインの設定 (画面高さの1/6の距離を移動したら)
let moveAmountYCloseLine: CGFloat = view.bounds.height / 6
let minBackgroundAlpha: CGFloat = 0.5
let maxBackgroundAlpha: CGFloat = 1.0
let panGesture = UIPanGestureRecognizer(target: self, action: nil)
panGesture.rx.event
.subscribe(onNext: { [weak self] sender in
guard let strongSelf = self else { return }
let currentPointY = sender.location(in: strongSelf.view).y
switch sender.state {
case .began:
// スワイプを開始したら呼ばれる 1回だけ
startPanPointY = currentPointY
distanceY = strongSelf.imageView.center.y - startPanPointY
strongSelf.updateHeaderFooterView(isHidden: true)
case .changed:
// スワイプ中呼ばれる 移動するたび
// ImageViewの移動
let calcedImageViewPosition = CGPoint(x: strongSelf.view.bounds.width / 2, y: distanceY + currentPointY)
strongSelf.imageView.center = calcedImageViewPosition
// 背景の透明度更新
let moveAmountY = fabs(currentPointY - startPanPointY)
var backgroundAlpha = moveAmountY / (-moveAmountYCloseLine) + 1
if backgroundAlpha > maxBackgroundAlpha {
backgroundAlpha = maxBackgroundAlpha
} else if backgroundAlpha < minBackgroundAlpha {
backgroundAlpha = minBackgroundAlpha
}
strongSelf.view.backgroundColor = strongSelf.view.backgroundColor?.withAlphaComponent(backgroundAlpha)
case .ended:
// 指を離すと呼ばれる
let moveAmountY = currentPointY - startPanPointY
let isCloseTop = moveAmountY > moveAmountYCloseLine
let isCloseBottom = moveAmountY < moveAmountYCloseLine * -1
if isCloseTop {
strongSelf.dismiss(animateDuration: 0.15, direction: .up)
return
}
if isCloseBottom {
strongSelf.dismiss(animateDuration: 0.15, direction: .down)
return
}
UIView.animate(withDuration: 0.25, animations: {
strongSelf.imageView.center = strongSelf.view.center
strongSelf.view.backgroundColor = strongSelf.view.backgroundColor?.withAlphaComponent(1.0)
})
strongSelf.updateHeaderFooterView(isHidden: false)
default: break
}
})
.disposed(by: disposeBag)
self.view.addGestureRecognizer(panGesture)
}
private func dismiss(animateDuration: TimeInterval, direction: CloseDirection) {
let imageViewCenterPoint: CGPoint = {
switch direction {
case .up:
return CGPoint(x: view.bounds.width / 2, y: view.bounds.height + imageView.bounds.height)
case .down:
return CGPoint(x: view.bounds.width / 2, y: -imageView.bounds.height)
}
}()
UIView.animate(withDuration: animateDuration, animations: { [weak self] in
self?.view.backgroundColor = self?.view.backgroundColor?.withAlphaComponent(0.0)
self?.imageView.center = imageViewCenterPoint
}, completion: { [weak self] _ in
self?.dismiss(animated: false, completion: nil)
})
}
// Twitterでいう、「リプライ」「お気に入り」などがあるViewの表示制御処理
private func updateHeaderFooterView(isHidden: Bool) {
print("isHidden = \(isHidden)")
}
}
Notes
- すっごいコード長くて汚いのでもっとスマートにかきたい!!!!