UIBezier path 與 IBAction的應用

The traffic of value

In todays topic, we’ll be discussing how to sent value to view controller and how pie chart works in swift. The goal is to let user interact with the chart and value, and get real-time feedback. Let’s dive into it.

Slider and stepper

In the interface builder window, we can easily add any view to the view controller. For this example, we’ll use a slider and stepper view. Now, let’s open the assistant manager. From here, we can simply control-click and drag our slider and stepper into the view controller. A pop-up window will appear.

Make sure the outlet is set to “action” and we’re almost there! Now, we can receive data from the slider and stepper.

Just keep a one things in mind:

  • We’ll need a computed property to redraw the pie-chart whenever the value has been altered. This way, we’ll get real-time feedback, just as I mentioned earlier.

Stepper

Stepper view
    //Receive value from stepper
@IBAction func stepDidStep(_ sender: UIStepper) {
let value = sender.value
division = Int(value)
print(division)
}

//This computed property will call the pieFunc when ever the value has been altered
var division:Int = 5{
didSet{
pieFunc(view, division)
}
}

Slider

Slider view
    //This line recieves data from the slider
@IBAction func sliderDidSlide(_ sender:UISlider){
let value = sender.value
progress = value
print("\(progress)")
}


//This property is a computed property, it calls "iterChart" when the value is altered
var progress:Float = 0.2{
didSet{
iterChart()
updateLabel()
}
}

Circular progress chart

Nothing special here, the circular progress chart is just a function that consists of two smaller functions and loops over 5 to 10 times. But still, let’s take a closer look at these two sub-functions.

  • Background circle function: This one represents the grey default area of the chart.
  • Progress chart function: This one is fed with the value from the slider and is called whenever the value of the slider (progress) changes. In other words, the parameter value pushes the progress chart further and further as the slider moves.
 
//We wrap two main functions in here so we can call both of them when evert the value changes
func iterChart(loops:Int=5){
for i in 0..<loops{

let radius = 300 / Double(loops) * Double(i+1)
let offset = 0.5 / Float(loops) * Float(i+1)
let colorOffset:CGFloat = 1/CGFloat(loops) * CGFloat(i)

//Add background circle
let bgCircleLayer = backgroundCircle(radius)
bgView.layer.addSublayer(bgCircleLayer)


//Add progress to circleView
let progressLayer = progressChart(radius, offset)
progressLayer.strokeColor = CGColor(red: colorOffset, green: 0, blue: 1-colorOffset, alpha: 1)
bgView.layer.addSublayer(progressLayer)
}
}


//Create background circle layer
func backgroundCircle(_ radius:Double = 300) -> CAShapeLayer {

//Create pie chart layer
let rect = CGRect(x: (-radius/2)+150, y: (-radius/2)+150, width: radius, height: radius)
let bgCirclePath = UIBezierPath(ovalIn: rect)
let bgCircleLayer = CAShapeLayer()

//Define property of layer, and assign path to it
bgCircleLayer.path = bgCirclePath.cgPath
bgCircleLayer.fillColor = UIColor.clear.cgColor
bgCircleLayer.lineWidth = 10.0
bgCircleLayer.strokeColor = UIColor.gray.cgColor
return bgCircleLayer
}


//Progress chart layer
func progressChart(_ radius:Double = 300, _ charOffset:Float) -> CAShapeLayer {

//Create instance of CAShapeLayer and UIBezierPath
let rect = CGRect(x: (-radius/2)+150, y: (-radius/2)+150, width: radius, height: radius)
let progressLayer = CAShapeLayer()
var progressPath = UIBezierPath(ovalIn: rect)

//Define property of layer, and assign path to it
progressPath = UIBezierPath(ovalIn: rect)
progressLayer.path = progressPath.cgPath
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.lineWidth = 10.0
progressLayer.strokeEnd = CGFloat(progress+charOffset)
progressLayer.strokeColor = CGColor.init(red: 0, green: 0, blue: 1, alpha: 1)
progressLayer.lineCap = .round
return progressLayer

}

Pie chart

