Edited at

Twitterのスワイプで画像を閉じる処理っぽいのを再現してみる with RxSwift

More than 1 year has passed since last update.


概要


  • Twitterの画像ビューアを閉じる動作をRxSwiftを使って再現してみる


    • どっちかというとLINEの画像閉じる動作に近い

    • というかRxSwiftほぼ活用していない



  • 加速度を計算して画面を閉じるのではなく、移動した距離を見て閉じるようにする

  • サンプルリポジトリ




環境


  • Xcode 9.4

  • Swift 4.1

  • RxSwift 4.2

  • RxCocoa 4.2

  • Cocoapods 1.5.3


イメージ

twitterswipe.gif


前提


  • 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


  • すっごいコード長くて汚いのでもっとスマートにかきたい!!!!