SwiftUI: How to draw filled and stroked shape? - swift

In UIKit drawing a stroked and filled path/shape is pretty easy.
Eg, the code below draws a red circle that is stroked in blue.
override func draw(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
let center = CGPoint(x: rect.midX, y: rect.midY)
ctx.setFillColor(UIColor.red.cgColor)
ctx.setStrokeColor(UIColor.blue.cgColor)
let arc = UIBezierPath(arcCenter: center, radius: rect.width/2, startAngle: 0, endAngle: CGFloat.pi * 2, clockwise: true)
arc.stroke()
arc.fill()
}
How does one do this with SwiftUI?
Swift UI seems to support:
Circle().stroke(Color.blue)
// and/or
Circle().fill(Color.red)
but not
Circle().fill(Color.red).stroke(Color.blue) // Value of type 'ShapeView<StrokedShape<Circle>, Color>' has no member 'fill'
// or
Circle().stroke(Color.blue).fill(Color.red) // Value of type 'ShapeView<Circle, Color>' has no member 'stroke'
Am I supposed to just ZStack two circles? That seems a bit silly.

You can also use strokeBorder and background in combination.
Code:
Circle()
.strokeBorder(Color.blue,lineWidth: 4)
.background(Circle().foregroundColor(Color.red))
Result:

You can draw a circle with a stroke border
struct ContentView: View {
var body: some View {
Circle()
.strokeBorder(Color.green,lineWidth: 3)
.background(Circle().foregroundColor(Color.red))
}
}

My workaround:
import SwiftUI
extension Shape {
/// fills and strokes a shape
public func fill<S:ShapeStyle>(
_ fillContent: S,
stroke : StrokeStyle
) -> some View {
ZStack {
self.fill(fillContent)
self.stroke(style:stroke)
}
}
}
Example:
struct ContentView: View {
// fill gradient
let gradient = RadialGradient(
gradient : Gradient(colors: [.yellow, .red]),
center : UnitPoint(x: 0.25, y: 0.25),
startRadius: 0.2,
endRadius : 200
)
// stroke line width, dash
let w: CGFloat = 6
let d: [CGFloat] = [20,10]
// view body
var body: some View {
HStack {
Circle()
// ⭐️ Shape.fill(_:stroke:)
.fill(Color.red, stroke: StrokeStyle(lineWidth:w, dash:d))
Circle()
.fill(gradient, stroke: StrokeStyle(lineWidth:w, dash:d))
}.padding().frame(height: 300)
}
}
Result:

Seems like it's either ZStack or .overlay at the moment.
The view hierarchy is almost identical - according to Xcode.
struct ContentView: View {
var body: some View {
VStack {
Circle().fill(Color.red)
.overlay(Circle().stroke(Color.blue))
ZStack {
Circle().fill(Color.red)
Circle().stroke(Color.blue)
}
}
}
}
Output:
View hierarchy:

Another simpler option just stacking the stroke on top of the fill with the ZStack
ZStack{
Circle().fill()
.foregroundColor(.red)
Circle()
.strokeBorder(Color.blue, lineWidth: 4)
}

For future reference, #Imran's solution works, but you also need to account for stroke width in your total frame by padding:
struct Foo: View {
private let lineWidth: CGFloat = 12
var body: some View {
Circle()
.stroke(Color.purple, lineWidth: self.lineWidth)
.overlay(
Circle()
.fill(Color.yellow)
)
.padding(self.lineWidth)
}
}

