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)
}
}
Related
I am new to swift and I have no experience handling core graphics.
I need to develop an application in ios swift to draw graphs. Here graph refers to graph theory. I need to draw the nodes by tapping on the screen and draw lines between two nodes when they are selected. So far I can only draw circles, but I need that when touching two points a line appears between the two. And I need that if I already draw a circle, when they tap again they can move. How can I draw the lines?
I will appreciate any support
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.lightGray
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
// Set the Center of the Circle
// 1
let circleCenter = touch.location(in: view)
// Set a random Circle Radius
// 2
let circleWidth = CGFloat(25.7) //+ (arc4random() % 50))
let circleHeight = circleWidth
// Create a new CircleView
// 3
let circleView = CircleView(frame: CGRect(x: circleCenter.x, y: circleCenter.y, width: circleWidth, height: circleHeight))
view.addSubview(circleView)
}
}
}
Class CircleView.swift
class CircleView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
// Get the Graphics Context
if let context = UIGraphicsGetCurrentContext() {
// Set the circle outerline-width
context.setLineWidth(5.0);
// Set the circle outerline-colour
UIColor.red.set()
// Create Circle
let center = CGPoint(x: frame.size.width/2, y: frame.size.height/2)
let radius = (frame.size.width - 10)/2
context.addArc(center: center, radius: radius, startAngle: 0.0, endAngle: .pi * 2.0, clockwise: true)
// Draw
context.strokePath()
}
}
}
I tried with many ways to draw buttons as shown in image, But I'm able to draw only hexagonal . Am not able to draw as show in image . Please help me how to draw as shown in image. Thank you
You can do this with UIBezierPath and CAShapeLayer
Create a UIButton subclass and override layoutSubviews(), which is where you'll update your path, connecting the points with lines.
Here is a starting point for you:
class AngledButton: UIButton {
var bPath = UIBezierPath()
var theShapeLayer = CAShapeLayer()
var fillColor: UIColor = UIColor.white
var borderColor: UIColor = UIColor.gray
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
layer.addSublayer(theShapeLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
let p1 = CGPoint.zero
let p2 = CGPoint(x: bounds.width - bounds.height * 0.3, y: 0.0)
let p3 = CGPoint(x: bounds.width, y: bounds.height * 0.5)
let p4 = CGPoint(x: bounds.width - bounds.height * 0.3, y: bounds.height)
let p5 = CGPoint(x: 0.0, y: bounds.height)
bPath.move(to: p1)
bPath.addLine(to: p2)
bPath.addLine(to: p3)
bPath.addLine(to: p4)
bPath.addLine(to: p5)
bPath.close()
theShapeLayer.path = bPath.cgPath
theShapeLayer.fillColor = self.fillColor.cgColor
theShapeLayer.strokeColor = self.borderColor.cgColor
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
// only allow tap *within* the bezier path
if bPath.contains(point) {
return self
}
return nil
}
}
By also overriding hitTest() the button will only be tapped if the tap is within the bezier path area. You can then overlap your buttons, and a tap at the upper-right corner will "pass through" to the button underneath.
Result:
Notes:
You can make this #IBDesignable to see the layout in Interface Builder
You'll need to play with the Title Insets to get the title labels to account for the angled edge
This is just a starting point, but should get you on your way
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
}
I need to have a view with borders, and only borders should be clickable.
Here is my code which creates custom view, but when I added gesture all view becomes clickable.
class RectangleView: UIView {
override func draw(_ rect: CGRect) {
let aPath = UIBezierPath()
aPath.move(to: CGPoint(x:0, y: 0))
aPath.addLine(to: CGPoint(x:rect.width, y: 0))
aPath.addLine(to: CGPoint(x:rect.width, y:rect.height))
aPath.addLine(to: CGPoint(x:0, y:rect.height))
aPath.close()
UIColor.green.setStroke()
aPath.stroke()
UIColor.clear.setFill()
aPath.fill()
}
}
Edited, here is the screenshot from other app
so I will click on view under my top view, this one will be in front and will be clickable, so I cant add smaller subView
The most correct way here is to override func point(inside point: CGPoint, with event: UIEvent?) -> Bool for UIView subclass.
Here is example:
class CustomView: UIView {
private let shapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
setupShapeLayer()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupShapeLayer() {
//Draw your own shape here
shapeLayer.frame = CGRect(x: 0, y: 0, width: frame.width, height: frame.height)
let innerPath = UIBezierPath(arcCenter: center, radius: frame.width / 3, startAngle: CGFloat(0), endAngle: CGFloat.pi * 2, clockwise: true)
let outerPath = UIBezierPath(arcCenter: center, radius: frame.width / 4, startAngle: CGFloat(0), endAngle: CGFloat.pi * 2, clockwise: true)
//Subtract inner path
innerPath.append(outerPath.reversing())
shapeLayer.path = innerPath.cgPath
shapeLayer.lineWidth = 10
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.fillColor = UIColor.yellow.cgColor
layer.addSublayer(shapeLayer)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return shapeLayer.path?.contains(point) ?? false
}
}
Now you can add UITapGestureRecognizer to check how it works:
class ViewController: UIViewController {
#IBOutlet weak var resultLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let customView = CustomView.init(frame: view.frame)
view.addSubview(customView)
let recognizer = UITapGestureRecognizer(target: self, action: #selector(tapAction))
view.addGestureRecognizer(recognizer)
}
#objc func tapAction(_ sender: UITapGestureRecognizer) {
let location = sender.location(in: sender.view)
let subview = view?.hitTest(location, with: nil)
if subview is CustomView {
resultLabel.text = "CustomView"
}
else {
resultLabel.text = "Other"
}
}
}
Result of tapAction:
If you need several views, there are two possible options:
Store CAShapeLayer inside one CustomView and iterate through it inside point function
Add CustomView for each instance and iterate it through inside UITapGestureRecognizer tap action
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))