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

# 概要

- Twitterの画像ビューアを閉じる動作をRxSwiftを使って再現してみる
    - どっちかというとLINEの画像閉じる動作に近い
    - というかRxSwiftほぼ活用していない
- 加速度を計算して画面を閉じるのではなく、移動した距離を見て閉じるようにする
- サンプルリポジトリ
    - [TwitterImageViewExampleiOSApp](https://github.com/ryo-takahashi/TwitterImageViewExampleiOSApp)

# 環境

- Xcode 9.4
- Swift 4.1
- RxSwift 4.2
- RxCocoa 4.2
- Cocoapods 1.5.3

# イメージ

![twitterswipe.gif](https://qiita-image-store.s3.amazonaws.com/0/126441/29c60f0a-9386-329b-ca0b-02ef3c0ff2f1.gif)

# 前提

- RxSwiftを導入している

# 構成

```
├── TwitterImageViewExample
│   ├── resources
│   │   ├── TwitterSwipeImageViewController.xib
│   │   └── ViewController.xib
│   └── sources
│       ├── AppDelegate.swift
│       ├── TwitterSwipeImageViewController.swift
│       └── ViewController.swift
```

- `ViewController.swift`から`TwitterSwipeImageViewController` をpresentしている

# コード

## 呼び出す側

```swift: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)
    }
    
}
```

## 呼ばれる側

```swift: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:
                    // スワイプを開始したら呼ばれる １回だけ
                    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

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