I put the following wrapper together based on the answers above. It makes this a bit more easy and the code a bit more simple to read.
struct FillAndStroke<Content:Shape> : View
{
let fill : Color
let stroke : Color
let content : () -> Content
init(fill : Color, stroke : Color, #ViewBuilder content : #escaping () -> Content)
{
self.fill = fill
self.stroke = stroke
self.content = content
}
var body : some View
{
ZStack
{
content().fill(self.fill)
content().stroke(self.stroke)
}
}
}
It can be used like this:
FillAndStroke(fill : Color.red, stroke : Color.yellow)
{
Circle()
}
Hopefully Apple will find a way to support both fill and stroke on a shape in the future.

my 2 cents for stroking and colouring the "flower sample from Apple
(// https://developer.apple.com/documentation/quartzcore/cashapelayer) moved to SwiftUI
extension Shape {
public func fill<Shape: ShapeStyle>(
_ fillContent: Shape,
strokeColor : Color,
lineWidth : CGFloat
) -> some View {
ZStack {
self.fill(fillContent)
self.stroke( strokeColor, lineWidth: lineWidth)
}
}
in my View:
struct CGFlower: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
let width = rect.width
let height = rect.height
stride(from: 0, to: CGFloat.pi * 2, by: CGFloat.pi / 6).forEach {
angle in
var transform = CGAffineTransform(rotationAngle: angle)
.concatenating(CGAffineTransform(translationX: width / 2, y: height / 2))
let petal = CGPath(ellipseIn: CGRect(x: -20, y: 0, width: 40, height: 100),
transform: &transform)
let p = Path(petal)
path.addPath(p)
}
return path
}
}
struct ContentView: View {
var body: some View {
CGFlower()
.fill( .yellow, strokeColor: .red, lineWidth: 5 )
}
}
img:

Building on the previous answer by lochiwei...
public func fill<S:ShapeStyle>(_ fillContent: S,
opacity: Double,
strokeWidth: CGFloat,
strokeColor: S) -> some View
{
ZStack {
self.fill(fillContent).opacity(opacity)
self.stroke(strokeColor, lineWidth: strokeWidth)
}
}
Used on a Shape object:
struct SelectionIndicator : Shape {
let parentWidth: CGFloat
let parentHeight: CGFloat
let radius: CGFloat
let sectorAngle: Double
func path(in rect: CGRect) -> Path { ... }
}
SelectionIndicator(parentWidth: g.size.width,
parentHeight: g.size.height,
radius: self.radius + 10,
sectorAngle: self.pathNodes[0].sectorAngle.degrees)
.fill(Color.yellow, opacity: 0.2, strokeWidth: 3, strokeColor: Color.white)

If we want to have a circle with no moved border effect as we can see doing it by using ZStack { Circle().fill(), Circle().stroke }
I prepared something like below:
First step
We are creating a new Shape
struct CircleShape: Shape {
// MARK: - Variables
var radius: CGFloat
func path(in rect: CGRect) -> Path {
let centerX: CGFloat = rect.width / 2
let centerY: CGFloat = rect.height / 2
var path = Path()
path.addArc(center: CGPoint(x: centerX, y: centerY), radius: radius, startAngle: Angle(degrees: .zero)
, endAngle: Angle(degrees: 360), clockwise: true)
return path
}
}
Second step
We are creating a new ButtonStyle
struct LikeButtonStyle: ButtonStyle {
// MARK: Constants
private struct Const {
static let yHeartOffset: CGFloat = 1
static let pressedScale: CGFloat = 0.8
static let borderWidth: CGFloat = 1
}
// MARK: - Variables
var radius: CGFloat
var isSelected: Bool
func makeBody(configuration: Self.Configuration) -> some View {
ZStack {
if isSelected {
CircleShape(radius: radius)
.stroke(Color.red)
.animation(.easeOut)
}
CircleShape(radius: radius - Const.borderWidth)
.fill(Color.white)
configuration.label
.offset(x: .zero, y: Const.yHeartOffset)
.foregroundColor(Color.red)
.scaleEffect(configuration.isPressed ? Const.pressedScale : 1.0)
}
}
}
Last step
We are creating a new View
struct LikeButtonView: View {
// MARK: - Typealias
typealias LikeButtonCompletion = (Bool) -> Void
// MARK: - Constants
private struct Const {
static let selectedImage = Image(systemName: "heart.fill")
static let unselectedImage = Image(systemName: "heart")
static let textMultiplier: CGFloat = 0.57
static var textSize: CGFloat { 30 * textMultiplier }
}
// MARK: - Variables
#State var isSelected: Bool = false
private var radius: CGFloat = 15.0
private var completion: LikeButtonCompletion?
init(isSelected: Bool, completion: LikeButtonCompletion? = nil) {
_isSelected = State(initialValue: isSelected)
self.completion = completion
}
var body: some View {
ZStack {
Button(action: {
withAnimation {
self.isSelected.toggle()
self.completion?(self.isSelected)
}
}, label: {
setIcon()
.font(Font.system(size: Const.textSize))
})
.buttonStyle(LikeButtonStyle(radius: radius, isSelected: isSelected))
}
}
// MARK: - Private methods
private func setIcon() -> some View {
isSelected ? Const.selectedImage : Const.unselectedImage
}
}
Output (Selected and unselected state):

There are several ways to achieve "fill and stroke" result. Here are three of them:
struct ContentView: View {
var body: some View {
let shape = Circle()
let gradient = LinearGradient(gradient: Gradient(colors: [.orange, .red, .blue, .purple]), startPoint: .topLeading, endPoint: .bottomTrailing)
VStack {
Text("Most modern way (for simple backgrounds):")
shape
.strokeBorder(Color.green,lineWidth: 6)
.background(gradient, in: shape) // Only `ShapeStyle` as background can be used (iOS15)
Text("For simple backgrounds:")
shape
.strokeBorder(Color.green,lineWidth: 6)
.background(
ZStack { // We are pretty limited with `shape` if we need to keep inside border
shape.fill(gradient) // Only `Shape` Views as background
shape.fill(.yellow).opacity(0.4) // Another `Shape` view
//Image(systemName: "star").resizable() //Try to uncomment and see the star spilling of the border
}
)
Text("For any content to be clipped:")
shape
.strokeBorder(Color.green,lineWidth: 6)
.background(Image(systemName: "star").resizable()) // Anything
.clipShape(shape) // clips everything
}
}
}
Also ZStack'ing two shapes (stroked and filled) for some cases is not a bad idea to me.
If you want to use an imperative approach, here is a small Playground example of Canvas view. The tradeoff is that you can't attach gestures to shapes and objects drawn on Canvas, only to Canvas itself.
import SwiftUI
import PlaygroundSupport
struct ContentView: View {
let lineWidth: CGFloat = 8
var body: some View {
Canvas { context, size in
let path = Circle().inset(by: lineWidth / 2).path(in: CGRect(origin: .zero, size: size))
context.fill(path, with: .color(.cyan))
context.stroke(path, with: .color(.yellow), style: StrokeStyle(lineWidth: lineWidth, lineCap: .round, dash: [30,20]))
}
.frame(width: 100, height: 200)
}
}
PlaygroundPage.current.setLiveView(ContentView())

Here are the extensions I use for filling and stroking a shape. None of the other answers allow full customization of the fill and stroke style.
extension Shape {
/// Fills and strokes a shape.
func style<F: ShapeStyle, S: ShapeStyle>(
fill: F,
stroke: S,
strokeStyle: StrokeStyle
) -> some View {
ZStack {
self.fill(fill)
self.stroke(stroke, style: strokeStyle)
}
}
/// Fills and strokes a shape.
func style<F: ShapeStyle, S: ShapeStyle>(
fill: F,
stroke: S,
lineWidth: CGFloat = 1
) -> some View {
self.style(
fill: fill,
stroke: stroke,
strokeStyle: StrokeStyle(lineWidth: lineWidth)
)
}
}
extension InsettableShape {
/// Fills and strokes an insettable shape.
func style<F: ShapeStyle, S: ShapeStyle>(
fill: F,
strokeBorder: S,
strokeStyle: StrokeStyle
) -> some View {
ZStack {
self.fill(fill)
self.strokeBorder(strokeBorder, style: strokeStyle)
}
}
/// Fills and strokes an insettable shape.
func style<F: ShapeStyle, S: ShapeStyle>(
fill: F,
strokeBorder: S,
lineWidth: CGFloat = 1
) -> some View {
self.style(
fill: fill,
strokeBorder: strokeBorder,
strokeStyle: StrokeStyle(lineWidth: lineWidth)
)
}
}

Related

How to implement a slider whose minimum track color is begin from center(value=0) not left in SwiftUI

I want to custom Slider in SwiftUI. Just something like this.
I tried Slider with GeometryReader but it isn't working.
//MARK: Left - Right Balance
GeometryReader { geo in
VStack {
Text("\(String(format: "%.2f", balanceVolume))")
HStack {
Text("L")
Slider(value: $balanceVolume, in: minValue...maxValue, step: 0.1) {editing in
print("editing", editing)
isEditing = editing
if !editing {
player.pan = Float(balanceVolume)
}
}
.tint(.none)
.accentColor(.gray)
Text("R")
}
}
.padding(20)
}
Thank you you all.
I have created a simple custom slider, I hope it helps
Output:
Use:
struct slider: View {
#State var sliderPosition: Float = 50
var body: some View {
SliderView(value: $sliderPosition, bounds: 1...100).padding(.all)
}
}
Code:
struct SliderView: View {
let currentValue: Binding<Float>
let sliderBounds: ClosedRange<Int>
public init(value: Binding<Float>, bounds: ClosedRange<Int>) {
self.currentValue = value
self.sliderBounds = bounds
}
var body: some View {
GeometryReader { geomentry in
sliderView(sliderSize: geomentry.size)
}
}
#ViewBuilder private func sliderView(sliderSize: CGSize) -> some View {
let sliderViewYCenter = sliderSize.height / 2
let sliderViewXCenter = sliderSize.width / 2
ZStack {
RoundedRectangle(cornerRadius: 2)
.fill(Color.gray)
.frame(height: 3)
ZStack {
let sliderBoundDifference = sliderBounds.count
let stepWidthInPixel = CGFloat(sliderSize.width) / CGFloat(sliderBoundDifference)
let thumbLocation = CGFloat(currentValue.wrappedValue) * stepWidthInPixel
// Path between starting point to thumb
lineBetweenThumbs(from: .init(x: sliderViewXCenter, y: sliderViewYCenter), to: .init(x: thumbLocation, y: sliderViewYCenter))
// Thumb Handle
let thumbPoint = CGPoint(x: thumbLocation, y: sliderViewYCenter)
thumbView(position: thumbPoint, value: Float(currentValue.wrappedValue))
.highPriorityGesture(DragGesture().onChanged { dragValue in
let dragLocation = dragValue.location
let xThumbOffset = min(dragLocation.x, sliderSize.width)
let newValue = Float(sliderBounds.lowerBound / sliderBounds.upperBound) + Float(xThumbOffset / stepWidthInPixel)
if newValue > Float(sliderBounds.lowerBound) && newValue < Float(sliderBounds.upperBound + 1) {
currentValue.wrappedValue = newValue
}
})
}
}
}
#ViewBuilder func lineBetweenThumbs(from: CGPoint, to: CGPoint) -> some View {
Path { path in
path.move(to: from)
path.addLine(to: to)
}.stroke(Color.blue, lineWidth: 4)
}
#ViewBuilder func thumbView(position: CGPoint, value: Float) -> some View {
ZStack {
Text(String(round(value)))
.font(.headline)
.offset(y: -20)
Circle()
.frame(width: 24, height: 24)
.foregroundColor(.accentColor)
.shadow(color: Color.black.opacity(0.16), radius: 8, x: 0, y: 2)
.contentShape(Rectangle())
}
.position(x: position.x, y: position.y)
}
}

