Changing bounds of a child view centers it in its parent - ios

I have been following a tutorial explaining a custom ios control. There is no problem with the tutorial but I got really confused about the frame/bounds clipping (not part of the tutorial).
I have a UIView instance in the scene in the storyboard. This UIView is sized at 120 x 120. I am adding a custom control (extending UIControl) to this container view with addSubview. I began to experiment with setting different widths and height to the frame and bounds of the custom control, this is the initializer of the control:
public override init(frame: CGRect) {
super.init(frame: frame)
createSublayers()
}
...and produces this result (red is the parent, blue circle is the child):
Now I change the init to this:
public override init(frame: CGRect) {
super.init(frame: frame)
createSublayers()
self.frame.size.width = 40
self.frame.size.height = 40
println("frame: \(self.frame)")
println("bounds: \(self.bounds)")
self.clipsToBounds = true
}
And that produces this result:
and prints:
frame: (0.0,0.0,40.0,40.0)
bounds: (0.0,0.0,40.0,40.0)
But when I change the initliazer to this:
public override init(frame: CGRect) {
super.init(frame: frame)
createSublayers()
self.bounds.size.width = 40
self.bounds.size.height = 40
println("frame: \(self.frame)")
println("bounds: \(self.bounds)")
self.clipsToBounds = true
}
I get this:
and prints: frame: (40.0,40.0,40.0,40.0)
bounds: (0.0,0.0,40.0,40.0)
I cannot seem to comprehend why this view is centered in its parent view when I change its bounds. What exactly is causing it? Is the 'frame' clipped always towards its center? Or is this view centered in its parent after the bounds have been modified and that causes the update of the 'frame'? Is it some property that can be changed? How could I manage to put it to top-left corner, for example (exactly in the same way as when I modify the 'frame')? Thanks a lot!
EDIT:
class ViewController: UIViewController {
#IBOutlet var knobPlaceholder: UIView!
#IBOutlet var valueLabel: UILabel!
#IBOutlet var valueSlider: UISlider!
#IBOutlet var animateSwitch: UISwitch!
var knob: Knob!
override func viewDidLoad() {
super.viewDidLoad()
knob = Knob(frame: knobPlaceholder.bounds)
knobPlaceholder.addSubview(knob)
knobPlaceholder.backgroundColor = UIColor.redColor();
}
#IBAction func sliderValueChanged(slider: UISlider) {
}
#IBAction func randomButtonTouched(button: UIButton) {
}
}
And the knob:
public class Knob: UIControl {
private let knobRenderer = KnobRenderer()
private var backingValue: Float = 0.0
/** Contains the receiver’s current value. */
public var value: Float {
get {
return backingValue
}
set {
setValue(newValue, animated: false)
}
}
/** Sets the receiver’s current value, allowing you to animate the change visually. */
public func setValue(value: Float, animated: Bool) {
if value != backingValue {
backingValue = min(maximumValue, max(minimumValue, value))
}
}
/** Contains the minimum value of the receiver. */
public var minimumValue: Float = 0.0
/** Contains the maximum value of the receiver. */
public var maximumValue: Float = 1.0
public override init(frame: CGRect) {
super.init(frame: frame)
createSublayers()
self.bounds.size.width = 40
self.bounds.size.height = 40
println("frame: \(self.frame)")
println("bounds: \(self.bounds)")
self.clipsToBounds = true
}
public required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func createSublayers() {
knobRenderer.update(bounds)
knobRenderer.strokeColor = tintColor
knobRenderer.startAngle = -CGFloat(M_PI * 11.0 / 8.0);
knobRenderer.endAngle = CGFloat(M_PI * 3.0 / 8.0);
knobRenderer.pointerAngle = knobRenderer.startAngle;
knobRenderer.lineWidth = 2.0
knobRenderer.pointerLength = 6.0
layer.addSublayer(knobRenderer.trackLayer)
layer.addSublayer(knobRenderer.pointerLayer)
}
}
private class KnobRenderer {
let trackLayer = CAShapeLayer()
let pointerLayer = CAShapeLayer()
var strokeColor: UIColor {
get {
return UIColor(CGColor: trackLayer.strokeColor)
}
set(strokeColor) {
trackLayer.strokeColor = strokeColor.CGColor
pointerLayer.strokeColor = strokeColor.CGColor
}
}
var lineWidth: CGFloat = 1.0 {
didSet {
update();
}
}
var startAngle: CGFloat = 0.0 {
didSet {
update();
}
}
var endAngle: CGFloat = 0.0 {
didSet {
update()
}
}
var backingPointerAngle: CGFloat = 0.0
var pointerAngle: CGFloat {
get { return backingPointerAngle }
set { setPointerAngle(newValue, animated: false) }
}
func setPointerAngle(pointerAngle: CGFloat, animated: Bool) {
backingPointerAngle = pointerAngle
}
var pointerLength: CGFloat = 0.0 {
didSet {
update()
}
}
init() {
trackLayer.fillColor = UIColor.clearColor().CGColor
pointerLayer.fillColor = UIColor.clearColor().CGColor
}
func updateTrackLayerPath() {
let arcCenter = CGPoint(x: trackLayer.bounds.width / 2.0, y: trackLayer.bounds.height / 2.0)
let offset = max(pointerLength, trackLayer.lineWidth / 2.0)
let radius = min(trackLayer.bounds.height, trackLayer.bounds.width) / 2.0 - offset;
trackLayer.path = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true).CGPath
}
func updatePointerLayerPath() {
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: pointerLayer.bounds.width - pointerLength - pointerLayer.lineWidth / 2.0, y: pointerLayer.bounds.height / 2.0))
path.addLineToPoint(CGPoint(x: pointerLayer.bounds.width, y: pointerLayer.bounds.height / 2.0))
pointerLayer.path = path.CGPath
}
func update(bounds: CGRect) {
let position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)
trackLayer.bounds = bounds
trackLayer.position = position
pointerLayer.bounds = bounds
pointerLayer.position = position
update()
}
func update() {
trackLayer.lineWidth = lineWidth
pointerLayer.lineWidth = lineWidth
updateTrackLayerPath()
updatePointerLayerPath()
}
}

