I. Pedan
I. Pedan

Reputation: 255

How to apply a blur to a view with a mask?

I need to implement an onboarding like in the image below. I can't apply blur avoiding the icon. I know how to do it using UIViewRepresentable, but I want to reach the goal using SwiftUI 2.0(min iOS 14.0). Is there a way to create such a masked blur without UIViewRepresentable?

UPD. I have a view hierarchy. I need to blur it(a gaussian blur effect with radius 5) and cover it with a black tint with an opacity 0.3 and a mask with a hole. Everything is ok, but the blur modifier also applies an effect to an element from the hole. It must not be blurred(like "Flag" icon at the screenshot). I can't separate this element from the view hierarchy. This onboarding view modifier must be reusable across the app.

enter image description here

Upvotes: 1

Views: 815

Answers (2)

I. Pedan
I. Pedan

Reputation: 255

I found a solution to my problem. I used some code from other answers and wrote my OnboardingViewModifier. I used content from ViewModifier body function twice in a ZStack. The first one is an original view, the second one is blurred and masked. It gave me the needed result.

extension Path {
    var reversed: Path {
        let reversedCGPath = UIBezierPath(cgPath: cgPath)
            .reversing()
            .cgPath
        return Path(reversedCGPath)
    }
}

struct ShapeWithHole: Shape {
    let hole: CGRect
    let cornerRadius: CGFloat

    func path(in rect: CGRect) -> Path {
        var path = Rectangle().path(in: rect)
        path.addPath(RoundedRectangle(cornerRadius: cornerRadius).path(in: hole).reversed)
        return path
    }
}

struct OnboardingViewModifier<DescriptionView>: ViewModifier where DescriptionView: View {
    let hole: CGRect
    let isPresented: Bool
    @ViewBuilder let descriptionOverlay: () -> DescriptionView

    func body(content: Content) -> some View {
        content
            .disabled(isPresented)
            .overlay(overlay(content))
    }
    
    @ViewBuilder
    func overlay(_ content: Content) -> some View {
        if isPresented {
            ZStack {
                content
                    .blur(radius: 5)
                    .disabled(isPresented)
                Color.black.opacity(0.3)
                
                descriptionOverlay()
            }
            .compositingGroup()
            .mask(ShapeWithHole(hole: hole, cornerRadius: 25))
            .ignoresSafeArea(.all)

        }
    }
}

extension View {
    func onboardingWithHole<DescriptionView>(
        isPresented: Bool,
        hole: CGRect,
        @ViewBuilder descriptionOverlay: @escaping () -> DescriptionView) -> some View where DescriptionView: View {
            
            modifier(OnboardingViewModifier(hole: hole, isPresented: isPresented, descriptionOverlay: descriptionOverlay))
    }
}

Upvotes: -1

Simon
Simon

Reputation: 1850

There are two ways of doing so.

Either use the blur just on the background:

struct ContentView: View {
    var body: some View {
        VStack {
            Icon()
            Spacer()
        }.background(Image("cat").blur(radius: 2.5))
    }
}

Or, when having a entire view in the background you can use the ZStack and just blur the View you want to be blurred. Make sure that the View/ Element you don't want blurred is above. Like so:

struct ContentView2: View {
    var body: some View {
        ZStack {
            Image("cat").blur(radius: 2.5)
            VStack {
                Icon()
                Spacer()
            }
        }
    }
}

I used both times a entire View for the Icon which is probably a little over engineered but it gives you the idea:

struct Icon: View {
    var body: some View {
        Image(systemName: "pencil.circle.fill")
            .resizable()
            .frame(width: 50, height: 50)
            .padding()
            .foregroundColor(.red)
    }
}

Both giving you this as result:

Example Image

Upvotes: 2

Related Questions