Updating Path in SwiftUI - ios

I have got a SwiftUI View with a chart which is made using Path and data from entries array.
The code:
struct TodayChartView: View {
var body: some View {
GeometryReader { reader in
Path { p in
for i in 1..<entries.count {
p.move(to: CGPoint(x: entries[i-1].x, entries[i-1].y: y))
p.addLine(to: CGPoint(entries[i].x: x, entries[i].y: y))
}
}
}.stroke(Color.blue, style: StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round))
}
}
When user presses the button in another View, a new entry is added to entries and I want to display it on my Path. So, basically, I need to somehow trigger redrawing it. Is there any way to achieve it?

Pass the entries as a parameter to TodayChartView. With the entries as a #State var in ContentView, the TodayChartView gets redrawn whenever entries changes.
Here is a complete example (tested in Xcode 11.7):
struct TodayChartView: View {
let entries: [CGPoint]
var body: some View {
// GeometryReader { reader in
Path { p in
for i in 1..<self.entries.count {
p.move(to: CGPoint(x: self.entries[i-1].x, y: self.entries[i-1].y))
p.addLine(to: CGPoint(x: self.entries[i].x, y: self.entries[i].y))
}
}
.stroke(Color.blue, style: StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round))
// }
}
}
struct ContentView: View {
#State private var entries: [CGPoint] = [CGPoint(x: 150, y: 150)]
var body: some View {
VStack(spacing: 20) {
Button("Add Point") {
let x = CGFloat.random(in: 0...300)
let y = CGFloat.random(in: 0...300)
self.entries.append(CGPoint(x: x, y: y))
}
TodayChartView(entries: self.entries)
.frame(width: 300, height: 300)
}
}
}
Note: Your TodayChartView is not currently using GeometryReader so I've commented it out.

Try this one
Path { p in
for i in 1..<entries.count {
p.move(to: CGPoint(x: entries[i-1].x, entries[i-1].y: y))
p.addLine(to: CGPoint(entries[i].x: x, entries[i].y: y))
}
}
}.stroke(Color.blue, style: StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round))
.id(entries) // << here !!

Related

SwiftUI draw a file icon

