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:

0:00
/0:08

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!

String Catalog

String Catalog - App Localization on Autopilot

Push to GitHub, and we'll automatically localize your app for 40+ languages, saving you hours of manual work.

Tagged in: