Draw resizable rectangle using swift 3 - ios

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

Related

How to make custom ripples like Square, Stare and other custom shapes in Swift 5?

I have facing issue to make ripples in Square and Stare figure like YRipple
Please help me and suggestion always welcome.
One easy way to achieve this is to use UIView animations. Each ripple is simply an instance of UIView. The shape can then be simply defined, drawn in one of many ways. I am using the override of draw rect method:
class RippleEffectView: UIView {
func addRipple(at location: CGPoint) {
let minRadius: CGFloat = 5.0
let maxRadius: CGFloat = 100.0
let startFrame = CGRect(x: location.x - minRadius, y: location.y - minRadius, width: minRadius*2.0, height: minRadius*2.0)
let endFrame = CGRect(x: location.x - maxRadius, y: location.y - maxRadius, width: maxRadius*2.0, height: maxRadius*2.0)
let view = ShapeView(frame: startFrame)
view.shape = .star(cornerCount: 5)
view.backgroundColor = .clear
view.contentMode = .redraw
view.strokeColor = .black
view.strokeWidth = 5.0
addSubview(view)
UIView.animate(withDuration: 1.0, delay: 0.0, options: [.allowUserInteraction]) {
view.frame = endFrame
view.alpha = 0.0
} completion: { _ in
view.removeFromSuperview()
}
}
}
private class ShapeView: UIView {
var fillColor: UIColor?
var strokeColor: UIColor?
var strokeWidth: CGFloat = 0.0
var shape: Shape = .rectangle
override func draw(_ rect: CGRect) {
super.draw(rect)
let path = generatePath()
path.lineWidth = strokeWidth
if let fillColor = fillColor {
fillColor.setFill()
path.fill()
}
if let strokeColor = strokeColor {
strokeColor.setStroke()
path.stroke()
}
}
private func generatePath() -> UIBezierPath {
switch shape {
case .rectangle: return UIBezierPath(rect: bounds.insetBy(dx: strokeWidth*0.5, dy: strokeWidth*0.5))
case .oval: return UIBezierPath(ovalIn: bounds.insetBy(dx: strokeWidth*0.5, dy: strokeWidth*0.5))
case .anglesOnCircle(let cornerCount):
guard cornerCount > 2 else { return .init() }
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width, bounds.height)*0.5 - strokeWidth*0.5
let path = UIBezierPath()
for index in 0..<cornerCount {
let angle = CGFloat(index)/CGFloat(cornerCount) * (.pi*2.0)
let point = CGPoint(x: center.x + cos(angle)*radius,
y: center.y + sin(angle)*radius)
if index == 0 {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
path.close()
return path
case .star(let cornerCount):
guard cornerCount > 2 else { return .init() }
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let outerRadius = min(bounds.width, bounds.height)*0.5 - strokeWidth*0.5
let innerRadius = outerRadius*0.7
let path = UIBezierPath()
for index in 0..<cornerCount*2 {
let angle = CGFloat(index)/CGFloat(cornerCount) * .pi
let radius = index.isMultiple(of: 2) ? outerRadius : innerRadius
let point = CGPoint(x: center.x + cos(angle)*radius,
y: center.y + sin(angle)*radius)
if index == 0 {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
path.close()
return path
}
}
}
private extension ShapeView {
enum Shape {
case rectangle
case oval
case anglesOnCircle(cornerCount: Int)
case star(cornerCount: Int)
}
}
I used it in a view controller where I replaced main view with this ripple view in Storyboard.
class ViewController: UIViewController {
private var rippleView: RippleEffectView? { view as? RippleEffectView }
override func viewDidLoad() {
super.viewDidLoad()
rippleView?.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
}
#objc private func onTap(_ recognizer: UIGestureRecognizer) {
let location = recognizer.location(in: rippleView)
rippleView?.addRipple(at: location)
}
}
I hope the code speaks for itself. It should be no problem to change colors. You could apply some rotation by using transform on each ripple view...
You could even use images instead of shapes. If image is set to be as templates you could even change colors using tint property on image view... So limitless possibilities.

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)

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
}

Getting the menu to start at the start of the array

So, I have this menu which is this project here but just update to the latest syntax of swift. the problem I have is that every time I load up the scene in which the menu is located in, it starts in the middle of the array instead of at the start of it like so.
What i'm trying to achieve is so when I load up the scene with the menu in it i want it to start at the start of array instead of the middle of the array the main functions that place and position the spritenodes in the scene are as follows:
func createPlayers() {
for _ in 0..<9 {
let player = SKSpriteNode(color: UIColor.blue , size: CGSize(width: 100, height: 200))
players.append(player)
}
}
func placePlayersOnPositions() {
for i in 0..<players.count/2 {
players[i].position = CGPoint(x: leftGuide, y: size.height/2)
}
players[players.count/2].position = CGPoint(x: size.width/2, y: size.height/2)
for i in players.count/2 + 1..<players.count {
players[i].position = CGPoint(x: rightGuide, y: size.height/2)
}
for player in players {
player.setScale(calculateScaleForX(x: player.position.x))
self.addChild(player)
}
}
How can I modify the above code so that it starts the menu at the start of the array instead of in the middle like this:
Here is the full menu code:
class GameScene: SKScene {
enum Zone {
case Left, Center, Right
}
var players = [SKSpriteNode]()
var leftPlayer: SKSpriteNode?
var centerPlayer: SKSpriteNode?
var rightPlayer: SKSpriteNode?
var leftGuide : CGFloat {
return round(view!.bounds.width / 6.0)
}
var rightGuide : CGFloat {
return view!.bounds.width - leftGuide
}
var gap : CGFloat {
return (size.width / 2 - leftGuide) / 2
}
// Initialization
override init(size: CGSize) {
super.init(size:size)
createPlayers()
centerPlayer = players[players.count/2]
setLeftAndRightPlayers()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func didMove(to view: SKView) {
placePlayersOnPositions()
calculateZIndexesForPlayers()
}
func createPlayers() {
for _ in 0..<9 {
let player = SKSpriteNode(color: UIColor.blue , size: CGSize(width: 100, height: 200))
players.append(player)
}
}
func placePlayersOnPositions() {
for i in 0..<players.count/2 {
players[i].position = CGPoint(x: leftGuide, y: size.height/2)
}
players[players.count/2].position = CGPoint(x: size.width/2, y: size.height/2)
for i in players.count/2 + 1..<players.count {
players[i].position = CGPoint(x: rightGuide, y: size.height/2)
}
for player in players {
player.setScale(calculateScaleForX(x: player.position.x))
self.addChild(player)
}
}
// Helper functions
func calculateScaleForX(x:CGFloat) -> CGFloat {
let minScale = CGFloat(0.5)
if x <= leftGuide || x >= rightGuide {
return minScale
}
if x < size.width/2 {
let a = 1.0 / (size.width - 2 * leftGuide)
let b = 0.5 - a * leftGuide
return (a * x + b)
}
let a = 1.0 / (frame.size.width - 2 * rightGuide)
let b = 0.5 - a * rightGuide
return (a * x + b)
}
func calculateZIndexesForPlayers() {
var playerCenterIndex : Int = players.count / 2
for i in 0..<players.count {
if centerPlayer == players[i] {
playerCenterIndex = i
}
}
for i in 0...playerCenterIndex {
players[i].zPosition = CGFloat(i)
}
for i in playerCenterIndex+1..<players.count {
players[i].zPosition = centerPlayer!.zPosition * 2 - CGFloat(i)
}
}
func movePlayerToX(player: SKSpriteNode, x: CGFloat, duration: TimeInterval) {
let moveAction = SKAction.moveTo(x: x, duration: duration)
let scaleAction = SKAction.scale(to: calculateScaleForX(x: x), duration: duration)
player.run(SKAction.group([moveAction, scaleAction]))
}
func movePlayerByX(player: SKSpriteNode, x: CGFloat) {
let duration = 0.01
if player.frame.midX <= rightGuide && player.frame.midX >= leftGuide {
player.run(SKAction.moveBy(x: x, y: 0, duration: duration), completion: {
player.setScale(self.calculateScaleForX(x: player.frame.midX))
})
if player.frame.midX < leftGuide {
player.position = CGPoint(x: leftGuide, y: player.position.y)
} else if player.frame.midX > rightGuide {
player.position = CGPoint(x: rightGuide, y: player.position.y)
}
}
}
func zoneOfCenterPlayer() -> Zone {
let gap = size.width / 2 - leftGuide
switch centerPlayer!.frame.midX {
case let x where x < leftGuide + gap/2:
return .Left
case let x where x > rightGuide - gap/2:
return .Right
default: return .Center
}
}
func setLeftAndRightPlayers() {
var playerCenterIndex : Int = players.count / 2
for i in 0..<players.count {
if centerPlayer == players[i] {
playerCenterIndex = i
}
}
if playerCenterIndex > 0 && playerCenterIndex < players.count {
leftPlayer = players[playerCenterIndex-1]
} else {
leftPlayer = nil
}
if playerCenterIndex > -1 && playerCenterIndex < players.count-1 {
rightPlayer = players[playerCenterIndex+1]
} else {
rightPlayer = nil
}
}
// Touch interactions
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch: UITouch = touches.first!
let node = self.atPoint(touch.location(in: self))
if node == centerPlayer {
let fadeOut = SKAction.fadeAlpha(to: 0.5, duration: 0.15)
let fadeIn = SKAction.fadeAlpha(to: 1, duration: 0.15)
centerPlayer!.run(fadeOut, completion: { self.centerPlayer!.run(fadeIn) })
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
let duration = 0.01
let touch: UITouch = touches.first!
let newPosition = touch.location(in: self)
let oldPosition = touch.previousLocation(in: self)
let xTranslation = newPosition.x - oldPosition.x
if centerPlayer!.frame.midX > size.width/2 {
if (leftPlayer != nil) {
let actualTranslation = leftPlayer!.frame.midX + xTranslation > leftGuide ? xTranslation : leftGuide - leftPlayer!.frame.midX
movePlayerByX(player: leftPlayer!, x: actualTranslation)
}
} else {
if (rightPlayer != nil) {
let actualTranslation = rightPlayer!.frame.midX + xTranslation < rightGuide ? xTranslation : rightGuide - rightPlayer!.frame.midX
movePlayerByX(player: rightPlayer!, x: actualTranslation)
}
}
movePlayerByX(player: centerPlayer!, x: xTranslation)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
//func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
let touch: UITouch = touches.first!
let duration = 0.25
switch zoneOfCenterPlayer() {
case .Left:
if (rightPlayer != nil) {
movePlayerToX(player: centerPlayer!, x: leftGuide, duration: duration)
if (leftPlayer != nil) {
movePlayerToX(player: leftPlayer!, x: leftGuide, duration: duration)
}
if (rightPlayer != nil) {
movePlayerToX(player: rightPlayer!, x: size.width/2, duration: duration)
}
centerPlayer = rightPlayer
setLeftAndRightPlayers()
} else {
movePlayerToX(player: centerPlayer!, x: size.width/2, duration: duration)
}
case .Right:
if (leftPlayer != nil) {
movePlayerToX(player: centerPlayer!, x: rightGuide, duration: duration)
if (rightPlayer != nil) {
movePlayerToX(player: rightPlayer!, x: rightGuide, duration: duration)
}
if (leftPlayer != nil) {
movePlayerToX(player: leftPlayer!, x: size.width/2, duration: duration)
}
centerPlayer = leftPlayer
setLeftAndRightPlayers()
} else {
movePlayerToX(player: centerPlayer!, x: size.width/2, duration: duration)
}
case .Center:
movePlayerToX(player: centerPlayer!, x: size.width/2, duration: duration)
if (leftPlayer != nil) {
movePlayerToX(player: leftPlayer!, x: leftGuide, duration: duration)
}
if (rightPlayer != nil) {
movePlayerToX(player: rightPlayer!, x: rightGuide, duration: duration)
}
}
calculateZIndexesForPlayers()
}
}
I had just a quick peek and did not try it, but this might work
func placePlayersOnPositions() {
players.first?.position = CGPoint(x: size.width/2, y: size.height/2)
if players.count > 1 {
for i in 1..<players.count {
players[i].position = CGPoint(x: rightGuide, y: size.height/2)
}
}
for player in players {
player.setScale(calculateScaleForX(x: player.position.x))
self.addChild(player)
}
}
For starters you will have to change your init function to not hard code the center player to count divided by 2 but rather to player zero.
from
centerPlayer = players[players.count/2]
to
centerPlayer = players[0]

Resources