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()
}
}
}
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 would like to know how to draw a line from one point to another where the user touch the first point and the next point he touches, it create a line in between. I know that using UIBezierPath will be useful in here but I am still new to Swift and iOS platform so any guidance will helpful.
I have created a function called drawLineFromPoint but does not work for me.
import UIKit
import GLKit
class ViewController: GLKViewController {
private var context: EAGLContext?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
EAGLcontext()
// Gesture Code
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first{
let position = touch.location(in: view)
var dot = UIView(frame: CGRect(x: position.x, y: position.y, width: 10, height: 10))
dot.backgroundColor = .red
view.addSubview(dot)
// View the x and y coordinates
print(position)
}
}
//Not sure how to do this part ???
func drawLineFromPoint(start : CGPoint, toPoint end:CGPoint, ofColor lineColor: UIColor, inView view:UIView) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = 1.0
view.layer.addSublayer(shapeLayer)
}
//Create EAGL Context for the GLKView
private func EAGLcontext() {
context = EAGLContext(api: .openGLES2)
EAGLContext.setCurrent(context)
if let view = self.view as? GLKView, let context = context {
view.context = context
delegate = self
}
}
override func glkView(_ view: GLKView, drawIn rect: CGRect) {
//Set the color
glClearColor(0.85, 0.85, 0.85, 1.0)
glClear(GLbitfield(GL_COLOR_BUFFER_BIT))
}
}
extension ViewController: GLKViewControllerDelegate {
func glkViewControllerUpdate(_ controller: GLKViewController) {
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Based On your code I changed some things and this code Works please check.
class ViewController: UIViewController {
var lastPosition: CGPoint?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// Gesture Code
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first{
let position = touch.location(in: view)
if let lastPosition = self.lastPosition {
self.drawLineFromPoint(start: lastPosition, toPoint: position, ofColor: UIColor.red, inView: self.view)
}
self.lastPosition = position
// View the x and y coordinates
let dot = UIView(frame: CGRect(x: position.x, y: position.y, width: 10, height: 10))
dot.backgroundColor = .red
view.addSubview(dot)
print(position)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first, let lastPosition = self.lastPosition{
let position = touch.location(in: view)
self.drawLineFromPoint(start: lastPosition, toPoint: position, ofColor: UIColor.red, inView: self.view)
self.lastPosition = position
let dot = UIView(frame: CGRect(x: position.x, y: position.y, width: 10, height: 10))
dot.backgroundColor = .red
view.addSubview(dot)
}
}
//Not sure how to do this part ???
func drawLineFromPoint(start : CGPoint, toPoint end:CGPoint, ofColor lineColor: UIColor, inView view:UIView) {
let path = UIBezierPath()
path.move(to: start)
path.addLine(to: end)
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = 1.0
view.layer.addSublayer(shapeLayer)
}
}
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 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 want to rotate a UIView with a drawing inside (circle, rectangle or line). The problem is when I rotate the view and refresh the drawing (I need ir when I change some properties of the drawing, i.e) the drawing doesn't follow the view...
UIView without rotation:
UIView with rotation:
Here is my simple code (little extract from the original code):
Main ViewController
class TestRotation: UIViewController {
// MARK: - Properties
var drawingView:DrawingView?
// MARK: - Actions
#IBAction func actionSliderRotation(_ sender: UISlider) {
let degrees = sender.value
let radians = CGFloat(degrees * Float.pi / 180)
drawingView?.transform = CGAffineTransform(rotationAngle: radians)
drawingView?.setNeedsDisplay()
}
// MARK: - Init
override func viewDidLoad() {
super.viewDidLoad()
drawingView = DrawingView(frame:CGRect(x:50, y:50, width: 100, height:75))
self.view.addSubview(drawingView!)
drawingView?.setNeedsDisplay()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
UIView
class DrawingView: UIView {
override func draw(_ rect: CGRect) {
let start = CGPoint(x: 0, y: 0)
let end = CGPoint(x: self.frame.size.width, y: self.frame.size.height)
drawCicrle(start: start, end: end)
}
func drawCicrle(start:CGPoint, end:CGPoint) {
//Contexto
let context = UIGraphicsGetCurrentContext()
if context != nil {
//Ancho
context!.setLineWidth(2)
//Color
context!.setStrokeColor(UIColor.black.cgColor)
context!.setFillColor(UIColor.orange.cgColor)
let rect = CGRect(origin: start, size: CGSize(width: end.x-start.x, height: end.y-start.y))
let path = UIBezierPath(ovalIn: rect)
path.lineWidth = 2
path.fill()
path.stroke()
}
}
// MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.gray
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
It seems that the problem is temporally solved if I don't refresh the view after rotating, but if I have to refresh later for some other reason (properties changed), the problem appears again.
What can I do? Thanks in advance
You are using the frame when you should be using the bounds.
The frame is the rectangle which contains the view. It is specified in the coordinate system of the superview of the view, and the frame changes when you rotate the view (because a rotated square extends further in the X and Y directions than a non-rotated square).
The bounds is the rectangle which contains the contents of the view. It is specified in the coordinate system of the view itself, and it doesn't change as the view is rotated or moved.
If you change frame to bounds in your draw(_:) function, it will work as you expect:
override func draw(_ rect: CGRect) {
let start = CGPoint(x: 0, y: 0)
let end = CGPoint(x: self.bounds.size.width, y: self.bounds.size.height)
drawCicrle(start: start, end: end)
}
Here's a demo showing the oval being redrawn as the slider moves.
Here's the changed code:
class DrawingView: UIView {
var fillColor = UIColor.orange {
didSet {
setNeedsDisplay()
}
}
override func draw(_ rect: CGRect) {
let start = CGPoint(x: 0, y: 0)
let end = CGPoint(x: self.bounds.size.width, y: self.bounds.size.height)
drawCircle(start: start, end: end)
}
func drawCircle(start:CGPoint, end:CGPoint) {
//Contexto
let context = UIGraphicsGetCurrentContext()
if context != nil {
//Ancho
context!.setLineWidth(2)
//Color
context!.setStrokeColor(UIColor.black.cgColor)
context!.setFillColor(fillColor.cgColor)
let rect = CGRect(origin: start, size: CGSize(width: end.x-start.x, height: end.y-start.y))
let path = UIBezierPath(ovalIn: rect)
path.lineWidth = 2
path.fill()
path.stroke()
}
}
// MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.gray
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
#IBAction func actionSliderRotation(_ sender: UISlider) {
let degrees = sender.value
let radians = CGFloat(degrees * Float.pi / 180)
drawingView?.transform = CGAffineTransform(rotationAngle: radians)
drawingView?.fillColor = UIColor(hue: CGFloat(degrees)/360.0, saturation: 1.0, brightness: 1.0, alpha: 1.0)
}