3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-08-15

概要

  • 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

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?