I have an UIImageView and another view overlapping it which allows me to draw in it. When I draw something, it has the correct dimensions and placement, but when I try to apply that as a mask on the image view, it has a weird y offset, as shown on Image 1 captured from view hierarchy.
I am applying the mask like this:
canvas.onTouchesEnd = { [weak self] in
guard let self = self else { return }
self.backgroundView.layer.mask = self.canvas.layer
}
canvas is the view where I draw shapes (red one on the image), and backgroundView is the image view I'm trying to mask. "onTouchesEnd" is just a callback which is called upon finishing drawing the shape.
UIImageView is created in storyboard and constraints are set there.
I've tried setting translatesAutoresizingMaskIntoConstraints = false
, returning the path from canvas and creating a new layer for mask, but whatever I do, it has the same offset and nothing really worked.
Any ideas how to correctly translate this path from canvas onto image view mask?
The reason your mask has a "weird y offset" is because of the way you are accessing the other view's layer.
With your code, the canvas.layer
will come with an origin relative to its superview - in this case, the "root" view of the controller.
If you put your image view and "canvas" at the top (of the view, not top of the safe area), it should line up.
However, that's really not an ideal way to do this - and self.backgroundView.layer.mask = self.canvas.layer
is not a good idea to begin with.
You didn't show us what you're doing with your "canvas" view, but assuming it has a CAShapeLayer
where you set the path (and line width, line join, etc), you could use this as your closure:
canvas.onTouchesEnd = { [weak self] in
guard let self = self else { return }
// new shape layer
let maskLayer = CAShapeLayer()
// any opaque color
maskLayer.strokeColor = UIColor.black.cgColor
// use same shape properties
maskLayer.lineWidth = self.canvas.shapeLayer.lineWidth
maskLayer.fillColor = self.canvas.shapeLayer.fillColor
maskLayer.lineJoin = self.canvas.shapeLayer.lineJoin
maskLayer.lineCap = self.canvas.shapeLayer.lineCap
// set the mask layer path to the same path
maskLayer.path = self.canvas.shapeLayer.path
// apply the mask
self.backgroundView.layer.mask = maskLayer
}
What you might want to consider, though, is a using a subclassed UIImageView
that would handle the touches and masking itself... avoiding all of that.
Here's a quick "scratch off" example:
class ScratchOffImageview: UIImageView {
public var onTouchesEnd: (()->())?
// default line width of 30, but can be set as desired
public var lineWidth: CGFloat = 40.0 {
didSet { maskLayer.lineWidth = lineWidth }
}
private var maskPath: CGMutablePath!
private var maskLayer: CAShapeLayer!
convenience init() {
self.init(frame: .zero)
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
// create the mask path
maskPath = CGMutablePath()
// create the mask shape layer
maskLayer = CAShapeLayer()
// any opaque color
maskLayer.strokeColor = UIColor.black.cgColor
// use same shape properties
maskLayer.lineWidth = self.lineWidth
maskLayer.fillColor = nil
maskLayer.lineJoin = .round
maskLayer.lineCap = .round
// if this is set here,
// view will start completely transparent
self.layer.mask = maskLayer
// UIImageView has interaction disabled by default
// need to set it to true so it will receive touches
self.isUserInteractionEnabled = true
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let t = touches.first else { return }
maskPath.move(to: t.location(in: self))
maskLayer.path = maskPath
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let t = touches.first else { return }
maskPath.addLine(to: t.location(in: self))
maskLayer.path = maskPath
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
// if you want to do something on touchesEnded
self.onTouchesEnd?()
}
}
This is now completely self-contained. If you add a UIImageView
in Storyboard, set its Custom Class to ScratchOffImageview
(and enable User Interaction), you're all done.
You can test it with this sample view controller - requires an image asset named "sampleImage" (or edit the code with your image name):
class ScratchTestVC: UIViewController {
var scratchView: ScratchOffImageview = ScratchOffImageview()
override func viewDidLoad() {
super.viewDidLoad()
guard let img = UIImage(named: "sampleImage") else {
fatalError("Could not load sampleImage !!!")
}
scratchView.image = img
scratchView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(scratchView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// let's inset the image view 20-points on each side
// same proportional height as sampleImage
// and centered vertically
scratchView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
scratchView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
scratchView.heightAnchor.constraint(equalTo: scratchView.widthAnchor, multiplier: img.size.height / img.size.width),
scratchView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
])
// because the image view starts with an empty mask, it will be completely transparent
// so let's put a bordered view behind the image view,
// 1-point larger on each side so it will "frame" the image
let frameView = UIView()
frameView.layer.borderColor = UIColor.black.cgColor
frameView.layer.borderWidth = 1
frameView.translatesAutoresizingMaskIntoConstraints = false
view.insertSubview(frameView, belowSubview: scratchView)
NSLayoutConstraint.activate([
frameView.topAnchor.constraint(equalTo: scratchView.topAnchor, constant: -1.0),
frameView.leadingAnchor.constraint(equalTo: scratchView.leadingAnchor, constant: -1.0),
frameView.trailingAnchor.constraint(equalTo: scratchView.trailingAnchor, constant: 1.0),
frameView.bottomAnchor.constraint(equalTo: scratchView.bottomAnchor, constant: 1.0),
])
// set the closure so we can "do something" on touches end
scratchView.onTouchesEnd = { [weak self] in
guard let self = self else { return }
print("Touches Ended")
}
}
}
You'll notice in the comments that the custom "scratch off" image view will start completely clear / transparent -- and you won't see anything and won't know where to touch/drag... so I inserted a bordered "frame" view below it.
Looks like this: