【SwiftUI】通知バナーのようなUIを作ってみた

## バナーを出したい
こんな感じの記述方法で簡単に画面上部にスライドインでバナーを出したい

```swift
View
  .bannerVisible(with: Binding<Bool>) 
```

## できたもの
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">通知バナーのようなUI作ってみたらアニメーションについてちょっとだけ理解が深まった<a href="https://t.co/nScc1QzTlb">https://t.co/nScc1QzTlb</a> <a href="https://t.co/55ifJxBh1y">pic.twitter.com/55ifJxBh1y</a></p>&mdash; Yusuke Miyata (@yuskey38) <a href="https://twitter.com/yuskey38/status/1469858927479525380?ref_src=twsrc%5Etfw">December 12, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## 作ってみた
大まかな手順はこの通りです
① Bannar表示用のModifierを作る
② ①で作ったModifierを簡単に使えるようにViewのExtensionを作成する

### Modifierを作る

```swift
struct Banner: ViewModifier {
    @Binding var isShow: Bool
    let title: String
    let icon: String
    let foregroundColor: Color
    let backgroundColor: Color

    @State private var offset: CGFloat = -100
    @State private var opacity: CGFloat = 0

    func body(content: Content) -> some View {
        let asyncHide = DispatchWorkItem() { hide() }

        ZStack {
            content
            if isShow {
                VStack {
                    bannerView
                    Spacer()
                }
                .padding()
                .onTapGesture {
                    asyncHide.cancel()
                    hide()
                }
                .onAppear {
                    show()
                    DispatchQueue.main.asyncAfter(deadline: .now() + 4, execute: asyncHide)
                }
                .offset(y: offset)
            }
        }
    }

    private var bannerView: some View {
        HStack {
            Image(systemName: icon)
                .opacity(opacity)
            Text(title).bold()
                .opacity(opacity)
            Spacer()
        }
        .foregroundColor(foregroundColor)
        .padding(12)
        .background(backgroundColor.opacity(opacity))
        .cornerRadius(8)
    }

    private func show() {
        withAnimation(.easeInOut(duration: 0.3)) {
            offset = 0
            withAnimation(.easeInOut(duration: 0.45)) {
                opacity = 0.75
            }
        }
    }

    private func hide() {
        withAnimation(.easeInOut(duration: 0.3)) {
            offset = -100
            withAnimation(.easeInOut(duration: 0.2)) {
                opacity = 0
                self.isShow = false
            }
        }
    }
}
```

`.animation(_ animation:)`がiOS15.0でdeprecatedになるため、`show()`や`hide()`で独自でoffsetを変更することで上からバナーが出てくるアニメーションを実現しています。
バナーを表示するために`isShow`の値を変えるときに、`withAnimation { isShow = true }` と記述すれば独自でoffsetを変更させる必要はないのですが、単に`isShow = true` とシンプルに記述したいのでこのようにしました。

### Viewのextensionを作る

```swift
extension View {
    func bannerVisible(with isShown: Binding<Bool>,
                       title: String = "TEST",
                       icon: String = "star.fill",
                       foregroundColor: Color = .white,
                       backgroundColor: Color = .red) -> some View {
        self.navigationBarTitleDisplayMode(.inline)
            .modifier(Banner(isShown: isShown,
                             title: title,
                             icon: icon,
                             foregroundColor: foregroundColor,
                             backgroundColor: backgroundColor
                            )
            )
    }
}
```

ある程度バナーの色などをカスタムできるようにしました。
また,`NavigationView`の中で使うとうまく画面トップにバナーが出なかったため
`.navigationTitleDisplayMode(.inline)`にして期待通りに位置にバナーを表示できるようにしました。

## 使い方
```swift
struct ContentView: View {
    @State private var isShow: Bool = false

    var body: some View {
        VStack {
            Button(action: { isShow = true }) {
                Text("バナー表示")
            }
        }
        .bannerVisible(with: $isShow)
    }
}
```
このようにバナーを表示したいViewの最上層のViewに`.bannerVisible`を追加すれば使用できます。


## 参考
`バナー`で検索すると広告についての記事ばかりが出てきましたが
この記事を見つけられて良かったです。

https://zenn.dev/woo_noo/articles/17f05786009d563ab498

↑の記事で作成したModifierを`.bannerVisible`と記述すれば使えるようにする方法を探してこの記事に辿り着きました。

https://capibara1969.com/1520/