[SwiftUI] iOS17からの触覚フィードバックの簡単実装方法

# はじめに
`いいねボタン`をタップしたときなどに端末が少し震えるような実装をするにはどうするのか気になり調べてみました。
触覚フィードバックを使うことで実現できるようです。
iOS17以降ではこれまでよりも簡単に実装できるようになったようです。

# 環境
Xcode 15.2
iOS17 ~

# 内容

`sensoryFeedback`を使うことで実現できます

```swift
func sensoryFeedback<T>(
    _ feedback: SensoryFeedback,
    trigger: T,
    condition: @escaping (T, T) -> Bool
) -> some View where T : Equatable
```

https://developer.apple.com/documentation/swiftui/view/sensoryfeedback(_:trigger:condition:)

### フィードバックの種類
[`SensoryFeedback`](https://developer.apple.com/documentation/swiftui/sensoryfeedback)

種類はたくさん存在しますが、iOSで使えるものは以下の通り

- `static let selection: SensoryFeedback`
UI要素が変更されたことを示す

- `static let success: SensoryFeedback`
アクションが完了したことを示す

- `static let warning: SensoryFeedback`
アクションが警告を発することを示す

- `static let error: SensoryFeedback`
エラーが発生したことを示す

その他、視覚体験を補完するのに使うものが存在する
- `static let impact: SensoryFeedback`

### トリガーとコンディション

enumでトリガーに使う状態を定義、触覚フィードバックを提供する条件を追加

```swift
struct MyView: View {
    @State private var phase = Phase.inactive

    var body: some View {
        ContentView(phase: $phase)
            .sensoryFeedback(.selection, trigger: phase) { old, new in
                old == .inactive || new == .expanded
            }
    }

    enum Phase {
        case inactive
        case preparing
        case active
        case expanded
    }
}
```

## いいねボタンをタップした時にトリガーする方法

`Bool`の値でトリガーする場合はより簡単です

```swift
func sensoryFeedback<T>(
    _ feedback: SensoryFeedback,
    trigger: T
) -> some View where T : Equatable
```

https://developer.apple.com/documentation/swiftui/view/sensoryfeedback(_:trigger:)

```diff_swift
    struct ListItemView: View {
        @State var isLiked: Bool = false

        var body: some View {
            VStack(spacing: 20) {
                HStack {
                    RoundedRectangle(cornerRadius: 20)
                        .fill(.gray)
                        .frame(width: 60, height: 60)
                        .padding(10)
                    Text("リストアイテム")
                        .frame(maxWidth: .infinity, alignment: .leading)
                    Button(action: {
                        isLiked.toggle()
                    }, label: {
                        Image(systemName: isLiked ? "heart.fill" : "heart")
                    })
                }
                Text("リストアイテムのテキスト")
                    .frame(maxWidth: .infinity, alignment: .leading)
                    .multilineTextAlignment(.leading)
                    .padding(10)
            }
+           .sensoryFeedback(.success, trigger: isLiked)
        }
    }
```

例えば`false`から`true`に変わった時だけトリガーしたい場合はこんな感じで実現できそうです

```swift
    .sensoryFeedback(.success, trigger: isLiked) { _, new in
        new == true
    }
```

### 条件によってフィードバックのタイプを変えたい場合

```swift
func sensoryFeedback<T>(
    trigger: T,
    _ feedback: @escaping (T, T) -> SensoryFeedback?
) -> some View where T : Equatable
```

https://developer.apple.com/documentation/swiftui/view/sensoryfeedback(trigger:_:)

```swift
struct MyView: View {
    @State private var phase = Phase.inactive


    var body: some View {
        ContentView(phase: $phase)
            .sensoryFeedback(trigger: phase) { old, new in
                switch (old, new) {
                    case (.inactive, _): return .success
                    case (_, .expanded): return .impact
                    default: return nil
                }
            }
    }


    enum Phase {
        case inactive
        case preparing
        case active
        case expanded
    }
}
```

# おわりに
これまでの`UINotificationFeedbackGenerator`を使った触覚フィードバックの実装よりも簡単に実装できそうですね。
触覚フィードバックはUXの向上も期待できるものなので、iOS17~の実装ではより意識していこうと思います。

# 参考
https://zenn.dev/shota_xx/articles/5df397376a2757

https://dev.classmethod.jp/articles/swift-vibrate-device/