LoginSignup
4
6

More than 5 years have passed since last update.

[iOS] 8個のコントロールポイントでリサイズできるビューを作ってみた

Last updated at Posted at 2019-03-28

写真アプリのマークアップ機能では、自由にリサイズ可能な図形が用意されています。
図形には8個のコントロールポイントがあり、それをタッチしてドラッグすると、図形の形を変化させることができます。
また、コントロールポイントではない部分をドラッグすることで図形を移動させることもできます。

markup.gif

これと同じようなものを作ってみたので、どうやって作ったかを紹介します。

作ったもの

resizableView.gif

ソースコードはGistに上げました。

ResizableView.swift

解説

ビューの構成

四角形部分のビューの構成は以下の通りです。

黒の枠線は背景が黒色のサブビューにCAShapeLayerでマスクをかけることによって表現しています。

なぜルートビューの背景を黒にしてマスクをかけるのではなく、サブビューに対してマスクをかけるのでしょうか?
実際にやってみるとわかりますが、ルートビューに対してマスクをかけてしまうと、マスク部分から少しはみだしているコントロールポイントの円形が欠けてしまいます。

image.png

ルートビューに対してマスクをかけると、サブビューであるコントロールポイントもマスクの対象になってしまいます。
そこで、コントロールポイントと同階層のビューを配置し、そのビューにだけマスクをかけるようにします。
こうすることでコントロールポイントのビューはマスクの対象になりません。

マスク部分のソース

マスクはUIBezierPathとCAShapeLayerで作成します。
作成したレイヤーをマスクをかけたいビューのlayer.maskプロパティにセットすると、マスクをかけることができます。
注意点としては、UIBezierPathのパスで描いた部分が表示されることです。
マスクというとパスで描いた部分で隠すようにイメージしがちですが、そうではありません。

枠線を表現するために、fillColornilにして塗りつぶしをしないようにしています。

let path = UIBezierPath()
path.move(to: CGPoint(x: halfOfBorderWidth, y: halfOfBorderWidth))
path.addLine(to: CGPoint(x: frame.width - halfOfBorderWidth, y: halfOfBorderWidth))
path.addLine(to: CGPoint(x: frame.width - halfOfBorderWidth, y: frame.height - halfOfBorderWidth))
path.addLine(to: CGPoint(x: halfOfBorderWidth, y: frame.height - halfOfBorderWidth))
path.close()

let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath
maskLayer.strokeColor = UIColor.black.cgColor
maskLayer.lineWidth = borderWidth
maskLayer.fillColor = nil

borderView.layer.mask = maskLayer

リサイズ部分のソース

ビューをドラッグして移動したりリサイズしたりする処理は、touchesBegan(_:with)touchesMoved(_:with)touchesEnded(_:with)に書きました。

ドラッグして動かすだけの実装は前に記事を書きました。

[iOS] ビューをドラッグして動かす

ルートビューのフレーム計算

リサイズ処理では、ドラッグ位置を使ってルートビューのフレームの計算を行います。
※ここでいうルートビューとは、リサイズを行うビューの階層におけるルートのことです。ViewControllerにおけるルートビューではありません。

ドラッグ位置を移動するたびにフレームを変化させていくので、リサイズ前のフレームをもとにリサイズ後のフレームを計算するというところがポイントです。
幅や高さの計算では、リサイズ前の位置からどれくらいの距離ドラッグしたか、というような計算を行うためです。

リサイズ前のフレームはtouchesBegan(_:with)で取得できます。

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let touch = touches.first,
        touch.view != nil else { return }

    // リサイズ前のフレームを保持
    frameAtTouchesBegan = frame
}

座標の計算でややこしいのは、どのビューの座標系で計算を行っているかを意識しなければならないことです。

例えば、リサイズ処理を行う関数(resize(control:locationInParentCoordinate:))は、ドラッグ位置を親ビューの座標系にしたものを受け取るようにしています。

let location = touch.location(in: superview)
resize(control: control, locationInParentCoordinate: location)

フレームの計算はドラッグ位置とリサイズ前のフレームをもとに行います。
リサイズ前のフレームは親ビューの座標系を使っているので、ドラッグ位置も同じ座標系を使ったほうが計算がしやすくなります。

フレームの計算処理は、どのコントロールポイントをドラッグしているかによって変わります。
コントロールポイント用のビュークラスを用意し、どの位置のコントロールポイントであるかを示すpositionというプロパティを用意することによってこれを実現しています。

switch control.position {
case .top:
    frame = CGRect(
    ...
case .bottom:
    frame = CGRect(
   ...

実際の計算処理はコードを見てもらえればと思いますが、resize(control:locationInParentCoordinate:)の中のisLocationUpperSideという変数についてだけ説明しておきます。

let isLocationUpperSide = locationInParentCoordinate.y < frameAtTouchesBegan.origin.y + frameAtTouchesBegan.height

例えば、中央上のコントロールポイントを下にドラッグしていくとき、ドラッグ位置がリサイズ前のフレームの下端を越えると図形が反転したような形になります。
ドラッグ位置がリサイズ前のフレームの下端より上にある時(isLocationUpperSide == true)は、ドラッグ位置がフレームのY軸上の始点だったのに対し、リサイズ前のフレームの下端を越えた時(isLocationUpperSide == false)はドラッグ位置はフレームのY軸上の終点になります。

isLocationUpperSideはこのような計算を行うための変数です。
isLocationLeftSideはX軸についてフレームのリサイズを行う時に使用します。

コントロールポイントの配置

コントロールポイントの配置処理はシンプルで、ルートビューのフレームが変更されたのに合わせて、それぞれ固定の位置に配置するだけです。
つまり、左上のコントロールポイントであれば、リサイズ後のフレームの左上に位置するように配置すればOKです。
ルートビューのフレームの変更に追随してコントロールポイントの配置を行うには、layoutSubviews()でコントロールポイントの再配置を行うメソッド(relocateControls())を呼べばOKです。

一つ注意点、というか認識しておくべきことがあります。
以下のGIFを見てください。

controlpoint.gif

右下の赤いコントロールポイントをドラッグしていて、それがリサイズ前のフレームの左端を越えたとき、赤いコントロールポイントが右下に戻ってしまっています。
赤いコントロールポイントはフレームの右下に来るように配置しているのでこのような挙動になります。
全てのコントロールポイントの色が同じであれば、ユーザはあたかも同じコントロールポイントをずっとドラッグしているように見えます。

4
6
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
4
6