UIPanGestureRecognizer - drag UIView only within a line path connecting 2 points - ios

I have a line (CAShapeLayer) which is connecting two points. Those two points can be anywhere on the screen and their coordinates are known. The point in the middle also has a UIPanGestureRecognizer but I want it to be draggable only within the line (so it can't leave the line - the line must go directly through the center of the middle dot at any time).
I know that I can calculate the draggable position of the point with
let translation = gesture.translation(in: self.view)
and limit it by X or Y axis but what kind of calculation is needed here to drag it only within the line?

Try using the two end points to compute the slope-intercept form if the line between them (y= mx + b). Then plug the x-coordinate from the translation point into the line equation to calculate y. Finally, position the draggable point at (x, y).

thank you for your answers! I managed to find the most suitable solution. I use the formula from this answer:
https://stackoverflow.com/a/57383437/1088286
function getPositionAlongTheLine(x1, y1, x2, y2, percentage) {
return {x : x1 * (1.0 - percentage) + x2 * percentage, y : y1 * (1.0 - percentage) + y2 * percentage};
}
And that is the code I use in didMove: UIPanGestureRecogniser method:
touchPoint.center = getPositionAlongTheLine(firstPoint: mainPoint1,
secondPoint: mainPoint2,
percentage: percentage)
Where mainPoint1 and mainPoint2 are the side buttons. Works perfectly!

You can keep the "draggable circle" on the line by using a function to calculate the "closest point":
// get closest point on a line from a third point
private func closestPoint(_ pt1: CGPoint, pt2: CGPoint, pt3: CGPoint) -> CGPoint {
let vx = pt2.x - pt1.x
let vy = pt2.y - pt1.y
let ax = pt3.x - pt1.x
let ay = pt3.y - pt1.y
let u = (ax * vx + ay * vy) / (vx * vx + vy * vy)
return CGPoint(x: pt1.x + vx * u, y: pt1.y + vy * u)
}
Here's a demo implementation, using a CAShapeLayer for the line, another CAShapeLayer for the "draggable circle" and tracking the movement using touchesBegan / touchesMoved. If you want to use a view and pan gestures for the "draggable circle" the idea will be the same (and should be easy to convert):
UIView subclass:
class StayOnLineView: UIView {
public var lineEnds: [CGPoint] = [.zero, .zero] {
didSet {
guard lineEnds.count == 2 else {
fatalError("lineEnds must be 2 points!")
}
linePT1 = lineEnds[0]
linePT2 = lineEnds[1]
minX = min(linePT1.x, linePT2.x)
maxX = max(linePT1.x, linePT2.x)
minY = min(linePT1.y, linePT2.y)
maxY = max(linePT1.y, linePT2.y)
currentDragPoint = centerPointOnLine(linePT1, pt2: linePT2)
}
}
private var minX: CGFloat = 0
private var minY: CGFloat = 0
private var maxX: CGFloat = 0
private var maxY: CGFloat = 0
private var linePT1: CGPoint = .zero
private var linePT2: CGPoint = .zero
private var currentDragPoint: CGPoint = .zero
private var isDragging: Bool = false
private let lineLayer: CAShapeLayer = CAShapeLayer()
private let dragCircleLayer: CAShapeLayer = CAShapeLayer()
private let dragCircleDiameter: CGFloat = 24
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
layer.addSublayer(lineLayer)
lineLayer.strokeColor = UIColor.red.cgColor
lineLayer.lineWidth = 4
layer.addSublayer(dragCircleLayer)
dragCircleLayer.strokeColor = UIColor.blue.cgColor
dragCircleLayer.lineWidth = 2
dragCircleLayer.fillColor = UIColor.clear.cgColor
}
func centerPointOnLine(_ pt1: CGPoint, pt2: CGPoint) -> CGPoint {
return CGPoint(x: pt1.x + ((pt2.x - pt1.x) * 0.5), y: pt1.y + ((pt2.y - pt1.y) * 0.5))
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let position = touch.location(in: self)
let dragRect = CGRect(x: currentDragPoint.x - dragCircleDiameter * 0.5, y: currentDragPoint.y - dragCircleDiameter * 0.5, width: dragCircleDiameter, height: dragCircleDiameter)
let dragPath = UIBezierPath(ovalIn: dragRect)
// only start dragging if touch is inside draggableCircle
isDragging = dragPath.contains(position)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if isDragging {
if let touch = touches.first {
let position = touch.location(in: self)
// get closest point on the line from the touch
let p = closestPoint(linePT1, pt2: linePT2, pt3: position)
// keep point from going past the ends of the line
let x = min(max(p.x, minX), maxX)
let y = min(max(p.y, minY), maxY)
// update current drag point
currentDragPoint = CGPoint(x: x, y: y)
setNeedsLayout()
layoutIfNeeded()
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
isDragging = false
}
// get closest point on a line from a third point
private func closestPoint(_ pt1: CGPoint, pt2: CGPoint, pt3: CGPoint) -> CGPoint {
let vx = pt2.x - pt1.x
let vy = pt2.y - pt1.y
let ax = pt3.x - pt1.x
let ay = pt3.y - pt1.y
let u = (ax * vx + ay * vy) / (vx * vx + vy * vy)
return CGPoint(x: pt1.x + vx * u, y: pt1.y + vy * u)
}
override func layoutSubviews() {
super.layoutSubviews()
let linePath = UIBezierPath()
linePath.move(to: linePT1)
linePath.addLine(to: linePT2)
lineLayer.path = linePath.cgPath
let dragCircleRect = CGRect(x: currentDragPoint.x - dragCircleDiameter * 0.5, y: currentDragPoint.y - dragCircleDiameter * 0.5, width: dragCircleDiameter, height: dragCircleDiameter)
let dragCirclePath = UIBezierPath(ovalIn: dragCircleRect)
dragCircleLayer.path = dragCirclePath.cgPath
}
}
Example View Controller:
class DragOnLineViewController: UIViewController {
let testView = StayOnLineView()
override func viewDidLoad() {
super.viewDidLoad()
testView.translatesAutoresizingMaskIntoConstraints = false
testView.backgroundColor = .yellow
view.addSubview(testView)
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
testView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
testView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
testView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
testView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
testView.lineEnds = [
CGPoint(x: 80.0, y: 80.0),
CGPoint(x: testView.frame.width - 80.0, y: testView.frame.height - 80.0)
]
}
}
Edit - simple example dragging a UIView with a pan gesture instead of dragging a CAShapeLayer using touches...
Simple UIView subclass - draws a line between two points:
class LineView: UIView {
public var lineEnds: [CGPoint] = [.zero, .zero] {
didSet {
guard lineEnds.count == 2 else {
fatalError("lineEnds must be 2 points!")
}
}
}
private let lineLayer: CAShapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
layer.addSublayer(lineLayer)
lineLayer.strokeColor = UIColor.red.cgColor
lineLayer.lineWidth = 4
}
override func layoutSubviews() {
super.layoutSubviews()
guard lineEnds.count == 2 else {
return
}
let linePath = UIBezierPath()
linePath.move(to: lineEnds[0])
linePath.addLine(to: lineEnds[1])
lineLayer.path = linePath.cgPath
}
}
Basic "draggable" view, using UIPanGestureRecognizer but limiting movement to a line between two points:
class DraggableCircleView: UIView {
public var lineEnds: [CGPoint] = [.zero, .zero] {
didSet {
guard lineEnds.count == 2 else {
fatalError("lineEnds must be 2 points!")
}
linePT1 = lineEnds[0]
linePT2 = lineEnds[1]
minX = min(linePT1.x, linePT2.x)
maxX = max(linePT1.x, linePT2.x)
minY = min(linePT1.y, linePT2.y)
maxY = max(linePT1.y, linePT2.y)
center = centerPointOnLine(linePT1, pt2: linePT2)
}
}
private var minX: CGFloat = 0
private var minY: CGFloat = 0
private var maxX: CGFloat = 0
private var maxY: CGFloat = 0
private var linePT1: CGPoint = .zero
private var linePT2: CGPoint = .zero
private var startPoint: CGPoint = .zero
private let dragCircleLayer: CAShapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
layer.addSublayer(dragCircleLayer)
dragCircleLayer.strokeColor = UIColor.blue.cgColor
dragCircleLayer.lineWidth = 2
dragCircleLayer.fillColor = UIColor.clear.cgColor
let p = UIPanGestureRecognizer(target: self, action: #selector(self.panHandler(_:)))
addGestureRecognizer(p)
}
#objc func panHandler(_ g: UIPanGestureRecognizer) -> Void {
let t = g.translation(in: self)
switch g.state {
case .began:
startPoint = center
case .changed:
let newPoint = CGPoint(x: startPoint.x + t.x, y: startPoint.y + t.y)
// get closest point on the line from the touch
let p = closestPoint(linePT1, pt2: linePT2, pt3: newPoint)
// keep point from going past the ends of the line
let x = min(max(p.x, minX), maxX)
let y = min(max(p.y, minY), maxY)
// update current center point
center = CGPoint(x: x, y: y)
default:
break
}
}
override func layoutSubviews() {
let dragCirclePath = UIBezierPath(ovalIn: bounds)
dragCircleLayer.path = dragCirclePath.cgPath
}
func centerPointOnLine(_ pt1: CGPoint, pt2: CGPoint) -> CGPoint {
return CGPoint(x: pt1.x + ((pt2.x - pt1.x) * 0.5), y: pt1.y + ((pt2.y - pt1.y) * 0.5))
}
// get closest point on a line from a third point
private func closestPoint(_ pt1: CGPoint, pt2: CGPoint, pt3: CGPoint) -> CGPoint {
let vx = pt2.x - pt1.x
let vy = pt2.y - pt1.y
let ax = pt3.x - pt1.x
let ay = pt3.y - pt1.y
let u = (ax * vx + ay * vy) / (vx * vx + vy * vy)
return CGPoint(x: pt1.x + vx * u, y: pt1.y + vy * u)
}
}
Simple view controller example with LineView and DraggableCircleView:
class DragViewOnLineViewController: UIViewController {
let dragCircleView = DraggableCircleView()
let lineView = LineView()
override func viewDidLoad() {
super.viewDidLoad()
lineView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(lineView)
lineView.backgroundColor = .green
lineView.addSubview(dragCircleView)
dragCircleView.frame = CGRect(origin: .zero, size: CGSize(width: 20, height: 20))
// respect safe area
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
lineView.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
lineView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
lineView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
lineView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
])
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
lineView.lineEnds = [
CGPoint(x: 80.0, y: 80.0),
CGPoint(x: lineView.frame.width - 80.0, y: lineView.frame.height - 80.0)
]
dragCircleView.lineEnds = lineView.lineEnds
}
}

