How can I create the metallic rim gradient as well as inner shadow as seen in the image below?
Here's my code:
import SwiftUI
struct GradientDishView: View {
private let containerSize: CGFloat = 300
var body: some View {
VStack {
Spacer()
ZStack {
Canvas { context, size in
let currentCenter = CGPoint(x: containerSize / 2, y: containerSize / 2)
let baseRadius = containerSize / 2
let innerRimRadius = baseRadius * 0.90
let fillRadius = innerRimRadius
let rimWidth = baseRadius - innerRimRadius
// inner fill
let innerPath = Path(ellipseIn: CGRect(
x: currentCenter.x - fillRadius, y: currentCenter.y - fillRadius,
width: fillRadius * 2, height: fillRadius * 2
))
context.fill(innerPath, with: .color(Color.green))
// gradient rim
let rimGradient = Gradient(stops: [
Gradient.Stop(color: Color(white: 0.9), location: 0.0),
Gradient.Stop(color: Color(white: 0.7), location: 0.2),
Gradient.Stop(color: Color(white: 0.9), location: 0.35),
Gradient.Stop(color: Color(white: 0.7), location: 0.45),
Gradient.Stop(color: Color(white: 0.9), location: 0.6),
Gradient.Stop(color: Color(white: 0.7), location: 0.7),
Gradient.Stop(color: Color(white: 0.9), location: 0.85),
Gradient.Stop(color: Color(white: 0.7), location: 0.95),
Gradient.Stop(color: Color(white: 0.9), location: 1.0)
])
let rimCenterRadius = (baseRadius + innerRimRadius) / 2
let rimStrokePath = Path(ellipseIn: CGRect(
x: currentCenter.x - rimCenterRadius,
y: currentCenter.y - rimCenterRadius,
width: rimCenterRadius * 2,
height: rimCenterRadius * 2
))
let gradientStart = CGPoint(x: currentCenter.x - baseRadius,
y: currentCenter.y - baseRadius)
let gradientEnd = CGPoint(x: currentCenter.x + baseRadius,
y: currentCenter.y + baseRadius)
context.stroke(rimStrokePath,
with: .linearGradient(rimGradient, startPoint: gradientStart, endPoint: gradientEnd), lineWidth: rimWidth)
}
.frame(width: containerSize, height: containerSize)
.shadow(color: .black.opacity(0.5), radius: 20, x: 0, y: 0)
}
Spacer()
}
}
}
#Preview {
GradientDishView()
}
I'm using a Canvas as I will have other elements drawn over it later.
Edit: I updated the rimGradient
colors and now I can somewhat get the darker gradients at 2 & 7 o'clock positions. How can I get similar lighter gradients at the opposite positions? I'd also like to get the inner shadow showing.
Edit 2: Updated rimGradient
to use gradient stops and while it's not exactly the same as the image, its a little closer.
You could try using an AngularGradient
for the rim.
As Sweeper pointed out in a comment, the ring also has edges. One way to achieve a similar effect is to add very narrow inner and outer rings as overlays. Then a small blur can be applied to the result.
Putting this together as a view which draws only the ring:
struct SteelRing: View {
let ringWidth: CGFloat
private func gradientRing(colors: [Color], width: CGFloat) -> some View {
AngularGradient(
colors: colors,
center: .center
)
.mask {
Circle()
.strokeBorder(lineWidth: width)
}
}
var body: some View {
ZStack {
gradientRing(
colors: [
Color(white: 0.97),
Color(white: 0.6),
Color(white: 0.92),
Color(white: 0.5),
Color(white: 0.97),
Color(white: 0.6),
Color(white: 0.92),
Color(white: 0.5),
Color(white: 0.97)
],
width: ringWidth
)
gradientRing(
colors: [
Color(white: 0.7),
Color(white: 0.4),
Color(white: 0.7),
Color(white: 1),
Color(white: 0.7),
],
width: 1
)
gradientRing(
colors: [
Color(white: 0.7),
Color(white: 1),
Color(white: 0.7),
Color(white: 0.4),
Color(white: 0.7),
],
width: 1
)
.padding(ringWidth - 1)
}
.blur(radius: 0.5)
.mask {
Circle()
.strokeBorder(lineWidth: ringWidth)
}
}
}
Using this to reproduce the pie chart example:
An AngularGradient
can also be used for the pie chart.
See How to make Inner shadow in SwiftUI for details of how to show an inner shadow.
private let containerSize: CGFloat = 300
private let rimWidth: CGFloat = 10
var body: some View {
ZStack {
SteelRing(ringWidth: rimWidth)
Circle()
.fill(.shadow(.inner(color: Color(white: 0.6), radius: 8)))
.foregroundStyle(
AngularGradient(
stops: [
.init(color: .orange, location: 0),
.init(color: .orange, location: 0.4),
.init(color: .green, location: 0.4),
.init(color: .green, location: 1)
],
center: .center)
)
.rotationEffect(.degrees(-90))
.padding(rimWidth)
}
.frame(width: containerSize, height: containerSize)
.compositingGroup()
.shadow(color: Color(white: 0.6), radius: 10)
}
If the content will be something more elaborate then you will probably want the inner shadow to be a layer above the content, which means the white parts need to be transparent.
The modifier .luminanceToAlpha
can be used to convert black to transparent. So if the shadow is drawn using white shadow color over black fill, the black can be converted to transparent by applying this modifier:
ZStack {
SteelRing(ringWidth: rimWidth)
Image(.image3)
.resizable()
.scaledToFill()
.clipShape(Circle())
.padding(rimWidth)
Circle()
.fill(.shadow(.inner(color: Color(white: 0.4), radius: 8)))
.foregroundStyle(.black)
.luminanceToAlpha()
.padding(rimWidth)
}
.frame(width: containerSize, height: containerSize)
.compositingGroup()
.shadow(color: Color(white: 0.6), radius: 10)