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.
Related
I would like to connect two SwiftUI Views with an arrow, one pointing to the other. For example, in this View:
struct ProfileIconWithPointer: View {
var body: some View {
VStack {
Image(systemName: "person.circle.fill")
.padding()
.font(.title)
Text("You")
.offset(x: -20)
}
}
}
I have a Text view and an Image view. I would like to have an arrow pointing from one to the other, like this:
However, all the solutions that I could find rely on knowing the position of each element apriori, which I'm not sure SwiftUI's declarative syntax allows for.
you can use the following code to display an arrow.
struct CurvedLine: Shape {
let from: CGPoint
let to: CGPoint
var control: CGPoint
var animatableData: AnimatablePair<CGFloat, CGFloat> {
get {
AnimatablePair(control.x, control.y)
}
set {
control = CGPoint(x: newValue.first, y: newValue.second)
}
}
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: from)
path.addQuadCurve(to: to, control: control)
let angle = atan2(to.y - control.y, to.x - control.x)
let arrowLength: CGFloat = 15
let arrowPoint1 = CGPoint(x: to.x - arrowLength * cos(angle - .pi / 6), y: to.y - arrowLength * sin(angle - .pi / 6))
let arrowPoint2 = CGPoint(x: to.x - arrowLength * cos(angle + .pi / 6), y: to.y - arrowLength * sin(angle + .pi / 6))
path.move(to: to)
path.addLine(to: arrowPoint1)
path.move(to: to)
path.addLine(to: arrowPoint2)
return path
}
}
you can use this in your code as follows.
struct ContentView: View {
var body: some View {
VStack {
Text("You")
.alignmentGuide(.top) { d in
d[.bottom] - 30
}
Image(systemName: "person.circle.fill")
.padding()
.font(.title)
}
.overlay(
CurvedLine(from: CGPoint(x: 50, y: 50),
to: CGPoint(x: 300, y: 200),
control: CGPoint(x: 100, y: -300))
.stroke(Color.blue, lineWidth: 4)
)
}
}
This will looks like as follows.
Depend on your scenario modify the code. Hope this will helps you.
I want to be able to drag an arrow shape around on top of an image in swiftUI. I have tried to simplify it and limit it to a rectangle here in the beginning. I have taken some inspiration from this post: Limit drag to circular bounds in SwiftUI
And this is my take on it ATM. I hope someone can point me in the right direction :)
So imagine that the gray rectangle is the image and the blue one is the arrow, then I only want to be able to drag the blue rectangle inside the gray rectangle.
struct ImageView: View {
#State private var position = CGPoint(x: 100, y: 100)
private var rectSize: CGFloat = 600
var body: some View {
VStack {
Text("Current postion = (x: \(Int(position.x)), y: \(Int(position.y))")
Rectangle()
.fill(.gray)
.frame(width: rectSize, height: rectSize)
.overlay(
Rectangle()
.fill(.blue)
.frame(width: rectSize / 4, height: rectSize / 4)
.position(x: position.x, y: position.y)
.gesture(
DragGesture()
.onChanged({ value in
let currentLocation = value.location
let center = CGPoint(x: self.rectSize/2, y: self.rectSize/2)
let distance = center.distance(to: currentLocation)
if distance > self.rectSize / 2 {
let const = (self.rectSize / 2) / distance
let newLocX = (currentLocation.x - center.x) * const+center.x
let newLocY = (currentLocation.y - center.y) * const+center.y
self.position = CGPoint(x: newLocX, y: newLocY)
} else {
self.position = value.location
}
})
)
)
}
}
}
You're almost there. As you're outer shape is a rect, you don't need to calculate complicated distances. It's enough to check whether the position is still in the outer rect, and limit drag to its size values (0...rectSize). If you don't want your selection frame to move over the borders, you have to correct by 1/2 of its size (rectSize / 4 / 2)
struct ContentView: View {
#State private var position = CGPoint(x: 100, y: 100)
private var rectSize: CGFloat = 350
var body: some View {
VStack {
Text("Current postion = (x: \(Int(position.x)), y: \(Int(position.y))")
Rectangle()
.fill(.gray)
.frame(width: rectSize, height: rectSize)
.overlay(
Rectangle()
.fill(.clear)
.border(.blue, width: 2.0)
.contentShape(Rectangle())
.frame(width: rectSize / 4, height: rectSize / 4)
.position(position)
.gesture(
DragGesture()
.onChanged { value in
// limit movement to min and max value
let limitedX = max(min(value.location.x, rectSize - rectSize / 8), rectSize / 8)
let limitedY = max(min(value.location.y, rectSize - rectSize / 8), rectSize / 8)
self.position = CGPoint(x: limitedX,
y: limitedY)
}
)
)
}
}
}
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 have a custom Shape in SwiftUI that is a series of CGPoints that form different lines.
I want to be able to draw a border around each individual path and right now the only thing I can think of is creating another Shape that takes the original CGPoints and adds x,y padding to each point but I wanted to see if there was another way that perhaps is built-in that I couldn't find.
For example, my shape is
-----
|
|
and I want to add a border Shape so I can detect when someone is in a bound where the # represents a new possible Shape around the original paths.
########
#----- #
######
#|#
#|#
#
Is there a way to implement this that is built in?
edit: code I'm currently thinking about to draw the border, very early but gives a gist of what I'm thinking
struct DrawShapeBorder: Shape {
var points: [CGPoint]
func path(in rect: CGRect) -> Path {
var path: Path = Path()
guard let startingPoint = points.first else {
return path
}
// go up
let upStartingPoint = CGPoint(x: startingPoint.x, y: startingPoint.y + 5)
path.move(to: upStartingPoint)
for pointLocation in points {
path.addLine(to: pointLocation)
}
return path
}
}
As #Evergreen suggested in the comments, you can use the stroke modifier... but the tricky thing is that you can only apply one of these, so you can't do .stroke().stroke().stroke(), unlike .shadow().shadow().shadow().
However, you can work around this by using a ZStack containing 2 copies of the shape, then adding a different stroke to each of them.
struct ContentView: View {
var body: some View {
DrawShapeBorder(points: [
CGPoint(x: 100, y: 150),
CGPoint(x: 300, y: 100),
CGPoint(x: 300, y: 200),
CGPoint(x: 100, y: 200)
])
.stroked()
}
}
struct DrawShapeBorder: Shape {
/// add the double border
func stroked() -> some View {
ZStack {
self.stroke(Color.red, style: StrokeStyle(lineWidth: 20, lineCap: .round, lineJoin: .round))
self.stroke(Color.white, style: StrokeStyle(lineWidth: 10, lineCap: .round, lineJoin: .round))
}
}
var points: [CGPoint]
func path(in rect: CGRect) -> Path {
var path: Path = Path()
guard let startingPoint = points.first else {
return path
}
// go up
let upStartingPoint = CGPoint(x: startingPoint.x, y: startingPoint.y + 5)
path.move(to: upStartingPoint)
for pointLocation in points {
path.addLine(to: pointLocation)
}
return path
}
}
Result:
No, there is no built-in function for this.
A very 'bug bug' fix for this is a very strong shadow repeated several times.
Text()
.shadow().shadow().shadow().shadow().shadow().shadow().shadow().shadow().shadow().shadow().shadow()
Yes, not ideal but it should work.
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))