👻

SwiftUIでブラー(ぼかし)を徐々に弱くしたい

2024/07/07に公開

何を言ってるのか分からないかもしれませんが、iOS 18の写真アプリや Globetrotter のヘッダー部分などで採用されているUIを再現したいという感じです。

Globetrotter

SwiftUIで UIVisualEffect のようなぼかしを利用するには Material を活用します。

https://developer.apple.com/documentation/swiftui/material

Color.clear
  .background(.regularMaterial)

のようにするとぼかしを一面に敷くことができます。

ぼけ写真

ただ、これだと強さが一定なので、徐々に強さを変えていくようにしたいです。
これをどう実現するかは悩んだのですが、別の調べ物をしている時に、SwiftUIでブレンドモードを利用できることを知り、活用できると思いました。

ブレンドモード

ブレンドモードはレイヤーの重なった部分の色をそれぞれのモードで計算することで、動的に画像処理を加えることができる手法です。
Photoshopなどに昔から実装されていたので馴染みがある人も多いかもしれません。
最近はCSSでもできるみたいですね。
UI系では destinationOut がくり抜きなどで便利なのでよく使われるのですが、ここでも例に漏れず使用します。

ZStack {
    Color.clear
      .background(.regularMaterial)

    LinearGradient(
      gradient: Gradient(colors: [.black.opacity(0), .black]),
      startPoint: .top,
      endPoint: .bottom
    )
    .blendMode(.destinationOut)
}
.compositingGroup()

.compositingGroup() でラップすると反映されます。

Before & After

ブレンドモードを指定する前と後はこんな感じです。

before

after

上の方にブラーが強くかかり、下に行くほど無くなっていくことが分かります。

出来上がったコード

public struct BlurBackgroundView: View {
  public init() {}

  let maxHeight: CGFloat = 160

  public var body: some View {
    ZStack {
      Color.clear
        .background(.regularMaterial)
        .frame(maxWidth: .infinity, maxHeight: maxHeight)

      LinearGradient(
        gradient: Gradient(colors: [.black.opacity(0), .black]),
        startPoint: .top,
        endPoint: .bottom
      )
      .blendMode(.destinationOut)
      .frame(maxWidth: .infinity, maxHeight: maxHeight)
    }
    .compositingGroup()
    .frame(maxHeight: maxHeight)
    .ignoresSafeArea()
    .allowsHitTesting(false)
  }
}

グラデーションの方向を入れ替えればタブ側にも利用できると思います。
allowsHitTesting(false) で触っても影響がないようにしています。

extension View {
  @ViewBuilder
  public func blurNavigationBar() -> some View {
    ZStack(alignment: .top) {
      self
      BlurBackgroundView()
    }
  }

こんな感じのextensionを生やしておくと、使いたいところだけぼかし効果を入れられて便利です。
navigationBarの背景色はAppearanceで消しておくのをお忘れなく。

Discussion