在 SwiftUI 中有模糊背景的方法吗?

我希望模糊一个视图的背景,但不希望突破到 UIKit 来完成它(例如 UIVisualEffectView)我在挖掘文档,但一无所获,似乎没有办法实时剪辑背景和应用效果。是我想错了,还是我想错了?

47726 次浏览

I haven't found a way to achieve that in SwiftUI yet, but you can use UIKit stuff via UIViewRepresentable protocol.

struct BlurView: UIViewRepresentable {


let style: UIBlurEffect.Style


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),
])
return view
}


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


}


}

Demo:

struct ContentView: View {


var body: some View {
NavigationView {
ZStack {
List(1...100) { item in
Rectangle().foregroundColor(Color.pink)
}
.navigationBarTitle(Text("A List"))
ZStack {
BlurView(style: .light)
.frame(width: 300, height: 300)
Text("Hey there, I'm on top of the blur")


}
}
}
}


}

I used ZStack to put views on top of it.

ZStack {
// List
ZStack {
// Blurred View
// Text
}
}

And ends up looking like this:

enter image description here

1. The Native SwiftUI way:

Just add .blur() modifier on anything you need to be blurry like:

Image("BG")
.blur(radius: 20)

Blur Demo Note the top and bottom of the view

Note that you can Group multiple views and blur them together.


2. The Visual Effect View:

You can bring the prefect UIVisualEffectView from the UIKit:

VisualEffectView(effect: UIBlurEffect(style: .dark))

With this tiny struct:

struct VisualEffectView: UIViewRepresentable {
var effect: UIVisualEffect?
func makeUIView(context: UIViewRepresentableContext<Self>) -> UIVisualEffectView { UIVisualEffectView() }
func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext<Self>) { uiView.effect = effect }
}

VEV Demo


3. iOS 15: Materials

You can use iOS predefined materials with one line code:

.background(.ultraThinMaterial)

Demo

The simplest way is here by Richard Mullinix:

struct Blur: UIViewRepresentable {
var style: UIBlurEffect.Style = .systemMaterial


func makeUIView(context: Context) -> UIVisualEffectView {
return UIVisualEffectView(effect: UIBlurEffect(style: style))
}


func updateUIView(_ uiView: UIVisualEffectView, context: Context) {
uiView.effect = UIBlurEffect(style: style)
}
}

Then just use it somewhere in your code like background:

    //...
MyView()
.background(Blur(style: .systemUltraThinMaterial))

As mentioned by @mojtaba, it's very peculiar to see white shade at top of image when you set resizable() along with blur().

As simple trick is to raise the Image padding to -ve.

 var body: some View {


return
ZStack {


Image("background_2").resizable()
.edgesIgnoringSafeArea(.all)
.blur(radius: 5)
.scaledToFill()
.padding(-20) //Trick: To escape from white patch @top & @bottom




}
}

Result: SwiftUI Image blue trick

@State private var amount: CGFLOAT = 0.0


var body: some View {
VStack{
Image("Car").resizable().blur(radius: amount, opaque: true)
}
}

Using "Opaque: true" with blur function will eliminate white noise

There is a very useful but unfortunately private (thanks Apple) class CABackdropLayer

It draws a copy of the layers below, I found it useful when using blend mode or filters, It can also be used for blur effect

Code

open class UIBackdropView: UIView {


open override class var layerClass: AnyClass {
NSClassFromString("CABackdropLayer") ?? CALayer.self
}
}


public struct Backdrop: UIViewRepresentable {


public init() {}


public func makeUIView(context: Context) -> UIBackdropView {
UIBackdropView()
}


public func updateUIView(_ uiView: UIBackdropView, context: Context) {}
}


public struct Blur: View {


public var radius: CGFloat
public var opaque: Bool


public init(radius: CGFloat = 3.0, opaque: Bool = false) {
self.radius = radius
self.opaque = opaque
}


public var body: some View {
Backdrop()
.blur(radius: radius, opaque: opaque)
}
}

Usage

struct Example: View {


var body: some View {
ZStack {
YourBelowView()
YourTopView()
.background(Blur())
.background(Color.someColor.opacity(0.4))
}
}
}

Source

New in iOS 15 , SwiftUI has a brilliantly simple equivalent to UIVisualEffectView, that combines ZStack, the background() modifier, and a range of built-in materials.

ZStack {
Image("niceLook")


Text("Click me")
.padding()
.background(.thinMaterial)
}

You can adjust the “thickness” of your material – how much of the background content shines through – by using one of several material types. From thinnest to thickest, they are:

.ultraThinMaterial
.thinMaterial
.regularMaterial
.thickMaterial
.ultraThickMaterial
Button("Test") {}
.background(Rectangle().fill(Color.red).blur(radius: 20))

I have found an interesting hack to solve this problem. We can use UIVisualEffectView to make live "snapshot" of its background. But this "snapshot" will have an applied effect of UIVisualEffectView. We can avoid applying this effect using UIViewPropertyAnimator.

I didn't find any side effect of this hack. You can find my solution here: my GitHub Gist

Code

/// A View which content reflects all behind it
struct BackdropView: UIViewRepresentable {


func makeUIView(context: Context) -> UIVisualEffectView {
let view = UIVisualEffectView()
let blur = UIBlurEffect(style: .extraLight)
let animator = UIViewPropertyAnimator()
animator.addAnimations { view.effect = blur }
animator.fractionComplete = 0
animator.stopAnimation(true)
animator.finishAnimation(at: .start)
return view
}
    

func updateUIView(_ uiView: UIVisualEffectView, context: Context) { }
    

}


/// A transparent View that blurs its background
struct BackdropBlurView: View {
    

let radius: CGFloat
    

@ViewBuilder
var body: some View {
BackdropView().blur(radius: radius)
}
    

}

Usage

ZStack(alignment: .leading) {
Image(systemName: "globe")
.resizable()
.frame(width: 200, height: 200)
.foregroundColor(.accentColor)
.padding()
BackdropBlurView(radius: 6)
.frame(width: 120)
}

Usage example