I have an animated Arc drawn in SwiftUI that represents data coming from two devices. When the arc animates to the left, the "gauge" is indicating that left device's data is higher than the right's and vice versa. The "top" is zero, which is 270 degrees when drawing an Arc shape. Because of this, I have a conditional set on a clockwise property so that the animation appears to go to the left or right of zero (270):
var clockwise: Bool {
get { endAngle.degrees > 270 ? false : true }
}
When the property endAngle goes from less than 270 to greater than 270, or the reverse of this, the arc isn't drawn properly and appears as a circle because clockwise isn't set for portion of the animation passing through 270 to the new endAngle.
Is there a way to delay the change in clockwise until the animation passes through 270 degrees?
I've included the code for the view below with some comments. In order to animate Angle, it has to conform to VectorArithmetic which is the reason for the extension.
struct AverageGauge: View {
#State var endAngle = Angle(degrees: 271.0)
// Property I'd like to update during the animation
var clockwise: Bool {
get { endAngle.degrees > 270 ? false : true }
}
var body: some View {
VStack {
Arc(startAngle: .degrees(270), endAngle: endAngle,
clockwise: clockwise)
.stroke(.red, style: StrokeStyle(lineWidth: 10, lineCap: .round, lineJoin: .round))
.frame(width: 100, height: 100)
// Tap gesture simulates changing data
.onTapGesture {
withAnimation(Animation.easeIn(duration: 2.0)) {
// endAngle animated here
endAngle = Angle(degrees: Double.random(in: 180...360))
}
}
Text(String(describing: endAngle.degrees))
Text("\(String(clockwise))")
}
}
}
struct Arc: Shape {
var startAngle: Angle
var endAngle: Angle
var clockwise: Bool
// var startAngleAnimatable: Angle {
// get { startAngle }
// set {startAngle = Angle(degrees: 270.0) }
// }
// Required to animate endAngle
var animatableData: Angle {
get { endAngle }
set { endAngle = newValue }
}
// var clockwiseAnimatable: Bool {
// get { clockwise }
// set { clockwise = newValue }
// }
func path(in rect: CGRect) -> Path {
var path = Path()
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: rect.width / 2, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise)
return path
}
}
extension Angle: VectorArithmetic {
public static var zero = Angle(degrees: 0.0)
public static func + (lhs: Angle, rhs: Angle) -> Angle {
Angle(degrees: lhs.degrees + rhs.degrees)
}
public static func - (lhs: Angle, rhs: Angle) -> Angle {
Angle(degrees: lhs.degrees - rhs.degrees)
}
public static func += (lhs: inout Angle, rhs: Angle) {
lhs = Angle(degrees: lhs.degrees + rhs.degrees)
}
public static func -= (lhs: inout Angle, rhs: Angle) {
lhs = Angle(degrees: lhs.degrees - rhs.degrees)
}
public mutating func scale(by rhs: Double) {
self.degrees = self.degrees * rhs
}
public var magnitudeSquared: Double {
get { 0.0 }
}
}
// Preview in case you want to paste it.
struct AverageGauge_Previews: PreviewProvider {
static var previews: some View {
AverageGauge()
}
}
Here's a gif of the animation. You can see that when the new value is on the same side as 270, it looks normal, however, when the animation traverses zero (270) it appears as a circle because clockwise is set incorrectly.
I simplified it with a trimmed circle, leading to the same result.
If change in value from + to - or vice versa, I wait for the first animation to finish before starting the second.
struct ContentView: View {
#State var gaugeValue: Double = 0.8 // now in values -1 (-45°) to +1 (+45°)
var body: some View {
VStack {
let fraction: CGFloat = abs(gaugeValue) * 0.25
let rotation = gaugeValue < 0 ? (gaugeValue * 90) - 90 : -90
Circle()
.trim(from: 0, to: fraction )
.rotation(Angle(degrees: rotation), anchor: .center)
.stroke(.red, style: StrokeStyle(lineWidth: 10, lineCap: .round, lineJoin: .round))
.frame(width: 100, height: 100)
// Tap gesture simulates changing data
Button {
let newGaugeValue = CGFloat.random(in: -1 ... 1)
if newGaugeValue * gaugeValue < 0 { // if change of +/-
withAnimation(Animation.easeOut(duration: 1.0)) {
gaugeValue = 0
}
// delay for reaching 0 point
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
withAnimation(Animation.easeIn(duration: 1.0)) {
gaugeValue = newGaugeValue
}
}
} else { // no change of +/-
withAnimation(Animation.easeIn(duration: 1.0)) {
gaugeValue = newGaugeValue
}
}
} label: {
Text("New Data")
}
Text("\(gaugeValue)")
}
}
}
Related
I am trying to make pie chart as below which is working fine but I have issue with end path (which is orange in below image).
What I want to do is make end of orange shape to below the green one so that I can achieve as below.
Any suggestion how this can be done?
Code can be found at below link.
https://drive.google.com/file/d/1ST0zNooLgRaI8s2pDK3NMjBQYjBSRoXB/view?usp=sharing
Below is what I have.
func drawBeizer(start_angle : CGFloat, end_angle : CGFloat, final_color : UIColor) {
let path1 : UIBezierPath = UIBezierPath()
path1.addArc(withCenter: CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2), radius: ((self.frame.size.width-main_view_width)/2), startAngle: start_angle, endAngle: end_angle, clockwise: true)
path1.lineWidth = main_view_width
path1.lineCapStyle = .round
final_color.setStroke()
path1.stroke()
}
This function I am passing start angle and end angle & color for the path.
This solution is for SwiftUI using Path.
struct DonutElement: Shape{
var width: CGFloat = 50
var startAngle: Angle
var endAngle: Angle
func path(in rect: CGRect) -> Path {
// define commonly used vars
// the extend of the shape
let outerRadius = min(rect.width, rect.height) / 2
// the middle of the shape
let midRadius = outerRadius - width / 2
// the inner radius
let innerRadius = outerRadius - width
// centerpoint used to move coordinate system in to center
let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
return Path{ path in
// calculate the startpoint from the startAngle. startAngle is allready normalized
let startPoint = startAngle.polarCoordinates(center: center, radius: outerRadius)
// move the path without drawing to the starting position
path.move(to: startPoint)
// add the starting concav arc
// the center point for this arc are the polar coordinates relative to midradius of the donut
path.addArc(center: startAngle.polarCoordinates(center: center, radius: midRadius), radius: width / 2, startAngle: startAngle.normalizing(), endAngle: (.init(degrees: 180) - startAngle), clockwise: true)
// add the arc that presents the inner donut line
// center point is the center of the drawing rect with normalized angles
path.addArc(center: center, radius: innerRadius, startAngle: startAngle.normalizing(), endAngle: endAngle.normalizing(), clockwise: true)
// add the convex arc at the end of the donut element
// switch clockwise to false and swap end and start angle
// replace startAngle with endAngle
path.addArc(center: endAngle.polarCoordinates(center: center, radius: midRadius), radius: width / 2, startAngle: (.init(degrees: 180) - endAngle), endAngle: endAngle.normalizing(), clockwise: false)
// add the outer stroke to close the shape
path.addArc(center: center, radius: outerRadius, startAngle: endAngle.normalizing(), endAngle: startAngle.normalizing(), clockwise: false)
// just in case
path.closeSubpath()
}
}
}
extension Shape {
func fillWithStroke<Fill: ShapeStyle, Stroke: ShapeStyle>(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: Double = 1) -> some View {
self
.stroke(strokeStyle, lineWidth: lineWidth)
.background(self.fill(fillStyle))
}
}
struct AngleContainer: Identifiable, Hashable{
let id = UUID()
var startAngle: Angle
var endAngle: Angle
var color: Color
}
struct Donut: View{
// each element ends here and starts at the previous or 0. Values in percent
var elementStops: [(CGFloat , Color)] = [(0.4 , .blue), (45 , .red), (55 , .green), (78 , .gray), (100 , .white)]
var width: CGFloat = 50.0
private var angles: [AngleContainer] {
var angles = [AngleContainer]()
for (index, stop) in elementStops.enumerated(){
if index == 0{
let startAngle = Angle(degrees: 0)
let endAngle = Angle(degrees: stop.0/100 * 360)
angles.append(AngleContainer(startAngle: startAngle,endAngle: endAngle,color: stop.1))
} else{
let startAngle = Angle(degrees: elementStops[index - 1].0 / 100 * 360)
let endAngle = Angle(degrees: stop.0/100 * 360)
angles.append(AngleContainer(startAngle: startAngle,endAngle: endAngle,color: stop.1))
}
}
return angles
}
var body: some View{
ZStack{
ForEach(angles){ angleContainer in
DonutElement(width: width, startAngle: angleContainer.startAngle, endAngle: angleContainer.endAngle)
.fillWithStroke(angleContainer.color, strokeBorder: .black.opacity(0.2))
}
}
.background(.white)
}
}
// used for transfering coordinate system
extension CGPoint{
mutating func transfered(to center: CGPoint){
x = center.x + x
y = center.y - y
}
func transfering(to center: CGPoint) -> CGPoint{
.init(x: center.x + x, y: center.y - y)
}
}
I am normalizing angles to behave more like in math with 90 degrees at the top and not at the bottom.
// convenience so angle feels more natural
extension Angle{
func normalizing() -> Angle {
Angle(degrees: 0) - self
}
func polarCoordinates(center: CGPoint, radius: CGFloat) -> CGPoint{
CGPoint(x: cos(self.radians) * radius, y: sin(self.radians) * radius )
.transfering(to: center)
}
}
Result:
I found a way to do this. Below is the full code.
I am adding other addArc using end angles.
ViewController.swift
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var myView: MyView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
myView.setupData(inner_color: [.green, .purple, .blue, .orange], inner_view_width: self.view.frame.width/5.0, inner_angle: [0.0, 90.0, 180.0, 270.0])
myView.backgroundColor = UIColor.white
}
}
MyView.swift
import UIKit
class MyView: UIView {
var main_radius : CGFloat = 0.0
var main_color : [UIColor] = [UIColor.black, UIColor.green]
var main_view_width : CGFloat = 1.0
var main_angle : [CGFloat] = [-CGFloat.pi, 0.0]
var actual_angle : [CGFloat] = []
func setupData(inner_color : [UIColor], inner_view_width : CGFloat, inner_angle : [CGFloat]) {
main_color = inner_color
main_view_width = inner_view_width
main_angle = inner_angle
actual_angle = inner_angle
var temp_main_angle : [CGFloat] = []
for i in 0..<main_angle.count {
temp_main_angle.append(main_angle[i]*Double.pi/180)
}
main_angle = temp_main_angle
draw(CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.width))
}
override func draw(_ rect: CGRect) {
if (main_color.count >= 2) {
var inner_position : Int = 0
// first draw with sharp corner
for i in 0..<main_color.count {
if (i == (main_color.count-1)) {
inner_position = 0
} else {
inner_position = i+1
}
drawBeizer(start_angle: main_angle[i], end_angle: main_angle[inner_position], final_color: main_color[i])
}
// append with ending icons for circle
for i in 0..<main_color.count {
if (i == (main_color.count-1)) {
inner_position = 0
} else {
inner_position = i+1
}
drawBeizer(start_angle: main_angle[inner_position], end_angle: main_angle[inner_position]+(1*Double.pi/180), final_color: main_color[i], withCircle: true) // make here final_color as .black to see what I am doing with this loop
}
}
}
func drawBeizer(start_angle : CGFloat, end_angle : CGFloat, final_color : UIColor, withCircle: Bool = false) {
let path1 : UIBezierPath = UIBezierPath()
path1.addArc(withCenter: CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2), radius: ((self.frame.size.width-main_view_width)/2), startAngle: start_angle, endAngle: end_angle, clockwise: true)
path1.lineWidth = main_view_width
if (withCircle) {
path1.lineCapStyle = .round
}
final_color.setStroke()
path1.stroke()
}
}
Full code here
I used swiftui to implement a demo that can recognize user gestures and draw lines.
Now I want it to add an arrow at the end of the line and different segments have arrows with different angles. What should I do?
This is my code
struct LineView: View {
#State var removeAll = false
#State var lines = [CGPoint]()
var body: some View {
ZStack {
Rectangle()
.cornerRadius(20)
.opacity(0.1)
.shadow(color: .gray, radius: 4, x: 0, y: 2)
Path { path in
path.addLines(lines)
}
.stroke(lineWidth: 3)
}
.gesture(
DragGesture()
.onChanged { state in
if removeAll {
lines.removeAll()
removeAll = false
}
lines.append(state.location)
}
.onEnded { _ in
removeAll = true
}
)
.frame(width: 370, height: 500)
}
}
There are plenty of ways to determine how to position and rotate the arrow at the end of the line. Mostly you need to have last two points which determine the arrow position and rotation.
If you already have a drawing of an arrow (a path or an image) then it may be easiest to use transform. It should look like this:
private func arrowTransform(lastPoint: CGPoint, previousPoint: CGPoint) -> CGAffineTransform {
let translation = CGAffineTransform(translationX: lastPoint.x, y: lastPoint.y)
let angle = atan2(lastPoint.y-previousPoint.y, lastPoint.x-previousPoint.x)
let rotation = CGAffineTransform(rotationAngle: angle)
return rotation.concatenating(translation)
}
The key here is using atan2 which is a real life saver. It is basically arctangent which does't need the extra if-statements to determine the angle between two points. It accepts delta-y and delta-x; that's all there is to know about it.
With this data you can create a translation matrix and a rotation matrix which can then be concatenated to get the end result.
To apply it you simply use it like the following:
arrowPath()
.transform(arrowTransform(lastPoint: lines[lines.count-1], previousPoint: lines[lines.count-2]))
.fill()
I simply used a path here to draw an arrow around (0, 0) and then fill it.
The whole thing together that I used is:
struct LineView: View {
#State var removeAll = false
#State var lines = [CGPoint]()
private func arrowPath() -> Path {
// Doing it rightwards
Path { path in
path.move(to: .zero)
path.addLine(to: .init(x: -10.0, y: 5.0))
path.addLine(to: .init(x: -10.0, y: -5.0))
path.closeSubpath()
}
}
private func arrowTransform(lastPoint: CGPoint, previousPoint: CGPoint) -> CGAffineTransform {
let translation = CGAffineTransform(translationX: lastPoint.x, y: lastPoint.y)
let angle = atan2(lastPoint.y-previousPoint.y, lastPoint.x-previousPoint.x)
let rotation = CGAffineTransform(rotationAngle: angle)
return rotation.concatenating(translation)
}
var body: some View {
ZStack {
Rectangle()
.cornerRadius(20)
.opacity(0.1)
.shadow(color: .gray, radius: 4, x: 0, y: 2)
Path { path in
path.addLines(lines)
}
.stroke(lineWidth: 3)
if lines.count >= 2 {
arrowPath()
.transform(arrowTransform(lastPoint: lines[lines.count-1], previousPoint: lines[lines.count-2]))
.fill()
}
}
.gesture(
DragGesture()
.onChanged { state in
if removeAll {
lines.removeAll()
removeAll = false
}
lines.append(state.location)
}
.onEnded { _ in
removeAll = true
}
)
.frame(width: 370, height: 500)
}
}
It still needs some work. Either removing the last point on line or moving the arrow to overlap the last line. But I think you should be able to take it from here.
I hope you are all doing good.
I'm very new to this community and I hope we can help each other grow.
I'm currently building a MacOS App with swiftUI and I have this capsule shape, that has a water animation inside. The water animation are just waves that move horizontally created with animatable data and path. My problem is, I have two other buttons on the screen, a play button and a stop button. They are supposed to start filling the capsule with the water and stop doing it respectively, which they do, but they are supposed to do it with an animation, and it's not.
Below is my code for further details. Thanks in advance.
GeometryReader { geometry in
VStack {
Spacer()
BreathingWave(progress: $progressValue, phase: phase)
.fill(Color(#colorLiteral(red: 0.14848575, green: 0.6356075406, blue: 0.5744615197, alpha: 1)))
.clipShape(Capsule())
.border(Color.gray, width: 3)
.frame(width: geometry.size.width / 12)
.onAppear {
withAnimation(.linear(duration: 1).repeatForever(autoreverses: false)) {
self.phase = .pi * 2
}
}
HStack {
Spacer()
Image(systemName: "play.circle.fill")
.resizable()
.renderingMode(.template)
.frame(width: 50, height: 50)
.foregroundColor(Color(#colorLiteral(red: 0.14848575, green: 0.6356075406, blue: 0.5744615197, alpha: 1)))
.scaleEffect(scalePlay ? 1.25 : 1)
.onHover(perform: { scalPlay in
withAnimation(.linear(duration: 0.1)) {
scalePlay = scalPlay
}
})
.onTapGesture {
withAnimation(
.easeInOut(duration: duration)
.repeatForever(autoreverses: true)) {
if(progressValue < 1) {
progressValue += 0.1
}
else {
progressValue = progressValue
}
}
}
Spacer()
Image(systemName: "stop.circle.fill")
.resizable()
.renderingMode(.template)
.frame(width: 50, height: 50)
.foregroundColor(Color(#colorLiteral(red: 0.14848575, green: 0.6356075406, blue: 0.5744615197, alpha: 1)))
.scaleEffect(scaleStop ? 1.25 : 1)
.onHover(perform: { scalStop in
withAnimation(.linear(duration: 0.1)) {
scaleStop = scalStop
}
})
.onTapGesture {
withAnimation(.linear) {
progressValue = 0.0
}
}
Spacer()
}
.padding(.bottom, 50)
}
}
And this is the code of the BreathingWave
struct BreathingWave: Shape {
#Binding var progress: Float
var applitude: CGFloat = 10
var waveLength: CGFloat = 20
var phase: CGFloat
var animatableData: CGFloat {
get { phase }
set { phase = newValue }
}
func path(in rect: CGRect) -> Path {
var path = Path()
let width = rect.width
let height = rect.height
let minWidth = width / 2
let progressHeight = height * (1 - CGFloat(progress))
path.move(to: CGPoint(x: 0, y: progressHeight))
for x in stride(from: 0, to: width + 5, by: 5) {
let relativeX = x / waveLength
let normalizedLength = (x - minWidth) / minWidth
let y = progressHeight + sin(phase + relativeX) * applitude * normalizedLength
path.addLine(to: CGPoint(x: x, y: y))
}
path.addLine(to: CGPoint(x: width, y: progressHeight))
path.addLine(to: CGPoint(x: width, y: height))
path.addLine(to: CGPoint(x: 0, y: height))
path.addLine(to: CGPoint(x: 0, y: progressHeight))
return path
}
}
The problem you are running into is simply that you never told BreathingWave how to animate for progress. You set a single dimension animation, when you really want it to animate in two dimensions. The fix is straightforward: use AnimatablePair to supply the variables to animatableData.
struct BreathingWave: Shape {
var applitude: CGFloat = 10
var waveLength: CGFloat = 20
// progress is no longer a Binding. Using #Binding never did anything
// as you were not changing the value to be used in the parent view.
var progress: Float
var phase: CGFloat
var animatableData: AnimatablePair<Float, CGFloat> { // <- AnimatablePair here
get { AnimatablePair(progress, phase) } // <- the getter returns the pair
set { progress = newValue.first // <- the setter sets each individually from
phase = newValue.second // the pair.
}
}
func path(in rect: CGRect) -> Path {
...
}
}
Lastly, you now call it like this:
BreathingWave(progress: progressValue, phase: phase)
One final thing. Please provide a Minimal Reproducible Example (MRE) in the future. There was a lot of code that was unnecessary to debug this, and I had to implementment the struct header to run it. Strip as much out as possible, but give us something that we can copy, paste and run.
I'm trying to animate an arc to the left and right form its center (270 deg in Shape terms). This requires animatable data, which I've added, but the arc still doesn't seem to animate.
The view using the Arc is below. I've commented my intentions with the properties.
struct AverageGauge: View {
#State var endAngle: Angle = Angle(degrees: 271.0)
var clockwise: Bool {
get { endAngle.degrees > 270 ? false : true }
}
var body: some View {
Arc(startAngle: .degrees(270), endAngle: endAngle,
clockwise: clockwise)
.stroke(.red, style: StrokeStyle(lineWidth: 10, lineCap: .round, lineJoin: .round))
.frame(width: 300, height: 300)
// Tap gesture as stand in for changing data
.onTapGesture {
withAnimation(Animation.easeIn(duration: 10.0)) {
endAngle = Angle(degrees: Double.random(in: 180...360))
}
}
}
}
struct Arc: Shape {
var startAngle: Angle
var endAngle: Angle
var clockwise: Bool
// Animatable endAngle
var endAngleAnimatable: Angle {
get { endAngle }
set { endAngle = newValue }
}
// Animatable clockwise bool
var clockwiseAnimatable: Bool {
get { clockwise }
set { clockwise = newValue }
}
func path(in rect: CGRect) -> Path {
var path = Path()
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: rect.width / 2, startAngle: startAngle, endAngle: endAngleAnimatable, clockwise: clockwiseAnimatable)
return path
}
}
When I set the clockwise in the Arc to a constant, it still doesn't animate, so I suppose the Bool isn't what's causing the problem.
Here's a gif of the arc being instantly redrawn rather than animated:
I got this working by using Double for endAngle rather than Angle. The Double is then converted to an Angle using .degrees(Double) in the arc. I guess creating Angles isn't animatable.
Per #Yrb's suggestion above, I conformed Angle to VectorArithmetic and the animation works without having to change endAngle do a Double:
extension Angle: VectorArithmetic {
public static var zero = Angle(degrees: 0.0)
public static func + (lhs: Angle, rhs: Angle) -> Angle {
Angle(degrees: lhs.degrees + rhs.degrees)
}
public static func - (lhs: Angle, rhs: Angle) -> Angle {
Angle(degrees: lhs.degrees - rhs.degrees)
}
public static func += (lhs: inout Angle, rhs: Angle) {
lhs = Angle(degrees: lhs.degrees + rhs.degrees)
}
public static func -= (lhs: inout Angle, rhs: Angle) {
lhs = Angle(degrees: lhs.degrees - rhs.degrees)
}
public mutating func scale(by rhs: Double) {
self.degrees = self.degrees * rhs
}
public var magnitudeSquared: Double {
get { 0.0 }
}
}
magnitudeSquared isn't properly implemented. Just a filler stub.
I am currently working on a graph view for a widget and I don't like the way the edges looks on my graph atm. I would like to make the edges on my graph line rounded instead of sharp (Graph).
I've tried with .cornerRadius(5) and .addQuadCurve() but nothing seems to work.
My code looks like this.
import SwiftUI
#available(iOS 14.0.0, *)
struct Graph: View {
var styling = ViewStyling()
var stockPrices: [CGFloat]
var body: some View {
ZStack {
LinearGradient(gradient:
Gradient(colors: [styling.gradientColor, styling.defaultWhite]), startPoint: .top, endPoint: .bottom)
.clipShape(LineGraph(dataPoints: stockPrices.normalized, closed: true))
LineGraph(dataPoints: stockPrices.normalized)
.stroke(styling.graphLine, lineWidth: 2)
//.cornerRadius(5)
}
}
}
#available(iOS 14.0.0, *)
struct LineGraph: Shape {
var dataPoints: [CGFloat]
var closed = false
func path(in rect: CGRect) -> Path {
func point(at ix: Int) -> CGPoint {
let point = dataPoints[ix]
let x = rect.width * CGFloat(ix) / CGFloat(dataPoints.count - 1)
let y = (1 - point) * rect.height
return CGPoint(x: x, y: y)
}
return Path { p in
guard dataPoints.count > 1 else { return }
let start = dataPoints[0]
p.move(to: CGPoint(x: 0, y: (1 - start) * rect.height))
//p.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.maxY), control: CGPoint(x: rect.maxX, y: rect.maxY))
for index in dataPoints.indices {
p.addLine(to: point(at: index))
}
if closed {
p.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
p.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
p.closeSubpath()
}
}
}
}
extension Array where Element == CGFloat {
// Return the elements of the sequence normalized.
var normalized: [CGFloat] {
if let min = self.min(), let max = self.max() {
return self.map{ ($0 - min) / (max - min) }
}
return []
}
}
How the joins between line segments are rendered is controlled by the lineJoin property of StrokeStyle, You're stroking with a color and a line width here:
.stroke(styling.graphLine, lineWidth: 2)
but you want something more like:
.stroke(styling.graphline, StrokeStyle(lineWidth: 2, lineJoin: .round))