iPhone
iOS
Swift
RxSwift

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

概要

  • 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

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