Empty view, with only clickable borders - ios

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

Related

Draw lines with touch

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()
}
}
}

CAShapeLayer doesn't show up

I am trying to add a CAShapeLayer to a view. But when I add the customView to ViewCOntroller's View, the shapeLayer doesn't show up. Capturing hierarchy shows that the layer doesn't even get inserted to the View. Here is the code:
override class var layerClass: AnyClass {
return CAShapeLayer.self
}
...
override func didMoveToSuperview() {
let diameter = min(self.frame.width, self.frame.height)
let path = UIBezierPath(arcCenter: self.center, radius: diameter/2, startAngle: 0, endAngle: CGFloat.pi*2, clockwise: true)
let sLayer = CAShapeLayer()
sLayer.frame = bounds
sLayer.path = path.cgPath
sLayer.fillMode = .backwards
sLayer.fillColor = UIColor.blue.cgColor//bubbleColor?.cgColor
layer.addSublayer(sLayer)
}
And in ViewController:
override func viewDidLoad() {
super.viewDidLoad()
let node = Node(frame: CGRect(origin: self.view.center, size: CGSize(width: 250, height: 250)))
self.view.addSubview(node)
...
}
I really do not understand what is happening. Please help!
I'd suggest:
adding your sublayer in init (so it's added once and only once);
configure its frame and path in layoutSubviews (so if it's resized, whether by constraints or some other mechanism, the layer will update accordingly);
when adding a subview/sublayer whose dimensions is based upon the size of the current view, reference the view’s bounds (the coordinate system within the current view), not the frame nor center (both of which are in the coordinate system of the superview); and
either use addSublayer approach or layerClass approach, but not both.
Thus, adding CAShapeLayer as sublayer:
#IBDesignable
class CircleView: UIView {
#IBInspectable var bubbleColor: UIColor = .blue { didSet { shapeLayer.fillColor = bubbleColor.cgColor } }
private lazy var shapeLayer: CAShapeLayer = {
let sLayer = CAShapeLayer()
sLayer.fillColor = bubbleColor.cgColor
return sLayer
}()
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
override func layoutSubviews() {
super.layoutSubviews()
shapeLayer.frame = bounds
updatePath()
}
}
// MARK: - Utility methods
private extension CircleView {
func configure() {
layer.addSublayer(shapeLayer)
}
func updatePath() {
let diameter = min(bounds.width, bounds.height)
let arcCenter = CGPoint(x: bounds.midX, y: bounds.midY)
shapeLayer.path = UIBezierPath(arcCenter: arcCenter, radius: diameter / 2, startAngle: 0, endAngle: .pi * 2, clockwise: true).cgPath
}
}
Yielding:
Or, if you want to use layerClass approach:
#IBDesignable
class CircleView: UIView {
#IBInspectable var bubbleColor: UIColor = .blue { didSet { shapeLayer.fillColor = bubbleColor.cgColor } }
override class var layerClass: AnyClass { return CAShapeLayer.self }
private var shapeLayer: CAShapeLayer { return layer as! CAShapeLayer}
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
}
// MARK: - Utility methods
private extension CircleView {
func configure() {
shapeLayer.fillColor = bubbleColor.cgColor
}
func updatePath() {
let diameter = min(bounds.width, bounds.height)
let arcCenter = CGPoint(x: bounds.midX, y: bounds.midY)
shapeLayer.path = UIBezierPath(arcCenter: arcCenter, radius: diameter / 2, startAngle: 0, endAngle: .pi * 2, clockwise: true).cgPath
}
}

Unexpected behavior in scaling CAShapeLayer

I am using the following code to scale a CAShapelayer instance (i.e., self.view.layer.sublayers![0]):
class ViewController: UIViewController {
override func viewDidLoad()
{
super.viewDidLoad()
self.view = Map(frame: CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height))
let pinchGR = UIPinchGestureRecognizer(target: self, action: #selector(self.didPinch(_:)))
self.view.addGestureRecognizer(pinchGR)
}
#objc func didPinch(_ pinchGR: UIPinchGestureRecognizer)
{
let transformation = CGAffineTransform(a: pinchGR.scale, b: 0, c: 0, d: pinchGR.scale, tx: 0, ty: 0)
if pinchGR.state == .began || pinchGR.state == .changed
{
self.view.layer.sublayers![0].setAffineTransform(transformation)
}
}}
class Map: UIView{
override init(frame: CGRect)
{
super.init(frame: frame)
self.backgroundColor = UIColor.blue
drawTestShape()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
}
func drawTestShape()
{
let shapeLayer = CAShapeLayer()
shapeLayer.frame = self.frame
let path = UIBezierPath()
shapeLayer.lineWidth = 5.0
shapeLayer.fillColor = nil
shapeLayer.strokeColor = UIColor.white.cgColor
shapeLayer.backgroundColor = UIColor.red.cgColor
path.move(to: CGPoint(x:200,y:200))
path.addLine(to: CGPoint(x:300,y:300))
path.addLine(to: CGPoint(x:100,y:300))
path.close()
shapeLayer.path = path.cgPath
self.layer.addSublayer(shapeLayer)
}}
The scaling is performed correctly in the first pinch gesture. However, when a second pinch gesture is applied, the CAShapelayer instance starts again from its initial size, and not from the size it had after the first gesture.
I can't really get behind the idea of scaling a layer, but if you must do it, try something more like this; this is the standard pattern for implementing a pinch gesture recognizer, so it's really best to stick with it:
#IBAction func didPinch(_ pinchGR: UIPinchGestureRecognizer) {
self.view.layer.sublayers![0].setAffineTransform(
self.view.layer.sublayers![0].affineTransform().scaledBy(
x:pinchGR.scale, y:pinchGR.scale))
pinchGR.scale = 1.0;
}

draw(_ rect: CGRect) not getting called in custom view

I am trying to create a custom view by extending the class UIView which can show a circle at the centre of the custom view.
In order to add custom drawing, I override draw(_ rect: CGRect) method as below.
public override func draw(_ rect: CGRect) {
super.draw(rect)
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
let minLength = bounds.width <= bounds.height ? bounds.width : bounds.height
let circlePath = UIBezierPath (arcCenter: center, radius: CGFloat(minLength / 2), startAngle: 0, endAngle: CGFloat(Float(360).degreesToRadians) , clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = circleColor.cgColor
layer.addSublayer(shapeLayer)
}
But when I try to add the same custom component in storyboard and added a breakpoint in draw method, it was not getting called. To my curiosity, I added a breakpoint to init?(coder aDecoder: NSCoder) method which was getting called. Also when I try to override the method as below, it worked fine.
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
drawCircle()
}
fileprivate func drawCircle() {
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
let minLength = bounds.width <= bounds.height ? bounds.width : bounds.height
let circlePath = UIBezierPath (arcCenter: center, radius: CGFloat(minLength / 2), startAngle: 0, endAngle: CGFloat(Float(360).degreesToRadians) , clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = circleColor.cgColor
layer.addSublayer(shapeLayer)
}
The issue with approach is I have a custom property circleColor
#IBInspectable
public var circleColor : UIColor = UIColor.gray
which is not yet set when init method is called and it always draws a gray circle.
Please help to get me know the reason why draw method is not being called.
Thanks in advance
I am not sure where your root mistake lies but you should not override draw(:) just to add a layer. That would mean a new layer will be added again every time the component rerenders. That's not what you want.
You should also make sure the layer color is updated when you change the color property. To do that, just save the reference to the layer:
private let shapeLayer = CAShapeLayer()
#IBInspectable
public var circleColor : UIColor = UIColor.gray {
didSet {
shapeLayer.fillColor = circleColor.cgColor
}
}
public override init(frame: CGRect = .zero) {
super.init(frame: frame)
updateLayer()
layer.addSublayer(shapeLayer)
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
updateLayer()
layer.addSublayer(shapeLayer)
}
fileprivate func updateLayer() {
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
let minLength = min(bounds.width, bounds.height)
let circlePath = UIBezierPath(arcCenter: center, radius: minLength / 2, startAngle: 0, endAngle: CGFloat(Float(360).degreesToRadians), clockwise: true)
shapeLayer.path = circlePath.cgPath
shapeLayer.fillColor = circleColor.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
// update layer position
updateLayer()
}
If you really want to override draw(rect:), you don't need to create a layer or override initializers at all:
#IBInspectable
public var circleColor : UIColor = UIColor.gray {
didSet {
setNeedsDisplay()
}
}
public override func draw(_ rect: CGRect) {
// no need to call super.draw(rect)
let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)
let minLength = min(bounds.width, bounds.height)
let circlePath = UIBezierPath(arcCenter: center, radius: minLength / 2, startAngle: 0, endAngle: .pi * 2, clockwise: true)
// the following methods use current CGContext
circleColor.set()
circlePath.fill()
}

My CoreGraphics code is working really slowly. How can I make this custom UIScrollView perform better?

I made a custom class that creates a sort of timeline that is meant to be scrolled horizontally. Here's the code for my custom UIScrollView :
import UIKit
struct DataPoint {
var fillColor: UIColor = UIColor.grayColor()
init(color: UIColor) {
fillColor = color
}
}
#IBDesignable
class MyView: UIScrollView {
#IBInspectable var lineColor: UIColor = UIColor.grayColor()
#IBInspectable var lineHeight: CGFloat = 65
#IBInspectable var lineWidth: CGFloat = 15
#IBInspectable var lineGap: CGFloat = 25
#IBInspectable var lineCount: Int = 0
var dataPoints = [DataPoint(color: UIColor.greenColor()), DataPoint(color: UIColor.blueColor())]
override init(frame: CGRect) {
super.init(frame: frame)
setupValues()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupValues()
//fatalError("init(coder:) has not been implemented")
}
func setupValues() {
self.contentSize = CGSize(width: self.frame.width * 2, height: self.frame.height)
self.backgroundColor = UIColor.whiteColor()
self.lineCount = Int(self.frame.width / lineGap)
}
override internal func layoutSubviews() {
super.layoutSubviews()
self.setNeedsDisplay()
self.layoutIfNeeded()
}
override func drawRect(rect: CGRect) {
let ctx = UIGraphicsGetCurrentContext()
CGContextSaveGState(ctx)
for i in 0...lineCount {
let start = CGPoint(x: CGFloat(i) * lineGap, y: self.frame.height)
let end = CGPoint(x: CGFloat(i) * lineGap, y: self.frame.height - lineHeight)
drawLine(from: start, to: end, color: UIColor.grayColor())
if i % (lineCount / (dataPoints.count + 2)) == 0 && i != 0 && i != lineCount {
drawPoint(at: end, radius: 5, color: UIColor.orangeColor())
}
}
CGContextRestoreGState(ctx)
}
func drawLine(from start: CGPoint, to end: CGPoint, color: UIColor) {
let path = UIBezierPath()
path.moveToPoint(start)
path.addLineToPoint(end)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.CGPath
shapeLayer.strokeColor = color.CGColor
shapeLayer.lineWidth = 1
self.layer.addSublayer(shapeLayer)
}
func drawPoint(at center: CGPoint, radius: CGFloat, color: UIColor) {
let path = UIBezierPath(arcCenter: center, radius: radius, startAngle: CGFloat(0), endAngle: CGFloat(M_PI * 2), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.CGPath
shapeLayer.fillColor = color.CGColor
shapeLayer.strokeColor = UIColor.blackColor().CGColor
shapeLayer.lineWidth = 0.5
self.layer.addSublayer(shapeLayer)
}
}
On my device and on the simulators, this is extremely slow and laggy. What exactly am I doing wrong here ? And what steps can I take to achieve a solid 60fps while scrolling ?
Hello I found your problem, your problem is
override internal func layoutSubviews() {
super.layoutSubviews()
self.setNeedsDisplay()
self.layoutIfNeeded()
}
you are accidentally in a never-ending paint loop
replace this by this
override internal func layoutSubviews() {
super.layoutSubviews()
}
or remove at all
I hope this Helps you, for me works great
It looks to me that you are drawing the same thing every time. Everything is static... So why not draw it all once into a graphics context and make a UIImage out of it to add to the background of the scrollview (or foreground, depending on anything else you are doing)?

Resources