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?
CodePudding user response:
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 yourCircularWidgetView
, theZStack
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.