How to fill a path with a radial gradient in SwiftUI - swift

I'm trying to fill a path with a radial gradient, but no matter what I try, it always ends up being one solid color (as in the image below). It should transition from red to blue.
How do you fill a path with a radial gradient in SwiftUI? What am I doing wrong?
Thank you!
struct ContentView: View {
var body: some View {
Rectangle()
.foregroundColor(Color.black)
Path { path in
path.addArc(center: CGPoint(x: 200, y: 200), radius: 100, startAngle: .degrees(0), endAngle: .degrees(360), clockwise: true)
}
.fill(RadialGradient(
gradient: Gradient(colors: [.red, .blue]),
center: UnitPoint(x: 200.0, y: 200),
startRadius: 20.0,
endRadius: 100.0
))
.edgesIgnoringSafeArea(.all)
}
}

As it turns out, UnitPoint's x and y are values between 0 and 1. Some simple math solved the problem.
This works:
struct ContentView: View {
private let x: Double = 200.0
private let y: Double = 200.0
private let radius: CGFloat = 100.0
var body: some View {
GeometryReader { geo in
Rectangle()
.foregroundColor(Color.black)
Path { path in
path.addArc(center: CGPoint(x: self.x, y: self.y), radius: self.radius, startAngle: .degrees(0), endAngle: .degrees(360), clockwise: true)
}
.fill(RadialGradient(
gradient: Gradient(colors: [.red, .blue]),
center: UnitPoint(
x: CGFloat(self.x / Double(geo.size.width)),
y: CGFloat(self.y / Double(geo.size.height))
),
startRadius: self.radius * 0.30,
endRadius: self.radius
))
}
}
}

Related

Stroking CGPath in swift

I'm trying to add a stroke to `CGPath` in a macOS app with the code below. The issue is that the stroked path can't be filled properly. With the current method, I'm adding the original path on top of the stroked path. This causes the overlapping parts to remain unfilled.
func getPreviewPath(path:CGPath, strokeWidth: CGFloat) ->CGPath{
let window = NSWindow()
let context = NSGraphicsContext(window: window).cgContext
context.setLineCap(CGLineCap(rawValue: 0)!)
context.setLineWidth(strokeWidth)
context.setLineJoin(.bevel)
context.addPath(path)
context.replacePathWithStrokedPath()
context.addPath(path)
return context.path!
}
I've read that this is caused by the even-odd fill rule. But there doesn't seem to be a way to change this.
Here's a minimal example using the stroke() method:
struct ContentView: View {
var body: some View {
Path { path in
path.move(to: CGPoint(x: 20, y: 20))
path.addLine(to: CGPoint(x: 50, y: 100))
path.addLine(to: CGPoint(x: 80, y: 20))
path.addLine(to: CGPoint(x: 120, y: 20))
}
.stroke(style: StrokeStyle(lineWidth: 12.0, lineCap: .round, lineJoin: .bevel))
.padding()
}
}
And this is the result:
Update
Her is a slightly more complex example. It first creates a path containing a stroked path. This path is then filled. The result is the same.
struct ContentView: View {
var body: some View {
Path(createPath())
.fill(.black)
.padding()
}
func createPath() -> CGPath {
let path = CGMutablePath()
path.move(to: CGPoint(x: 20, y: 20))
path.addLine(to: CGPoint(x: 50, y: 100))
path.addLine(to: CGPoint(x: 80, y: 20))
path.addLine(to: CGPoint(x: 120, y: 20))
return path.copy(strokingWithWidth: 12.0, lineCap: .round, lineJoin: .bevel, miterLimit: 10)
}
}

How to implement the same iOS 16 lock screen circular widget myself?

