はじめに
チュートリアル画面などで、画面全体を半透明な黒いUIView
でおおい、一部分をハイライトして、なんらかの説明文言を表示するといったことがあるかと思います。
この際に、ハイライトしたい部分にUIImage
などを上から重ねるのも手としてはありますが、その部分について黒いUIView
に穴を開けられると便利です。
本記事では該当部分に穴を開ける方法と開けたい場所を指定する際に便利な方法を記載します。
環境設定
以下の環境を使用しています。
- Xcode10.3.0
- Swift 5.0.1
穴を開ける方法
UIView
のlayer
に対してmask
を使用することで円形の穴を開けます。
詳細な説明はコードに記載いたしましたが、大まかな流れとしては、mask用のlayer
を作成し、そこに円形のPathを作成、
穴を開けたいUIView
のlayer
のmask
にmask用に作成したlayer
を指定することで穴をあけるとなります。
ここでPathとして円の代わりに星型等を使用すれば星型の穴をあける等も可能です。
なお、UIViewController
のviewDidAppear(_:)
などで穴をあけるlayer
を追加したところ穴が問題なくあきましたので、
UIViewにextensionでメソッドを作成すると便利に使用できて良いと思います。
当然UIView
を継承しているUIButton
などでも同様に使用可能です。
public extension UIView { func makeHole(at point: CGPoint, radius: CGFloat) { let maskLayer = CAShapeLayer() // fillはPathの内部を指定の色で塗りつぶします // そのためには内部かどうかの判定が必要になりますが、fillRuleはこの判定方法です // .evenOddはある点Pから任意の方向へ無限遠点に射線を引きPathとの交差回数を数えます // Pathとの交差回数が奇数回の場合は内側、偶数回の場合は外側と判定します // 詳細は参考文献を参照ください maskLayer.fillRule = .evenOdd maskLayer.fillColor = UIColor.black.cgColor // 画面全体にPathを描きます let maskPath = UIBezierPath(rect: self.frame) maskPath.move(to: point) // addArcで弧形を描画します // centerとradiusを指定し、 0.0 ~ 2πで描画するため円形となります maskPath.addArc(withCenter: point, radius: radius, startAngle: 0.0, endAngle: 2.0 * CGFloat.pi, clockwise: true) // 上記から円の内側から無限遠まで射線を引くと、円のPathと画面全体の外縁のPathとで2回交わるため // 外側に、円の外側から無限遠まで射線を引くと、奇数回Pathと交わるため内側になります // すなわち画面全体のうち円以外の部分が黒色に塗りつぶされます maskLayer.path = maskPath.cgPath // 自身のlayerのmaskとして上記で作成したmask用のlayerを指定しました // maskの黒色の部分と重複している箇所の色が残るため、 // 結果として円の内側は色がなくなり、円形の穴があくことになります self.layer.mask = maskLayer } }
穴を開けたい場所を指定する際の方法
穴をあける場所に関しては、既存のいづれかのUIView
の上に開けたい場合はその場所を指定するのが良さそうです。
ただ、 view.frame
で指定した場合の座標は親のUIView
からの相対位置となるため、画面全体に広げたUIView
からの位置を指定する場合は正しい値となりません。
そのため、以下のように画面全体のViewに対する相対座標を求めるのがおすすめです。
// MARK: IBOutlet @IBOutlet private weak var targetView: UIView! @IBOutlet private weak var parentView: UIView! override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() let frame = targetView.frame print("frame") print(frame) print("parentView frame") print(parentView.frame) print("converted frame") let convertedFrame = parentView.convert(targetView.frame, to: self.view) print(convertedFrame) }
上記でtargetViewが対象のView、parentViewがtargetViewの親Viewです。 上記の実行結果は下記になります。
frame (20.0, 20.0, 40.0, 40.0) parentView frame (107.0, 348.0, 200.0, 200.0) converted frame (127.0, 368.0, 40.0, 40.0)
規定のself.viewからの座標が出力されているのがわかります。 この値を使うことで、画面全体を覆った場合等に適切な箇所に穴をあけることが可能になります。
まとめ
チュートリアルのような画面でUIViewに穴を開けたい際には以下のように行うのがおすすめです。
- maskLayerをつかって穴を開ける
- 穴の場所を指定するために基準とするViewからの相対位置を指定する(convertPoint:toView:など)
参考
- Apple Document CAShapeLayer
- Apple Document mask
- Apple Document convertPoint:fromView:
- Apple Document convertPoint:toView:
- [iOS]CAShapeLayerの二つのfillRuleの違い(修正版)
- fillRuleに関するわかりやすい説明が記載されています
- 穴を空ける for チュートリアル
- UIViewに穴を開ける方法が記載されています。大いに参考にさせていただきました。
- Swift CAShapeLayerで図形にグラデーションをつける方法
- maskに関する説明がわかりやすかったです
- 【iOS】【Swift】convertRectを使ってframe位置を計算する親要素を変更する
- ViewのFrameの考え方に関してわかりやすく説明が記載されています。