SwiftUI - Drag the object along the path - swift

I would like to make my rectangle only move along a path. I have no idea how I could do that
struct _FollowingPathView: View {
#State private var position = CGSize()
#State private var finalPosition = CGSize()
#State private var currentPosition: CGFloat = 0.5
var body: some View {
VStack {
Rectangle()
.frame(width: 300, height: 300)
.foregroundColor(.clear)
.border(.black, width: 2)
.overlay {
CustomPath().path(in: CGRect(x: 0, y: 0, width: 300, height: 300))
.stroke(Color.red, style: StrokeStyle(lineWidth: 2))
Rectangle()
.frame(width: 50, height: 50)
.foregroundColor(.clear)
.border(.black, width: 2)
.offset(
)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged {
position.width = min(max(-125, $0.translation.width + finalPosition.width), 125)
position.height = min(max(-125, $0.translation.height + finalPosition.height), 125)
}
.onEnded {
position.width = min(max(-125, $0.translation.width + finalPosition.width), 125)
position.height = min(max(-125, $0.translation.height + finalPosition.height), 125)
finalPosition = position
}
)
}
}
}
}
struct _FollowingPathView_Previews: PreviewProvider {
static var previews: some View {
_FollowingPathView()
}
}
enter image description here
I tried with trimmedPath, and modifying the current position by using dragGesture. But it wasn't working properly
.position(
x: CustomPath().path(in: CGRect(x: 0, y: 0, width: 300, height: 300)).trimmedPath(from: 0, to: currentPosition).currentPoint?.x ?? CGPoint(),
y: CustomPath().path(in: CGRect(x: 0, y: 0, width: 300, height: 300)).trimmedPath(from: 0, to: currentPosition).currentPoint?.y ?? CGPoint(),
)

Related

SwiftUI animate opacity in gradient mask