I'm trying to implement lock screen widget myself
Widget I currently implement
I want to implement this ios16 lock screen widget
I've made almost everything, but I haven't been able to implement the small circle's transparent border.
I couldn't find a way to make even the background of the ring behind it transparent.
My code
struct RingTipShape: Shape { // small circle
var currentPercentage: Double
var thickness: CGFloat
func path(in rect: CGRect) -> Path {
var path = Path()
let angle = CGFloat((240 * currentPercentage) * .pi / 180)
let controlRadius: CGFloat = rect.width / 2 - thickness / 2
let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
let x = center.x + controlRadius * cos(angle)
let y = center.y + controlRadius * sin(angle)
let pointCenter = CGPoint(x: x, y: y)
path.addEllipse(in:
CGRect(
x: pointCenter.x - thickness / 2,
y: pointCenter.y - thickness / 2,
width: thickness,
height: thickness
)
)
return path
}
var animatableData: Double {
get { return currentPercentage }
set { currentPercentage = newValue }
}
}
struct RingShape: Shape {
var currentPercentage: Double
var thickness: CGFloat
func path(in rect: CGRect) -> Path {
var path = Path()
path.addArc(center: CGPoint(x: rect.width / 2, y: rect.height / 2), radius: rect.width / 2 - (thickness / 2), startAngle: Angle(degrees: 0), endAngle: Angle(degrees: currentPercentage * 240), clockwise: false)
return path.strokedPath(.init(lineWidth: thickness, lineCap: .round, lineJoin: .round))
}
var animatableData: Double {
get { return currentPercentage}
set { currentPercentage = newValue}
}
}
struct CircularWidgetView: View { // My customizing widget view
#State var percentage: Double = 1.0
var body: some View {
GeometryReader { geo in
ZStack {
RingBackgroundShape(thickness: 5.5)
.rotationEffect(Angle(degrees: 150))
.frame(width: geo.size.width, height: geo.size.height)
.foregroundColor(.white.opacity(0.21))
RingShape(currentPercentage: 0.5, thickness: 5.5)
.rotationEffect(Angle(degrees: 150))
.frame(width: geo.size.width, height: geo.size.height)
.foregroundColor(.white.opacity(0.385))
RingTipShape(currentPercentage: 0.5, thickness: 5.5)
.rotationEffect(Angle(degrees: 150))
.frame(width: geo.size.width, height: geo.size.height)
.foregroundColor(.white)
/*
I want to make RingTipShape completely
transparent. Ignoring even the RingShape behind it
*/
VStack(spacing: 4) {
Image(systemName: "scooter")
.resizable()
.frame(width: 24, height: 24)
Text("hello")
.font(.system(size: 10, weight: .semibold))
.lineLimit(1)
.minimumScaleFactor(0.1)
}
}
}
}
}
How can I make a transparent border that also ignores the background of the view behind it?
This is a great exercise. The missing piece is a mask.
Note: Despite the fact that there are numerous ways to improve the existing code, I will try to stick to the original solution since the point is to gain experience through practice (based on the comments). However I will share some tips at the end.
So we can think of it in two steps:
We need some way to make another RingTipShape at the same (centered) position as our existing but a bit larger.
We need to find a way to create a mask that removes only that shape from other content (in our case the track rings)
The first point is an easy one, we just need to define the outer thickness in order to place the ellipse on top of the track at the correct location:
struct RingTipShape: Shape { // small circle
//...
let outerThickness: CGFloat
//...
let controlRadius: CGFloat = rect.width / 2 - outerThickness / 2
//...
}
then our existing code changes to:
RingTipShape(currentPercentage: percentage, thickness: 5.5, outerThickness: 5.5)
now for the second part we need something to create a larger circle, which is easy:
RingTipShape(currentPercentage: percentage, thickness: 10.0, outerThickness: 5.5)
ok so now for the final part, we are going to use this (larger) shape to create a kind of inverted mask:
private var thumbMask: some View {
ZStack {
Color.white // This part will be transparent
RingTipShape(currentPercentage: percentage, thickness: 10.0, outerThickness: 5.5)
.fill(Color.black) // This will be masked out
.rotationEffect(Angle(degrees: 150))
}
.compositingGroup() // Rasterize the shape
.luminanceToAlpha() // Map luminance to alpha values
}
and we apply the mask like this:
RingShape(currentPercentage: percentage, thickness: 5.5)
.rotationEffect(Angle(degrees: 150))
.foregroundColor(.white.opacity(0.385))
.mask(thumbMask)
which results to this:
Some observations/tips:
You don't need the GeometryReader (and all the frame modifiers) in your CircularWidgetView, the ZStack will offer all available space to views.
You can add .aspectRatio(contentMode: .fit) to your image in order to avoid stretching.
You could take advantage of existing apis for making your track shapes.
For example:
struct MyGauge: View {
let value: Double = 0.5
let range = 0.1...0.9
var body: some View {
ZStack {
// Backing track
track().opacity(0.2)
// Value track
track(showsProgress: true)
}
}
private var mappedValue: Double {
(range.upperBound + range.lowerBound) * value
}
private func track(showsProgress: Bool = false) -> some View {
Circle()
.trim(from: range.lowerBound, to: showsProgress ? mappedValue : range.upperBound)
.stroke(.white, style: .init(lineWidth: 5.5, lineCap: .round))
.rotationEffect(.radians(Double.pi / 2))
}
}
would result to:
which simplifies things a bit by utilizing the trim modifier.
I hope that this makes sense.