In your update function you are centering the view.
func update(bounds: CGRect) {
**let position = CGPoint(x: bounds.width / 2.0, y: bounds.height / 2.0)**
trackLayer.bounds = bounds
trackLayer.position = position
pointerLayer.bounds = bounds
pointerLayer.position = position
update()
}
If you take that out, then you're view won't be centered anymore. The reason setting the frame would leave it in the upper left hand corner while setting the bounds resulted in it being in the center is because setting the bounds x/y does not override the frame x/y. When you set the frame then later on your code only sets the bounds, so the frame x/y is never overwritten so the view stays in the upper left hand corner. However, in the second there is no x/y set for the frame, so I guess it's taken what you set for the bounds, so it get's centered.
I would recommend not setting the bounds x/y for the view as that should always be 0, 0. If you want to reposition it then use the frame. Remember the frame is relative the parent while the bounds is relative to it's self.

Related

Simple Pie Chart in Core Animation

I'm trying to include a simple pie chart in my app using Core Animation.
I found an article on-line to copy and adjust, which seems to be close to what I need.
https://github.com/tomnoda/piechart_ios
The code refers to Nib files (which I don't really understand), but can I do this programmatically instead? I think this is the line of code that needs to change, and maybe I need to add some other coding as well:-
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let view: UIView = Bundle.main.loadNibNamed("PieChartView", owner: self, options: nil)!.first as! UIView
addSubview(view)
The let line refers to the Nib file, but how can I get it to refer to my View Controller instead?
This obviously results in a series of unresolved identifier errors, as the 2 files aren't linked as they should be. On the View Controller I have the following, as well as a number of other outlets:-
#IBOutlet weak var pieChartView: PieChartView!
As I'm new to Xcode hopefully there is a simple fix to this problem.
"I'm trying to include a simple pie chart in my app using Core Animation"
First, remove the word simple from that statement. Not to sound like a jerk, but if you are a beginner and don't even understand elements laid-out in a nib (xib) vs creating elements via code, you will have a long road ahead of you.
While the example you linked to "works," it has a lot of limitations and takes some rather odd approaches to the task. For example:
it is limited to 5 or fewer segments
the sum of the segment values must equal 1.0
it has very little in the way of error checking
That said, it could be a good place for you to start learning.
Here is the same code, modified to NOT need the xib file. It can be used like this:
class ViewController: UIViewController {
#IBOutlet var pieChartView: MyPieChartView!
override func viewDidLoad() {
super.viewDidLoad()
pieChartView.slices = [
Slice(percent: 0.4, color: UIColor.red),
Slice(percent: 0.3, color: UIColor.blue),
Slice(percent: 0.2, color: UIColor.purple),
Slice(percent: 0.1, color: UIColor.green)
]
}
override func viewDidAppear(_ animated: Bool) {
pieChartView.animateChart()
}
}
This is MyPieChartView.swift ...
First changes from the original PieChartView.swift file are at the top, between the:
// MARK: Changes start here
// MARK: Changes end here
Additional changes to allow "anti-clockwise" ... look for instances of new Bool var drawClockwise
import UIKit
class MyPieChartView: UIView {
static let ANIMATION_DURATION: CGFloat = 1.4
// MARK: Changes start here
var canvasView: UIView!
var label1: UILabel!
var label2: UILabel!
var label3: UILabel!
var label4: UILabel!
var label5: UILabel!
var label1XConst: NSLayoutConstraint!
var label2XConst: NSLayoutConstraint!
var label3XConst: NSLayoutConstraint!
var label4XConst: NSLayoutConstraint!
var label5XConst: NSLayoutConstraint!
var label1YConst: NSLayoutConstraint!
var label2YConst: NSLayoutConstraint!
var label3YConst: NSLayoutConstraint!
var label4YConst: NSLayoutConstraint!
var label5YConst: NSLayoutConstraint!
var drawClockwise: Bool = true
var slices: [Slice]?
var sliceIndex: Int = 0
var currentPercent: CGFloat = 0.0
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
if canvasView == nil {
let container = UIView()
addSubview(container)
canvasView = UIView()
container.addSubview(canvasView)
canvasView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
canvasView.topAnchor.constraint(equalTo: container.topAnchor),
canvasView.leadingAnchor.constraint(equalTo: container.leadingAnchor),
canvasView.trailingAnchor.constraint(equalTo: container.trailingAnchor),
canvasView.bottomAnchor.constraint(equalTo: container.bottomAnchor),
])
canvasView.backgroundColor = .yellow
label1 = UILabel()
label2 = UILabel()
label3 = UILabel()
label4 = UILabel()
label5 = UILabel()
[label1, label2, label3, label4, label5].forEach {
guard let v = $0 else { fatalError("Bad Setup!") }
v.translatesAutoresizingMaskIntoConstraints = false
v.textColor = .white
v.textAlignment = .center
addSubview(v)
}
label1XConst = label1.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor)
label1YConst = label1.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor)
label2XConst = label2.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor)
label2YConst = label2.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor)
label3XConst = label3.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor)
label3YConst = label3.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor)
label4XConst = label4.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor)
label4YConst = label4.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor)
label5XConst = label5.centerXAnchor.constraint(equalTo: canvasView.centerXAnchor)
label5YConst = label5.centerYAnchor.constraint(equalTo: canvasView.centerYAnchor)
[label1XConst, label2XConst, label3XConst, label4XConst, label5XConst,
label1YConst, label2YConst, label3YConst, label4YConst, label5YConst].forEach {
$0?.isActive = true
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
subviews[0].frame = bounds
}
// don't do this
//override func draw(_ rect: CGRect) {
// subviews[0].frame = bounds
//}
// MARK: Changes end here
/// Get an animation duration for the passed slice.
/// If slice share is 40%, for example, it returns 40% of total animation duration.
///
/// - Parameter slice: Slice struct
/// - Returns: Animation duration
func getDuration(_ slice: Slice) -> CFTimeInterval {
return CFTimeInterval(slice.percent / 1.0 * PieChartView.ANIMATION_DURATION)
}
/// Convert slice percent to radian.
///
/// - Parameter percent: Slice percent (0.0 - 1.0).
/// - Returns: Radian
func percentToRadian(_ percent: CGFloat) -> CGFloat {
//Because angle starts wtih X positive axis, add 270 degrees to rotate it to Y positive axis.
var angle = 270 + percent * 360
if angle >= 360 {
angle -= 360
}
return angle * CGFloat.pi / 180.0
}
/// Add a slice CAShapeLayer to the canvas.
///
/// - Parameter slice: Slice to be drawn.
func addSlice(_ slice: Slice) {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1
animation.duration = getDuration(slice)
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
animation.delegate = self
let canvasWidth = canvasView.frame.width
let toPercent = currentPercent + (drawClockwise ? slice.percent : -slice.percent)
let path = UIBezierPath(arcCenter: canvasView.center,
radius: canvasWidth * 3 / 8,
startAngle: percentToRadian(currentPercent),
endAngle: percentToRadian(toPercent),
clockwise: drawClockwise)
let sliceLayer = CAShapeLayer()
sliceLayer.path = path.cgPath
sliceLayer.fillColor = nil
sliceLayer.strokeColor = slice.color.cgColor
sliceLayer.lineWidth = canvasWidth * 2 / 8
sliceLayer.strokeEnd = 1
sliceLayer.add(animation, forKey: animation.keyPath)
canvasView.layer.addSublayer(sliceLayer)
}
/// Get label's center position based on from and to percentages.
/// This is always relative to canvasView's center.
///
/// - Parameters:
/// - fromPercent: End of previous slice.
/// - toPercent: End of current slice.
/// - Returns: Center point for label.
func getLabelCenter(_ fromPercent: CGFloat, _ toPercent: CGFloat) -> CGPoint {
let radius = canvasView.frame.width * 3 / 8
let labelAngle = percentToRadian((toPercent - fromPercent) / 2 + fromPercent)
let path = UIBezierPath(arcCenter: canvasView.center,
radius: radius,
startAngle: labelAngle,
endAngle: labelAngle,
clockwise: drawClockwise)
path.close()
return path.currentPoint
}
/// Re-position and draw label such as "43%".
///
/// - Parameter slice: Slice whose label is drawn.
func addLabel(_ slice: Slice) {
let center = canvasView.center
let labelCenter = getLabelCenter(currentPercent, currentPercent + (drawClockwise ? slice.percent : -slice.percent))
let xConst = [label1XConst, label2XConst, label3XConst, label4XConst, label5XConst][sliceIndex]
let yConst = [label1YConst, label2YConst, label3YConst, label4YConst, label5YConst][sliceIndex]
xConst?.constant = labelCenter.x - center.x
yConst?.constant = labelCenter.y - center.y
let label = [label1, label2, label3, label4, label5][sliceIndex]
label?.isHidden = true
label?.text = String(format: "%d%%", Int(slice.percent * 100))
}
/// Call this to start pie chart animation.
func animateChart() {
sliceIndex = 0
currentPercent = 0.0
canvasView.layer.sublayers = nil
if slices != nil && slices!.count > 0 {
let firstSlice = slices![0]
addLabel(firstSlice)
addSlice(firstSlice)
}
}
}
extension MyPieChartView: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if flag {
currentPercent += (drawClockwise ? slices![sliceIndex].percent : -slices![sliceIndex].percent)
sliceIndex += 1
if sliceIndex < slices!.count {
let nextSlice = slices![sliceIndex]
addLabel(nextSlice)
addSlice(nextSlice)
} else {
//After animation is done, display all labels. Can be animated.
for label in [label1, label2, label3, label4, label5] {
label?.isHidden = false
}
}
}
}
}
Example:
class ViewController: UIViewController {
#IBOutlet var pieChartView: MyPieChartView!
#IBOutlet var antiPieChartView: MyPieChartView!
override func viewDidLoad() {
super.viewDidLoad()
pieChartView.slices = [
Slice(percent: 0.4, color: UIColor.red),
Slice(percent: 0.3, color: UIColor.blue),
Slice(percent: 0.2, color: UIColor.purple),
Slice(percent: 0.1, color: UIColor(red: 0.0, green: 0.75, blue: 0.0, alpha: 1.0))
]
antiPieChartView.slices = [
Slice(percent: 0.4, color: UIColor.red),
Slice(percent: 0.3, color: UIColor.blue),
Slice(percent: 0.2, color: UIColor.purple),
Slice(percent: 0.1, color: UIColor(red: 0.0, green: 0.75, blue: 0.0, alpha: 1.0))
]
// draw this pie anti-clockwise
antiPieChartView.drawClockwise = false
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
pieChartView.animateChart()
antiPieChartView.animateChart()
}
}