How can I make a custom BlurMaterial using blur modifier in Swift?

I am trying to over come to making a custom BlurMaterial challenge, as you could read from question I am trying to make a custom BlurMaterial, the idea is simple, I am accessing the all available view and then trying to cut it and blur the cut part, but the issue happens when the blur radius goes up and up, the wired rectangular happens and it seems the blur got issue at edges the yellow rectangular, I think this issue could simply solved, as you can see the logic of codes works almost, need help to solve the wired rectangular issue and blur issue at edges the yellow rectangular when we increases the blur radius.
INFO: the issue of wired rectangular happens in macOS SwiftUI but the blur issue at edges the yellow rectangular happens in iOS and macOS SwiftUI. My target is macOS SwiftUI not just iOS SwiftUI.
struct ContentView: View {
#State private var radius: CGFloat = 8.0
#State private var opacity: CGFloat = 0.2
#State private var accentColorOfBlurMaterial: Color = Color.black.opacity(0.5)
#State private var backgroundColor: Color = Color.green
#State private var offset: CGSize = CGSize.zero
#State private var lastOffset: CGSize = CGSize.zero
#State private var blurSize: CGSize = CGSize(width: 200.0, height: 200.0)
var body: some View {
let baseView = VStack {
Button("Tap") { print("tapped!") }
swift
slider(label: "radius:", value: $radius, range: 0.0...50.0)
slider(label: "opacity:", value: $opacity, range: 0.0...1.0)
}
.padding()
.background(Color.yellow.cornerRadius(10.0))
.padding(50.0)
.background(backgroundColor)
.fixedSize()
return ZStack {
baseView
baseView
.blur(radius: radius)
.frame(width: blurSize.width, height: blurSize.height)
.offset(x: -offset.width, y: -offset.height)
.clipped()
.contentShape(Rectangle())
.overlay(accentColorOfBlurMaterial.opacity(opacity))
.border(Color.black, width: 5.0)
.offset(offset)
.gesture(gesture)
.onChange(of: radius, perform: { newValue in
print("blur radius =", newValue)
})
}
}
var swift: some View { return Image(systemName: "swift").resizable().scaledToFit().frame(width: 300.0).foregroundColor(Color.red) }
func slider(label: String, value: Binding<CGFloat>, range: ClosedRange<CGFloat>) -> some View {
return HStack { Text(label); Spacer(); Slider(value: value, in: range) }
}
private var gesture: some Gesture {
return DragGesture(minimumDistance: .zero, coordinateSpace: .global)
.onChanged { value in
offset = CGSize(width: lastOffset.width + value.translation.width, height: lastOffset.height + value.translation.height)
}
.onEnded { value in
lastOffset = CGSize(width: lastOffset.width + value.translation.width, height: lastOffset.height + value.translation.height)
offset = lastOffset
}
}
}