Unable to change the value of a #State variable in swift?

Swift 5.x iOS 15
Just playing around with an idea and hit a roadblock? Why can I not change the value of this state variable in this code?
struct ContentView: View {
let timer = Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()
#State var rexShape = CGRect(x: 0, y: 0, width: 128, height: 128)
var body: some View {
Arc(startAngle: .degrees(0), endAngle: .degrees(180), clockwise: true)
.stroke(Color.red, lineWidth: 2)
.frame(width: 128, height: 128, alignment: .center)
.onReceive(timer) { _ in
rexShape.height -= 10
}
}
}
struct Arc: Shape {
var startAngle: Angle
var endAngle: Angle
var clockwise: Bool
func path(in rect: CGRect) -> Path {
var path = Path()
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: rect.width / 2, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise)
return path
}
}
The line rexShape.height is telling me height is a get-only property? which makes no sense to me cause rexShape should be a variable? Am I simply losing my mind...
If you pay close attention to the error message, the compiler isn't complaining about rexShape not being mutable, but about CGRect.height not being mutable.
To change the only height or weight of a CGRect, you need to do it via its size property.
rexShape.size.height -= 10

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 change shape color on tap

I would like to implement the logic to change the shape color on tap. I tried to modify the example for changing the shape on tap as follows:
import SwiftUI
struct PathView: View {
#State private var insetAmount: CGFloat = 50
#State private var fillColor: UIColor = UIColor.white
var body: some View {
GeometryReader {
geometry in
ZStack {
Trapezoid(insetAmount: insetAmount, fillColor: fillColor)
.frame(width: 200, height: 100)
.onTapGesture {
self.insetAmount = CGFloat.random(in: 10...90)
let demoColors = [UIColor.blue, UIColor.green, UIColor.red]
self.fillColor = demoColors.randomElement() ?? UIColor.white
}
}.frame(width: geometry.size.width, height: geometry.size.height, alignment: .topLeading)
}
}
}
struct Trapezoid: Shape {
var insetAmount: CGFloat
var fillColor: UIColor
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: rect.maxY))
path.addLine(to: CGPoint(x: insetAmount, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX - insetAmount, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: 0, y: rect.maxY))
path.closeSubpath()
fillColor.setFill()
UIColor.white.setStroke()
path.fill()
return path
}
}
The path, however, is black and the line path.fill() displays a warning:
Result of call to 'fill(style:)' is unused
Does anyone know how to set the color property of shape and change it on tap?
A fill should be applied to Shape.
Here is possible solution (tested with Xcode 12)
struct PathView: View {
#State private var insetAmount: CGFloat = 50
#State private var fillColor: UIColor = UIColor.white
var body: some View {
GeometryReader {
geometry in
ZStack {
Trapezoid(insetAmount: insetAmount)
.fill(Color(fillColor)) // << here !!
.frame(width: 200, height: 100)
.onTapGesture {
self.insetAmount = CGFloat.random(in: 10...90)
let demoColors = [UIColor.blue, UIColor.green, UIColor.red]
self.fillColor = demoColors.randomElement() ?? UIColor.white
}
}.frame(width: geometry.size.width, height: geometry.size.height, alignment: .topLeading)
}
}
}
struct Trapezoid: Shape {
var insetAmount: CGFloat
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: 0, y: rect.maxY))
path.addLine(to: CGPoint(x: insetAmount, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX - insetAmount, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: 0, y: rect.maxY))
path.closeSubpath()
return path
}
}