【SwiftUI】iPhoneのロック画面で使用できるライトボタンを再現してみた

# はじめに
iPhone標準の動作を再現してみると勉強になるので、ロック画面で使用できるライトボタンを再現してみました。

# サンプルアプリ
![Simulator Screen Recording - iPhone 15 - 2024-01-10 at 22.14.23.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1745371/32dfa2b9-4793-e0e9-a002-739a7594a2e0.gif)

# 実装
```swift
import SwiftUI
import AVFoundation

struct ContentView: View {
    @GestureState private var isDetectingLongPress = false
    @State private var isOn = false
    
    var longPress: some Gesture {
        LongPressGesture()
            .updating($isDetectingLongPress) { currentState, gestureState, transaction in
                gestureState = currentState
            }
            .onEnded { _ in
                isOn.toggle()
            }
    }

    var body: some View {
        VStack {
            Color.cyan.ignoresSafeArea()
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .overlay(alignment: .bottomLeading) {
            Image(systemName: isOn ? "flashlight.on.fill" : "flashlight.off.fill")
                .resizable()
                .scaledToFit()
                .frame(height: 25)
                .foregroundStyle(isOn ? .black : .white)
                .contentShape(.circle)
                .frame(width: 45, height: 45)
                .background(isOn ? .white.opacity(0.5) : .black.opacity(0.5), in: .circle)
                .background(Material.bar, in: .circle)
                .padding(40)
                .scaleEffect(isDetectingLongPress ? 1.5 : 1.0)
                .animation(.bouncy(duration: 0.4, extraBounce: 0.25), value: isDetectingLongPress)
                .gesture(longPress)
                .sensoryFeedback(.impact(flexibility: .rigid, intensity: 1), trigger: isDetectingLongPress)
        }
        .onChange(of: isOn) {
            guard let device = AVCaptureDevice.default(for: .video) else { return }
            if device.hasTorch {
                try? device.lockForConfiguration()
                device.torchMode = isOn ? .on : .off
            }
        }
    }
}
```

# 解説
まずは`@GestureState`です。
これに関しては[こちらの記事](https://qiita.com/SNQ-2001/items/df8c61ff82ffc7bdad33)で解説しているのでそちらを見てください
```swift
@GestureState private var isDetectingLongPress = false
```

ライトのオンオフを管理している変数です。
```swift
@State private var isOn = false
```

`updating`で`isDetectingLongPress`を変更しています。
`onEnded`で指を離した時に`isOn`を変更しています。
```swift
var longPress: some Gesture {
    LongPressGesture()
        .updating($isDetectingLongPress) { currentState, gestureState, transaction in
            gestureState = currentState
        }
        .onEnded { _ in
            isOn.toggle()
        }
}
```

続いてViewです。
animationに`.bouncy(duration: 0.4, extraBounce: 0.25)`を指定することによってバウンドするようなアニメーションにすることができます。
```swift
.animation(.bouncy(duration: 0.4, extraBounce: 0.25), value: isDetectingLongPress)
```

`sensoryFeedback`で押した時のブルッとような触覚フィードバックを実装することができます。
こちらはiOS17から使用可能です。
```swift
.sensoryFeedback(.impact(flexibility: .rigid, intensity: 1), trigger: isDetectingLongPress)
```

`onChange`で`isOn`の変更を監視して、ライトをつける処理を実行しています。
```swift
.onChange(of: isOn) {
    guard let device = AVCaptureDevice.default(for: .video) else { return }
    if device.hasTorch {
        try? device.lockForConfiguration()
        device.torchMode = isOn ? .on : .off
    }
}
```

# おわり
ライトを付ける処理初めて書きました
権限とか必要なしに使えるんですね