Related

Ball path not following semi-circle exactly

Bit stumped here.
First of all, here is what's going on:
As you can see, my ball is not following the curved quarter-circle path exactly, but vaguely.
Here is the code creating the quarter-circle (p.s. - my container view is 294 units tall and wide):
let startAngle = CGFloat(Double.pi * 2) // top of circle
let endAngle = startAngle + 2 * Double.pi * 0.25
view.layoutIfNeeded()
smallCircleView.parentVC = self
smallCircleView.layer.cornerRadius = 45/2
let circlePath = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: containerView.frame.self.width, startAngle: startAngle, endAngle: endAngle, clockwise: true)
And here is the code shifting the ball around:
func shiftSmallCircleView(newX : CGFloat){
smallCircleViewLeadingConstraint.constant = newX
let angle = (newX/containerView.frame.self.width)*90 + 180
let y = containerView.frame.size.width * cos((Double.pi * 2 * angle) / 360)
smallCircleViewBottomConstraint.constant = y + containerView.frame.origin.y
}
Since I'm using the cos function, should the ball's path be identical to the original quarter-circle path? How can they be similar but not identical?
Edit:
New outcome with updated code:
let angle = (distanceDelta/containerView.frame.self.width) * -90.0
containerView.transform = CGAffineTransform.init(rotationAngle: angle * Double.pi/180)
Most recent edit:
let angle = (distanceDelta/pathContainerView.frame.self.width) * .pi / -180.0
containerView.transform = CGAffineTransform.init(rotationAngle: angle)
All code:
class SmallCircleView : UIView {
var parentVC : ViewController!
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as? UITouch {
let point = touch.location(in: self)
}
super.touchesBegan(touches, with: event)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first as? UITouch {
let point = touch.location(in: self.superview)=
parentVC.shiftSmallCircleView(distanceDelta: point.x)=
}
}
}
class ViewController: UIViewController {
#IBOutlet var containerView : UIView!
#IBOutlet var pathContainerView : UIView!
#IBOutlet var smallCircleView : SmallCircleView!
#IBOutlet var smallCircleViewLeadingConstraint : NSLayoutConstraint!
#IBOutlet var smallCircleViewBottomConstraint : NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let startAngle = CGFloat(Double.pi * 2) // top of circle
let endAngle = startAngle + 2 * Double.pi * 0.25
view.layoutIfNeeded()
smallCircleView.parentVC = self
smallCircleView.layer.cornerRadius = 45/2
let circlePath = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: pathContainerView.frame.self.width, startAngle: startAngle, endAngle: endAngle, clockwise: true)
let shapeLayer = CAShapeLayer()
// The Bezier path that we made needs to be converted to
// a CGPath before it can be used on a layer.
shapeLayer.path = circlePath.cgPath
// apply other properties related to the path
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.fillColor = UIColor.white.cgColor
shapeLayer.lineWidth = 1.0
shapeLayer.position = CGPoint(x: 0, y: 0)
// add the new layer to our custom view
pathContainerView.layer.addSublayer(shapeLayer)
containerView.bringSubviewToFront(smallCircleView)
}
func shiftSmallCircleView(distanceDelta : CGFloat){
let degrees = min(1, (distanceDelta/pathContainerView.frame.size.width)) * -90
containerView.transform = CGAffineTransform.init(rotationAngle: degrees * M_PI/180)
}
}
I’m on my phone right now and so I can’t provide the code but there is a much much easier way to do this. Don’t bother trying to work out what coordinates the ball needs to be at. Just place the ball into a rectangular view with the centre of this view being at the centre of your circle and the ball being on the path. (Make the container view invisible).
Now… rotate the container view.
That’s it.
Because the ball is a child of the view it will be moved as part of the rotation. And the movement will follow a circle centred around the point of rotation. Which is the centre of the container view.
Example
I made a quick example to show what I mean. In essence... cheat. Don't actually do the hard maths to work out where the ball will be. Use methods to make it look the same in an easier way...
Here is my storyboard...
And the code...
class ViewController: UIViewController {
#IBOutlet weak var containerView: UIView!
#IBOutlet weak var circleView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
circleView.layer.cornerRadius = 20
}
#IBAction func sliderChanged(_ sender: UISlider) {
let rotationAngle = sender.value * .pi / 180
containerView.transform = CGAffineTransform(rotationAngle: CGFloat(rotationAngle))
}
}
And an animation...
And if you make the container background clear...
To answer your original question...
I haven't double-checked your math, but this is another method of positioning your "small circle" view.
Using these two "helper" extensions:
extension CGPoint {
static func pointOnCircle(center: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
let x = center.x + radius * cos(angle)
let y = center.y + radius * sin(angle)
return CGPoint(x: x, y: y)
}
}
extension CGFloat {
var degreesToRadians: Self { self * .pi / 180 }
var radiansToDegrees: Self { self * 180 / .pi }
}
We can find the point on the arc for a given angle like this:
let arcCenter: CGPoint = .zero
let radius: CGFloat = 250
let degree: CGFloat = 45
let p = CGPoint.pointOnCircle(center: arcCenter, radius: radius, angle: degree.degreesToRadians)
We can then move the "ball" to that point:
circleView.center = p
To get the circle view to "roll along the inside" of the arc, we use the same center point, but decrease the radius of the arc by the radius of the circle (half the width of the view).
If you want to use that approach (rather than rotating a view with the circle in the corner), here is some example code.
Start with our extensions, an enum, and a "small circle view":
extension CGPoint {
static func pointOnCircle(center: CGPoint, radius: CGFloat, angle: CGFloat) -> CGPoint {
let x = center.x + radius * cos(angle)
let y = center.y + radius * sin(angle)
return CGPoint(x: x, y: y)
}
}
extension CGFloat {
var degreesToRadians: Self { self * .pi / 180 }
var radiansToDegrees: Self { self * 180 / .pi }
}
enum FollowType: Int {
case inside, center, outside
}
class SmallCircleView : UIView {
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = bounds.size.height / 2.0
}
}
Next, a UIView subclass that will handle drawing the arc, adding the circle subview, and it will use a UIViewPropertyAnimator with key frames to make it interactive:
class FollowArcView: UIView {
public var circleColor: UIColor = .red {
didSet {
circleView.backgroundColor = circleColor
}
}
public var arcColor: UIColor = .blue { didSet { setNeedsLayout() } }
public var arcLineWidth: CGFloat = 1 { didSet { setNeedsLayout() } }
public var arcInset: CGFloat = 0 { didSet { setNeedsLayout() } }
public var circleRadius: CGFloat = 25 { didSet { setNeedsLayout() } }
public var followType: FollowType = .inside { didSet { setNeedsLayout() } }
public var fractionComplete: CGFloat = 0 {
didSet {
if animator != nil {
animator.fractionComplete = fractionComplete
}
}
}
private let circleView = SmallCircleView()
private let arcLayer = CAShapeLayer()
private var animator: UIViewPropertyAnimator!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
arcLayer.fillColor = UIColor.clear.cgColor
layer.addSublayer(arcLayer)
circleView.frame = CGRect(x: 0, y: 0, width: circleRadius * 2.0, height: circleRadius * 2.0)
circleView.backgroundColor = circleColor
addSubview(circleView)
}
override func layoutSubviews() {
super.layoutSubviews()
var curFraction: CGFloat = 0
// we may be changing properties (such as arc color, inset, etc)
// after we've moved the circleView, so
// save the current .fractionComplete
if animator != nil {
curFraction = animator.fractionComplete
animator.stopAnimation(true)
}
// these properties can be changed after initial view setup
arcLayer.lineWidth = arcLineWidth
arcLayer.strokeColor = arcColor.cgColor
circleView.frame.size = CGSize(width: circleRadius * 2.0, height: circleRadius * 2.0)
let arcCenter = CGPoint(x: arcInset, y: arcInset)
let arcRadius = bounds.width - (arcInset * 2.0)
let followRadius = followType == .inside ? arcRadius - circleRadius : followType == .center ? arcRadius : arcRadius + circleRadius
let pth = UIBezierPath(arcCenter: arcCenter, radius: arcRadius, startAngle: .pi * 0.5, endAngle: 0, clockwise: false)
arcLayer.path = pth.cgPath
let p = CGPoint.pointOnCircle(center: arcCenter, radius: followRadius, angle: CGFloat(90).degreesToRadians)
circleView.center = p
// the animator will take the current position of the circleView
// as Frame 0, so we need to let UIKit update the circleView's position
// before setting up the animator
DispatchQueue.main.async {
self.setupAnim()
self.animator.fractionComplete = curFraction
}
}
private func setupAnim() {
if animator != nil {
animator.stopAnimation(true)
}
// starting point
var startDegrees: CGFloat = 90
// ending point
let endDegrees: CGFloat = 0
// we'll be using percentages
let numSteps: CGFloat = 100
let arcCenter = CGPoint(x: arcInset, y: arcInset)
let arcRadius = bounds.width - (arcInset * 2.0)
let followRadius = followType == .inside ? arcRadius - circleRadius : followType == .center ? arcRadius : arcRadius + circleRadius
animator = UIViewPropertyAnimator(duration: 0.3, curve: .linear)
animator.addAnimations {
UIView.animateKeyframes(withDuration: 0.1, delay: 0.0, animations: {
let stepDegrees: Double = (startDegrees - endDegrees) / Double(numSteps)
for i in 1...Int(numSteps) {
// decrement degrees by step value
startDegrees -= stepDegrees
// get point on discPathRadius circle
let p = CGPoint.pointOnCircle(center: arcCenter, radius: followRadius, angle: startDegrees.degreesToRadians)
// duration is 1 divided by number of steps
let duration = 1.0 / Double(numSteps)
// start time for this frame is duration * this step
let startTime = duration * Double(i)
// add the keyframe
UIView.addKeyframe(withRelativeStartTime: startTime, relativeDuration: duration) {
self.circleView.center = p
}
}
})
}
// start and immediately pause the animation
animator.startAnimation()
animator.pauseAnimation()
}
}
and an example controller class:
class FollowArcVC: UIViewController {
let followArcView = FollowArcView()
override func viewDidLoad() {
super.viewDidLoad()
let slider = UISlider()
let typeControl = UISegmentedControl(items: ["Inside", "Center", "Outside"])
[followArcView, typeControl, slider].forEach { v in
v.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(v)
}
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
followArcView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
followArcView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
followArcView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
// 1:1 ratio (square)
followArcView.heightAnchor.constraint(equalTo: followArcView.widthAnchor),
typeControl.topAnchor.constraint(equalTo: g.topAnchor, constant: 40.0),
typeControl.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
typeControl.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
slider.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
slider.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
slider.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
typeControl.selectedSegmentIndex = 0
typeControl.addTarget(self, action: #selector(typeChanged(_:)), for: .valueChanged)
slider.addTarget(self, action: #selector(sliderChanged(_:)), for: .valueChanged)
// if we want to see the view frame
//followArcView.backgroundColor = .yellow
}
#objc func typeChanged(_ sender: UISegmentedControl) -> Void {
switch sender.selectedSegmentIndex {
case 0:
followArcView.followType = .inside
case 1:
followArcView.followType = .center
case 2:
followArcView.followType = .outside
default:
()
}
}
#objc func sliderChanged(_ sender: Any?) {
guard let sldr = sender as? UISlider else { return }
followArcView.fractionComplete = CGFloat(sldr.value)
}
}
The result:
Edit
After playing around a bit, this is another way to interactively follow the path -- it uses layer path animation, and avoids the need to manually calculate keyframe positions.
Works with the same sample view controller as above - just replace the FollowArcView class:
class FollowArcView: UIView {
public var circleColor: UIColor = .red {
didSet {
circleView.backgroundColor = circleColor
}
}
public var arcColor: UIColor = .blue {
didSet {
arcLayer.strokeColor = arcColor.cgColor
}
}
public var arcLineWidth: CGFloat = 1 {
didSet {
arcLayer.lineWidth = arcLineWidth
}
}
public var arcInset: CGFloat = 0 { didSet { setNeedsLayout() } }
public var circleRadius: CGFloat = 25 { didSet { setNeedsLayout() } }
public var followType: FollowType = .inside { didSet { setNeedsLayout() } }
public var fractionComplete: CGFloat = 0 {
didSet {
circleView.layer.timeOffset = CFTimeInterval(fractionComplete)
}
}
private let circleView = SmallCircleView()
private let arcLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() {
arcLayer.fillColor = UIColor.clear.cgColor
arcLayer.lineWidth = arcLineWidth
arcLayer.strokeColor = arcColor.cgColor
layer.addSublayer(arcLayer)
circleView.frame = CGRect(x: 0, y: 0, width: circleRadius * 2.0, height: circleRadius * 2.0)
circleView.backgroundColor = circleColor
addSubview(circleView)
}
override func layoutSubviews() {
super.layoutSubviews()
circleView.frame.size = CGSize(width: circleRadius * 2.0, height: circleRadius * 2.0)
let arcCenter = CGPoint(x: arcInset, y: arcInset)
let arcRadius = bounds.width - (arcInset * 2.0)
let pth = UIBezierPath(arcCenter: arcCenter, radius: arcRadius, startAngle: .pi * 0.5, endAngle: 0, clockwise: false)
arcLayer.path = pth.cgPath
self.setupAnim()
}
private func setupAnim() {
let arcCenter = CGPoint(x: arcInset, y: arcInset)
let arcRadius = bounds.width - (arcInset * 2.0)
let followRadius = followType == .inside ? arcRadius - circleRadius : followType == .center ? arcRadius : arcRadius + circleRadius
let pth = UIBezierPath(arcCenter: arcCenter, radius: followRadius, startAngle: .pi * 0.5, endAngle: 0, clockwise: false)
let animation = CAKeyframeAnimation(keyPath: #keyPath(CALayer.position))
animation.duration = 1
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
animation.path = pth.cgPath
circleView.layer.speed = 0
circleView.layer.timeOffset = 0
circleView.layer.add(animation, forKey: "PathAnim")
DispatchQueue.main.async {
self.circleView.layer.timeOffset = self.fractionComplete
}
}
}

Adding labels to a circle using CATextLayers

I have created a circle using a CAShapeLayer. Now I want to add text to the control but I am not quite sure on how to do so (so it looks good).
I have the following code:
import Foundation
import UIKit
class Gauge : UIView
{
private var shapeLayer = CAShapeLayer()
private var maskingLayer = CAShapeLayer()
private var gradientLayer = CAGradientLayer()
private var textLayers: [CATextLayer] = []
private var mValue: CGFloat = 0.0
private var mSegments = 9
private let textHeight: CGFloat = 24.0
// MARK: Properties
var lineWidth: CGFloat = 32.0
var min: CGFloat = 0.0
var max: CGFloat = 100.0
var segments: Int
{
get { return self.mSegments - 1 }
set
{
self.mSegments = newValue + 1
self.commonInit()
}
}
var progress: CGFloat
{
get
{
let diff = abs(self.min) + self.max
return self.value / diff
}
}
var segmentSize: CGFloat = 270.0
{
didSet
{
self.value = 0.0
self.commonInit()
}
}
var value: CGFloat
{
get { return self.mValue }
set
{
if self.mValue == newValue { return }
if newValue < 0.0
{
self.mValue = 0.0
}
else if newValue > self.max
{
self.mValue = self.max
}
else
{
self.mValue = newValue
}
self.maskingLayer.strokeStart = 0.0
self.maskingLayer.strokeEnd = 0.5
}
}
override init(frame: CGRect)
{
super.init(frame: frame)
self.commonInit()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
self.commonInit()
}
fileprivate func commonInit()
{
self.value = 50
self.determineLineWidth()
self.initLayers()
self.initDataLayers()
self.initTextLayers()
}
override func layoutSubviews()
{
super.layoutSubviews()
self.commonInit()
}
fileprivate func initTextLayers()
{
for textLayer in self.textLayers
{
textLayer.removeFromSuperlayer()
}
let fontSize: CGFloat = self.getFontSize()
for i in 0 ... self.segments
{
let orientation = CGFloat(i) * (1.0 / CGFloat(self.segments))
let span = self.max + abs(self.min)
let step = span / CGFloat(self.segments)
let value = CGFloat(i) * step
let font = UIFont.systemFont(ofSize: fontSize, weight: .bold)
let width = Utilities.measure(Int(value).description, .zero, font)
let point = self.getLabelPosition(orientation, width)
let layer = CATextLayer()
layer.contentsScale = UIScreen.main.scale
layer.font = font
layer.foregroundColor = UIColor.black.cgColor
layer.fontSize = fontSize
layer.string = Int(value).description
layer.alignmentMode = .center
layer.frame = CGRect(origin: point, size: .init(width: 48.0, height: self.textHeight))
self.textLayers.append(layer)
self.layer.addSublayer(layer)
}
}
fileprivate func gaugeFont() -> UIFont
{
let valueFontSize = self.getFontSize()
return UIFont.boldSystemFont(ofSize: valueFontSize)
}
fileprivate func getFontSize() -> CGFloat
{
if self.bounds.height < 128.0
{
return 10.0
}
else if self.bounds.height < 256.0
{
return 14.0
}
else
{
return 18.0
}
}
fileprivate func initDataLayers()
{
self.maskingLayer.removeFromSuperlayer()
let fillPath = self.createPath()
self.maskingLayer.frame = self.bounds
self.maskingLayer.path = fillPath.cgPath
self.maskingLayer.lineCap = .round
self.maskingLayer.fillColor = UIColor.clear.cgColor
self.maskingLayer.strokeColor = UIColor.black.cgColor
self.maskingLayer.lineWidth = self.lineWidth / 2.0
self.maskingLayer.position = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
self.layer.addSublayer(self.maskingLayer)
}
fileprivate func calculateAngle(_ value: CGFloat) -> CGFloat
{
let diff = abs(self.min) + self.max
return value / diff
}
fileprivate func getLabelPosition(_ progress: CGFloat, _ width: CGFloat) -> CGPoint
{
let size = Swift.min(self.bounds.width - self.lineWidth, self.bounds.height - self.lineWidth)
let center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
let alpha = (180.0 - self.segmentSize) / 2.0
let radius = size / 2.0 - self.lineWidth - width
let cx = center.x
let cy = center.y
let angle = self.segmentSize * progress
let x2 = self.deg2rad(180.0 + alpha + angle)
let outerX = cx + (radius + self.lineWidth / 2.0) * CGFloat(cos(x2))
let outerY = cy + (radius + self.lineWidth / 2.0) * CGFloat(sin(x2))
return CGPoint(x: outerX, y: outerY)
}
fileprivate func initLayers()
{
self.shapeLayer.removeFromSuperlayer()
let path = self.createPath()
self.shapeLayer = CAShapeLayer()
self.shapeLayer.frame = self.bounds
self.shapeLayer.path = path.cgPath
self.shapeLayer.strokeColor = UIColor.lightGray.cgColor
self.shapeLayer.fillColor = nil
self.shapeLayer.lineWidth = self.lineWidth / 2.0
self.shapeLayer.lineCap = .round
self.layer.addSublayer(self.shapeLayer)
}
fileprivate func createPath() -> UIBezierPath
{
let size = Swift.min(self.frame.width - self.lineWidth / 2, self.frame.height - self.lineWidth)
let center = CGPoint(x: self.frame.width / 2.0, y: self.frame.height / 2.0)
let alpha = (180.0 - self.segmentSize) / 2.0
let path = UIBezierPath(arcCenter: center, radius: size / 2.0, startAngle: self.deg2rad(180.0 + alpha), endAngle: self.deg2rad(360.0 - alpha), clockwise: true)
return path
}
fileprivate func determineLineWidth()
{
if self.bounds.height < 192.0
{
self.lineWidth = 20.0
}
else if self.bounds.height < 320
{
self.lineWidth = 32.0
}
else
{
self.lineWidth = 40.0
}
}
fileprivate func deg2rad(_ number: CGFloat) -> CGFloat
{
return number * .pi / 180
}
}
The result looks like this:
But I want the text to be positioned perfectly like this:
I tried adding various offsets manually, but when the control gets resized, it started to look bad again. Is there some kind of formula which I can use to calculate the exact position?
It looks like getLabelPosition returns a point that should be used as the centre of the text but you're passing it to the frame so it's used as the upper left point.
You need to offset the point by hals the size of the label to get the origin.
let size = CGSize(width: 48.0, height: self.textHeight)
var origin = point
origin.x -= size.width / 2
origin.y -= size.height / 2
layer.frame = CGRect(origin: origin, size: size)

How to curve a view edge to looks like an arc

I want to make an view to looks like this:
If there is a way, something like define point A, point B, with a predefined angle.
The only solution that I found is to make a giant rounded view and insert it as a subview of another view with clipToBounds = true. But this has some problems in multiple screen sizes because I'm using constraints.
Edit1: After some search, i'm trying to create that view with CAShapeLayer, without success. I'm creating that view by storyboard, with constraints, that view is connected by IBOutlet and your leading constraint too. Here's the code:
On viewDidLoad:
self.cnstRoundedLeading.constant = -(self.vwRounded.frame.width/3)
let maskPath : UIBezierPath = UIBezierPath(roundedRect: CGRect(x: self.vwRounded.bounds.minX*4,
y: self.vwRounded.bounds.minY*4,
width: self.vwRounded.bounds.width*4,
height: self.vwRounded.bounds.height*4),
byRoundingCorners: .topLeft,
cornerRadii: CGSize(width: self.vwRounded.frame.size.width*2,
height: self.vwRounded.frame.size.height))
let maskLayer : CAShapeLayer = CAShapeLayer()
maskLayer.frame = self.vwRounded.bounds
maskLayer.path = maskPath.cgPath
self.vwRounded.layer.mask = maskLayer
And on viewWillLayoutSubviews:
gradient2.colors = [startColorBlue.cgColor, endColorBlue.cgColor]
gradient2.locations = [0.0, 1.0]
gradient2.startPoint = CGPoint(x: 0, y: 1)
gradient2.endPoint = CGPoint(x: 1, y: 0)
vwRounded.applyGradient(gradient2)
applyGradient it's a extension of UIView:
func applyGradient(_ gradient: CAGradientLayer) -> Void {
gradient.frame = self.bounds
self.layer.insertSublayer(gradient, at: 0)
}
Does not work properly, i don't know the right way to construct that 'arc edge' effect
You can create that shape - and use it as a mask - with a UIBezierPath (the black border is showing the actual view frame):
Basically,
Find the midpoint of the line from pt1 to pt2.
Find the point perpendicular to that line, with a distance from the line that makes the curve look the way you want. Using the same length as half-the-line-length is what's shown here.
Create a UIBezierPath, connecting pt1 to pt2 with a quadratic curve.
Here is example code that you can run directly in a Playground page. I based the pt1 and pt2 y-positions based on the image you posted... If you change the frame of the view, it will maintain the proportions you've shown.
import PlaygroundSupport
import UIKit
class TestViewController: UIViewController {
override public var preferredContentSize: CGSize {
get { return CGSize(width: 800, height: 800) }
set { super.preferredContentSize = newValue }
}
let myPlainView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let myBorderView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
func customCurvedPath(for rect: CGRect) -> UIBezierPath {
// curve start point Y is 490/544ths of the height of the view
let curveStartPoint = CGPoint(x: 0.0, y: rect.size.height * 490.0 / 544.0)
// curve end point Y is 22/544ths of the height of the view
let curveEndPoint = CGPoint(x: rect.size.width, y: rect.size.height * 22.0 / 544.0)
var x1 = curveStartPoint.x
var y1 = curveStartPoint.y
let x2 = curveEndPoint.x
let y2 = curveEndPoint.y
// get the midpoint of the line from x1,y1 to x2,y2
x1 = (x1 + x2) / 2.0
y1 = (y1 + y2) / 2.0
// get the length of half the line (midpoint to endpoint)
var dx = x1 - x2
var dy = y1 - y2
let dist = sqrt(dx*dx + dy*dy)
// use length of helf the line for distance from line
// increase or decrease this value to get the desired curve
let distFromLine = dist
dx /= dist
dy /= dist
// get perpendicular point at distFromLine
let x3 = x1 - (distFromLine/2)*dy
let y3 = y1 + (distFromLine/2)*dx
let curveControlPoint = CGPoint(x: x3, y: y3)
let myBezier = UIBezierPath()
// pt1
myBezier.move(to: curveStartPoint)
// quad curve to pt2
myBezier.addQuadCurve(to: curveEndPoint, controlPoint: curveControlPoint)
// line to pt3 (bottom right corner)
myBezier.addLine(to: CGPoint(x: rect.width, y: rect.height))
// line to pt4 (bottom left corner)
myBezier.addLine(to: CGPoint(x: 0.0, y: rect.height))
// close the path (automatically add a line from bottom left corner to curve start point)
myBezier.close()
return myBezier
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
let vwWidth = CGFloat(710.0)
let vwHeight = CGFloat(544.0)
view.addSubview(myBorderView)
myBorderView.backgroundColor = .clear
NSLayoutConstraint.activate([
myBorderView.widthAnchor.constraint(equalToConstant: vwWidth + 2.0),
myBorderView.heightAnchor.constraint(equalToConstant: vwHeight + 2.0),
myBorderView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myBorderView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
myBorderView.layer.borderWidth = 2.0
// comment this next line (or set to false) to see the actual view frame
myBorderView.isHidden = true
view.addSubview(myPlainView)
myPlainView.backgroundColor = .red
NSLayoutConstraint.activate([
myPlainView.widthAnchor.constraint(equalToConstant: vwWidth),
myPlainView.heightAnchor.constraint(equalToConstant: vwHeight),
myPlainView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myPlainView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
let bezPath = customCurvedPath(for: CGRect(x: 0, y: 0, width: vwWidth, height: vwHeight))
// add the bezier path as a layer mask
let maskForPath = CAShapeLayer()
maskForPath.path = bezPath.cgPath
myPlainView.layer.mask = maskForPath
}
}
let viewController = TestViewController()
PlaygroundPage.current.liveView = viewController
As I mentioned, this would work better as part of a custom view class, as you could override layoutSubviews() to keep the path shape consistent.
Here's an example using a gradient layer + a layer mask in a custom view, set to 300 x 250:
And the Playground-runnable source:
import PlaygroundSupport
import UIKit
class MaskedGradientView: UIView {
var gradLayer: CAGradientLayer!
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
gradLayer = CAGradientLayer()
gradLayer.colors = [UIColor.blue.cgColor, UIColor.cyan.cgColor]
gradLayer.locations = [0.0, 1.0]
gradLayer.startPoint = CGPoint(x: 0, y: 1)
gradLayer.endPoint = CGPoint(x: 1, y: 0)
layer.addSublayer(gradLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
let rect = self.bounds
gradLayer.frame = self.bounds
// curve start point Y is 490/544ths of the height of the view
let curveStartPoint = CGPoint(x: 0.0, y: rect.size.height * 490.0 / 544.0)
// curve end point Y is 22/544ths of the height of the view
let curveEndPoint = CGPoint(x: rect.size.width, y: rect.size.height * 22.0 / 544.0)
var x1 = curveStartPoint.x
var y1 = curveStartPoint.y
let x2 = curveEndPoint.x
let y2 = curveEndPoint.y
// get the midpoint of the line from x1,y1 to x2,y2
x1 = (x1 + x2) / 2.0
y1 = (y1 + y2) / 2.0
// get the length of half the line (midpoint to endpoint)
var dx = x1 - x2
var dy = y1 - y2
let dist = sqrt(dx*dx + dy*dy)
// use length of helf the line for distance from line
// increase or decrease this value to get the desired curve
let distFromLine = dist
dx /= dist
dy /= dist
// get perpendicular point at distFromLine
let x3 = x1 - (distFromLine/2)*dy
let y3 = y1 + (distFromLine/2)*dx
let curveControlPoint = CGPoint(x: x3, y: y3)
let myBezier = UIBezierPath()
// pt1
myBezier.move(to: curveStartPoint)
// quad curve to pt2
myBezier.addQuadCurve(to: curveEndPoint, controlPoint: curveControlPoint)
// line to pt3 (bottom right corner)
myBezier.addLine(to: CGPoint(x: rect.width, y: rect.height))
// line to pt4 (bottom left corner)
myBezier.addLine(to: CGPoint(x: 0.0, y: rect.height))
// close the path (automatically add a line from bottom left corner to curve start point)
myBezier.close()
// add the bezier path as a layer mask
let maskForPath = CAShapeLayer()
maskForPath.path = myBezier.cgPath
layer.mask = maskForPath
}
}
class TestViewController: UIViewController {
override public var preferredContentSize: CGSize {
get { return CGSize(width: 400, height: 400) }
set { super.preferredContentSize = newValue }
}
let myMaskedGradientView: MaskedGradientView = {
let v = MaskedGradientView()
v.translatesAutoresizingMaskIntoConstraints = false
return v
}()
let myPlainView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .blue
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(myMaskedGradientView)
NSLayoutConstraint.activate([
myMaskedGradientView.widthAnchor.constraint(equalToConstant: 300.0),
myMaskedGradientView.heightAnchor.constraint(equalToConstant: 250.0),
myMaskedGradientView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
myMaskedGradientView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
}
let viewController = TestViewController()
PlaygroundPage.current.liveView = viewController

Don't understand how to fix Thread 1: ECX_BAD_ACCESS (code = EXC_I386_GPFLT) (line chart swift iOS)

I'm trying to make a line graph with no libraries, but I just cmd+c, cmd+v all the code. Yes, I know that I shouldn't do so, but I don't have much time
So I did everything with help of this - https://medium.com/#tstenerson/lets-make-a-line-chart-in-swift-3-5e819e6c1a00
Also added a view to the view controller and called it LineChart
But on line 42 I get an error Thread 1: ECX_BAD_ACCESS (code = EXC_I386_GPFLT)
lineChart.deltaX = 20
I don't know how to fix it
I coded only in ViewController.swift, here it is:
import UIKit
extension String {
func size(withSystemFontSize pointSize: CGFloat) -> CGSize {
return (self as NSString).size(attributes: [NSFontAttributeName: UIFont.systemFont(ofSize: pointSize)])
}
}
extension CGPoint {
func adding(x: CGFloat) -> CGPoint { return CGPoint(x: self.x + x, y: self.y) }
func adding(y: CGFloat) -> CGPoint { return CGPoint(x: self.x, y: self.y + y) }
}
class ViewController: UIViewController {
#IBOutlet var lineChart: LineChart!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let f: (CGFloat) -> CGPoint = {
let noiseY = (CGFloat(arc4random_uniform(2)) * 2 - 1) * CGFloat(arc4random_uniform(4))
let noiseX = (CGFloat(arc4random_uniform(2)) * 2 - 1) * CGFloat(arc4random_uniform(4))
let b: CGFloat = 5
let y = 2 * $0 + b + noiseY
return CGPoint(x: $0 + noiseX, y: y)
}
let xs = [Int](1..<20)
let points = xs.map({f(CGFloat($0 * 10))})
lineChart.deltaX = 20
lineChart.deltaY = 30
lineChart.plot(points)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
class LineChart: UIView {
let lineLayer = CAShapeLayer()
let circlesLayer = CAShapeLayer()
var chartTransform: CGAffineTransform?
#IBInspectable var lineColor: UIColor = UIColor.green {
didSet {
lineLayer.strokeColor = lineColor.cgColor
}
}
#IBInspectable var lineWidth: CGFloat = 1
#IBInspectable var showPoints: Bool = true { // show the circles on each data point
didSet {
circlesLayer.isHidden = !showPoints
}
}
#IBInspectable var circleColor: UIColor = UIColor.green {
didSet {
circlesLayer.fillColor = circleColor.cgColor
}
}
#IBInspectable var circleSizeMultiplier: CGFloat = 3
#IBInspectable var axisColor: UIColor = UIColor.white
#IBInspectable var showInnerLines: Bool = true
#IBInspectable var labelFontSize: CGFloat = 10
var axisLineWidth: CGFloat = 1
var deltaX: CGFloat = 10 // The change between each tick on the x axis
var deltaY: CGFloat = 10 // and y axis
var xMax: CGFloat = 100
var yMax: CGFloat = 100
var xMin: CGFloat = 0
var yMin: CGFloat = 0
var data: [CGPoint]?
override init(frame: CGRect) {
super.init(frame: frame)
combinedInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
combinedInit()
}
func combinedInit() {
layer.addSublayer(lineLayer)
lineLayer.fillColor = UIColor.clear.cgColor
lineLayer.strokeColor = lineColor.cgColor
layer.addSublayer(circlesLayer)
circlesLayer.fillColor = circleColor.cgColor
layer.borderWidth = 1
layer.borderColor = axisColor.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
lineLayer.frame = bounds
circlesLayer.frame = bounds
if let d = data{
setTransform(minX: xMin, maxX: xMax, minY: yMin, maxY: yMax)
plot(d)
}
}
func setAxisRange(forPoints points: [CGPoint]) {
guard !points.isEmpty else { return }
let xs = points.map() { $0.x }
let ys = points.map() { $0.y }
// МИНИМАЛЬНЫЕ И МАКСИМАЛЬНЫЕ ЗНАЧЕНИЯ
xMax = ceil(xs.max()! / deltaX) * deltaX
yMax = ceil(ys.max()! / deltaY) * deltaY
xMin = 0
yMin = 0
setTransform(minX: xMin, maxX: xMax, minY: yMin, maxY: yMax)
}
func setAxisRange(xMin: CGFloat, xMax: CGFloat, yMin: CGFloat, yMax: CGFloat) {
self.xMin = xMin
self.xMax = xMax
self.yMin = yMin
self.yMax = yMax
setTransform(minX: xMin, maxX: xMax, minY: yMin, maxY: yMax)
}
func setTransform(minX: CGFloat, maxX: CGFloat, minY: CGFloat, maxY: CGFloat) {
let xLabelSize = "\(Int(maxX))".size(withSystemFontSize: labelFontSize)
let yLabelSize = "\(Int(maxY))".size(withSystemFontSize: labelFontSize)
let xOffset = xLabelSize.height + 2
let yOffset = yLabelSize.width + 5
let xScale = (bounds.width - yOffset - xLabelSize.width/2 - 2)/(maxX - minX)
let yScale = (bounds.height - xOffset - yLabelSize.height/2 - 2)/(maxY - minY)
chartTransform = CGAffineTransform(a: xScale, b: 0, c: 0, d: -yScale, tx: yOffset, ty: bounds.height - xOffset)
setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
// draw rect comes with a drawing context, so lets grab it.
// Also, if there is not yet a chart transform, we will bail on performing any other drawing.
// I like guard statements for this because it's kind of like a bouncer to a bar.
// If you don't have your transform yet, you can't enter drawAxes.
guard let context = UIGraphicsGetCurrentContext(), let t = chartTransform else { return }
drawAxes(in: context, usingTransform: t)
}
func drawAxes(in context: CGContext, usingTransform t: CGAffineTransform) {
context.saveGState()
// Make two paths, one for thick lines, one for thin.
let thickerLines = CGMutablePath()
let thinnerLines = CGMutablePath()
// The two line chart axes.
let xAxisPoints = [CGPoint(x: xMin, y: 0), CGPoint(x: xMax, y: 0)]
let yAxisPoints = [CGPoint(x: 0, y: yMin), CGPoint(x: 0, y: yMax)]
// Add each to thicker lines but apply our transform too.
thickerLines.addLines(between: xAxisPoints, transform: t)
thickerLines.addLines(between: yAxisPoints, transform: t)
// Next we go from xMin to xMax by deltaX using stride
for x in stride(from: xMin, through: xMax, by: deltaX) {
// Tick points are the points for the ticks on each axis.
// We check showInnerLines first to see if we are drawing small ticks or full lines.
// Yip for new guys: `let a = someBool ? b : c` is called a ternary operator.
// In English it means "let a = b if somebool is true, or c if it is false."
let tickPoints = showInnerLines ?
[CGPoint(x: x, y: yMin).applying(t), CGPoint(x: x, y: yMax).applying(t)] :
[CGPoint(x: x, y: 0).applying(t), CGPoint(x: x, y: 0).applying(t).adding(y: -5)]
thinnerLines.addLines(between: tickPoints)
if x != xMin { // draw the tick label (it is too buy if you draw it at the origin for both x & y
let label = "\(Int(x))" as NSString // Int to get rid of the decimal, NSString to draw
let labelSize = "\(Int(x))".size(withSystemFontSize: labelFontSize)
let labelDrawPoint = CGPoint(x: x, y: 0).applying(t)
.adding(x: -labelSize.width/2)
.adding(y: 1)
label.draw(at: labelDrawPoint,
withAttributes:
[NSFontAttributeName: UIFont.systemFont(ofSize: labelFontSize),
NSForegroundColorAttributeName: axisColor])
}
}
// Repeat for y.
for y in stride(from: yMin, through: yMax, by: deltaY) {
let tickPoints = showInnerLines ?
[CGPoint(x: xMin, y: y).applying(t), CGPoint(x: xMax, y: y).applying(t)] :
[CGPoint(x: 0, y: y).applying(t), CGPoint(x: 0, y: y).applying(t).adding(x: 5)]
thinnerLines.addLines(between: tickPoints)
if y != yMin {
let label = "\(Int(y))" as NSString
let labelSize = "\(Int(y))".size(withSystemFontSize: labelFontSize)
let labelDrawPoint = CGPoint(x: 0, y: y).applying(t)
.adding(x: -labelSize.width - 1)
.adding(y: -labelSize.height/2)
label.draw(at: labelDrawPoint,
withAttributes:
[NSFontAttributeName: UIFont.systemFont(ofSize: labelFontSize),
NSForegroundColorAttributeName: axisColor])
}
}
// Finally set stroke color & line width then stroke thick lines, repeat for thin.
context.setStrokeColor(axisColor.cgColor)
context.setLineWidth(axisLineWidth)
context.addPath(thickerLines)
context.strokePath()
context.setStrokeColor(axisColor.withAlphaComponent(0.5).cgColor)
context.setLineWidth(axisLineWidth/2)
context.addPath(thinnerLines)
context.strokePath()
context.restoreGState()
// Whenever you change a graphics context you should save it prior and restore it after.
// If we were using a context other than draw(rect) we would have to also end the graphics context.
}
func plot(_ points: [CGPoint]) {
lineLayer.path = nil
circlesLayer.path = nil
data = nil
guard !points.isEmpty else { return }
self.data = points
if self.chartTransform == nil {
setAxisRange(forPoints: points)
}
let linePath = CGMutablePath()
linePath.addLines(between: points, transform: chartTransform!)
lineLayer.path = linePath
if showPoints {
circlesLayer.path = circles(atPoints: points, withTransform: chartTransform!)
}
}
func circles(atPoints points: [CGPoint], withTransform t: CGAffineTransform) -> CGPath {
let path = CGMutablePath()
let radius = lineLayer.lineWidth * circleSizeMultiplier/2
for i in points {
let p = i.applying(t)
let rect = CGRect(x: p.x - radius, y: p.y - radius, width: radius * 2, height: radius * 2)
path.addEllipse(in: rect)
}
return path
}
} // <- I didn't close the LineChart class up top, closing it now
}
In storyboard remove reference outlet link to 'lineChart' and try this:
import UIKit
extension String {
func size(withSystemFontSize pointSize: CGFloat) -> CGSize {
return (self as NSString).size(attributes: [NSFontAttributeName: UIFont.systemFont(ofSize: pointSize)])
}
}
extension CGPoint {
func adding(x: CGFloat) -> CGPoint { return CGPoint(x: self.x + x, y: self.y) }
func adding(y: CGFloat) -> CGPoint { return CGPoint(x: self.x, y: self.y + y) }
}
class ViewController: UIViewController {
// #IBOutlet var lineChart: LineChart! ////////////REMOVED THIS
var lineChart = LineChart(frame: CGRect.zero) ////////////ADDED THIS
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let f: (CGFloat) -> CGPoint = {
let noiseY = (CGFloat(arc4random_uniform(2)) * 2 - 1) * CGFloat(arc4random_uniform(4))
let noiseX = (CGFloat(arc4random_uniform(2)) * 2 - 1) * CGFloat(arc4random_uniform(4))
let b: CGFloat = 5
let y = 2 * $0 + b + noiseY
return CGPoint(x: $0 + noiseX, y: y)
}
let xs = [Int](1..<20)
let points = xs.map({f(CGFloat($0 * 10))})
////////////ADDED THIS
self.lineChart.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height)
self.view.addSubview(self.lineChart)
lineChart.deltaX = 20
lineChart.deltaY = 30
lineChart.plot(points)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
class LineChart: UIView {
let lineLayer = CAShapeLayer()
let circlesLayer = CAShapeLayer()
var chartTransform: CGAffineTransform?
#IBInspectable var lineColor: UIColor = UIColor.green {
didSet {
lineLayer.strokeColor = lineColor.cgColor
}
}
#IBInspectable var lineWidth: CGFloat = 1
#IBInspectable var showPoints: Bool = true { // show the circles on each data point
didSet {
circlesLayer.isHidden = !showPoints
}
}
#IBInspectable var circleColor: UIColor = UIColor.green {
didSet {
circlesLayer.fillColor = circleColor.cgColor
}
}
#IBInspectable var circleSizeMultiplier: CGFloat = 3
#IBInspectable var axisColor: UIColor = UIColor.white
#IBInspectable var showInnerLines: Bool = true
#IBInspectable var labelFontSize: CGFloat = 10
var axisLineWidth: CGFloat = 1
var deltaX: CGFloat = 10 // The change between each tick on the x axis
var deltaY: CGFloat = 10 // and y axis
var xMax: CGFloat = 100
var yMax: CGFloat = 100
var xMin: CGFloat = 0
var yMin: CGFloat = 0
var data: [CGPoint]?
override init(frame: CGRect) {
super.init(frame: frame)
combinedInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
combinedInit()
}
func combinedInit() {
layer.addSublayer(lineLayer)
lineLayer.fillColor = UIColor.clear.cgColor
lineLayer.strokeColor = lineColor.cgColor
layer.addSublayer(circlesLayer)
circlesLayer.fillColor = circleColor.cgColor
layer.borderWidth = 1
layer.borderColor = axisColor.cgColor
}
override func layoutSubviews() {
super.layoutSubviews()
lineLayer.frame = bounds
circlesLayer.frame = bounds
if let d = data{
setTransform(minX: xMin, maxX: xMax, minY: yMin, maxY: yMax)
plot(d)
}
}
func setAxisRange(forPoints points: [CGPoint]) {
guard !points.isEmpty else { return }
let xs = points.map() { $0.x }
let ys = points.map() { $0.y }
// МИНИМАЛЬНЫЕ И МАКСИМАЛЬНЫЕ ЗНАЧЕНИЯ
xMax = ceil(xs.max()! / deltaX) * deltaX
yMax = ceil(ys.max()! / deltaY) * deltaY
xMin = 0
yMin = 0
setTransform(minX: xMin, maxX: xMax, minY: yMin, maxY: yMax)
}
func setAxisRange(xMin: CGFloat, xMax: CGFloat, yMin: CGFloat, yMax: CGFloat) {
self.xMin = xMin
self.xMax = xMax
self.yMin = yMin
self.yMax = yMax
setTransform(minX: xMin, maxX: xMax, minY: yMin, maxY: yMax)
}
func setTransform(minX: CGFloat, maxX: CGFloat, minY: CGFloat, maxY: CGFloat) {
let xLabelSize = "\(Int(maxX))".size(withSystemFontSize: labelFontSize)
let yLabelSize = "\(Int(maxY))".size(withSystemFontSize: labelFontSize)
let xOffset = xLabelSize.height + 2
let yOffset = yLabelSize.width + 5
let xScale = (bounds.width - yOffset - xLabelSize.width/2 - 2)/(maxX - minX)
let yScale = (bounds.height - xOffset - yLabelSize.height/2 - 2)/(maxY - minY)
chartTransform = CGAffineTransform(a: xScale, b: 0, c: 0, d: -yScale, tx: yOffset, ty: bounds.height - xOffset)
setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
// draw rect comes with a drawing context, so lets grab it.
// Also, if there is not yet a chart transform, we will bail on performing any other drawing.
// I like guard statements for this because it's kind of like a bouncer to a bar.
// If you don't have your transform yet, you can't enter drawAxes.
guard let context = UIGraphicsGetCurrentContext(), let t = chartTransform else { return }
drawAxes(in: context, usingTransform: t)
}
func drawAxes(in context: CGContext, usingTransform t: CGAffineTransform) {
context.saveGState()
// make two paths, one for thick lines, one for thin
let thickerLines = CGMutablePath()
let thinnerLines = CGMutablePath()
// the two line chart axes
let xAxisPoints = [CGPoint(x: xMin, y: 0), CGPoint(x: xMax, y: 0)]
let yAxisPoints = [CGPoint(x: 0, y: yMin), CGPoint(x: 0, y: yMax)]
// add each to thicker lines but apply our transform too.
thickerLines.addLines(between: xAxisPoints, transform: t)
thickerLines.addLines(between: yAxisPoints, transform: t)
// next we go from xMin to xMax by deltaX using stride
for x in stride(from: xMin, through: xMax, by: deltaX) {
// tick points are the points for the ticks on each axis
// we check showInnerLines first to see if we are drawing small ticks or full lines
// tip for new guys: `let a = someBool ? b : c` is called a ternary operator
// in english it means "let a = b if somebool is true, or c if it is false."
let tickPoints = showInnerLines ?
[CGPoint(x: x, y: yMin).applying(t), CGPoint(x: x, y: yMax).applying(t)] :
[CGPoint(x: x, y: 0).applying(t), CGPoint(x: x, y: 0).applying(t).adding(y: -5)]
thinnerLines.addLines(between: tickPoints)
if x != xMin { // draw the tick label (it is too buy if you draw it at the origin for both x & y
let label = "\(Int(x))" as NSString // Int to get rid of the decimal, NSString to draw
let labelSize = "\(Int(x))".size(withSystemFontSize: labelFontSize)
let labelDrawPoint = CGPoint(x: x, y: 0).applying(t)
.adding(x: -labelSize.width/2)
.adding(y: 1)
label.draw(at: labelDrawPoint,
withAttributes:
[NSFontAttributeName: UIFont.systemFont(ofSize: labelFontSize),
NSForegroundColorAttributeName: axisColor])
}
}
// repeat for y
for y in stride(from: yMin, through: yMax, by: deltaY) {
let tickPoints = showInnerLines ?
[CGPoint(x: xMin, y: y).applying(t), CGPoint(x: xMax, y: y).applying(t)] :
[CGPoint(x: 0, y: y).applying(t), CGPoint(x: 0, y: y).applying(t).adding(x: 5)]
thinnerLines.addLines(between: tickPoints)
if y != yMin {
let label = "\(Int(y))" as NSString
let labelSize = "\(Int(y))".size(withSystemFontSize: labelFontSize)
let labelDrawPoint = CGPoint(x: 0, y: y).applying(t)
.adding(x: -labelSize.width - 1)
.adding(y: -labelSize.height/2)
label.draw(at: labelDrawPoint,
withAttributes:
[NSFontAttributeName: UIFont.systemFont(ofSize: labelFontSize),
NSForegroundColorAttributeName: axisColor])
}
}
// finally set stroke color & line width then stroke thick lines, repeat for thin
context.setStrokeColor(axisColor.cgColor)
context.setLineWidth(axisLineWidth)
context.addPath(thickerLines)
context.strokePath()
context.setStrokeColor(axisColor.withAlphaComponent(0.5).cgColor)
context.setLineWidth(axisLineWidth/2)
context.addPath(thinnerLines)
context.strokePath()
context.restoreGState()
// whenever you change a graphics context you should save it prior and restore it after
// if we were using a context other than draw(rect) we would have to also end the graphics context
}
func plot(_ points: [CGPoint]) {
lineLayer.path = nil
circlesLayer.path = nil
data = nil
guard !points.isEmpty else { return }
self.data = points
if self.chartTransform == nil {
setAxisRange(forPoints: points)
}
let linePath = CGMutablePath()
linePath.addLines(between: points, transform: chartTransform!)
lineLayer.path = linePath
if showPoints {
circlesLayer.path = circles(atPoints: points, withTransform: chartTransform!)
}
}
func circles(atPoints points: [CGPoint], withTransform t: CGAffineTransform) -> CGPath {
let path = CGMutablePath()
let radius = lineLayer.lineWidth * circleSizeMultiplier/2
for i in points {
let p = i.applying(t)
let rect = CGRect(x: p.x - radius, y: p.y - radius, width: radius * 2, height: radius * 2)
path.addEllipse(in: rect)
}
return path
}
} // <- I didn't close the LineChart class up top, closing it now
}

