I am trying to create a non-rectangular shaped UIView, with a non-rectangular shaped touch area. I already know how to draw my shape with bezier paths.
According to this comment, I need to override point(inside:with:) to create a custom shaped touch area.
So I tried this:
class MyView: UIView {
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
path.move(to: .zero)
path.addLine(to: CGPoint(x: 0, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.midY))
path.addLine(to: CGPoint(x: bounds.midX, y: bounds.midY))
path.addLine(to: CGPoint(x: bounds.midX, y: 0))
path.addLine(to: CGPoint(x: 0, y: 0))
UIColor.red.setFill()
path.fill()
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
print("point(inside:with:) called")
func isInRegion(_ point: CGPoint) -> Bool {
return (0...bounds.midX).contains(point.x) || (bounds.midY...bounds.maxY).contains(point.y)
}
guard let touches = event?.touches(for: self) else { return false }
guard !touches.isEmpty else { return false }
print("passed all the guards")
let first = touches.first!
print("first touch: \(first.location(in: self))")
return touches.map { $0.location(in: self) }.contains(where: isInRegion)
}
}
The draw method draws a red L shape. And I tried enable touch only inside the L shape.
I created a MyView called blueView, made its background blue so that the touch area is red and the non-touch area is blue.
I also added a regular green UIView under part of the blue, like this:
I enabled user interactions for both of these views and added UITapGestureRecognisers, so that if the red area is tapped, blue view tapped will be printed. If the green view is tapped, green view tapped will be printed.
override func viewDidLoad() {
super.viewDidLoad()
blueView.isUserInteractionEnabled = true
greenView.isUserInteractionEnabled = true
blueView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(blueViewTapped)))
greenView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(greenViewTapped)))
}
#objc func blueViewTapped() {
print("Blue view tapped")
}
#objc func greenViewTapped() {
print("Green view tapped")
}
When I run the app and tap the red bit, only point(inside:with:) called is printed twice and nothing else. I expected blue view tapped and all the other print statements in point(inside:with:) to be run as well. If I tap the blue bit, the same message is printed twice again. If I tap the green bit that is covered by the blue bit, Green view tapped is printed after point(inside:with:) called is printed twice.
Why is blue view tapped not printed? I must have implemented point(inside:with:) wrongly, right?
EDIT:
After some debugging, I found out that the reason why point(inside:with:) is not returning true is because event.allTouches is empty. This is very strange because I am 100% sure that I touched the view!
Since you are using UIBezierPath to draw the area you want to tap, you can detect points inside your non-rectangular area by saving a reference to your path and using .contains(_:):
class MyView: UIView {
var path: UIBezierPath!
override func draw(_ rect: CGRect) {
path = UIBezierPath()
path.move(to: .zero)
path.addLine(to: CGPoint(x: 0, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.midY))
path.addLine(to: CGPoint(x: bounds.midX, y: bounds.midY))
path.addLine(to: CGPoint(x: bounds.midX, y: 0))
path.addLine(to: CGPoint(x: 0, y: 0))
UIColor.red.setFill()
path.fill()
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
print("path containsPoint?", path.contains(point))
return path.contains(point)
}
}
point(inside:with:) is used by hitTest(_:with:) to determine whether subviews should be traversed or not.
In your case, if user taps on blue area you should return false.
You can do it simply by checking
if (point is inside blue+red rect) {
return !(point is inside blue)
} else {
return false
}
You don't need to test for touches:
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
print("point(inside:with:) called")
func isInRegion(_ point: CGPoint) -> Bool {
return (0...bounds.midX).contains(point.x) || (bounds.midY...bounds.maxY).contains(point.y)
}
let a = isInRegion(point)
print(a)
return a
}
Related
I'm developing an application that uses custom annotationview pins on a map. The head of the pin is a button that dynamically resizes. I'm pretty new to swift and noticed that the button only registers if it is on top of the annotationviews frame, therefore I set the frame to be large enough to always contain the uibutton.
The problem: when I have multiple pins on the map the large frame overlaps other pinheads and interferes with tapping on them. I want to slim the frame as small as possible so it doesn't interfere with tapping on other pins.
The frame could certainly be slimmed down by just making the frame rectangle smaller, but I'm wondering if I could do better.
My questions is: is there a way to modify the frame shape of the annotation view so it is the same shape as the pin, instead of a rectangle?
I thought to use bezierpaths to achieve this but haven't been able to figure out how
Here is an image of the custom pin
-the blue is the frame and the red is a bezier path which I tried to set as the frame but just got drawn over
I got a test for custom size button,it is only triggered when click green part.
class TestButton: UIButton {
var path:UIBezierPath!
var drawLayer:CAShapeLayer!
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
path = UIBezierPath()
path.move(to: CGPoint(x: 60, y: 100))
path.addLine(to: CGPoint(x: 0, y: 100))
path.addArc(withCenter: CGPoint(x: 100, y: 100), radius: 100, startAngle: .pi, endAngle: .pi*1.5, clockwise: true)
path.addLine(to: CGPoint(x: 100, y: 60))
path.addArc(withCenter: CGPoint(x: 100, y: 100), radius: 40, startAngle: .pi*1.5, endAngle: .pi, clockwise: false)
path.close()
drawLayer = CAShapeLayer.init()
drawLayer.path = self.path.cgPath
drawLayer.fillColor = UIColor.green.cgColor
self.layer.addSublayer(self.drawLayer)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if self.path.contains(point) {
return true
} else {
return false
}
}
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
print(point)
let tmp=super.hitTest(point, with: event)
print(tmp)
return tmp
}
}
class vc_UnitTest: UIViewController {
#IBAction func openLoginHome(_ sender: Any) {
let bbb = TestButton.init(frame: CGRect.init(x: 50, y: 400, width: 150, height: 150))
bbb.backgroundColor = .red
self.view.addSubview(bbb)
}
}
I'm trying to achieve that effect like scratch lottery in iOS. The basic effect is achieved by using clear(_rect: CGRect), but how to erase a custom shape rather than a rectangle?
Here is my code
class ImageMaskView: UIView {
var image = UIImage(named: "maskL")
var line = [CGPoint ]()
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {return}
image?.draw(at: CGPoint(x: 0, y: 300))
// erase
line.forEach { (position) in
// print(position)
let rect = CGRect(x: position.x,y:position.y,width:50,height:50)
context.clear(rect)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let position = touches.first?.location(in: nil) else{return }
print(position)
line.append(position)
setNeedsDisplay()
}
}
Maybe I can build a custom function clear? Anyway, I would appreciate it if you can give me some advice
Have you tried UIBezierPath? You definitely can use UIBezierPath to build the custom shape. But I'm not 100% sure how to clear instead of draw this path in context. Try something like this:
let starPath: UIBezierPath = UIBezierPath()
starPath.move(to: CGPoint(x: 45.25, y: 0))
starPath.addLine(to: CGPoint(x: 61.13, y: 23))
starPath.addLine(to: CGPoint(x: 88.29, y: 30.75))
starPath.addLine(to: CGPoint(x: 70.95, y: 52.71))
starPath.addLine(to: CGPoint(x: 71.85, y: 80.5))
starPath.addLine(to: CGPoint(x: 45.25, y: 71.07))
starPath.addLine(to: CGPoint(x: 18.65, y: 80.5))
starPath.addLine(to: CGPoint(x: 19.55, y: 52.71))
starPath.addLine(to: CGPoint(x: 2.21, y: 0.75))
starPath.addLine(to: CGPoint(x: 29.37, y: 23))
starPath.close();
context.setBlendMode(.destinationOut)
context.setFillColor(UIColor.clear.cgColor)
starPath.fill()
Also, don't forget to set opaque of your view to false.
I have to draw a shape and detect if user touches are inside the shape or not, so I defined a custom class inherit from UIView as follow :
class ShapeView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
drawShape()
}
func drawShape() {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
let zero = CGPoint(x: frame.midX, y: frame.midY)
let size: CGFloat = 50
ctx.beginPath()
ctx.move(to: .zero)
ctx.addLine(to: CGPoint(x: -size, y: -size))
ctx.addLine(to: CGPoint(x: zero.x , y: (-size * 2)))
ctx.addLine(to: CGPoint(x: size, y: -size))
ctx.closePath()
ctx.setFillColor(UIColor.red.cgColor)
ctx.fillPath()
}
this code should draw shape like this
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let path = UIBezierPath(ovalIn: self.frame)
return path.contains(point)
}
}
and in the viewController I wrote this code to add this custom view to UIViewController :
var shape: ShapeView?
override func viewDidLoad() {
super.viewDidLoad()
let x = view.frame.midX
let y = view.frame.midY
self.shape = ShapeView(frame: CGRect(x: x, y: y, width: 100, height: 100))
shape?.backgroundColor = .green
view.addSubview(shape!)
}
I knew that the shape is inside the view after wrote this method :
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let location = touches.first!.location(in: self.view)
if (shape?.point(inside: location, with: event))! {
print("inside view")
} else {
print("outside view")
}
but overall result was this image it just has one color of the view as green and there's a subview but no color appeared
So what's wrong with this code ?
You should use the size of the rect of the override func draw(_ rect: CGRect) to do your calculations on.
I also think your calculations are incorrect, haven't tested it but I think it should be something like this:
ctx.move(to: CGPoint(x: rect.width / 2, y: 0))
ctx.addLine(to: CGPoint(x: rect.width, y: rect.height / 2))
ctx.addLine(to: CGPoint(x: rect.width / 2 , y: rect.height))
ctx.addLine(to: CGPoint(x: 0, y: rect.height / 2))
I have seen several answers about adding a GestureRecognzier to subViews but my problem is that I don't have the subView's frame available beforehand. I am drawing a CGPath and at the Touches Ended method, I want to create a new subView with frame equal to CGPath bounding box. After that, I want to drag that subView with PanGestureRecognizer. I am trying to implement Evernote crop functionality where the user selects a certain area of view and move it to other position. Is this the right approach to that solution?
Not quite understand the relationship between UIGestureRecognizer and .frame. you can simply add UIGestureRecognizer to object, once its init work finished.
Try to add gesture in TouchEnd method directly after drawing subview.
import UIKit
class GestureResearchVC: UIViewController{
var subViewByCGPath: UIView?
override func viewDidLoad() {
super.viewDidLoad()
}
func createSubView(){
//creat subview
subViewByCGPath = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
subViewByCGPath?.backgroundColor = UIColor.yellow
let circlePath = UIBezierPath(arcCenter: CGPoint(x: 50,y: 50), radius: CGFloat(20), startAngle: CGFloat(0), endAngle:CGFloat(M_PI * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
subViewByCGPath?.layer.addSublayer(shapeLayer)
self.view.addSubview(subViewByCGPath!)
//add pan gesture to subViewByCGPath
let gesture = UIPanGestureRecognizer(target: self, action: #selector(panGestureAction(rec:)))
subViewByCGPath?.addGestureRecognizer(gesture)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if subViewByCGPath == nil {
print("touch end once")
createSubView()
}else{
print("touch end repeatedly")
}
}
func panGestureAction(rec: UIPanGestureRecognizer){
print("pannnnnnn~")
let transPoint = rec.translation(in: self.view)
let x = rec.view!.center.x + transPoint.x
let y = rec.view!.center.y + transPoint.y
rec.view!.center = CGPoint(x: x, y: y)
rec.setTranslation(CGPoint(x: 0, y: 0), in: self.view)
//distinguish state
switch rec.state {
case .began:
print("began")
case .changed:
print("changed")
case .ended:
print("ended")
default:
print("???")
}
}
}
This is my first time in which I've encountered problems with drawRect. I have a simple UIView subclass with a path to fill. Everything is fine. The path gets filled properly, but the background remains black no matter what. What should I do? My code is listed below:
var color: UIColor = UIColor.red {didSet{setNeedsDisplay()}}
private var spaceshipBezierPath: UIBezierPath{
let path = UIBezierPath()
let roomFromTop: CGFloat = 10
let roomFromCenter: CGFloat = 7
path.move(to: CGPoint(x: bounds.minX, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.minX, y: bounds.minY+roomFromTop))
path.addLine(to: CGPoint(x: bounds.midX-roomFromCenter, y: bounds.minY+roomFromTop))
path.addLine(to: CGPoint(x: bounds.midX-roomFromCenter, y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.midX+roomFromCenter, y: bounds.minY))
path.addLine(to: CGPoint(x: bounds.midX+roomFromCenter, y: bounds.minY+roomFromTop))
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.minY+roomFromTop))
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
return path
}
override func draw(_ rect: CGRect) {
color.setFill()
spaceshipBezierPath.fill()
}
Here is what the view looks like:
I have a simple UIView subclass ... but the background remains black no matter what
Typically that sort of thing is because you have forgotten to set the UIView subclass's isOpaque to false. It is a good idea to do this in the UIView subclass's initializer in order for it to be early enough.
For example, here I've adapted your code very slightly. This is the complete code I'm using:
class MyView : UIView {
private var spaceshipBezierPath: UIBezierPath{
let path = UIBezierPath()
// ... identical to your code
return path
}
override func draw(_ rect: CGRect) {
UIColor.red.setFill()
spaceshipBezierPath.fill()
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let v = MyView(frame:CGRect(x: 100, y: 100, width: 200, height: 200))
self.view.addSubview(v)
}
}
Notice the black background:
Now I add these lines to MyView:
override init(frame:CGRect) {
super.init(frame:frame)
self.isOpaque = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
See the difference that makes?
In my case what I solved was to switch from Background-Color "No Color", to Background-Color "Clear Color"
For future seekers....
I had the same problem but #matt solution didn’t work for me.
In my scenario, I had a UIView (A) behind another UIView (B).
UIView (A) had a background color and UIView (B) was transparent. Worth to note that B was subclassed to create a custom shape using UIBezier by overriding draw method.
Turned out that in this situation a transparent UIView means nothing and I had to set the same background color for B or either remove background color of A.
there was no other way, so don’t waste your time if you have the same scenario!