I want to animate a mountain line chart (see screenshot) so that the line appears smoothly from left to right. I tried animating the endPoint of a LinearGradient in a mask. This works as it should, but leaves the right end of the line with an opacity value < 1. I also tried animating the opacity value itself, but that doesn't animate anything. This is the View:
import SwiftUI
struct ContentView: View {
private var datapoints: [CGFloat] = [100, 250, 200, 220, 200, 250, 350, 300, 325, 250]
#State private var pct: Double = 0.0
var body: some View {
GeometryReader { geometry in
ZStack {
Path() { path in
var x = 0.0
path.move(to: CGPoint(x: x, y: datapoints[0]))
for i in (1 ..< datapoints.count) {
x += geometry.size.width / CGFloat(datapoints.count - 1)
path.addLine(to: CGPoint(x: x, y: datapoints[i]))
}
}
.stroke()
Path() { path in
var x: Double = 0.0
path.move(to: CGPoint(x: x, y: datapoints[0]))
for i in (1 ..< datapoints.count) {
x += geometry.size.width / CGFloat(datapoints.count - 1)
path.addLine(to: CGPoint(x: x, y: datapoints[i]))
}
path.addLine(to: CGPoint(x: path.currentPoint!.x, y: geometry.size.height))
path.addLine(to: CGPoint(x: 0, y: geometry.size.height))
}
.fill(LinearGradient(colors: [.black.opacity(0.25), .black.opacity(0)], startPoint: .top, endPoint: .bottom))
}
.mask(LinearGradient(stops: [Gradient.Stop(color: .black, location: 0), Gradient.Stop(color: .black.opacity(pct), location: 1)], startPoint: .leading, endPoint: .trailing))
.onAppear() {
withAnimation(.linear(duration: 3)) {
pct = 1.0
}
}
// .mask(LinearGradient(stops: [Gradient.Stop(color: .black, location: 0), Gradient.Stop(color: .black.opacity(0), location: 1)], startPoint: .leading, endPoint: pct == 1 ? .trailing : .leading))
// .onAppear() {
// withAnimation(.linear(duration: 3)) {
// pct = 1.0
// }
// }
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
This is the result if uncomment the commented out lines:

Working with dial view in SwiftUI with drag gesture and displaying value of selected number

I'm new to SwiftUI and what I trying to build is a dial view like this.
As you can see it has:
a view indicator which is for displaying the currently selected value from the user.
CircularDialView with all the numbers to chose from.
text view for displaying actual selected number
Maximum what I tried to accomplish is that for now CircularDialView is intractable with a two-finger drag gesture and for the rest, I have no even close idea how to do it.
Please help, because I tried everything I could and have to say it's super hard to do with SwiftUI
Here's the code:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Rectangle()
.frame(width: 4, height: 25)
.padding()
CircularDialView()
Text("\(1)")
.font(.title)
.fontWeight(.bold)
.padding()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ClockText: View {
var numbers: [String]
var angle: CGFloat
private struct IdentifiableNumbers: Identifiable {
var id: Int
var number: String
}
private var dataSource: [IdentifiableNumbers] {
numbers.enumerated().map { IdentifiableNumbers(id: $0, number: $1) }
}
var body: some View {
GeometryReader { geometry in
ZStack {
ForEach(dataSource) {
Text("\($0.number)")
.position(position(for: $0.id, in: geometry.frame(in: .local)))
}
}
}
}
private func position(for index: Int, in rect: CGRect) -> CGPoint {
let rect = rect.insetBy(dx: angle, dy: angle)
let angle = ((2 * .pi) / CGFloat(numbers.count) * CGFloat(index)) - .pi / 2
let radius = min(rect.width, rect.height) / 2
return CGPoint(x: rect.midX + radius * cos(angle),
y: rect.midY + radius * sin(angle))
}
}
struct CircularDialView: View {
#State private var rotateState: Double = 0
var body: some View {
ZStack {
Circle()
.stroke(Color.black, lineWidth: 5)
.frame(width: 250, height: 250, alignment: .center)
ClockText(
numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map {"\($0)"},
angle: 45
)
.font(.system(size: 20))
.frame(width: 290, height: 290, alignment: .center)
}
.rotationEffect(Angle(degrees: rotateState))
.gesture(
RotationGesture()
.simultaneously(with: DragGesture())
.onChanged { value in
self.rotateState = value.first?.degrees ?? rotateState
}
)
}
}

Rotate view to always face mouse pointer in SwiftUI

Background: I am trying to have a view rotate to face the mouse at all times.
Details of issue: Moving the view based solely on the mouse moving left or right or on up or down works fine (see version 1). But using both at the same time (non-linear or arc motion with the mouse) fails to work as soon as the mouse arcs down (as y goes negative) and around (as x goes negative) and the position of the view regresses.
For this I am attempting to dynamically switch rotation values from negative to positive depending on what side of the screen the mouse is on (see version 2). But this feels like a poor implementation and is quite buggy.
Question: How can I better do this?
I am basing my cursor fetching code off of here.
Version 1-
Problem: moving the view based solely on the mouse moving left or right or on up or down works fine but this issue occurs when trying to move the mouse in a non-linear way:
import SwiftUI
struct ContentView: View {
var window: NSWindow! = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
var mouseLocation: NSPoint { NSEvent.mouseLocation }
var location: NSPoint { window.mouseLocationOutsideOfEventStream }
#State var position: NSPoint = NSPoint.init()
var body: some View {
GeometryReader { geometry in
VStack {
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)
.rotationEffect(
(self.position.y > 60) ?
.degrees(Double(self.position.x) / 2)
: .degrees(Double(self.position.x) / 2)
)
.rotationEffect(
(self.position.x > 320) ?
.degrees(Double(self.position.y * -1))
: .degrees(Double(self.position.y) / 2)
)
}.frame(width: geometry.size.width, height: geometry.size.height)
.onAppear() {
//Setup
self.window.center();
self.window.setFrameAutosaveName("Main Window")
/* Get mouse location on movement*/
NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
self.position = self.location //Save location
print("mouse location:", Double(self.position.x), Double(self.position.y))
return $0
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
Version 2- kind of works but is buggy:
import SwiftUI
struct ContentView: View {
var window: NSWindow! = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
var mouseLocation: NSPoint { NSEvent.mouseLocation }
var location: NSPoint { window.mouseLocationOutsideOfEventStream }
#State var position: NSPoint = NSPoint.init()
var body: some View {
GeometryReader { geometry in
VStack {
Rectangle()
.fill(Color.red)
.frame(width: 200, height: 200)
.rotationEffect(
(self.position.x > 181) ?
.degrees(Double(self.position.x) / 2)
: .degrees(Double(self.position.x * -1) / 2)
)
.rotationEffect(
(self.position.x > 181) ?
.degrees(Double(self.position.y * -1) / 2)
: .degrees(Double(self.position.y) / 2)
)
}.frame(width: geometry.size.width, height: geometry.size.height)
.onAppear() {
//Setup
self.window.center();
self.window.setFrameAutosaveName("Main Window")
/* Get mouse location on movement*/
NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) {
self.position = self.location //Save location
print("mouse location:", Double(self.position.x), Double(self.position.y))
return $0
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
check this out:
few remarks:
1) i changed it to ios (should be easy to change back to macos because the difference is just the getting of the position
2) i made a gradient to the rectangle so that is clear, in which direction the rectangle rotates
3) i have no idea what you are calculating, but in my opinion you can only do it correctly with trigonometric functions
struct ContentView: View {
#State var position : CGPoint = .zero
#State var offset : CGSize = .zero
#State var translation : CGSize = .zero
#State var angle : Double = 0
func calcAngle(_ geometry: GeometryProxy) -> Angle {
let rectPosition = CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2)
// tan alpha = dx / dy
var alpha : Double
if rectPosition.y == position.y {
alpha = 0
} else {
var dx = (position.x - rectPosition.x)
let dy = (position.y - rectPosition.y)
if dy > 0 {
dx = -dx
}
let r = sqrt(abs(dx * dx) + abs(dy * dy))
alpha = Double(asin(dx / r))
if dy > 0 {
alpha = alpha + Double.pi
}
}
return Angle(radians: alpha)
}
var body: some View {
GeometryReader { geometry in
ZStack {
Rectangle()
.fill(LinearGradient(gradient: Gradient(colors: [Color.red, Color.yellow]), startPoint: UnitPoint.top, endPoint: UnitPoint.bottom))
.frame(width: 200, height: 200)
.position(CGPoint(x: geometry.size.width / 2, y: geometry.size.height / 2))
.rotationEffect(self.calcAngle(geometry), anchor: UnitPoint(x: 0.5, y: 0.5))
Circle().fill(Color.blue).frame(width:20,height:20).position(self.position)
Circle().fill(Color.green).frame(width:20,height:20).position(self.position)
}.frame(width: geometry.size.width, height: geometry.size.height)
.gesture(
DragGesture()
.onChanged { gesture in
self.position = gesture.location
self.translation = gesture.translation
}
.onEnded { _ in
}
)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

Crop image according to rectangle in SwiftUI

I have an Image that I can drag around using DragGesture(). I want to crop the image visible inside the Rectangle area. Here is my code...
struct CropImage: View {
#State private var currentPosition: CGSize = .zero
#State private var newPosition: CGSize = .zero
var body: some View {
VStack {
ZStack {
Image("test_pic")
.resizable()
.scaledToFit()
.offset(x: self.currentPosition.width, y: self.currentPosition.height)
Rectangle()
.fill(Color.black.opacity(0.3))
.frame(width: UIScreen.screenWidth * 0.7 , height: UIScreen.screenHeight/5)
.overlay(Rectangle().stroke(Color.white, lineWidth: 3))
}
.gesture(DragGesture()
.onChanged { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
}
.onEnded { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
self.newPosition = self.currentPosition
})
Button ( action : {
// how to crop the image according to rectangle area
} ) {
Text("Crop Image")
.padding(.all, 10)
.background(Color.blue)
.foregroundColor(.white)
.shadow(color: .gray, radius: 1)
.padding(.top, 50)
}
}
}
}
For easier understanding...
Here is possible approach using .clipShape. Tested with Xcode 11.4 / iOS 13.4
struct CropFrame: Shape {
let isActive: Bool
func path(in rect: CGRect) -> Path {
guard isActive else { return Path(rect) } // full rect for non active
let size = CGSize(width: UIScreen.screenWidth * 0.7, height: UIScreen.screenHeight/5)
let origin = CGPoint(x: rect.midX - size.width / 2, y: rect.midY - size.height / 2)
return Path(CGRect(origin: origin, size: size).integral)
}
}
struct CropImage: View {
#State private var currentPosition: CGSize = .zero
#State private var newPosition: CGSize = .zero
#State private var clipped = false
var body: some View {
VStack {
ZStack {
Image("test_pic")
.resizable()
.scaledToFit()
.offset(x: self.currentPosition.width, y: self.currentPosition.height)
Rectangle()
.fill(Color.black.opacity(0.3))
.frame(width: UIScreen.screenWidth * 0.7 , height: UIScreen.screenHeight/5)
.overlay(Rectangle().stroke(Color.white, lineWidth: 3))
}
.clipShape(
CropFrame(isActive: clipped)
)
.gesture(DragGesture()
.onChanged { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
}
.onEnded { value in
self.currentPosition = CGSize(width: value.translation.width + self.newPosition.width, height: value.translation.height + self.newPosition.height)
self.newPosition = self.currentPosition
})
Button (action : { self.clipped.toggle() }) {
Text("Crop Image")
.padding(.all, 10)
.background(Color.blue)
.foregroundColor(.white)
.shadow(color: .gray, radius: 1)
.padding(.top, 50)
}
}
}
}
Thanks to Asperi's answer, I have implement a lightweight swiftUI library to crop image.Here is the library and demo. Demo
The magic is below:
public var body: some View {
GeometryReader { proxy in
// ...
Button(action: {
// how to crop the image according to rectangle area
if self.tempResult == nil {
self.cropTheImageWithImageViewSize(proxy.size)
}
self.resultImage = self.tempResult
}) {
Text("Crop Image")
.padding(.all, 10)
.background(Color.blue)
.foregroundColor(.white)
.shadow(color: .gray, radius: 1)
.padding(.top, 50)
}
}
}
func cropTheImageWithImageViewSize(_ size: CGSize) {
let imsize = inputImage.size
let scale = max(inputImage.size.width / size.width,
inputImage.size.height / size.height)
let zoomScale = self.scale
let currentPositionWidth = self.dragAmount.width * scale
let currentPositionHeight = self.dragAmount.height * scale
let croppedImsize = CGSize(width: (self.cropSize.width * scale) / zoomScale, height: (self.cropSize.height * scale) / zoomScale)
let xOffset = (( imsize.width - croppedImsize.width) / 2.0) - (currentPositionWidth / zoomScale)
let yOffset = (( imsize.height - croppedImsize.height) / 2.0) - (currentPositionHeight / zoomScale)
let croppedImrect: CGRect = CGRect(x: xOffset, y: yOffset, width: croppedImsize.width, height: croppedImsize.height)
if let cropped = inputImage.cgImage?.cropping(to: croppedImrect) {
//uiimage here can write to data in png or jpeg
let croppedIm = UIImage(cgImage: cropped)
tempResult = croppedIm
result = Image(uiImage: croppedIm)
}
}
Hear is code
// Created by Deepak Gautam on 15/02/22.
import SwiftUI
struct ImageCropView: View {
#Environment(\.presentationMode) var pm
#State var imageWidth:CGFloat = 0
#State var imageHeight:CGFloat = 0
#Binding var image : UIImage
#State var dotSize:CGFloat = 13
var dotColor = Color.init(white: 1).opacity(0.9)
#State var center:CGFloat = 0
#State var activeOffset:CGSize = CGSize(width: 0, height: 0)
#State var finalOffset:CGSize = CGSize(width: 0, height: 0)
#State var rectActiveOffset:CGSize = CGSize(width: 0, height: 0)
#State var rectFinalOffset:CGSize = CGSize(width: 0, height: 0)
#State var activeRectSize : CGSize = CGSize(width: 200, height: 200)
#State var finalRectSize : CGSize = CGSize(width: 200, height: 200)
var body: some View {
ZStack {
Image(uiImage: image)
.resizable()
.scaledToFit()
.overlay(GeometryReader{geo -> AnyView in
DispatchQueue.main.async{
self.imageWidth = geo.size.width
self.imageHeight = geo.size.height
}
return AnyView(EmptyView())
})
Text("Crop")
.padding(6)
.foregroundColor(.white)
.background(Capsule().fill(Color.blue))
.offset(y: -250)
.onTapGesture {
let cgImage: CGImage = image.cgImage!
let scaler = CGFloat(cgImage.width)/imageWidth
if let cImage = cgImage.cropping(to: CGRect(x: getCropStartCord().x * scaler, y: getCropStartCord().y * scaler, width: activeRectSize.width * scaler, height: activeRectSize.height * scaler)){
image = UIImage(cgImage: cImage)
}
pm.wrappedValue.dismiss()
}
Rectangle()
.stroke(lineWidth: 1)
.foregroundColor(.white)
.offset(x: rectActiveOffset.width, y: rectActiveOffset.height)
.frame(width: activeRectSize.width, height: activeRectSize.height)
Rectangle()
.stroke(lineWidth: 1)
.foregroundColor(.white)
.background(Color.green.opacity(0.3))
.offset(x: rectActiveOffset.width, y: rectActiveOffset.height)
.frame(width: activeRectSize.width, height: activeRectSize.height)
.gesture(
DragGesture()
.onChanged{drag in
let workingOffset = CGSize(
width: rectFinalOffset.width + drag.translation.width,
height: rectFinalOffset.height + drag.translation.height
)
self.rectActiveOffset.width = workingOffset.width
self.rectActiveOffset.height = workingOffset.height
activeOffset.width = rectActiveOffset.width - activeRectSize.width / 2
activeOffset.height = rectActiveOffset.height - activeRectSize.height / 2
}
.onEnded{drag in
self.rectFinalOffset = rectActiveOffset
self.finalOffset = activeOffset
}
)
Image(systemName: "arrow.up.left.and.arrow.down.right")
.font(.system(size: 12))
.background(Circle().frame(width: 20, height: 20).foregroundColor(dotColor))
.frame(width: dotSize, height: dotSize)
.foregroundColor(.black)
.offset(x: activeOffset.width, y: activeOffset.height)
.gesture(
DragGesture()
.onChanged{drag in
let workingOffset = CGSize(
width: finalOffset.width + drag.translation.width,
height: finalOffset.height + drag.translation.height
)
let changeInXOffset = finalOffset.width - workingOffset.width
let changeInYOffset = finalOffset.height - workingOffset.height
if finalRectSize.width + changeInXOffset > 40 && finalRectSize.height + changeInYOffset > 40{
self.activeOffset.width = workingOffset.width
self.activeOffset.height = workingOffset.height
activeRectSize.width = finalRectSize.width + changeInXOffset
activeRectSize.height = finalRectSize.height + changeInYOffset
rectActiveOffset.width = rectFinalOffset.width - changeInXOffset / 2
rectActiveOffset.height = rectFinalOffset.height - changeInYOffset / 2
}
}
.onEnded{drag in
self.finalOffset = activeOffset
finalRectSize = activeRectSize
rectFinalOffset = rectActiveOffset
}
)
}
.onAppear {
activeOffset.width = rectActiveOffset.width - activeRectSize.width / 2
activeOffset.height = rectActiveOffset.height - activeRectSize.height / 2
finalOffset = activeOffset
}
}
func getCropStartCord() -> CGPoint{
var cropPoint : CGPoint = CGPoint(x: 0, y: 0)
cropPoint.x = imageWidth / 2 - (activeRectSize.width / 2 - rectActiveOffset.width )
cropPoint.y = imageHeight / 2 - (activeRectSize.height / 2 - rectActiveOffset.height )
return cropPoint
}
}
struct TestCrop : View{
#State var imageWidth:CGFloat = 0
#State var imageHeight:CGFloat = 0
#State var image:UIImage
#State var showCropper : Bool = false
var body: some View{
VStack{
Text("Open Cropper")
.font(.system(size: 17, weight: .medium))
.padding(.horizontal, 15)
.padding(.vertical, 10)
.foregroundColor(.white)
.background(Capsule().fill(Color.blue))
.onTapGesture {
showCropper = true
}
Image(uiImage: image)
.resizable()
.scaledToFit()
}
.sheet(isPresented: $showCropper) {
//
} content: {
ImageCropView(image: $image)
}
}
}
struct TestViewFinder_Previews: PreviewProvider {
static var originalImage = UIImage(named: "food")
static var previews: some View {
TestCrop(image: originalImage ?? UIImage())
}
}

SwiftUI - Position a rectangle relative to another view in ZStack

The task
I am creating a bar chart with SwiftUI and I am looking for the best approach to position the average indicator, the horizontal yellow line, shown in the picture below:
The goal
I need to be able to position the yellow line (rectangle with 1 point in height) relative to the bars (also rectangles) of the chart. For simplicity let us imagine that the tallest possible bar is 100 points and the average is 75. My goal is to draw the yellow line exactly 75 points above the bottom of the bars.
The problem
In my example I am placing the line on top of the chart using a ZStack. I can position the line vertically using a GeometryReader, however I do not know how to position it in reference to the bottom of the bars.
Here my playground code:
import SwiftUI
import PlaygroundSupport
struct BarChart: View {
var measurements : [Int]
var pastColor: Color
var todayColor: Color
var futureColor: Color
let labelsColor = Color(UIColor(white: 0.5, alpha: 1.0))
func monthAbbreviationFromInt(_ day: Int) -> String {
let da = Calendar.current.shortWeekdaySymbols
return da[day]
}
var body: some View {
ZStack {
VStack {
HStack {
Rectangle()
.fill(Color.yellow)
.frame(width: 12, height: 2, alignment: .center)
Text("Desired level")
.font(.custom("Helvetica", size: 10.0))
.foregroundColor(labelsColor)
}
.padding(.bottom , 50.0)
HStack {
ForEach(0..<7) { day in
VStack {
Spacer()
Text(String(self.measurements[day]))
.frame(width: CGFloat(40), height: CGFloat(20))
.font(.footnote)
.lineLimit(1)
.minimumScaleFactor(0.5)
.foregroundColor(self.labelsColor)
Rectangle()
.fill(day > 0 ? self.futureColor : self.pastColor)
.frame(width: 20, height: CGFloat(self.measurements[day]*2))
.transition(.slide)
.cornerRadius(3.0)
Text("\(self.monthAbbreviationFromInt(day))\n\(day)")
.multilineTextAlignment(.center)
.font(.footnote)
.frame(height: 20)
.lineLimit(2)
.minimumScaleFactor(0.2)
.foregroundColor(self.labelsColor)
}
.frame(height: 200, alignment: .bottom )
}
}
}
.padding()
VStack {
GeometryReader { geometry in
//Yellow line
Rectangle()
.path(in: CGRect(x: 0,
y: 350, //This is now a random value, but I should detect and use the bottom coordinate of the bars to place the line at exactly 75 points above that coordinate
width: geometry.size.width,
height: 1))
.fill(Color.yellow)
}
}
}
}
}
How can I get the relative position of the bottom of the bars so that I can place the line correctly?
Is there another approach you would suggest?
Thank you in advance
example
import SwiftUI
struct Data: Identifiable {
let id = UUID()
let value: CGFloat
let color: Color
}
struct ContentView: View {
let data = [Data(value: 35, color: .red),
Data(value: 100, color: .green),
Data(value: 70, color: .yellow),
Data(value: 25, color: .blue),
Data(value: 55, color: .orange)]
var maxValue: CGFloat {
data.map { (element) -> CGFloat in
element.value
}.max() ?? 1.0
}
var average: CGFloat {
guard !data.isEmpty else { return 0 }
let sum = data.reduce(into: CGFloat.zero) { (res, data) in
res += data.value
}
return sum / CGFloat(data.count)
}
var body: some View {
HStack {
ForEach(data) { (bar) in
Bar(t: bar.value / self.maxValue, color: bar.color, width: 20)
}
}
.background(Color.pink.opacity(0.1))
.overlay(
GeometryReader { proxy in
Color.primary.frame(height: 1).position(x: proxy.size.width / 2, y: proxy.size.height * ( 1 - self.average / self.maxValue))
}
).padding(.vertical, 100).padding(.horizontal, 20)
}
}
struct Bar: View {
let t: CGFloat
let color: Color
let width: CGFloat
var body: some View {
GeometryReader { proxy in
Path { (path) in
path.move(to: .init(x: proxy.size.width / 2, y: proxy.size.height))
path.addLine(to: .init(x: proxy.size.width / 2, y: 0))
}
.trim(from: 0, to: self.t)
.stroke(lineWidth: self.width)
.foregroundColor(self.color)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
and result
or slightly adopted to show labels