SwiftUI, create a circle for each CGpoint in Array - swift

I have an array of CGPoint and I'm trying to draw a little circle for each CGPoint location in this array (Using SwiftUI).
I have tried different approaches but have not been successful, I'm looking for some help.
my array : CGPoint is called "allFaceArray"
first try(doesn't working) throws an error : Generic struct 'ForEach' requires that 'CGPoint' conform to 'Hashable'
struct FaceFigure : View {
#ObservedObject var ce : CameraEngine
var size: CGSize
var body: some View {
if !ce.allFaceArray.isEmpty{
ZStack{
ForEach(ce.allFaceArray, id: \.self) { point in
Circle().stroke(lineWidth: 2.0).fill(Color.green)
}
}
}
}
}
second try I make a custom shape
struct MakeCircle: Shape {
var arrayViso: [CGPoint]
func path(in rect: CGRect) -> Path {
var path = Path()
for punti in arrayViso{
let radius = 2
path.addArc(
center: punti,
radius: CGFloat(radius),
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: 360),
clockwise: true)
}
return path
}
}
and I use it like this:
struct FaceFigure : View {
#ObservedObject var ce : CameraEngine
var size: CGSize
var body: some View {
if !ce.allFaceArray.isEmpty{
ZStack{
MakeCircle(arrayViso: ce.allFaceArray)
.stroke(lineWidth: 2.0)
.fill(Color.green)
}
}
}
}
but like this every points is connect each other with a line I don't know why...
thanks for the help

For your second approach, you can create a separate path for each point in your array and add them to the path you already defined as var path = Path(). This way your second approach would work fine
struct MakeCircle: Shape {
var arrayViso: [CGPoint]
func path(in rect: CGRect) -> Path {
var path = Path()
for punti in arrayViso{
var circle = Path()
let radius = 2
circle.addArc(
center: punti,
radius: CGFloat(radius),
startAngle: Angle(degrees: 0),
endAngle: Angle(degrees: 360),
clockwise: true)
path.addPath(circle)
}
return path
}
}

When you find Generic struct 'ForEach' requires that 'CGPoint' conform to 'Hashable', making CGPoint conform to Hashable would be one way to solve it.
Please use this extension and re-try your first try:
extension CGPoint: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(x)
hasher.combine(y)
}
}

The first method can be used. Although you can't directly call ForEach() for CGPoint arrays. Instead you can use ForEach with a Range<T> like follows
struct ContentView: View {
var allFaceArray = [
CGPoint(x: 0, y: 0),
CGPoint(x: 30, y: 50),
CGPoint(x: 100, y: 100),
CGPoint(x: 100, y: 500),
]
var body: some View {
ZStack {
ForEach(0..<allFaceArray.count) { x in //<- Passing the count of the array as a range
Circle().stroke(lineWidth: 2.0).fill(Color.green)
.position(allFaceArray[x])
}
}
}
}
The output of the above code is,

Related

Draw multiple circles in view | ForEach

What I am trying to achieve
I am learning swift. I am trying to draw a line with multiple circle on it.
Then I want to be able to modify the Color of a circle when the user tap the circle.
And eventually do a drag and drop for moving the circle. Such as a custom bezier curve.
Currently, I am stuck on drawing multiple circle on the line.
What I trying to do :
1 - I create an array containing CGPoint
public var PointArray:[CGPoint]=[]
2 - Then doing an Identifiable struct
private struct Positions: Identifiable {
var id: Int
let point: CGPoint
}
3 - With CurveCustomInit, I fill the array. I got this error
No exact matches in call to instance method 'append'
I have done a lot, but I never success at using a ForEach function in one view.
I am using struct shapes as I want to customise shapes and then reuse the component. And also add a gesture function to each circle.
There is the whole code :
Note that a GeometryReader is sending the size of the view
import SwiftUI
public var PointArray:[CGPoint]=[]
public var PointArrayInit:Bool = false
private struct Positions: Identifiable {
var id: Int
let point: CGPoint
}
struct Arc: Shape {
var startAngle: Angle
var endAngle: Angle
var clockwise: Bool
var centerCustom:CGPoint
func path(in rect: CGRect) -> Path {
let rotationAdjustment = Angle.degrees(90)
let modifiedStart = startAngle - rotationAdjustment
let modifiedEnd = endAngle - rotationAdjustment
var path = Path()
path.addArc(center: centerCustom, radius: 20, startAngle: modifiedStart, endAngle: modifiedEnd, clockwise: !clockwise)
return path
}
}
struct CurveCustomInit: Shape {
private var Divider:Int = 10
func path(in rect: CGRect) -> Path {
var path = Path()
let xStep:CGFloat = DrawingZoneWidth / CGFloat(Divider)
let yStep:CGFloat = DrawingZoneHeight / 2
var xStepLoopIncrement:CGFloat = 0
path.move(to: CGPoint(x: 0, y: yStep))
var incr = 0
for _ in 0...Divider {
let Point:CGPoint = CGPoint(x: xStepLoopIncrement, y: yStep)
let value = Positions(id:incr, point:Point )
PointArray.append(value)
path.addLine(to: Point)
xStepLoopIncrement += xStep
incr += 1
}
PointArrayInit = true
return (path)
}
}
struct TouchCurveBasic: View {
var body: some View {
if !PointArrayInit {
// initialisation
CurveCustomInit()
.stroke(Color.red, style: StrokeStyle(lineWidth: 10, lineCap: .round, lineJoin: .round))
.frame(width: 300, height: 300)
.overlay(Arc(startAngle: .degrees(0),endAngle: .degrees(360),clockwise:true).stroke(Color.blue, lineWidth: 10))
} else {
// Update point
}
}
}
struct TouchCurveBasic_Previews: PreviewProvider {
static var previews: some View {
TouchCurveBasic()
}
}
What I am trying to achieve :
Your array PointArray contains values of type CGFloat but you are trying to append a value of type Positions. That's why you're getting this error.

