I have two paths I'm trying to animate in one continuous motion (one after the other). I'm drawing a circle and trying to follow that up with a line.
#State private var revealStroke = false
Path { path in
path.addArc(center: CGPoint(x: 100, y: 100), radius: CGFloat(50), startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 360), clockwise: true)
path.addLines([CGPoint(x: 200, y: 100), CGPoint(x: 150, y: 100)])
}
.trim(from: revealStroke ? 0 : 1, to: 1)
.stroke(Color.purple, lineWidth: 3)
.animation(Animation.easeOut(duration: 3))
.onAppear() {
self.revealStroke.toggle()
}
You toggled trim in wrong way that should be done in to:
import SwiftUI
struct ContentView: View {
#State private var startDraw: Bool = Bool()
var body: some View {
VStack(spacing: 30.0) {
Path { path in
path.addArc(center: CGPoint(x: 100, y: 100), radius: 50.0, startAngle: Angle(degrees: 0.0), endAngle: Angle(degrees: 360.0), clockwise: true)
path.addLine(to: CGPoint(x: 200, y: 100))
}
.trim(from: 0.0, to: startDraw ? 1.0 : 0.0)
.stroke(style: StrokeStyle(lineWidth: 10.0, lineCap: .round, lineJoin: .round))
.shadow(color: Color.black.opacity(0.2), radius: 0.0, x: 20, y: 20)
.frame(width: 250, height: 200, alignment: .center)
.background(Color.yellow)
.foregroundColor(Color.purple)
.cornerRadius(10.0)
.animation(Animation.easeOut(duration: 3), value: startDraw)
Button("start") { startDraw.toggle() }.font(Font.body.bold())
}
.shadow(radius: 10.0)
}
}
Related
I have the following simple SwiftUI Path of an arrow which I wish to rotate around it's center:
#State var heading: Double = 0.0
#State var xOffset = 0.0
#State var yOffset = 0.0
var body: some View {
TabView {
NavigationStack {
Path { path in
path.move(to: CGPoint(x: xOffset, y: yOffset))
path.addLine(to: CGPoint(x: xOffset + 0, y: yOffset + 20))
path.addLine(to: CGPoint(x: xOffset + 50, y: yOffset + 0))
path.addLine(to: CGPoint(x: xOffset + 0, y: yOffset - 20))
path.addLine(to: CGPoint(x: xOffset + 0, y: yOffset + 0))
}
.stroke(lineWidth: 3)
.foregroundColor(.red)
.rotationEffect(Angle(degrees: heading), anchor: .center)
.transformEffect(CGAffineTransform(translationX: 80, y: 80))
Slider(value: $heading, in: 0...360, step: 30)
}
}
}
It does however not rotate around its center:
How can I rotate a Path around its center (or around any other relative point on its bounds)?
The Path view greedily takes up all of the space available to it, so the view is much larger than you think, and its center is far away. So when you rotate the path, it moves off the screen. You can see this by adding .background(Color.yellow) to the Path view.
It's easier to manage the rotation of the arrow if you make it an .overlay of another View (Color.clear) and then rotate that View. You can make the view visible by using Color.yellow while tuning, and then position the arrow relative to its parent view. The nice thing about this is that when you rotate the parent view, the arrow will stick to it and rotate predictably.
struct ContentView: View {
#State var heading: Double = 0.0
var body: some View {
TabView {
NavigationStack {
Color.clear
.frame(width: 60, height: 60)
.overlay (
Path { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 0, y: 20))
path.addLine(to: CGPoint(x: 50, y: 0))
path.addLine(to: CGPoint(x: 0, y: -20))
path.addLine(to: CGPoint(x: 0, y: 0))
}
.stroke(lineWidth: 3)
.foregroundColor(.red)
.offset(x: 5, y: 30)
)
.rotationEffect(Angle(degrees: heading), anchor: .center)
Slider(value: $heading, in: 0...360, step: 30)
}
}
}
}
How do I make my code work?
You need to give your Path view a reasonable sized frame. Here I added .frame(width: 60, height: 60) which is big enough to hold your arrow path.
Since you have defined xOffset and yOffset, use them to move the path drawing within its view. Temporarily add a background to your path view with .background(Color.yellow) and then adjust xOffset and yOffset until your arrow is drawing inside of that view. You can then remove this .background.
This has the same effect as the overlay method presented above:
struct ContentView: View {
#State var heading: Double = 0.0
#State var xOffset = 5.0 // here
#State var yOffset = 30.0 // here
var body: some View {
TabView {
NavigationStack {
Path { path in
path.move(to: CGPoint(x: xOffset, y: yOffset))
path.addLine(to: CGPoint(x: xOffset + 0, y: yOffset + 20))
path.addLine(to: CGPoint(x: xOffset + 50, y: yOffset + 0))
path.addLine(to: CGPoint(x: xOffset + 0, y: yOffset - 20))
path.addLine(to: CGPoint(x: xOffset + 0, y: yOffset + 0))
}
.stroke(lineWidth: 3)
.foregroundColor(.red)
// .background(Color.yellow)
.frame(width: 60, height: 60) // here
.rotationEffect(Angle(degrees: heading), anchor: .center)
//.transformEffect(CGAffineTransform(translationX: 80, y: 80))
Slider(value: $heading, in: 0...360, step: 30)
}
}
}
}
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 make the lower right corner sharp? the result is shown in the screenshot
here is my code now:
struct Test123: View {
var body: some View {
Text("Hello, World!")
.foregroundColor(.white)
.frame(width: 200, height: 50)
.background(Color.black)
}
}
The possible solution is by using .clipShape by preparing any custom shape you want.
Here a demo (prepared with Xcode 13 / iOS 15)
struct SharpShape: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
let chunk = rect.height * 0.5
path.move(to: .zero)
path.addLine(to: CGPoint(x: rect.width, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: chunk))
path.addLine(to: CGPoint(x: max(rect.width - chunk, rect.width / 2), y: rect.height))
path.addLine(to: CGPoint(x: 0, y: rect.height))
}
}
}
struct Test123: View {
var body: some View {
Text("Hello, World!")
.foregroundColor(.white)
.frame(width: 200, height: 50)
.background(Color.black)
.clipShape(SharpShape())
}
}
I've made a SemiRoundedRectangle shape which I'm using to clipShape a side menu. I need to flip it if the user is in an RTL language, but not sure the best way of achieving this.
I've tried .flipsForRightToLeftLayoutDirection(true) but this flips the actual Arabic text too. When I try rotating the shape it no longer conforms to the Shape protocol and so I can no longer use it in .clipShape. Everything else in SwiftUI just magically flips itself when I switch into Arabic, is there something I could add to my shape to give it these magic powers too?
Thanks for your help :)
import SwiftUI
struct SemiRoundedRectangle: Shape {
var cornerRadius: CGFloat
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.maxX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX+cornerRadius, y: rect.maxY))
path.addArc(center: CGPoint(x: cornerRadius, y: rect.height - cornerRadius),
radius: cornerRadius,
startAngle: .degrees(90),
endAngle: .degrees(180), clockwise: false)
path.addLine(to: CGPoint(x: 0, y: cornerRadius))
path.addArc(center: CGPoint(x: cornerRadius, y: cornerRadius),
radius: cornerRadius,
startAngle: .degrees(180),
endAngle: .degrees(270), clockwise: false)
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
return path
}
}
struct TestView {
var body: some View {
HStack {
Text("ايه الأخبار؟")
.padding()
.background(Color.green)
.clipShape(SemiRoundedRectangle(cornerRadius: 10.0))
Spacer()
}
}
}
Try attaching the clipShape to the Color.green instead:
struct TestView: View {
var body: some View {
HStack {
Text("ايه الأخبار؟")
.padding()
.background(
Color.green /// here!
.clipShape(SemiRoundedRectangle(cornerRadius: 10.0))
.flipsForRightToLeftLayoutDirection(true)
)
Spacer()
}
}
}
Result:
English
RTL Language
There is a generic solution:
extension Shape {
func flipped(_ axis: Axis = .horizontal, anchor: UnitPoint = .center) -> ScaledShape<Self> {
switch axis {
case .horizontal:
return scale(x: -1, y: 1, anchor: anchor)
case .vertical:
return scale(x: 1, y: -1, anchor: anchor)
}
}
}
Use for this case:
struct TestView: View {
var body: some View {
HStack {
Text("ايه الأخبار؟")
.padding()
.background(Color.green)
.clipShape(SemiRoundedRectangle(cornerRadius: 20.0).flipped())
Spacer()
}
.padding()
}
}
I need to create a single dashed line. I tried going about it by creating a Rectangle view with a dashed stroke. However, when setting the height of the rectangle to 1, it results in a double line as its showing both the top and bottom borders of the view.
This is the code:
Rectangle()
.fill(Color.clear)
.frame(height: 1, alignment: .bottom)
.overlay(
RoundedRectangle(cornerRadius: 0)
.stroke(style: StrokeStyle(lineWidth: 1, dash: [5]))
.foregroundColor(Color(UIColor.blue))
)
Change the dash value to increase or decrease the number of dash in the line.
struct ContentView: View {
var body: some View {
Line()
.stroke(style: StrokeStyle(lineWidth: 1, dash: [5]))
.frame(height: 1)
}
}
struct Line: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: rect.width, y: 0))
return path
}
}
Result:
Depending on what you want to do, you can do something like this:
VStack {
Path{ path in
path.move(to: CGPoint(x: 20, y: 300))
path.addLine(to: CGPoint(x: 200, y: 300))
}
.stroke(style: StrokeStyle( lineWidth: 10, dash: [5]))
.foregroundColor(Color(UIColor.blue))
}
You will get something like this:
Improved #kazi.munshimun solution. Vetical line and horizontal line:
struct VLine: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: CGPoint(x: rect.midX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
}
}
}
struct HLine: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: CGPoint(x: rect.minX, y: rect.midY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
}
}
}
Usage:
VLine().stroke(style: StrokeStyle(lineWidth: 1, dash: [5]))
HLine().stroke(style: StrokeStyle(lineWidth: 1, dash: [5]))
Here is an ultimate way for you to add and draw lines with more easier options:
struct CustomLineShapeWithAlignment: Shape {
let stratPoint: Alignment
let endPoint: Alignment
init(stratPoint: Alignment, endPoint: Alignment) {
self.stratPoint = stratPoint
self.endPoint = endPoint
}
private func cgPointTranslator(alignment: Alignment, rect: CGRect) -> CGPoint {
switch alignment {
case .topLeading: return CGPoint(x: rect.minX, y: rect.minY)
case .top: return CGPoint(x: rect.midX, y: rect.minY)
case .topTrailing: return CGPoint(x: rect.maxX, y: rect.minY)
case .leading: return CGPoint(x: rect.minX, y: rect.midY)
case .center: return CGPoint(x: rect.midX, y: rect.midY)
case .trailing: return CGPoint(x: rect.maxX, y: rect.midY)
case .bottomLeading: return CGPoint(x: rect.minX, y: rect.maxY)
case .bottom: return CGPoint(x: rect.midX, y: rect.maxY)
case .bottomTrailing: return CGPoint(x: rect.maxX, y: rect.maxY)
default: return CGPoint(x: rect.minX, y: rect.minY)
}
}
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: cgPointTranslator(alignment: stratPoint, rect: rect))
path.addLine(to: cgPointTranslator(alignment: endPoint, rect: rect))
}
}
}
use case:
struct ContentView: View {
var body: some View {
CustomLineShapeWithAlignment(stratPoint: .top, endPoint: .bottom)
.stroke(style: StrokeStyle(lineWidth: 1.0, dash: [5]))
.background(Color.red)
ZStack {
CustomLineShapeWithAlignment(stratPoint: .top, endPoint: .bottom)
.stroke(style: StrokeStyle(lineWidth: 1.0, dash: [5]))
.frame(width: 1.0)
CustomLineShapeWithAlignment(stratPoint: .leading, endPoint: .trailing)
.stroke(style: StrokeStyle(lineWidth: 1.0, dash: [5]))
.frame(height: 1.0)
}
.background(Color.gray)
CustomLineShapeWithAlignment(stratPoint: .topLeading, endPoint: .bottomTrailing)
.stroke(style: StrokeStyle(lineWidth: 1.0, dash: [5]))
.background(Color.blue)
}
}
result:
import SwiftUI
public struct DashedDivider: View {
private let overlayColor: Color
public init(_ overlayColor: Color = Color(UIColor.systemGray)) {
self.overlayColor = overlayColor
}
public var body: some View {
HLine()
.stroke(style: StrokeStyle(lineWidth: 1, dash: [5]))
.foregroundColor(overlayColor)
.frame(height: 1)
}
}
struct HLine: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.minX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
return path
}
}
struct DashedDivider_Previews: PreviewProvider {
static var previews: some View {
DashedDivider()
}
}