Draw resizable rectangle using swift 3

How to draw resizable rectangle in UIView , i did many search on google and github and i found this one Click Here using swift 2.3 and i converted it to swift 3 .. but i can't resize rectangle after drawing it and thats the code
//
// ResizableRectangleView.swift
// DrawShapes
//
// Created by Jordan Focht on 3/9/15.
// Copyright (c) 2015 Jordan Focht. All rights reserved.
//
import Foundation
import UIKit
private let DefaultTint = UIColor(red: 0, green: 164 / 255.0, blue: 1.0, alpha: 1.0).cgColor
private let DefaultStrokeTint = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0).cgColor
private let ClearColor = UIColor.clear.cgColor
private let DefaultCircleRadius: CGFloat = 8
private let CornerTouchSize: CGFloat = 44
protocol ResizableRectangleViewDelegate : class {
func didSelectResizableRectangleView(_ view: ResizableRectangleView)
func didDeselectResizableRectangleView(_ view: ResizableRectangleView)
}
class ResizableRectangleView: UIControl {
fileprivate var borderLayer: CALayer = CALayer()
fileprivate var topLeftCircle = CALayer()
fileprivate var topRightCircle = CALayer()
fileprivate var bottomLeftCircle = CALayer()
fileprivate var bottomRightCircle = CALayer()
weak var delegate: ResizableRectangleViewDelegate?
var strokeTintColor: CGColor = DefaultStrokeTint
var circleRadius: CGFloat = DefaultCircleRadius
var nLocation : CGPoint!
override var frame: CGRect {
get {
return super.frame
}
set {
super.frame = newValue
self.updateLayers()
}
}
override var isSelected: Bool {
get {
return super.isSelected
}
set {
let changed = self.isSelected != newValue
super.isSelected = newValue
if changed {
if isSelected {
self.delegate?.didSelectResizableRectangleView(self)
} else {
self.delegate?.didDeselectResizableRectangleView(self)
}
}
}
}
func updateLayers() {
if self.layer.sublayers == nil {
self.layer.addSublayer(self.borderLayer)
self.layer.addSublayer(self.topLeftCircle)
self.layer.addSublayer(self.topRightCircle)
self.layer.addSublayer(self.bottomLeftCircle)
self.layer.addSublayer(self.bottomRightCircle)
let layers = (self.layer.sublayers ?? []) as [CALayer]
for layer in layers {
layer.contentsScale = UIScreen.main.scale
}
}
self.updateBorderLayer()
let circleFrame = self.borderLayer.frame
updateCircleLayer(topLeftCircle, center: CGPoint(x: circleFrame.origin.x, y: circleFrame.origin.y))
updateCircleLayer(topRightCircle, center: CGPoint(x: circleFrame.origin.x, y: circleFrame.maxY))
updateCircleLayer(bottomLeftCircle, center: CGPoint(x: circleFrame.maxX, y: circleFrame.origin.y))
updateCircleLayer(bottomRightCircle, center: CGPoint(x: circleFrame.maxX, y: circleFrame.maxY))
}
func borderedFrame() -> CGRect {
return self.borderLayer.frame
}
// var trackingFrameTransform: ((CGPoint) -> ())?
func moveFrame(_ originalFrame: CGRect, initialTouchLocation: CGPoint, _ location: CGPoint) {
let targetX = originalFrame.origin.x + location.x - initialTouchLocation.x
let targetY = originalFrame.origin.y + location.y - initialTouchLocation.y
let insetBounds = self.insetBounds()
self.frame.origin.x = max(insetBounds.origin.x, min(insetBounds.maxX - self.frame.width, targetX))
self.frame.origin.y = max(insetBounds.origin.y, min(insetBounds.maxY - self.frame.height, targetY))
nLocation = location
}
fileprivate func insetBounds() -> CGRect {
let inset = self.inset()
let contentBounds = (self.superview as? DrawableView)?.contentBounds ?? self.bounds
return contentBounds.insetBy(dx: -inset, dy: -inset)
}
func updateRect(_ anchor: CGPoint, initialTouchLocation: CGPoint, originalCorner: CGPoint , _ location: CGPoint) {
let insetBounds = self.insetBounds()
let locationX = max(insetBounds.origin.x, min(insetBounds.maxX, location.x))
let locationY = max(insetBounds.origin.y, min(insetBounds.maxY, location.y))
let targetX = originalCorner.x + locationX - initialTouchLocation.x
let targetY = originalCorner.y + locationY - initialTouchLocation.y
let minSize = self.inset() + circleRadius
if insetBounds.origin.x < targetX && targetX < insetBounds.maxX {
self.frame.origin.x = min(targetX, anchor.x)
self.frame.size.width = max(minSize * 2, abs(anchor.x - targetX))
}
if insetBounds.origin.y < targetY && targetY < insetBounds.maxY {
self.frame.origin.y = min(targetY, anchor.y)
self.frame.size.height = max(minSize * 2, abs(anchor.y - targetY))
}
nLocation = location
}
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
CATransaction.begin()
CATransaction.setDisableActions(true)
if let superview = self.superview as? DrawableView {
for view in superview.subviews {
if let view = view as? ResizableRectangleView {
if view != self {
view.isSelected = false
view.updateLayers()
}
}
}
superview.bringSubview(toFront: self)
}
let location = touch.location(in: self.superview)
nLocation = location
var anchor: CGPoint?
var corner: CGPoint?
switch (location.x, location.y) {
case (let x, let y) where x < self.frame.origin.x + CornerTouchSize && y < self.frame.origin.y + CornerTouchSize:
anchor = CGPoint(x: self.frame.maxX, y: self.frame.maxY)
corner = CGPoint(x: self.frame.minX, y: self.frame.minY)
case (let x, let y) where x < self.frame.origin.x + CornerTouchSize && y > self.frame.maxY - CornerTouchSize:
anchor = CGPoint(x: self.frame.maxX, y: self.frame.minY)
corner = CGPoint(x: self.frame.minX, y: self.frame.maxY)
case (let x, let y) where x > self.frame.maxX - CornerTouchSize && y < self.frame.origin.y + CornerTouchSize:
anchor = CGPoint(x: self.frame.minX, y: self.frame.maxY)
corner = CGPoint(x: self.frame.maxX, y: self.frame.minY)
case (let x, let y) where x > self.frame.maxX - CornerTouchSize && y > self.frame.maxY - CornerTouchSize:
anchor = CGPoint(x: self.frame.minX, y: self.frame.minY)
corner = CGPoint(x: self.frame.maxX, y: self.frame.maxY)
default:
self.moveFrame(self.frame, initialTouchLocation: location , nLocation)
}
if let anchor = anchor {
if let corner = corner {
self.didMove = true
self.isSelected = true
self.updateRect(anchor, initialTouchLocation: location, originalCorner: corner, nLocation)
self.updateLayers()
}
}
CATransaction.commit()
return true
}
var didMove = false
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
CATransaction.begin()
CATransaction.setDisableActions(true)
didMove = true
let location = touch.location(in: self.superview)
nLocation = location
//self.trackingFrameTransform?(location)
self.updateLayers()
CATransaction.commit()
return true
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
CATransaction.begin()
CATransaction.setDisableActions(true)
if !didMove {
self.isSelected = !self.isSelected
}
didMove = false
self.updateLayers()
// self.trackingFrameTransform = nil
nLocation = nil
CATransaction.commit()
}
func updateCircleLayer(_ layer: CALayer, center: CGPoint) {
layer.isHidden = !self.isSelected
layer.frame = CGRect(x: center.x - circleRadius, y: center.y - circleRadius, width: 2 * circleRadius, height: 2 * circleRadius)
layer.backgroundColor = self.tintColor.cgColor
layer.borderColor = strokeTintColor
layer.cornerRadius = self.circleRadius
layer.borderWidth = 1
layer.setNeedsDisplay()
}
func inset() -> CGFloat {
let circleInset = (CornerTouchSize - (self.circleRadius * 2)) / 2
return self.circleRadius + circleInset
}
func updateBorderLayer() {
self.borderLayer.masksToBounds = false
self.borderLayer.borderWidth = 1
self.borderLayer.borderColor = self.tintColor.cgColor
let inset = self.inset()
self.borderLayer.frame = self.bounds.insetBy(dx: inset, dy: inset)
self.borderLayer.setNeedsDisplay()
}
}
DrawableView.swift
import Foundation
import UIKit
struct ColoredRect {
let color: UIColor
let origin: CGPoint
let size: CGSize
var width: CGFloat {
get {
return self.size.width
}
}
var height: CGFloat {
get {
return self.size.height
}
}
}
class DrawableView: UIControl {
fileprivate let colorPicker = ColorPicker()
fileprivate var currentRect: ResizableRectangleView?
fileprivate var originalLocation: CGPoint?
fileprivate var rectIsPending = false
var contentSize: CGSize?
var contentBounds: CGRect? {
get {
if let contentSize = self.contentSize {
let scale = min(self.bounds.width / contentSize.width, self.bounds.height / contentSize.height)
let scaledWidth = contentSize.width * scale
let scaledHeight = contentSize.height * scale
let x = round(0.5 * (self.bounds.width - scaledWidth))
let y = round(0.5 * (self.bounds.height - scaledHeight))
return CGRect(x: x, y: y, width: scaledWidth, height: scaledHeight)
} else {
return nil
}
}
}
var shapes: [ColoredRect] {
get {
var shapes = [ColoredRect]()
for view in self.subviews {
if let view = view as? ResizableRectangleView {
let f = view.convert(view.borderedFrame(), to: self)
let relX = min(1.0, max(0.0, f.origin.x / self.bounds.width))
let relY = min(1.0, max(0.0, f.origin.y / self.bounds.height))
let relWidth = min(1.0, max(0.0, f.width / self.bounds.width))
let relHeight = min(1.0, max(0.0, f.height / self.bounds.height))
let relOrigin = CGPoint(x: relX, y: relY)
let relSize = CGSize(width: relWidth, height: relHeight)
let rect = ColoredRect(color: view.tintColor, origin: relOrigin, size: relSize)
shapes.append(rect)
}
}
return shapes
}
set {
let shapes = newValue
for view in self.subviews {
if let view = view as? ResizableRectangleView {
view.removeFromSuperview()
}
}
self.colorPicker.alpha = 0
for shape in shapes {
let x = shape.origin.x * self.bounds.width
let y = shape.origin.y * self.bounds.height
let width = shape.width * self.bounds.width
let height = shape.height * self.bounds.height
let rectFrame = CGRect(x: x, y: y, width: width, height: height)
let view = ResizableRectangleView()
let inset = view.inset()
view.tintColor = shape.color
view.frame = rectFrame.insetBy(dx: -inset, dy: -inset)
view.delegate = self
self.addSubview(view)
}
self.bringSubview(toFront: self.colorPicker)
}
}
// override init() {
// super.init()
// self.addColorPicker()
// }
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.addColorPicker()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.addColorPicker()
}
override func awakeFromNib() {
super.awakeFromNib()
self.addColorPicker()
}
fileprivate func addColorPicker() {
colorPicker.delegate = self
colorPicker.alpha = 0
self.addSubview(colorPicker)
self.bringSubview(toFront: self.colorPicker)
colorPicker.frame = CGRect(x: self.bounds.width - 44, y: 0, width: 44, height: self.bounds.height)
}
override func layoutSubviews() {
super.layoutSubviews()
colorPicker.frame = CGRect(x: self.bounds.width - 44, y: 0, width: 44, height: self.bounds.height)
}
override var canBecomeFirstResponder : Bool {
return true
}
override func motionEnded(_ motion: UIEventSubtype, with event: UIEvent?) {
if (motion == UIEventSubtype.motionShake) {
self.shapes = []
}
}
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let location = touch.location(in: self)
if let contentBounds = self.contentBounds {
if (!contentBounds.contains(location)) {
return false
}
}
rectIsPending = true
let newRect = ResizableRectangleView()
newRect.frame = CGRect(x: location.x, y: location.y, width: 1, height: 1)
newRect.tintColor = UIColor(cgColor: self.colorPicker.color)
self.currentRect = newRect
self.originalLocation = location
CATransaction.begin()
CATransaction.setDisableActions(true)
for view in self.subviews {
if let view = view as? ResizableRectangleView {
view.isSelected = false
view.updateLayers()
}
}
CATransaction.commit()
return true
}
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
if let currentRect = self.currentRect {
if rectIsPending {
currentRect.delegate = self
self.addSubview(currentRect)
self.bringSubview(toFront: self.colorPicker)
}
CATransaction.begin()
CATransaction.setDisableActions(true)
if let originalLocation = self.originalLocation {
let location = touch.location(in: self)
currentRect.updateRect(originalLocation, initialTouchLocation: originalLocation, originalCorner: originalLocation, location)
// currentRect.updateRect(originalLocation, initialTouchLocation: originalLocation, originalCorner: originalLocation ,location: location)
}
CATransaction.commit()
}
return super.continueTracking(touch, with: event)
}
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
self.currentRect = nil
self.rectIsPending = false
}
}
extension DrawableView: ColorPickerDelegate {
func colorPicker(_ picker: ColorPicker, didChangeColor color: CGColor) {
CATransaction.begin()
CATransaction.setDisableActions(true)
for view in self.subviews {
if let view = view as? ResizableRectangleView {
if view.isSelected {
view.tintColor = UIColor(cgColor: color)
view.updateLayers()
}
}
}
CATransaction.commit()
}
}
extension DrawableView: ResizableRectangleViewDelegate {
func didSelectResizableRectangleView(_ view: ResizableRectangleView) {
self.bringSubview(toFront: self.colorPicker)
if self.colorPicker.alpha == 0 {
UIView.animate(withDuration: 0.15, animations: {
self.colorPicker.alpha = 1
})
}
}
func didDeselectResizableRectangleView(_ view: ResizableRectangleView) {
self.bringSubview(toFront: self.colorPicker)
if colorPicker.alpha == 1 {
let selectionCount = self.subviews.reduce(0) {
acc, view in
if let view = view as? ResizableRectangleView {
return acc + (view.isSelected ? 1 : 0)
}
return acc
}
if selectionCount == 0 {
UIView.animate(withDuration: 0.15, animations: {
self.colorPicker.alpha = 0
})
}
}
}
}

Resources