I would like to be able to get an image like this, with the word "File" in the center.
But I only managed to do this, you know how to give me a hand.
import SwiftUI
import CoreGraphics
struct File: Shape {
func path(in rect: CGRect) -> Path {
let topsx = CGPoint(x: 0, y: rect.minY)
let topdx = CGPoint(x: rect.maxX/1.5, y: 0)
let bottomsx = CGPoint(x: 0, y: rect.maxY)
let bottomdx = CGPoint(x: rect.maxX, y: rect.maxY)
var path = Path()
path.move(to: bottomdx)
path.addLine(to: bottomdx)
path.addLine(to: bottomsx)
path.addLine(to: topsx)
path.addLine(to: topdx)
return path
}
}
struct ContentView: View {
var body: some View {
ZStack {
File().fill(Color.black)
Text("File")
.font(.largeTitle)
.foregroundColor(.white)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Personally I would not try to draw the icon with code.
Here's a simplified solution with a SF Symbol, but you can just find a nice document icon, download it, add it to your assets and use that instead.
ZStack {
Image(systemName: "doc.fill").resizable()
.scaledToFit()
.foregroundColor(.gray)
.overlay {
Text("File").font(.title).foregroundColor(.white).shadow(color: .black, radius: 10, x: 5, y: 5).offset(x:0, y: 20)
}
}.frame(height:200)

SwiftUI: How to connect more than 2 dots, one by one

I am making an app where I am generating points with random position. I want to connect first point with the second one with the line, and that the second with the third, etc.
My issue is that it is connecting second with the third but in the same time the first is being connected with the third one. And I don't want that. Please help.
This is my code:
struct Dot: Hashable {
var x: CGFloat
var y: CGFloat
}
struct TestShit: View {
#State var lastx = 0.0
#State var lasty = 0.0
#State var dots = [Dot]()
var body: some View {
VStack{
ZStack{
ForEach(dots, id: \.self){ dot in
Circle()
.frame(width: 5, height: 5)
.position(x: dot.x, y: dot.y)
Path { path in
path.move(to: CGPoint(x: lastx, y: lasty))
path.addLine(to: CGPoint(x: dot.x, y: dot.y))
}.stroke(.green, lineWidth: 5)
}
}
Spacer()
Button {
let randx = CGFloat.random(in: 100...300)
let randy = CGFloat.random(in: 100...300)
dots.append(Dot(x: randx, y: randy))
lastx = randx
lasty = randy
} label: {
Text("Button")
}
}
}
}
Here I only maintain 1 path, and append to it each time rather than more than 1 path in the ForEach
import Combine
import SwiftUI
struct Dot: Hashable {
var x: CGFloat
var y: CGFloat
}
class Model: ObservableObject {
#Published
var dots: [Dot] = []
#Published
var path: UIBezierPath?
func addDot(x: CGFloat, y: CGFloat) {
dots.append(Dot(x: x, y: y))
let randomPoint = CGPoint(x: x, y: y)
guard let path = path else {
path = UIBezierPath()
path?.move(to: randomPoint)
path?.lineWidth = 2
return
}
path.addLine(to: CGPoint(x: x, y: y))
}
}
struct TestIt: View {
#ObservedObject
var model: Model
var body: some View {
VStack{
ZStack{
if let path = model.path {
Path(path.cgPath)
.stroke(.green, lineWidth: 5)
}
ForEach(model.dots, id: \.self){ dot in
Circle()
.frame(width: 5, height: 5)
.position(x: dot.x, y: dot.y)
}
}
Spacer()
Button {
let randx = CGFloat.random(in: 100...300)
let randy = CGFloat.random(in: 100...300)
model.addDot(x: randx, y: randy)
} label: {
Text("Button")
}
}
}
}
You might want something a little nicer looking like
Path(path.cgPath.copy(strokingWithWidth: 5, lineCap: .round, lineJoin: .round, miterLimit: 0))
.stroke(.green, lineWidth: 5)
The problem is that with this loop:
Path { path in
path.move(to: CGPoint(x: lastx, y: lasty))
path.addLine(to: CGPoint(x: dot.x, y: dot.y))
}.stroke(.green, lineWidth: 5)
you are creating multiple line segments by moving to the same point, and then adding a line to a different point.
What you want to do is:
set a "start" point
move to that point
for each dot, add a line to the next dot
Take a look at the difference here:
import SwiftUI
struct Dot: Hashable {
var x: CGFloat
var y: CGFloat
}
struct DotsView: View {
// initialize start point
var startX = CGFloat.random(in: 100...300)
var startY = CGFloat.random(in: 100...300)
#State var dots = [Dot]()
var body: some View {
VStack{
ZStack{
Path { path in
// move to start point
path.move(to: CGPoint(x: startX, y: startY))
dots.forEach { dot in
// add line to each dot
path.addLine(to: CGPoint(x: dot.x, y: dot.y))
}
}.stroke(.green, lineWidth: 5)
// draw the start point circle in red
Circle()
.fill(Color.red)
.frame(width: 5, height: 5)
.position(x: startX, y: startY)
// draw each dot circle in blue
ForEach(dots, id: \.self){ dot in
Circle()
.fill(Color.blue)
.frame(width: 5, height: 5)
.position(x: dot.x, y: dot.y)
}
}
Spacer()
Button {
let randx = CGFloat.random(in: 100...300)
let randy = CGFloat.random(in: 100...300)
// add a dot point
dots.append(Dot(x: randx, y: randy))
} label: {
Text("Button")
}
}
}
}

Framming trimmed circle view

I'm fairly new to SwiftUI (worked with UIKit before), and I got this internal, learning/testing project to do, and I ran into this problem.
I have two trimmed circles in a ZStack (and those two texts), in a HStack so I can add that little icon on its right side. How can I trim that bottom, invisible part of the circles so it fits my needs because it pushes everything down?
Edit: I tried clipping and padding with EdgeInsets, but still, changing the frame size pushes everything around. I'm looking for a proper way of doing this, and not some "hack".
screenshot
private struct ActivityCircleComponent: View {
private let widthOfCircle: CGFloat = 15
private let headerIconSize = UIScreen.main.bounds.height * 0.03
private let rotationAmount: Double = 152.5
private let progress: Double = 0.4
private let trimAmount: CGFloat = 40
var body: some View {
GeometryReader { g in
HStack(alignment: .bottom) {
ZStack {
ZStack(alignment: .center) {
Circle()
.trim(from: 0.0, to: 0.65)
.stroke(style: StrokeStyle(lineWidth: self.widthOfCircle, lineCap: .round, lineJoin: .round))
.foregroundColor(.black)
.rotationEffect(Angle(degrees: self.rotationAmount))
Circle()
.trim(from: 0.0, to: self.progress)
.stroke(style: StrokeStyle(lineWidth: self.widthOfCircle, lineCap: .round, lineJoin: .round))
.foregroundColor(.red)
.rotationEffect(Angle(degrees: self.rotationAmount))
}
.padding([.leading, .top, .trailing], self.widthOfCircle / 2)
VStack {
Text("10.000")
.font(Fonts.Roboto.bold.of(size: 30))
Text("meters")
.font(Fonts.Roboto.light.of(size: 20))
}
}
Image("Goal")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: self.headerIconSize)
}
.frame(width: g.size.width, height: g.size.height)
}
}
}
PS Don't mind the state of the code, I'm just playing around
Thanks a bunch!
Here is how you would use the link I provided in my comment. I used .overlay instead of ZStack as you get one view that you don't have to worry about tweaking things when your screen size changes. Easy peasy:
struct ActivityCircleComponent: View {
private let widthOfCircle: CGFloat = 15
private let headerIconSize = UIScreen.main.bounds.height * 0.03
private let progress: Double = 0.4
private let trimAmount: CGFloat = 40
var body: some View {
HStack(alignment: .bottom) {
Arc(startAngle: .degrees(0), endAngle: .degrees(220), clockwise: true)
.stroke(style: StrokeStyle(lineWidth: widthOfCircle, lineCap: .round, lineJoin: .round))
.frame(width: 300, height: 120)
.fixedSize()
.overlay(
Arc(startAngle: .degrees(0), endAngle: .degrees(progress * 220), clockwise: true)
.stroke(style: StrokeStyle(lineWidth: widthOfCircle, lineCap: .round, lineJoin: .round))
.foregroundColor(.red)
.frame(width: 300, height: 120)
.fixedSize()
)
Image(systemName: "rosette")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: self.headerIconSize)
}
.overlay(
VStack {
Text("10.000")
Text("meters")
}
)
}
}
struct Arc: Shape {
var startAngle: Angle
var endAngle: Angle
var clockwise: Bool
func path(in rect: CGRect) -> Path {
let rotationAdjustment = Angle.degrees(200)
let modifiedStart = startAngle - rotationAdjustment
let modifiedEnd = endAngle - rotationAdjustment
var path = Path()
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: rect.width / 2, startAngle: modifiedStart, endAngle: modifiedEnd, clockwise: !clockwise)
return path
}
}