Animating a AVPlayerLayer using UIPropertyAnimator [duplicate]

I have implemented a custom view with adding CALayer as sublayer for UIView. When I animate the view with the following:UIView.animateWithDuration(2.0) { self.slider.bounds.size *= 2.0}, the scaling animation is kind of wrong. The CALayer start at the wrong position with scaled size and move to the final position instead of scaling with the view.
The CustomeView Code :
import UIKit
class GridMaskView: UIView {
private let cornerLayer: CAShapeLayer
private let borderLayer: CAShapeLayer
private let gridLayer: CAShapeLayer
private let gridSize: (horizontal: UInt, vertical: UInt) = (3, 3)
private let cornerThickness: CGFloat = 3.0
private let cornerLength: CGFloat = 20.0
private let borderThickness: CGFloat = 2.0
private let gridThickness: CGFloat = 1.0
private let lineColor: UIColor = UIColor(r: 120, g: 179, b: 193, a: 1)
var showGridLines: Bool = true {
didSet {
gridLayer.hidden = !showGridLines
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
cornerLayer = CAShapeLayer()
cornerLayer.fillColor = lineColor.CGColor
borderLayer = CAShapeLayer()
borderLayer.fillColor = UIColor.clearColor().CGColor
borderLayer.strokeColor = lineColor.CGColor
borderLayer.lineWidth = borderThickness
gridLayer = CAShapeLayer()
gridLayer.strokeColor = lineColor.CGColor
gridLayer.lineWidth = gridThickness
super.init(frame: frame)
layer.addSublayer(cornerLayer)
layer.addSublayer(borderLayer)
layer.addSublayer(gridLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
layoutLayers()
}
private func layoutLayers() {
drawCorner()
drawBorder()
drawGrid()
}
private func drawCorner() {
cornerLayer.frame = bounds.insetBy(dx: -cornerThickness, dy: -cornerThickness)
cornerLayer.path = cornerPath(forBounds: cornerLayer.bounds)
}
private func cornerPath(forBounds bounds: CGRect) -> CGPathRef {
let horizontalSize = CGSize(width: cornerLength, height: cornerThickness)
let verticalSize = CGSize(width: cornerThickness, height: cornerLength)
let corners: [(CGRectEdge, CGRectEdge)] = [(.MinXEdge, .MinYEdge), (.MinXEdge, .MaxYEdge), (.MaxXEdge, .MinYEdge), (.MaxXEdge, .MaxYEdge)]
var cornerRects = [CGRect]()
for corner in corners {
cornerRects.append(bounds.align(horizontalSize, corner: corner.0, corner.1))
cornerRects.append(bounds.align(verticalSize, corner: corner.0, corner.1))
}
let cornerPath = CGPathCreateMutable()
CGPathAddRects(cornerPath, nil, cornerRects, cornerRects.count)
return cornerPath
}
private func drawBorder() {
borderLayer.frame = bounds
borderLayer.path = borderPath(forBounds: borderLayer.bounds)
}
private func borderPath(forBounds bounds: CGRect) -> CGPathRef {
let borderPath = CGPathCreateMutable()
let borderCornerPoints = [bounds.topLeft, bounds.topRight, bounds.bottomRight, bounds.bottomLeft, bounds.topLeft]
CGPathAddLines(borderPath, nil, borderCornerPoints, borderCornerPoints.count)
return borderPath
}
private func drawGrid() {
gridLayer.frame = bounds
gridLayer.path = gridPath(forBounds: gridLayer.bounds)
}
private func gridPath(forBounds bounds: CGRect) -> CGPathRef {
let stepSize = bounds.size / (CGFloat(gridSize.horizontal), CGFloat(gridSize.vertical))
let gridPath = CGPathCreateMutable()
for i in (1...gridSize.vertical) {
let x = CGFloat(i) * stepSize.width
CGPathMoveToPoint(gridPath, nil, x, 0)
CGPathAddLineToPoint(gridPath, nil, x, bounds.size.height)
}
for i in (1...gridSize.horizontal) {
let y = CGFloat(i) * stepSize.height
CGPathMoveToPoint(gridPath, nil, 0, y)
CGPathAddLineToPoint(gridPath, nil, bounds.size.width, y)
}
return gridPath
}
override func intrinsicContentSize() -> CGSize {
return CGSize(width: cornerLength * 2, height: cornerLength * 2)
}
}
Anyone know how to fit this?
The problem is that when you do view animation you don't get any automatic animation of sublayers. You'd be better off using a subview of your original UIView, because view animation will animate that together with the original view, according to its autolayout constraints.

Swift callback function for draw()?

I have a customView where I overwrite drawRect:. I also have some cases, where I want to display a UILabel ontop of my customView:
if self.ticks.count < 50 {
self.label.frame = self.bounds
self.label.text = "This chart requires 50 or more ticks.\nKeep Ticking."
self.addSubview(label)
}
However if I call this code before the drawRect is called, then my label doesn't show up. Is there a callback method like viewDidDraw or something?
EDIT
Someone asked for it, so here is all the code that is now doing what I need it to do. I think the answer to the question is to override layoutSubviews.
#IBDesignable open class CustomView: UIView {
#objc open var ticks:[Tick] = []
let label = UILabel()
required public init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
required public override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
func setup(){
self.backgroundColor = UIColor.clear
self.label.frame = self.bounds
self.label.text = "This chart requires 50 or more ticks.\nKeep Ticking."
self.label.numberOfLines = 0
self.label.textAlignment = .center
self.addSubview(label)
}
override open func layoutSubviews() {
self.label.frame = self.bounds
}
#objc open func setSessions(sessions:[Session]){
self.ticks = []
for session in sessions.reversed(){
if self.ticks.count < 250{
self.ticks = self.ticks + (session.getShots() as! [Shot])
}else{
break
}
}
if self.ticks.count < 50 {
self.label.isHidden = false
}else{
self.label.isHidden = true
}
self.bringSubview(toFront: self.label)
}
override open func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
let width = rect.width - 10;
let height = rect.height - 10;
let center = CGPoint(x: width/2+5, y: height/2+5)
let big_r = min(width, height)/2
context.setLineWidth(1.5)
UIColor.white.setStroke()
//draw the rings
for i in stride(from: 1, to: 7, by: 1){
let r = big_r * CGFloat(i) / 6.0;
context.addArc(center:center, radius:r, startAngle: 0, endAngle: CGFloat(2*Double.pi), clockwise: false)
context.strokePath()
}
//draw the ticks
if self.ticks.count > 49 {
UIColor.red().setFill()
for (i, tick) in self.ticks.prefix(250).enumerated(){
let radius = min((100.0-CGFloat(shot.score))/30.0 * big_r, big_r);
let x = Utilities.calculateX(tick, radius)
let y = Utilities.calculateY(tick, radius)
let point = CGPoint(x: x+center.x,y: y+center.y)
context.addArc(center:point, radius:CGFloat(DOT_WIDTH/2), startAngle: 0, endAngle: CGFloat(2*Double.pi), clockwise: false)
context.fillPath()
}
}
}
}
You can make your custom view redraw by calling setNeedsDisplay() when needed. After that, you can add the label
You can set a delegation for the View class and call the delegate method at the end of the draw function. However, it seems that if you call that directly, the draw function will only end until the delegate function get processed. Which means that the code will be executed before the new drawing refresh on the screen. In that case, you may need to call it asynchronously.
// at end of draw function
DispatchQueue.main.async {
self.delegate?.drawEnd()
}