Draw Shape over another Shape

Problem :
I cannot draw a shape on another shape.
What I am trying to achieve :
Draw circles on the line.
Anyway, the circle is shifting the line. I didn't find a way to make it as swift UI seems relatively new. I am currently learning swift and I prefer swift UI rater than storyboard.
If circle and line are different struct, this is because I want to reuse the shape later on.
There is the code :
import SwiftUI
public var PointArray = [CGPoint]()
public var PointArrayInit:Bool = false
struct Arc: Shape {
var startAngle: Angle
var endAngle: Angle
var clockwise: Bool
var centerCustom:CGPoint
func path(in rect: CGRect) -> Path {
let rotationAdjustment = Angle.degrees(90)
let modifiedStart = startAngle - rotationAdjustment
let modifiedEnd = endAngle - rotationAdjustment
var path = Path()
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: 20, startAngle: modifiedStart, endAngle: modifiedEnd, clockwise: !clockwise)
return path
}
}
struct CurveCustomInit: Shape {
private var Divider:Int = 10
func path(in rect: CGRect) -> Path {
var path = Path()
let xStep:CGFloat = DrawingZoneWidth / CGFloat(Divider)
let yStep:CGFloat = DrawingZoneHeight / 2
var xStepLoopIncrement:CGFloat = 0
path.move(to: CGPoint(x: 0, y: yStep))
for _ in 0...Divider {
let Point:CGPoint = CGPoint(x: xStepLoopIncrement, y: yStep)
PointArray.append(Point)
path.addLine(to: Point)
xStepLoopIncrement += xStep
}
PointArrayInit = true
return (path)
}
}
struct TouchCurveBasic: View {
var body: some View {
if !PointArrayInit {
Arc(startAngle: .degrees(0), endAngle: .degrees(360), clockwise: true, centerCustom: CGPoint(x: 50, y: 400))
.stroke(Color.blue, lineWidth: 4)
CurveCustomInit()
.stroke(Color.red, style: StrokeStyle(lineWidth: 10, lineCap: .round, lineJoin: .round))
.frame(width: 300, height: 300)
} else {
}
}
}
struct TouchCurveBasic_Previews: PreviewProvider {
static var previews: some View {
TouchCurveBasic()
}
}
There is what I get :
Here is an other way for you, you can limit the size of drawing with giving a frame or you can use the available size of view without limiting it or even you can use the current limit coming from parent and updated it, like i did on drawing the Line. The method that I used was overlay modifier.
struct ContentView: View {
var body: some View {
ArcView(radius: 30.0)
.stroke(lineWidth: 10)
.foregroundColor(.blue)
.frame(width: 60, height: 60)
.overlay(LineView().stroke(lineWidth: 10).foregroundColor(.red).frame(width: 400))
}
}
struct ArcView: Shape {
let radius: CGFloat
func path(in rect: CGRect) -> Path {
return Path { path in
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: radius, startAngle: Angle(degrees: 0.0), endAngle: Angle(degrees: 360.0), clockwise: true)
}
}
}
struct LineView: Shape {
func path(in rect: CGRect) -> Path {
return Path { path in
path.move(to: CGPoint(x: rect.minX, y: rect.midY))
path.addLines([CGPoint(x: rect.minX, y: rect.midY), CGPoint(x: rect.maxX, y: rect.midY)])
}
}
}
result:

How to draw a radial slice of a circle in SwiftUI ? (ie. pie chart like)