How to erase Path stroke in swiftUI?

I am trying to erase Path stroke by setting blendMode modifier, but no luck there is no clear mode available like in UIGraphicsBeginImageContext is setBlendMode(CGBlendMode.clear).
I tried all modes but no luck, it multiply colours producing some new colours of layers.
I think there will be some mechanism available to clear bottom layers colour,
If you know please post the answer.
Thank you.
my code :
struct Point : Hashable {
let x: CGFloat
let y: CGFloat
init(_ p:CGPoint) {
x = p.x
y = p.y
}
func toCGPoint() -> CGPoint {
CGPoint(x: x, y: y)
}
}
struct line:Hashable {
var line:[Point] = []
var color:Color
var eraseMode:Bool
}
struct editImage: View {
#State private var eraseMode = false
#State private var lines:[line] = [line(color: .red, eraseMode: false)]
var body: some View {
ZStack{
VStack{
Toggle(isOn: $eraseMode) {
Text("Erase Mode")
}.padding(.horizontal)
.onChange(of: eraseMode) {_eraseMode in
lines[lines.count-1].eraseMode = _eraseMode
}
Button(action: {
lines.removeAll()
lines.append(line(color: .green, eraseMode: eraseMode))
}, label: {
Text("Clear Canvas").padding()
})
ZStack{
ForEach(lines,id:\.self){line in
Path{p in
var f = true
line.line.forEach { point in
if f {
p.move(to: point.toCGPoint())
f = false
}else{
p.addLine(to: point.toCGPoint())
}
}
}.stroke(line.color,style: StrokeStyle(lineWidth: 14, lineCap: .round, lineJoin: .bevel, miterLimit: 0, dash: [], dashPhase: 0))
.blendMode(line.eraseMode ? BlendMode.hue : BlendMode.normal)
}
}.frame(width: 300, height: 300)
.background(Color.gray)
.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local).onChanged({ v in
lines[lines.count-1].line.append(.init(v.location))
}).onEnded({ _ in
lines.append(line(color: .red, eraseMode: eraseMode))
}))
}
}
}
}
what i need is in the GIF demo;

How to animate a circle to fill instead of using .trim?

I have this ring style view below, this animation starts at a filled ring of 100% and "unfills" to the trim amount, I'd like to reverse the animation so it starts at 0 and fills to the trim amount.
var progressAnimation: Animation {
Animation
.linear
.speed(0.5)
.delay(0.2)
}
var body: some View {
VStack {
ZStack {
Circle()
.stroke(Color(.systemFill).opacity(0.5), style: StrokeStyle(lineWidth: lineWidth))
.frame(height: 125)
Circle()
.trim(from: CGFloat(getTrimLevelForRingFromExertionLevel()), to: 1)
.stroke(ColorManager.getColorsForMetric(metricType: .Exertion(value: mostRecentEffortLevelObject?.effortLevel ?? 0)),
style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, lineJoin: .round, miterLimit: .infinity, dash: [20, 0], dashPhase: 0))
.animation(self.progressAnimation, value: getTrimLevelForRingFromExertionLevel())
.rotationEffect(Angle(degrees: 90))
.rotation3DEffect(Angle(degrees: 180), axis: (x: 1, y: 0, z: 0))
.frame(height: 125)
Probably you have issue in different code or model, because approach in referenced answer works fine.
Here is a complete demo. Tested with Xcode 12.4 / iOS 14.4
struct DemoView: View {
#State private var progress = 0.0
var body: some View {
VStack {
Circle()
.trim(from: 0.0, to: CGFloat(min(self.progress, 1.0)))
.stroke(Color.blue ,style: StrokeStyle(lineWidth: 25.0, lineCap: .round, lineJoin: .round))
.animation(.linear, value: progress)
.rotationEffect(Angle(degrees: 270.0))
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
progress = 0.73
}
}
}.padding()
}
}

Resources