LoginSignup
13
9

More than 5 years have passed since last update.

Instagramライクなパン/ピンチ操作できるイメージビューの作成

Last updated at Posted at 2017-03-27

概要

Instagramアプリのようなパン/ピンチ操作を受け取り、拡大/移動ができるイメージビューを作成する方法です。
サンプルでは、パン操作とピンチ操作を使用していますが、同じ方法で回転(Rotation gesture)することもできます。

animation

Interface Builder構築手順

1.フィルタービューの追加

イメージビューの下の階層に透明なビューを用意します。
イメージビューが操作を受け取り拡大率に応じて、フィルタービューの背景色を濃くすることで画像を見やすくします。

image1

2.イメージビューの追加

フィルタービューの上の階層にイメージビューを配置します。
User Interaction EnabledとMultiple Touchにチェックをします。

image2

3.パン/ピンチジェスチャーの追加

Pan Gesture RecognizerおよびPinch Gesture Recognizerをイメージビューの上にドラッグ&ドロップします。
また、delegateおよびIBActionをViewControllerにセットします。

image3

4.パンジェスチャーのプロパティ

1本指では、パン操作を受け付けたくない場合には、Touchesを2に変更します。

image4

プログラム説明

TransformProperty構造体

イメージビューの変形状態を保持します。

fileprivate struct TransformProperty {
    private let kMaxBackgroundAlpha: CGFloat = 0.77
    private let kMinBackgroundAlpha: CGFloat = 0.4

    var point: CGPoint
    var scale: CGFloat
    var backgroundAlpha: CGFloat {
        didSet {
            // Round the value
            backgroundAlpha = min(kMaxBackgroundAlpha, max(kMinBackgroundAlpha, backgroundAlpha))
        }
    }

    init() {
        point = CGPoint(x: 0, y: 0)
        scale = 1.0
        backgroundAlpha = kMinBackgroundAlpha
    }
}

ViewControllerとIBAction

onPinchGestureとonPanGestureで操作を受け取り、イメージビューを変形します。

class ViewController: UIViewController {
    @IBOutlet fileprivate weak var filterView: UIView!
    @IBOutlet fileprivate weak var imageView: UIImageView!

    lazy fileprivate var transformProperty = TransformProperty()

    override func viewDidLoad() {
        super.viewDidLoad()

        // Bring the expanded view to the forefront if needed
//        self.view.bringSubview(toFront: imageView)
    }

    @IBAction private func onPinchGesture(_ sender: UIPinchGestureRecognizer) {
        switch sender.state {
        case .changed:
            let scale = sender.scale
            if scale <= 1 {
                break
            }
            transformProperty.scale = (sender.scale - 1.0) * 0.5 + 1.0
            transform()
            // Darken the background color when scaled
            transformProperty.backgroundAlpha = (sender.scale - 1.0) * 0.8
            changeBaseViewBackgroundColor()
        case .ended, .cancelled:
            revertTransform()
        default:
            break
        }
    }

    @IBAction private func onPanGesture(_ sender: UIPanGestureRecognizer) {
        guard let view = sender.view else {
            return
        }
        switch sender.state {
        case .changed:
            transformProperty.point = sender.translation(in: view)
            transform()
            changeBaseViewBackgroundColor()
        case .ended, .cancelled:
            revertTransform()
        default:
            break
        }
    }
}

イメージビューの変形

transformPropertyの値に基づき、イメージビューを変形します。
指が離れて、操作が終わった時には、アニメーションを使って、元の位置にイメージビューを戻します。

fileprivate extension ViewController {
    func transform() {
        imageView.transform = CGAffineTransform(translationX: transformProperty.point.x, y: transformProperty.point.y)
            .scaledBy(x: transformProperty.scale, y: transformProperty.scale)
    }

    func revertTransform() {
        transformProperty = TransformProperty()
        UIView.animate(withDuration: 0.6, delay: 0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: 1.0, options: .curveEaseOut, animations: { () -> Void in
            self.imageView.transform = CGAffineTransform.identity
            self.transform()
            self.filterView.backgroundColor = UIColor.clear
        }, completion: nil)
    }

    func changeBaseViewBackgroundColor() {
        filterView.backgroundColor = UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: transformProperty.backgroundAlpha)
    }
}

UIGestureRecognizerDelegateの実装

trueを返すことにより、一つのジェスチャー中に他のジェスチャーも受け取ることができます。

extension ViewController: UIGestureRecognizerDelegate {
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
}

注意点

機能を実現するためには、イメージビューを最前面に表示し、その下にフィルタービューを表示する必要があります。
Interface Builderでのビューの重なりによっては、前面にビューを持ってくる必要があります。
また、イメージビューが入れ子構造になっている場合は、親要素ごと最前面に表示する必要があります。

self.view.bringSubview(toFront: imageView)

or

self.view.bringSubview(toFront: scrollView)
scrollView.bringSubview(toFront: stackView)
stackView.bringSubview(toFront: imageView)

サンプル

Scale-ImageView@githubに動作するプロジェクトがあります。

動作確認

このTipsは、「スマホの写真素材が売買できるサイトSnapmart」を開発する中で生まれました。
実際の動作をSnapmartアプリ(iOS)から確認できますので、是非ダウンロードしてみてください!

13
9
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
13
9