I am trying to create a slice of a circle in SwiftUI.
I am playing around with trim but it behaves oddly.
It picks the amount starting from the outmost point of the area.
Such that 0.5 is half a circle ok. But 0.25 of a circle is not a quarter slice (as a naive programming newbie would guess) but rather the shape filled in a non logical way. There also doesnt seem a way to modify this.
It would be sensible to define a Circle by saying Circle(radius: x, angle: y) instead such that angle: 360 would be a full circle filled.
Does one need to program this oneself in SwiftUI?
import SwiftUI
struct CircleView: View {
var body: some View {
let trimValue: CGFloat = 0.25
Circle()
.trim(from: 0, to: trimValue)
.fill(Color.red)
}
}
struct CircleView_Previews: PreviewProvider {
static var previews: some View {
CircleView()
}
}
Using a Path with an arc is the normal way to do this:
struct PieSegment: Shape {
var start: Angle
var end: Angle
func path(in rect: CGRect) -> Path {
var path = Path()
let center = CGPoint(x: rect.midX, y: rect.midY)
path.move(to: center)
path.addArc(center: center, radius: rect.midX, startAngle: start, endAngle: end, clockwise: false)
return path
}
}
struct ContentView: View {
#State var fileData: Data?
var body: some View {
PieSegment(start: .zero, end: .degrees(45))
}
}

Applying gradient to UIBezierPath in SwiftUI Shape

I'm trying to accomplish the following:
struct MultiSegmentPaths: Shape {
func path(in rect: CGRect) -> Path {
let path1 = UIBezierPath()
// apply gradient to path 1
let path2 = UIBezierPath()
// apply gradient to path 2
return Path(path1.append(path2).cgPath)
}
}
Now I thought I could wrap each segment with Path, but that returns some View which means I can't append the two together.
And applying a gradient directly to UIBezierPath requires having access to context - which isn't passed in via the path(CGRect) -> Path method.
Finally the reason for creating the two paths within the same Shape is because I need to scale them while maintaining their offsets to each other.
The UIBezierPath(UIKIt) and Path(SwiftUI) are different things... To create Path you have to use its own instruments. Next, the fill operation (w/ gradient or anything) is drawing time operation, not part of path/shape creation.
Here is some example of combining to shapes filled by gradients. Tested with Xcode 11.4.
struct DemoShape1: Shape {
func path(in rect: CGRect) -> Path {
return Path { path in
path.addRect(rect)
}
}
}
struct DemoShape2: Shape {
func path(in rect: CGRect) -> Path {
return Path { path in
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: rect.size.height/2, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 360), clockwise: true)
}
}
}
struct DemoCombinedShape: View {
var gradient1 = LinearGradient(gradient: Gradient(colors:[.blue, .yellow]), startPoint: .top, endPoint: .bottom)
var gradient2 = LinearGradient(gradient: Gradient(colors:[.red, .black]), startPoint: .leading, endPoint: .trailing)
var body: some View {
// both shapes are rendered in same coordinate space
DemoShape1().fill(gradient1)
.overlay(DemoShape2().fill(gradient2))
}
}
struct TestCombinedShape_Previews: PreviewProvider {
static var previews: some View {
DemoCombinedShape()
.frame(width: 400, height: 300)
}
}

Simple SwiftUI Arc endAngle animation not working, only jump from one state to another

I'm trying to make a simple animation of the appearance of the arc.
This is very simple, and I don't have any idea why is not working.
I make this struct
struct Arc: Shape {
var center: CGPoint
var radius: CGFloat
var endAngle: Double
func path(in rect: CGRect) -> Path {
var path = Path()
path.addArc(center: center, radius: radius, startAngle: .degrees(180), endAngle: .degrees(endAngle), clockwise: false)
return path
}
}
And after that load it like this
struct TestView: View {
#State var endAngle: Double = 180
var body: some View {
Arc(center: CGPoint(x: 250, y: 250), radius: 100, endAngle: self.endAngle)
.stroke(Color.orange, lineWidth: 5)
.onAppear() {
withAnimation(Animation.linear(duration: 20)) {
self.endAngle = 0
}
}
}
}
But is not animate, only jump from 180 to 0.
I try OnTapGesture too, but also dosn't work.
I don't now, why this dosn't work
My ContentView is simple
struct ContentView: View {
var body: some View {
TestView()
}
}
How can I fix it?
Just add animatable data to your Arc, to indicate which parameter should be animated, as below
Tested with Xcode 11.4 / iOS 13.4
struct Arc: Shape {
var center: CGPoint
var radius: CGFloat
var endAngle: Double
var animatableData: CGFloat { // << here !!
get { CGFloat(endAngle) }
set { endAngle = Double(newValue) }
}
func path(in rect: CGRect) -> Path {
var path = Path()
path.addArc(center: center, radius: radius, startAngle: .degrees(180), endAngle: .degrees(endAngle), clockwise: false)
return path
}
}