How can I scale proportionally in view? Swiftui

I would like to make my view scalable. However, when I change the size, the size of the ticks remain. The ticks should also scale down to fit the circle proportionally.
Is there a best practice approach here?
here is my code which I have used:
struct TestView: View {
var body: some View {
GeometryReader { geometry in
ZStack {
Circle()
.fill(Color.gray)
ForEach(0..<60*4) { tick in
Ticks.tick(at: tick)
}
}
}.frame(height: 100)
}
}
struct Ticks{
static func tick(at tick: Int) -> some View {
VStack {
Rectangle()
.fill(Color.primary)
.opacity(tick % 20 == 0 ? 1 : 0.4)
.frame(width: 2, height: tick % 4 == 0 ? 15 : 7)
Spacer()
}.rotationEffect(Angle.degrees(Double(tick)/(60) * 360))
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
Thanks!
You can pass along a scale factor based on the GeometryReader that you already have in place.
struct ContentView: View {
var body: some View {
TestView()
}
}
struct Hand: Shape {
let inset: CGFloat
let angle: Angle
func path(in rect: CGRect) -> Path {
let rect = rect.insetBy(dx: inset, dy: inset)
var p = Path()
p.move(to: CGPoint(x: rect.midX, y: rect.midY))
p.addLine(to: position(for: CGFloat(angle.radians), in: rect))
return p
}
private func position(for angle: CGFloat, in rect: CGRect) -> CGPoint {
let angle = angle - (.pi/2)
let radius = min(rect.width, rect.height)/2
let xPos = rect.midX + (radius * cos(angle))
let yPos = rect.midY + (radius * sin(angle))
return CGPoint(x: xPos, y: yPos)
}
}
struct TickHands: View {
var scale: Double
#State private var dateTime = Date()
private let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
var body: some View {
ZStack {
Hand(inset: 105 * scale, angle: dateTime.hourAngle)
.stroke(style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round))
Hand(inset: 70 * scale, angle: dateTime.minuteAngle)
.stroke(style: StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round))
Hand(inset: 40 * scale, angle: dateTime.secondAngle)
.stroke(Color.orange, style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round))
Circle().fill(Color.orange).frame(width: 10)
}
.onReceive(timer) { (input) in
self.dateTime = input
}
}
}
extension Date {
var hourAngle: Angle {
return Angle (degrees: (360 / 12) * (self.hour + self.minutes / 60))
}
var minuteAngle: Angle {
return Angle(degrees: (self.minutes * 360 / 60))
}
var secondAngle: Angle {
return Angle (degrees: (self.seconds * 360 / 60))
}
}
extension Date {
var hour: Double {
return Double(Calendar.current.component(.hour, from: self))
}
var minutes: Double {
return Double(Calendar.current.component(.minute, from: self))
}
var seconds: Double {
return Double(Calendar.current.component(.second, from: self))
}
}
struct TestView: View {
var clockSize: CGFloat = 500
var body: some View {
GeometryReader { geometry in
ZStack {
let scale = geometry.size.height / 200
TickHands(scale: scale)
ForEach(0..<60*4) { tick in
Ticks.tick(at: tick, scale: scale)
}
}
}.frame(width: clockSize, height: clockSize)
}
}
struct Ticks{
static func tick(at tick: Int, scale: CGFloat) -> some View {
VStack {
Rectangle()
.fill(Color.primary)
.opacity(tick % 20 == 0 ? 1 : 0.4)
.frame(width: 2 * scale, height: (tick % 5 == 0 ? 15 : 7) * scale)
Spacer()
}
.rotationEffect(Angle.degrees(Double(tick)/(60) * 360))
}
}
Note that you may want to change how the scale effect changes the width vs the height -- for example, maybe you want the width to always be 2. Or, perhaps you want to use something like min(1, 2 * scale) to prevent the ticks from going above or below a certain size. But, the principal will be the same (ie using the scale factor). You can also adjust the 200 that I have to something that fits your ideal scaling algorithm.

