LoginSignup
7
5

More than 5 years have passed since last update.

iOS/macOS で Zoom/Scroll 可能な Core Graphics ベースの 2D アプリの開発を便利に

Last updated at Posted at 2017-01-19

Core Graphics で 2D のコンテンツを UIScrollView や NSScrollView に入れてスクロールしたりズームしようとするとそれなりに骨が折れます。さらに一つのコードベースで iOS と macOS の両方をサポートしようとすると、iOS と macOS の似て非なるネーミングや挙動のおかげで結構苦労します。

でやっと動作するよういなっても、また別のアプリで同じような事をしようとすると再度細かい作り込みを強いられます。可能であれば、UI/NSScrollView の挙動やAPIの違い、さらに座標系の違いなどにあまりエネルギーを取られたくないと思うはずです。

今回はこの手の問題を解決する手段として Panorama を公開したので紹介したいと思います。

使い方

iOS または macOS またその両方の Storyboard に PanoramaView を配置します。同等の事をコードで行なっても構いません。

次に Panorama クラスのサブクラスを用意します。そして、典型的には次の二つのメソッドをオーバーライドします。draw(in:) メソッドでは self.bounds 内に Core Graphics でコンテンツを描画します。座標系は iOS と同じ左上が原点になります。macOS でもこの座標系でコードが書けます。didMove(to:) メソッドは、実際の View の配下になった時に呼ばれ、リソースを準備したり何か準備をする際に適したオーバーライドポイントです。オーバーライドした際は必ず super を呼び出してください。

class MyPanorama: Panorama {

    override func draw(in context: CGContext) {
        // draw within self.bounds
    }
    override func didMove(to panoramaView: PanoramaView?) {
        super.didMove(to: panoramaView)
    }
}

iOS でタッチの処理をしたい時は以下の任意のメソッドをオーバーライドします。macOS 側でエラーにならないように、#if os(iOS) でプラットフォームを切り分けます。

func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)

mouse の処理を行いたい時には、以下の任意のメソッドをオーバーライドします。iOS 側でエラーにならないように、#if os(iOS) でプラットフォームを切り分けます。

func mouseDown(with event: NSEvent)
func mouseDragged(with event: NSEvent)
func mouseUp(with event: NSEvent)
func mouseMoved(with event: NSEvent)
func rightMouseDown(with event: NSEvent)
func rightMouseDragged(with event: NSEvent)
func rightMouseUp(with event: NSEvent)
func otherMouseDown(with event: NSEvent)
func otherMouseDragged(with event: NSEvent)
func otherMouseUp(with event: NSEvent)
func mouseExited(with event: NSEvent)
func mouseEntered(with event: NSEvent)

NSEvent にも UITouch にも location(in: Panorama) メソッドが extension で用意されているので、ズームやスクロールされている状態でも、Panorama 内の座標を得る事ができます。

    #if os(iOS)
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        for touch in touches {
            if let location = touch.location(in: self) {
                // location in Panorama
            }
        }
    }
    #endif

そして、何らかの理由で画面の更新が必要になった時は、setNeedsDisplay() を呼びます。決して、draw() を自分では呼び出さないでください。Panorama は実際には UIView でも NSView ではありませんが、結果的に良しなにしてくれます。

    self.setNeedsDisplay()

そして、MyPanorama をインスタンス化します。ズーム倍率に上限下限を儲けたい場合は値を設定します。macOS を対象にした場合も maxMagnification ではなく maximumZoomScale で一本化されています。

let myPanorama = MyPanorama(bounds: CGRect(...))
myPanorama.maximumZoomScale = 4.0
myPanorama.minimumZoomScale = 1.0

そして、ViewController などで、PanoramaView にインスタンス化したMyPanoramaをセットします。

self.panoramaView.panorama = myPanorama

これだけで、iOS/macOSの面倒な違いを PanoramaViewPanorama が吸収してくれて、ズームしたりスクロールしたり、表示もiOSでもmacOSでも左上を原点にCoreGraphics で描画でき、#if でプラットフォームに切り分けは必要ですが、iOSのタッチやmacOSのマウスのイベントなどを拾う事が出来るようになります。

GestureRecognizer にはまだ対応していませんが、今後何かいい方法を考えて見たいと思います。

Architecture

Panorama は前回の二つの記事をベースとしています。どちらも、扱いにくい UI/NSScrollView をどう扱うかをテーマにしています。

「Metal + UIScrollView でズーム&スクロール可能な2Dアプリに挑戦 TAKE 2」
http://qiita.com/codelynx/items/0434cdf453c8db6bc357#_reference-5f9ee5f88dd5fd1186f7

「UIScrollView内でPDFなどのベクター表示をうまく拡大表示する方法」
http://qiita.com/codelynx/items/a2a87b053f8225782a9c

よって Panorama の実際にコンテンツを表示するビューはこれらの記事同様に実際にズームされているわけではなく、Panorama のコンテンツの座標系から、透明なコンテントビューへ座標が変換され、結果として、Panorama は自分の座標系のみを意識して描画すればよい事になっています。

実際には、描画は PanoramaBackView より、タッチやマウスのイベントはPanoramaContentView を起点として Panorama を呼び出し、結果的には様々な面倒なプラットフォームの違いなどが吸収されている事になります。が、これらはほぼ意識しないでよいように設計されています。

Screen Shot 2017-01-19 at 4.46.58.png

Github

コードは Github で公開しています。
https://github.com/codelynx/Panorama

手書き

プロジェクトには Living Example として手書きアプリを実装しています。iOS と macOS で動作します。Panorama のサブクラス MyPanorama はタッチやマウスのイベントを処理して、画面に表示するだけの簡単なサンプルです。線の補正も保存も消去もできませんが、Panorama でこんな事が簡単にできそうだという事を感じていただければ幸いです。

Screen Shot.png

ロードマップ

考え中ですが、とりあえずテストは全然足りていないと思います。

Xcode

Versions
Xcode Version 8.2.1 (8C1002)
Apple Swift version 3.0.2 (swiftlang-800.0.63 clang-800.0.42.1)
7
5
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
7
5