Just like circular progress chart. But this one is slightly easier, it’s only a for loop that draw each section of the chart.

    func pieFunc(_ superView:UIView, _ divideNum:Int = 5) {
for _ in 1...divideNum{
let percentageLayer = CAShapeLayer()
let endDegree = startDegree + 360.0/Double(divideNum)
let percentagePath = UIBezierPath(arcCenter: .init(x: centerX, y: centerY), radius: 200, startAngle: unitRadians * startDegree, endAngle: unitRadians * endDegree, clockwise: true)
percentageLayer.path = percentagePath.cgPath
percentageLayer.strokeColor = UIColor(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1), alpha: 1).cgColor
percentageLayer.lineWidth = 60
percentageLayer.fillColor = UIColor.clear.cgColor

superView.layer.addSublayer(percentageLayer)
startDegree = endDegree
}

}

Overall source code

Some comments are added by GPT. And it’s been doing to great job.

import UIKit
import SwiftUI

class ViewController: UIViewController {

// These attributes are for the pie chart

// A constant to convert degrees to radians
let unitRadians = Double.pi/180.0

// The starting degree for drawing the pie chart segments
var startDegree: Double = 270


// Function to draw the pie chart
func pieFunc(_ superView: UIView, _ divideNum: Int = 5) {
// Loop through the specified number of divisions to draw each segment
for _ in 1...divideNum {
let percentageLayer = CAShapeLayer()
// Calculate the ending degree for the current segment
let endDegree = startDegree + 360.0 / Double(divideNum)
// Create a Bezier path for the pie chart segment
let percentagePath = UIBezierPath(arcCenter: .init(x: centerX, y: centerY), radius: 200, startAngle: unitRadians * startDegree, endAngle: unitRadians * endDegree, clockwise: true)
// Set the path and appearance properties for the segment's layer
percentageLayer.path = percentagePath.cgPath
percentageLayer.strokeColor = UIColor(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1), alpha: 1).cgColor
percentageLayer.lineWidth = 60
percentageLayer.fillColor = UIColor.clear.cgColor

// Add the segment's layer to the superView's layer
superView.layer.addSublayer(percentageLayer)
// Update the starting degree for the next segment
startDegree = endDegree
}
}

// Receive value from the stepper
@IBAction func stepDidStep(_ sender: UIStepper) {
// Get the current value from the stepper
let value = sender.value
// Convert the value to an integer and assign it to the 'division' variable
division = Int(value)
// Print the 'division' value to the console
print(division)
}

// This computed property will call 'pieFunc' whenever the value of 'division' has been altered
var division: Int = 5 {
didSet {
pieFunc(view, division)
}
}

//-----------below this line is the circular progress chart--------------------

// Screen size and view size

// Calculate the x-coordinate of the center of the screen
let centerX = UIScreen.main.bounds.width/2.0
// Calculate the y-coordinate of the center of the screen
let centerY = UIScreen.main.bounds.height/2.0
// Define the size of the circular progress chart
let size = 300.0
// Calculate the offset for centering the circular progress chart
var offset: Double { size / -2.0 }

// View initialize

// Create a view to be used as the background for the circular progress chart
let bgView = UIView()
// Create a label to display the progress percentage
let label = UILabel(frame: CGRect(x: 0 , y: 0, width: 300, height: 300))

// This line receives data from the slider
@IBAction func sliderDidSlide(_ sender: UISlider) {
// Get the current value from the slider
let value = sender.value
// Assign the value to the 'progress' variable
progress = value
// Print the 'progress' value to the console
print("\(progress)")
}

// This property is a computed property, it calls 'iterChart' and 'updateLabel' when the value of 'progress' has been altered
var progress: Float = 0.2 {
didSet {
// Call 'iterChart' to update the circular progress chart
iterChart()
// Call 'updateLabel' to update the label displaying the progress percentage
updateLabel()
}
}

