Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Recreating a masked blur effect in SwiftUI

Tags:

uikit

swiftui

I've created this masked blur effect (code below), it runs in SwiftUI, but uses UIViewRepresentable for the masking, is it possible to re-create the same effect, but just in pure SwiftUI?

Blur with mask

Here's the current code, if you run it, use your finger to drag on the screen, this moves the mask to reveal underneath.

import SwiftUI
import UIKit

struct TestView: View {

  @State var position: CGPoint = .zero

  var simpleDrag: some Gesture {
    DragGesture()
      .onChanged { value in
        self.position = value.location
      }
  }

  var body: some View {
    ZStack {
  
      Circle()
        .fill(Color.green)
        .frame(height: 200)
  
      Circle()
        .fill(Color.pink)
        .frame(height: 200)
        .offset(x: 50, y: 100)
  
      Circle()
        .fill(Color.orange)
        .frame(height: 100)
        .offset(x: -50, y: 00)
  
      BlurView(style: .light, position: $position)
        .frame(maxWidth: .infinity, maxHeight: .infinity)
  
    }
    .gesture(
      simpleDrag
    )

  }
}

struct BlurView: UIViewRepresentable {

  let style: UIBlurEffect.Style

  @Binding var position: CGPoint

  func makeUIView(context: UIViewRepresentableContext<BlurView>) -> UIView {
    let view = UIView(frame: .zero)
    view.backgroundColor = .clear
    let blurEffect = UIBlurEffect(style: style)
    let blurView = UIVisualEffectView(effect: blurEffect)
    blurView.translatesAutoresizingMaskIntoConstraints = false
    view.insertSubview(blurView, at: 0)
    NSLayoutConstraint.activate([
      blurView.heightAnchor.constraint(equalTo: view.heightAnchor),
      blurView.widthAnchor.constraint(equalTo: view.widthAnchor),
    ])

    let clipPath = UIBezierPath(rect: UIScreen.main.bounds)

    let circlePath = UIBezierPath(ovalIn: CGRect(x: 100, y: 0, width: 200, height: 200))

    clipPath.append(circlePath)

    let layer = CAShapeLayer()
    layer.path = clipPath.cgPath
    layer.fillRule = .evenOdd
    view.layer.mask = layer
    view.layer.masksToBounds = true

    return view
  }

  func updateUIView(_ uiView: UIView,
                    context: UIViewRepresentableContext<BlurView>) {

    let clipPath = UIBezierPath(rect: UIScreen.main.bounds)

    let circlePath = UIBezierPath(ovalIn: CGRect(x: position.x, y: position.y, width: 200, height: 200))

    clipPath.append(circlePath)

    let layer = CAShapeLayer()
    layer.path = clipPath.cgPath
    layer.fillRule = .evenOdd
    uiView.layer.mask = layer
    uiView.layer.masksToBounds = true

  }

}

struct TestView_Previews: PreviewProvider {
  static var previews: some View {
    TestView()
  }
}
like image 329
Chris Avatar asked Dec 16 '20 16:12

Chris


1 Answers

I think I nearly have a solution, I can use a viewmodifer to render the result twice on top of each other with a ZStack, I can blur one view, and use a mask to knock a hole in it.

blur

import SwiftUI

struct TestView2: View {

  @State var position: CGPoint = .zero

  var simpleDrag: some Gesture {
    DragGesture()
      .onChanged { value in
        self.position = value.location
      }
  }

  var body: some View {
    ZStack {
  
      Circle()
        .fill(Color.green)
        .frame(height: 200)
  
      Circle()
        .fill(Color.pink)
        .frame(height: 200)
        .offset(x: 50, y: 100)
  
      Circle()
        .fill(Color.orange)
        .frame(height: 100)
        .offset(x: -50, y: 00)
    }
    .maskedBlur(position: $position)
    .gesture(
      simpleDrag
    )

  }
}

struct MaskedBlur: ViewModifier {

  @Binding var position: CGPoint

  /// Render the content twice
  func body(content: Content) -> some View {

    ZStack {
      content
  
      content
        .blur(radius: 10)
        .mask(
          Hole(position: $position)
            .fill(style: FillStyle(eoFill: true))
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        )
    }

  }
}

extension View {
  func maskedBlur(position: Binding<CGPoint>) -> some View {
    self.modifier(MaskedBlur(position: position))
  }
}

struct Hole: Shape {

  @Binding var position: CGPoint

  func path(in rect: CGRect) -> Path {
    var path = Path()

    path.addRect(UIScreen.main.bounds)

    path.addEllipse(in: CGRect(x: position.x, y: position.y, width: 200, height: 200))

    return path
  }
}


#if DEBUG

struct TestView2_Previews: PreviewProvider {
  static var previews: some View {
    TestView2()
  }
}

#endif
like image 198
Chris Avatar answered Oct 12 '22 20:10

Chris