Custom view with CALayer wired effect when animating bounds change

I have implemented a custom view with adding CALayer as sublayer for UIView. When I animate the view with the following:UIView.animateWithDuration(2.0) { self.slider.bounds.size *= 2.0}, the scaling animation is kind of wrong. The CALayer start at the wrong position with scaled size and move to the final position instead of scaling with the view.
The CustomeView Code :
import UIKit
class GridMaskView: UIView {
private let cornerLayer: CAShapeLayer
private let borderLayer: CAShapeLayer
private let gridLayer: CAShapeLayer
private let gridSize: (horizontal: UInt, vertical: UInt) = (3, 3)
private let cornerThickness: CGFloat = 3.0
private let cornerLength: CGFloat = 20.0
private let borderThickness: CGFloat = 2.0
private let gridThickness: CGFloat = 1.0
private let lineColor: UIColor = UIColor(r: 120, g: 179, b: 193, a: 1)
var showGridLines: Bool = true {
didSet {
gridLayer.hidden = !showGridLines
}
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
cornerLayer = CAShapeLayer()
cornerLayer.fillColor = lineColor.CGColor
borderLayer = CAShapeLayer()
borderLayer.fillColor = UIColor.clearColor().CGColor
borderLayer.strokeColor = lineColor.CGColor
borderLayer.lineWidth = borderThickness
gridLayer = CAShapeLayer()
gridLayer.strokeColor = lineColor.CGColor
gridLayer.lineWidth = gridThickness
super.init(frame: frame)
layer.addSublayer(cornerLayer)
layer.addSublayer(borderLayer)
layer.addSublayer(gridLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
layoutLayers()
}
private func layoutLayers() {
drawCorner()
drawBorder()
drawGrid()
}
private func drawCorner() {
cornerLayer.frame = bounds.insetBy(dx: -cornerThickness, dy: -cornerThickness)
cornerLayer.path = cornerPath(forBounds: cornerLayer.bounds)
}
private func cornerPath(forBounds bounds: CGRect) -> CGPathRef {
let horizontalSize = CGSize(width: cornerLength, height: cornerThickness)
let verticalSize = CGSize(width: cornerThickness, height: cornerLength)
let corners: [(CGRectEdge, CGRectEdge)] = [(.MinXEdge, .MinYEdge), (.MinXEdge, .MaxYEdge), (.MaxXEdge, .MinYEdge), (.MaxXEdge, .MaxYEdge)]
var cornerRects = [CGRect]()
for corner in corners {
cornerRects.append(bounds.align(horizontalSize, corner: corner.0, corner.1))
cornerRects.append(bounds.align(verticalSize, corner: corner.0, corner.1))
}
let cornerPath = CGPathCreateMutable()
CGPathAddRects(cornerPath, nil, cornerRects, cornerRects.count)
return cornerPath
}
private func drawBorder() {
borderLayer.frame = bounds
borderLayer.path = borderPath(forBounds: borderLayer.bounds)
}
private func borderPath(forBounds bounds: CGRect) -> CGPathRef {
let borderPath = CGPathCreateMutable()
let borderCornerPoints = [bounds.topLeft, bounds.topRight, bounds.bottomRight, bounds.bottomLeft, bounds.topLeft]
CGPathAddLines(borderPath, nil, borderCornerPoints, borderCornerPoints.count)
return borderPath
}
private func drawGrid() {
gridLayer.frame = bounds
gridLayer.path = gridPath(forBounds: gridLayer.bounds)
}
private func gridPath(forBounds bounds: CGRect) -> CGPathRef {
let stepSize = bounds.size / (CGFloat(gridSize.horizontal), CGFloat(gridSize.vertical))
let gridPath = CGPathCreateMutable()
for i in (1...gridSize.vertical) {
let x = CGFloat(i) * stepSize.width
CGPathMoveToPoint(gridPath, nil, x, 0)
CGPathAddLineToPoint(gridPath, nil, x, bounds.size.height)
}
for i in (1...gridSize.horizontal) {
let y = CGFloat(i) * stepSize.height
CGPathMoveToPoint(gridPath, nil, 0, y)
CGPathAddLineToPoint(gridPath, nil, bounds.size.width, y)
}
return gridPath
}
override func intrinsicContentSize() -> CGSize {
return CGSize(width: cornerLength * 2, height: cornerLength * 2)
}
}
Anyone know how to fit this?
The problem is that when you do view animation you don't get any automatic animation of sublayers. You'd be better off using a subview of your original UIView, because view animation will animate that together with the original view, according to its autolayout constraints.

Pie Chart slices in swift

I'm trying to make a pie chart. Actually it's done, but I would like to get some values, and each value should be a slice of the pie. The only thing I could do is fill the pie with a slider. How can I make different slices with different colors for some values?
Here is my code for drawing the chart (I got here in stack) :
import UIKit
#IBDesignable class ChartView: UIView {
#IBInspectable var progress : Double = 0.0 {
didSet {
self.setNeedsDisplay()
}
}
#IBInspectable var noProgress : Double = 0.0 {
didSet {
self.setNeedsDisplay()
}
}
required init(coder aDecoder: NSCoder) {
super.init(coder:aDecoder)
self.contentMode = .Redraw
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clearColor()
self.contentMode = .Redraw
}
override func drawRect(rect: CGRect) {
let color = UIColor.blueColor().CGColor
let lineWidth : CGFloat = 2.0
// Calculate box with insets
let margin: CGFloat = lineWidth
let box0 = CGRectInset(self.bounds, margin, margin)
let side : CGFloat = min(box0.width, box0.height)
let box = CGRectMake((self.bounds.width-side)/2, (self.bounds.height-side)/2,side,side)
let ctx = UIGraphicsGetCurrentContext()
// Draw outline
CGContextBeginPath(ctx)
CGContextSetStrokeColorWithColor(ctx, UIColor.blackColor().CGColor)
CGContextSetLineWidth(ctx, lineWidth)
CGContextAddEllipseInRect(ctx, box)
CGContextClosePath(ctx)
CGContextStrokePath(ctx)
// Draw arc
let delta : CGFloat = -CGFloat(M_PI_2)
let radius : CGFloat = min(box.width, box.height)/2.0
func prog_to_rad(p: Double) -> CGFloat {
let rad = CGFloat((p * M_PI)/180)
return rad
}
func draw_arc(s: CGFloat, e: CGFloat, color: CGColor) {
CGContextBeginPath(ctx)
CGContextMoveToPoint(ctx, box.midX, box.midY)
CGContextSetFillColorWithColor(ctx, color)
CGContextAddArc(ctx, box.midX, box.midY, radius-lineWidth/2, s, e, 0)
CGContextClosePath(ctx)
CGContextFillPath(ctx)
}
if progress > 0 {
let s = prog_to_rad(noProgress * 360/100)
let e = prog_to_rad(progress * 360/100)
draw_arc(s, e, color)
}
}
}
And here is my ViewController:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var pieChartView: ChartView!
#IBOutlet weak var slider: UISlider!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func setValue(sender: UISlider) {
pieChartView.progress = Double(sender.value)
}
}
This code is from my blogpost, it uses CAShapeLayer and UIBezierPath. You can create any number of segments with whichever choice of colour you like.
extension CGFloat {
func radians() -> CGFloat {
let b = CGFloat(M_PI) * (self/180)
return b
}
}
extension UIBezierPath {
convenience init(circleSegmentCenter center:CGPoint, radius:CGFloat, startAngle:CGFloat, endAngle:CGFloat)
{
self.init()
self.moveToPoint(CGPointMake(center.x, center.y))
self.addArcWithCenter(center, radius:radius, startAngle:startAngle.radians(), endAngle: endAngle.radians(), clockwise:true)
self.closePath()
}
}
func pieChart(pieces:[(UIBezierPath, UIColor)], viewRect:CGRect) -> UIView {
var layers = [CAShapeLayer]()
for p in pieces {
let layer = CAShapeLayer()
layer.path = p.0.CGPath
layer.fillColor = p.1.CGColor
layer.strokeColor = UIColor.whiteColor().CGColor
layers.append(layer)
}
let view = UIView(frame: viewRect)
for l in layers {
view.layer.addSublayer(l)
}
return view
}
let rectSize = CGRectMake(0,0,400,400)
let centrePointOfChart = CGPointMake(CGRectGetMidX(rectSize),CGRectGetMidY(rectSize))
let radius:CGFloat = 100
let piePieces = [(UIBezierPath(circleSegmentCenter: centrePointOfChart, radius: radius, startAngle: 250, endAngle: 360),UIColor.brownColor()), (UIBezierPath(circleSegmentCenter: centrePointOfChart, radius: radius, startAngle: 0, endAngle: 200),UIColor.orangeColor()), (UIBezierPath(circleSegmentCenter: centrePointOfChart, radius: radius, startAngle: 200, endAngle: 250),UIColor.lightGrayColor())]
pieChart(piePieces, viewRect: CGRectMake(0,0,400,400))
You posted a bunch of code that appears to draw a single pie chart "slice" in a single color.
Are you saying that you don't know how to make it draw an entire pie, with slices of different sizes, and that you don't know how to make each slice a different color?
It sounds to me like you are copy/pasting code you got from somewhere and have no idea how it works. How about you walk us through what your code does and give us a clearer idea of where you're stuck?
We're not here to take your copy/paste code and modify it for you to make it meet your requirements. Sounds like custom development to me. I don't know about the other posters on this board, but I get paid for that.
As it happens I've written a development blog post that includes a sample app that generates pie charts in Swift. You can see it here:
http://wareto.com/swift-piecharts
Instead of overriding drawRect like the code you posted, it creates a CAShapeLayer that holds the pie chart. It manages a pie chart with a variable number of "slices", and will either change the arc of each slice, the radius, or both.
It is not set up to make each slice a different color. For that you'd have to modify it to use separate shape layers for each slice, which would be a fairly big structural change to the program.
It does at least show you how to draw a pie chart in Swift for iOS:
Below Code is useful for Pie Chart Slice space in swift. Check out once
import UIKit
private extension CGFloat {
/// Formats the CGFloat to a maximum of 1 decimal place.
var formattedToOneDecimalPlace : String {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.minimumFractionDigits = 0
formatter.maximumFractionDigits = 1
return formatter.string(from: NSNumber(value: self.native)) ?? "\(self)"
}
}
/// Defines a segment of the pie chart
struct Segment {
/// The color of the segment
var color : UIColor
/// The name of the segment
var name : String
/// The value of the segment
var value : CGFloat
}
class PieChartView: UIView {
/// An array of structs representing the segments of the pie chart
var segments = [Segment]() {
didSet {
totalValue = segments.reduce(0) { $0 + $1.value }
setupLabels()
setNeedsDisplay() // re-draw view when the values get set
layoutLabels();
} // re-draw view when the values get set
}
/// Defines whether the segment labels should be shown when drawing the pie chart
var showSegmentLabels = true {
didSet { setNeedsDisplay() }
}
/// Defines whether the segment labels will show the value of the segment in brackets
var showSegmentValueInLabel = false {
didSet { setNeedsDisplay() }
}
/// The font to be used on the segment labels
var segmentLabelFont = UIFont.systemFont(ofSize: 14) {
didSet {
textAttributes[NSAttributedStringKey.font] = segmentLabelFont
setNeedsDisplay()
}
}
private let paragraphStyle : NSParagraphStyle = {
var p = NSMutableParagraphStyle()
p.alignment = .center
return p.copy() as! NSParagraphStyle
}()
private lazy var textAttributes : [NSAttributedStringKey : NSObject] = {
return [NSAttributedStringKey.paragraphStyle : self.paragraphStyle, NSAttributedStringKey.font : self.segmentLabelFont]
}()
override init(frame: CGRect) {
super.init(frame: frame)
isOpaque = false // when overriding drawRect, you must specify this to maintain transparency.
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
private var labels: [UILabel] = []
private var totalValue: CGFloat = 1;
override func draw(_ rect: CGRect) {
let anglePI2 = (CGFloat.pi * 2)
let center = CGPoint.init(x: bounds.size.width / 2, y: bounds.size.height / 2)
let radius = min(bounds.size.width, bounds.size.height) / 2;
let lineWidth: CGFloat = 1.5;
let ctx = UIGraphicsGetCurrentContext()
ctx?.setLineWidth(lineWidth)
var currentAngle: CGFloat = 0
if totalValue <= 0 {
totalValue = 1
}
let iRange = 0 ..< segments.count
for i in iRange {
let segment = segments[i]
// calculate percent
let percent = segment.value / totalValue
let angle = anglePI2 * percent
ctx?.beginPath()
ctx?.move(to: center)
ctx?.addArc(center: center, radius: radius - lineWidth, startAngle: currentAngle, endAngle: currentAngle + angle, clockwise: false)
ctx?.closePath()
ctx?.setFillColor(segment.color.cgColor)
ctx?.fillPath()
ctx?.beginPath()
ctx?.move(to: center)
ctx?.addArc(center: center, radius: radius - (lineWidth / 2), startAngle: currentAngle, endAngle: currentAngle + angle, clockwise: false)
ctx?.closePath()
ctx?.setStrokeColor(UIColor.white.cgColor)
ctx?.strokePath()
currentAngle += angle
}
}
override func layoutSubviews() {
super.layoutSubviews()
self.layoutLabels()
}
private func setupLabels() {
var diff = segments.count - labels.count;
if diff >= 0 {
for _ in 0 ..< diff {
let lbl = UILabel()
self.addSubview(lbl)
labels.append(lbl)
}
} else {
while diff != 0 {
var lbl: UILabel!
if labels.count <= 0 {
break;
}
lbl = labels.removeLast()
if lbl.superview != nil {
lbl.removeFromSuperview()
}
diff += 1;
}
}
for i in 0 ..< segments.count {
let lbl = labels[i]
lbl.textColor = UIColor.white
// Change here for your text display
// I currently display percent of each pies
lbl.text = "\(segments[i].value.formattedToOneDecimalPlace)%" //String.init(format: "%0.0f", segments[i].value)
lbl.font = UIFont.systemFont(ofSize: 14)
}
}
func layoutLabels() {
let anglePI2 = CGFloat.pi * 2
let center = CGPoint.init(x: bounds.size.width / 2, y: bounds.size.height / 2)
let radius = min(bounds.size.width / 2, bounds.size.height / 2) / 1.5
var currentAngle: CGFloat = 0;
let iRange = 0 ..< labels.count
for i in iRange {
let lbl = labels[i]
let percent = segments[i].value / totalValue
let intervalAngle = anglePI2 * percent;
lbl.frame = .zero;
lbl.sizeToFit()
let x = center.x + radius * cos(currentAngle + (intervalAngle / 2))
let y = center.y + radius * sin(currentAngle + (intervalAngle / 2))
lbl.center = CGPoint.init(x: x, y: y)
currentAngle += intervalAngle
}
}
}

Resources