Limit rectangle to screen edge on drag gesture

I'm just getting started with SwiftUI and I was hoping for the best way to tackle the issue of keeping this rectangle in the bounds of a screen during a drag gesture. Right now it goes off the edge until it reaches the middle of the square (I think cause I'm using CGPoint).
I tried doing some math to limit the rectangle and it succeeds on the left side only but it seems like an awful way to go about this and doesn't account for varying screen sizes. Can anyone help?
struct ContentView: View {
#State private var pogPosition = CGPoint()
var body: some View {
PogSound()
.position(pogPosition)
.gesture(
DragGesture()
.onChanged { value in
self.pogPosition = value.location
// Poor solve
if(self.pogPosition.x < 36) {
self.pogPosition.x = 36
}
}
.onEnded { value in
print(value.location)
}
)
}
}
Here is a demo of possible approach (for any view, cause view frame is read dynamically).
Demo & tested with Xcode 12 / iOS 14
struct ViewSizeKey: PreferenceKey {
static var defaultValue = CGSize.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
value = nextValue()
}
}
struct ContentView: View {
#State private var pogPosition = CGPoint()
#State private var size = CGSize.zero
var body: some View {
GeometryReader { gp in
PogSound()
.background(GeometryReader {
Color.clear
.preference(key: ViewSizeKey.self, value: $0.frame(in: .local).size)
})
.onPreferenceChange(ViewSizeKey.self) {
self.size = $0
}
.position(pogPosition)
.gesture(
DragGesture()
.onChanged { value in
let rect = gp.frame(in: .local)
.inset(by: UIEdgeInsets(top: size.height / 2.0, left: size.width / 2.0, bottom: size.height / 2.0, right: size.width / 2.0))
if rect.contains(value.location) {
self.pogPosition = value.location
}
}
.onEnded { value in
print(value.location)
}
)
.onAppear {
let rect = gp.frame(in: .local)
self.pogPosition = CGPoint(x: rect.midX, y: rect.midY)
}
}.edgesIgnoringSafeArea(.all)
}
}

