I had this use case where I wanted to create a scope-like view that the user could drag across the screen. The background of the screen is an image, and the part of the image is only visible through the scope when the user drags on the view. When the user lifts their finger, the scope disappears.
Failing with ZStack
My initial approach was to use a ZStack
to layer the scope view on top of the background image, but I could not quite get it to work as I wanted.
Masking the Image
Then I stumbled upon a different solution using the mask(alignment:_:)
modifier:
func mask<Mask>(
alignment: Alignment = .center,
@ViewBuilder _ mask: () -> Mask
) -> some View where Mask : View
The mask
modifier allows us to apply a shape or image as a mask to a view, hiding everything outside of the mask and revealing only the masked area. This is perfect for creating a scope-like view that the user can drag around the screen.
struct ScopeView: View {
@State private var scopePosition = CGPoint(x: 0, y: 0)
@State private var isDragging = false
var body: some View {
Image("Landscape")
.resizable()
.ignoresSafeArea()
.mask(
Circle()
.frame(width: 250, height: 250)
.position(scopePosition)
.opacity(isDragging ? 1 : 0)
)
}
}
First, I create a Circle
shape with a fixed frame size of 250 x 250 points. I then position this circle using the scopePosition
variable, which updates as the user drags the circle around the screen.
I add an opacity
to the circle, setting it to 1 when the user is dragging the circle and 0 when they are not. This creates a transition between the visible and hidden states of the circle.
Next, I apply the mask
modifier to the background image, passing in the Circle shape
. This masks the image, revealing only the area inside the circle!
Playing with DragGesture
To achieve the dragging part, I used a DragGesture
in SwiftUI. It allows to drag across the screen and tracks the movement of the user's fingers.
I added the DragGesture
to the background image and set the minimumDistance
to 0, so the circle shows as soon as the user taps on the screen.
I then added two gesture handlers, onChanged
updates the scopePosition
as the user drags the circle around the screen from the location value. The onEnded
handler sets the isDragging
state variable back to false, which hides the circle again.
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
scopePosition = value.location
isDragging = true
}
.onEnded { _ in
isDragging = false
}
)
Creating the Scope View
Here's the final code for a simple scope view using the mask
modifier and DragGesture
:
struct ScopeView: View {
@State private var scopePosition = CGPoint(x: 0, y: 0)
@State private var isDragging = false
var body: some View {
Image("Landscape")
.resizable()
.ignoresSafeArea()
.mask(
Circle()
.frame(width: 250, height: 250)
.position(scopePosition)
.opacity(isDragging ? 1 : 0)
)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
scopePosition = value.location
isDragging = true
}
.onEnded { _ in
isDragging = false
}
)
}
}
Here's a video showing the scope view:
Conclusion
And that's it! With just a few lines of code, I created a simple scope view that the user can drag around the screen. While it took me way more time than I anticipated to get to the mask
modifier solution and waste hours using ZStack
, I am glad I got it working.
There are many ways to customise and extend this basic implementation, but this gives me a good starting point. Happy coding!