// Label function
func updateLabel() {
// Set the frame and appearance properties for the label displaying the progress percentage
label.frame = CGRect(x: -75 + centerX, y: -25 + 130, width: 150, height: 50)
label.textAlignment = .center
label.font = .systemFont(ofSize: 40)
// Display the progress percentage as an integer
label.text = "\(Int(progress * 100)) % "
}

// We wrap two main functions in here so we can call both of them whenever the value changes
func iterChart(loops: Int = 5) {
// Loop through the specified number of iterations to draw the circular progress chart
for i in 0..<loops {
// Calculate the radius for the current circle segment
let radius = 300 / Double(loops) * Double(i + 1)
// Calculate the offset for the current circle segment
let offset = 0.5 / Float(loops) * Float(i + 1)
// Calculate the color offset for the current circle segment
let colorOffset: CGFloat = 1 / CGFloat(loops) * CGFloat(i)

// Add background circle

// Create a background circle layer with the specified radius
let bgCircleLayer = backgroundCircle(radius)
// Add the background circle layer to the 'bgView'
bgView.layer.addSublayer(bgCircleLayer)

// Add progress to circleView

// Create a progress chart layer with the specified radius and offset
let progressLayer = progressChart(radius, offset)
// Set the stroke color for the progress chart layer based on the color offset
progressLayer.strokeColor = CGColor(red: colorOffset, green: 0, blue: 1 - colorOffset, alpha: 1)
// Add the progress chart layer to the 'bgView'
bgView.layer.addSublayer(progressLayer)
}
}

// Create background circle layer
func backgroundCircle(_ radius: Double = 300) -> CAShapeLayer {
// Create a rectangular frame for the background circle
let rect = CGRect(x: (-radius/2) + 150, y: (-radius/2) + 150, width: radius, height: radius)
// Create a Bezier path for the background circle
let bgCirclePath = UIBezierPath(ovalIn: rect)
// Create a CAShapeLayer to represent the background circle
let bgCircleLayer = CAShapeLayer()

// Define properties of the layer and assign the path to it
bgCircleLayer.path = bgCirclePath.cgPath
bgCircleLayer.fillColor = UIColor.clear.cgColor
bgCircleLayer.lineWidth = 10.0
bgCircleLayer.strokeColor = UIColor.gray.cgColor
return bgCircleLayer
}

// Progress chart layer
func progressChart(_ radius: Double = 300, _ charOffset: Float) -> CAShapeLayer {
// Create a rectangular frame for the progress chart
let rect = CGRect(x: (-radius/2) + 150, y: (-radius/2) + 150, width: radius, height: radius)
// Create a CAShapeLayer to represent the progress chart
let progressLayer = CAShapeLayer()
var progressPath = UIBezierPath(ovalIn: rect)

// Define properties of the layer and assign the path to it
progressPath = UIBezierPath(ovalIn: rect)
progressLayer.path = progressPath.cgPath
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.lineWidth = 10.0
progressLayer.strokeEnd = CGFloat(progress + charOffset)
progressLayer.strokeColor = CGColor.init(red: 0, green: 0, blue: 1, alpha: 1)
progressLayer.lineCap = .round
return progressLayer
}


//----------This line below is viewDidLoad function-----------------------


override func viewDidLoad() {
super.viewDidLoad()

// Define views property
// Set the frame and background color for the 'bgView'
bgView.frame = CGRect(x: centerX + offset, y: centerY + offset, width: size, height: size)
bgView.backgroundColor = UIColor(red: 0.75, green: 0.6, blue: 0.01, alpha: 1)


// Add 'bgView' and 'label' as subviews of the main view
view.addSubview(bgView)
view.addSubview(label)

// Call function to make the pie chart
pieFunc(view)

// Call function to initialize the circular progress chart
iterChart()
// Call function to update the label displaying the progress percentage
updateLabel()
}
}

Conclusion/Git

I’ve had a very challenging experience this time. Of course, there were a few bumps along the way that could be a bit frustrating, but the feeling of accomplishment after solving each problem is rewarded.

I’m having a great time! I love creating interactive things. There’s this unique sense of fulfillment that comes with it, which makes all the hard work feel incredibly satisfying.

--

--