Swift で UIView を穴を開ける
検証環境:
Xcode 11.4
Swift 5.2
Swift で UIView の穴を開ける実装を Playground で試してみます。
今回のサンプルはまず画像を配置し、その上に UIView を被せます。
被せた UView の内部に穴を開けることで下の画像が見えるようにします。
また、ついでに画像上に三分割法のグリッドと、枠線も入れてみます。
サンプルコード
import UIKit import PlaygroundSupport class CutoutView: UIView { let cutoutLayer: CALayer = { let cutoutLayer = CALayer() cutoutLayer.backgroundColor = UIColor.gray.cgColor cutoutLayer.opacity = 0.5 return cutoutLayer }() let maskLayer = CAShapeLayer() let borderLayer: CAShapeLayer = { let shapeLayer = CAShapeLayer() shapeLayer.lineWidth = 2 shapeLayer.strokeColor = UIColor.white.cgColor shapeLayer.fillColor = UIColor.clear.cgColor return shapeLayer }() let gridLayer: CAShapeLayer = { let shapeLayer = CAShapeLayer() shapeLayer.lineWidth = 0.5 shapeLayer.strokeColor = UIColor.white.cgColor return shapeLayer }() var cutoutPath: UIBezierPath? override init(frame: CGRect) { super.init(frame: frame) layer.backgroundColor = UIColor.clear.cgColor layer.addSublayer(cutoutLayer) layer.addSublayer(borderLayer) layer.addSublayer(gridLayer) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layoutSubviews() { super.layoutSubviews() updateLayer() } private func updateLayer() { let maskPath = UIBezierPath(rect: bounds) // くり抜くサイズを指定。今回は比率 16:9 の矩形にする let width = bounds.insetBy(dx: 16, dy: 0).width let height = width * (9 / 16) let x = bounds.midX - width / 2 let y = bounds.midY - height / 2 let cutoutRect = CGRect(x: x, y: y, width: width, height: height) let cutoutPath = UIBezierPath(rect: cutoutRect) maskPath.append(cutoutPath) maskLayer.fillRule = .evenOdd maskLayer.path = maskPath.cgPath cutoutLayer.frame = bounds cutoutLayer.mask = maskLayer borderLayer.path = cutoutPath.cgPath addGridPath(x: x, y: y, width: width, height: height) self.cutoutPath = cutoutPath } private func addGridPath(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat) { let gridPath = UIBezierPath() gridPath.move(to: .init(x: x + (width * 0.33), y: y)) gridPath.addLine(to: .init(x: x + (width * 0.33), y: y + height)) gridPath.move(to: .init(x: x + (width * 0.66), y: y)) gridPath.addLine(to: .init(x: x + (width * 0.66), y: y + height)) gridPath.move(to: .init(x: x, y: y + (height * 0.33))) gridPath.addLine(to: .init(x: x + width, y: y + (height * 0.33))) gridPath.move(to: .init(x: x, y: y + (height * 0.66))) gridPath.addLine(to: .init(x: x + width, y: y + (height * 0.66))) gridLayer.path = gridPath.cgPath } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let cutoutPath = cutoutPath, cutoutPath.contains(point) { return nil } return bounds.contains(point) ? self : nil } } class MyViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemTeal addImageView() addCutoutView() } private func addImageView() { let image = UIImage(named: "0.jpg")! let imageView = UIImageView(image: image) imageView.contentMode = .scaleAspectFit view.addSubview(imageView) imageView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ imageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), imageView.centerYAnchor.constraint(equalTo: view.centerYAnchor), imageView.widthAnchor.constraint(equalToConstant: 300), imageView.heightAnchor.constraint(equalToConstant: 300), ]) } private func addCutoutView() { let cView = CutoutView() view.addSubview(cView) cView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ cView.centerXAnchor.constraint(equalTo: view.centerXAnchor), cView.centerYAnchor.constraint(equalTo: view.centerYAnchor), cView.widthAnchor.constraint(equalToConstant: 300), cView.heightAnchor.constraint(equalToConstant: 300), ]) } } PlaygroundPage.current.liveView = MyViewController()
マスク用の CAShapeLayer の fillRule は .evenOdd
にします。
これにより穴の開いた矩形を作ることができます。
今回は穴を開けた部分にグリッドを追加しますが、Root layer をマスクすると子 layer すべてが影響を受け、マスクされてしまうので、穴を開けた layer は別 layer として Root layer に追加しています。
hitTest メソッドをオーバーライドし、穴が空いた部分のタッチイベントは nil を返すようにして、下のビューにタッチイベントをスルーしています。
参考:
Shape Fill Mode Values
https://developer.apple.com/documentation/quartzcore/cashapelayer/shape_fill_mode_values