How to animate shape via state variable in SwiftUI?

Trying to animate right side of rectangle in SwiftUI on tap. But it's not working(
It has ratio state var and it's of animatable type (CGFloat). Totally out of ideas why. Please advise.
struct ContentView: View {
#State private var animated = false
var body: some View {
Bar(ratio: animated ? 0.0 : 1.0).animation(Animation.easeIn(duration: 1))
.onTapGesture {
self.animated.toggle()
}.foregroundColor(.green)
}
}
struct Bar: Shape {
#State var ratio: CGFloat
var animatableData: CGFloat {
get { return ratio }
set { ratio = newValue }
}
func path(in rect: CGRect) -> Path {
var p = Path()
p.move(to: CGPoint.zero)
let width = rect.size.width * ratio
p.addLine(to: CGPoint(x: width, y: 0))
let height = rect.size.height
p.addLine(to: CGPoint(x: width, y: height))
p.addLine(to: CGPoint(x: 0, y: height))
p.closeSubpath()
return p
}
}
It is not needed #State for animatable data, so fix is simple
struct Bar: Shape {
var ratio: CGFloat
Tested with this demo view (on Xcode 11.2 / iOS 13.2)
struct TestAnimateBar: View {
#State private var animated = false
var body: some View {
VStack {
Bar(ratio: animated ? 0.0 : 1.0).animation(Animation.easeIn(duration: 1))
.foregroundColor(.green)
}
.background(Color.gray)
.frame(height: 40)
.onTapGesture {
self.animated.toggle()
}
}
}