๐ณ Core Haptics
์ ๊ตํ ์ด๊ฐ ํผ๋๋ฐฑ ๋์์ธ ํ๋ ์์ํฌ
โจ Core Haptics๋?
Core Haptics๋ iPhone์ Taptic Engine์ ํ์ฉํ์ฌ ์ ๊ตํ๊ณ ์ปค์คํฐ๋ง์ด์ฆ ๊ฐ๋ฅํ ์ด๊ฐ ํผ๋๋ฐฑ์ ๋ง๋๋ ํ๋ ์์ํฌ์ ๋๋ค. ๋จ์ํ ์ง๋์ ๋์ด ๊ฐ๋, ์ ๋ช ๋, ์ง์ ์๊ฐ์ ์กฐ์ ํ์ฌ ๊ฒ์, ์ฑ, ์ฌ์ฉ์ ์ธํฐ๋์ ์ ๋ชฐ์ ๊ฐ ์๋ ์ด๊ฐ ๊ฒฝํ์ ์ ๊ณตํฉ๋๋ค. AHAP ํ์ผ์ ํตํด ๋ณต์กํ ํ ํฑ ํจํด์ ์ ์ฅํ๊ณ ์ฌ์ํ ์ ์์ต๋๋ค.
๐ฎ 1. CHHapticEngine ์ด๊ธฐํ
CHHapticEngine์ ์์ฑํ๊ณ ์์ํฉ๋๋ค.
import CoreHaptics import SwiftUI @Observable class HapticManager { private var engine: CHHapticEngine? var supportsHaptics = false init() { prepareHaptics() } // ํ ํฑ ์์ง ์ค๋น func prepareHaptics() { // ๋๋ฐ์ด์ค๊ฐ ํ ํฑ์ ์ง์ํ๋์ง ํ์ธ guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { print("โ ๏ธ ํ ํฑ ๋ฏธ์ง์ ๋๋ฐ์ด์ค") return } do { // ์์ง ์์ฑ engine = try CHHapticEngine() supportsHaptics = true // ์์ง ์์ try engine?.start() // ์์ง ์ ์ง ํธ๋ค๋ฌ engine?.stoppedHandler = { reason in print("โ ๏ธ ์์ง ์ ์ง: \(reason)") } // ์์ง ๋ฆฌ์ ํธ๋ค๋ฌ engine?.resetHandler = { [weak self] in print("๐ ์์ง ๋ฆฌ์ ") do { try self?.engine?.start() } catch { print("โ ์์ง ์ฌ์์ ์คํจ: \(error)") } } print("โ ํ ํฑ ์์ง ์ค๋น ์๋ฃ") } catch { print("โ ํ ํฑ ์์ง ์์ฑ ์คํจ: \(error)") } } // ์์ง ์์ func startEngine() { guard supportsHaptics else { return } do { try engine?.start() } catch { print("โ ์์ง ์์ ์คํจ: \(error)") } } // ์์ง ์ ์ง func stopEngine() { engine?.stop() } }
๐ฅ 2. ์๊ฐ ํ ํฑ ์ด๋ฒคํธ
์๊ฐ์ ์ธ ํญ์ด๋ ์ถฉ๊ฒฉ์ ํํํ๋ Transient ์ด๋ฒคํธ๋ฅผ ๋ง๋ญ๋๋ค.
import CoreHaptics extension HapticManager { // ๊ธฐ๋ณธ ํญ ํ ํฑ func playSimpleTap() { guard supportsHaptics, let engine = engine else { return } do { // ์๊ฐ ์ด๋ฒคํธ ์์ฑ let event = CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0), CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0) ], relativeTime: 0 ) // ํจํด ์์ฑ ๋ฐ ์ฌ์ let pattern = try CHHapticPattern(events: [event], parameters: []) let player = try engine.makePlayer(with: pattern) try player.start(atTime: 0) print("โ ํญ ํ ํฑ ์ฌ์") } catch { print("โ ํ ํฑ ์ฌ์ ์คํจ: \(error)") } } // ์ปค์คํ ๊ฐ๋์ ํญ func playTap(intensity: Float, sharpness: Float) { guard supportsHaptics, let engine = engine else { return } do { let event = CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: intensity), CHHapticEventParameter(parameterID: .hapticSharpness, value: sharpness) ], relativeTime: 0 ) let pattern = try CHHapticPattern(events: [event], parameters: []) let player = try engine.makePlayer(with: pattern) try player.start(atTime: 0) } catch { print("โ ํ ํฑ ์ฌ์ ์คํจ: \(error)") } } // ์ฐ์ ํญ ํจํด func playDoubleTap() { guard supportsHaptics, let engine = engine else { return } do { let events = [ CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0), CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0) ], relativeTime: 0 ), CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0), CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0) ], relativeTime: 0.2 // 0.2์ด ํ ๋ ๋ฒ์งธ ํญ ) ] let pattern = try CHHapticPattern(events: events, parameters: []) let player = try engine.makePlayer(with: pattern) try player.start(atTime: 0) print("โ ๋๋ธ ํญ ํ ํฑ ์ฌ์") } catch { print("โ ํ ํฑ ์ฌ์ ์คํจ: \(error)") } } }
๐ 3. ์ฐ์ ํ ํฑ ์ด๋ฒคํธ
์ง์์ ์ธ ์ง๋์ ํํํ๋ Continuous ์ด๋ฒคํธ๋ฅผ ๋ง๋ญ๋๋ค.
import CoreHaptics extension HapticManager { // ์ฐ์ ์ง๋ (1์ด) func playContinuousHaptic(duration: TimeInterval = 1.0) { guard supportsHaptics, let engine = engine else { return } do { // ์ฐ์ ์ด๋ฒคํธ ์์ฑ let event = CHHapticEvent( eventType: .hapticContinuous, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.7), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5) ], relativeTime: 0, duration: duration ) let pattern = try CHHapticPattern(events: [event], parameters: []) let player = try engine.makePlayer(with: pattern) try player.start(atTime: 0) print("โ ์ฐ์ ํ ํฑ ์ฌ์") } catch { print("โ ํ ํฑ ์ฌ์ ์คํจ: \(error)") } } // ๊ฐ๋๊ฐ ๋ณํ๋ ์ฐ์ ํ ํฑ func playFadingHaptic() { guard supportsHaptics, let engine = engine else { return } do { // ๊ฐ๋๊ฐ ์ ์ฐจ ๊ฐ์ํ๋ ์ฐ์ ์ด๋ฒคํธ let event = CHHapticEvent( eventType: .hapticContinuous, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5) ], relativeTime: 0, duration: 2.0 ) // ๊ฐ๋ ๊ฐ์ ๋งค๊ฐ๋ณ์ ์ปค๋ธ let parameterCurve = CHHapticParameterCurve( parameterID: .hapticIntensityControl, controlPoints: [ CHHapticParameterCurve.ControlPoint(relativeTime: 0, value: 1.0), CHHapticParameterCurve.ControlPoint(relativeTime: 2.0, value: 0.0) ], relativeTime: 0 ) let pattern = try CHHapticPattern( events: [event], parameterCurves: [parameterCurve] ) let player = try engine.makePlayer(with: pattern) try player.start(atTime: 0) print("โ ํ์ด๋ ์์ ํ ํฑ ์ฌ์") } catch { print("โ ํ ํฑ ์ฌ์ ์คํจ: \(error)") } } // ํ์ค ํจํด (๋ฐ๋) func playPulsePattern() { guard supportsHaptics, let engine = engine else { return } do { var events: [CHHapticEvent] = [] // 5๋ฒ์ ํ์ค ์์ฑ for i in 0..<5 { let event = CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.8), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.5) ], relativeTime: TimeInterval(i) * 0.3 ) events.append(event) } let pattern = try CHHapticPattern(events: events, parameters: []) let player = try engine.makePlayer(with: pattern) try player.start(atTime: 0) print("โ ํ์ค ํจํด ์ฌ์") } catch { print("โ ํ ํฑ ์ฌ์ ์คํจ: \(error)") } } }
๐ 4. AHAP ํ์ผ
๋ณต์กํ ํ ํฑ ํจํด์ AHAP ํ์ผ๋ก ์ ์ฅํ๊ณ ๋ก๋ํฉ๋๋ค.
import CoreHaptics extension HapticManager { // AHAP ํ์ผ์์ ํจํด ๋ก๋ func playAHAPFile(named fileName: String) { guard supportsHaptics, let engine = engine else { return } do { // AHAP ํ์ผ ๊ฒฝ๋ก ๊ฐ์ ธ์ค๊ธฐ guard let url = Bundle.main.url( forResource: fileName, withExtension: "ahap" ) else { print("โ AHAP ํ์ผ์ ์ฐพ์ ์ ์์") return } // ํจํด ์์ฑ let pattern = try CHHapticPattern(contentsOf: url) // ํ๋ ์ด์ด ์์ฑ ๋ฐ ์ฌ์ let player = try engine.makePlayer(with: pattern) try player.start(atTime: 0) print("โ AHAP ํ์ผ ์ฌ์: \(fileName)") } catch { print("โ AHAP ํ์ผ ์ฌ์ ์คํจ: \(error)") } } // AHAP ํจํด์ ํ์ผ๋ก ์ ์ฅ func savePattern(_ pattern: CHHapticPattern, to fileName: String) { do { // ํจํด์ ๋์ ๋๋ฆฌ๋ก ๋ณํ let dictionary = pattern.exportDictionary() // JSON ๋ฐ์ดํฐ ์์ฑ let jsonData = try JSONSerialization.data( withJSONObject: dictionary, options: .prettyPrinted ) // ํ์ผ๋ก ์ ์ฅ let url = FileManager.default .urls(for: .documentDirectory, in: .userDomainMask)[0] .appendingPathComponent("\(fileName).ahap") try jsonData.write(to: url) print("โ AHAP ํ์ผ ์ ์ฅ: \(url)") } catch { print("โ AHAP ํ์ผ ์ ์ฅ ์คํจ: \(error)") } } } // AHAP ํ์ผ ์์ ๊ตฌ์กฐ (JSON) /* { "Pattern": [ { "Event": { "Time": 0.0, "EventType": "HapticTransient", "EventParameters": [ { "ParameterID": "HapticIntensity", "ParameterValue": 1.0 }, { "ParameterID": "HapticSharpness", "ParameterValue": 1.0 } ] } } ] } */
๐จ 5. ๊ฒ์์ฉ ํ ํฑ ํจํด
๊ฒ์์์ ์ฌ์ฉํ ์ ์๋ ๋ค์ํ ํ ํฑ ํจํด์ ๊ตฌํํฉ๋๋ค.
import CoreHaptics extension HapticManager { // ์ถฉ๋ ํจ๊ณผ func playCollisionHaptic(intensity: Float) { guard supportsHaptics, let engine = engine else { return } do { let event = CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: intensity), CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0) ], relativeTime: 0 ) let pattern = try CHHapticPattern(events: [event], parameters: []) let player = try engine.makePlayer(with: pattern) try player.start(atTime: 0) } catch { print("โ ์ถฉ๋ ํ ํฑ ์คํจ: \(error)") } } // ํญ๋ฐ ํจ๊ณผ func playExplosionHaptic() { guard supportsHaptics, let engine = engine else { return } do { // ๊ฐํ ์ด๊ธฐ ์ถฉ๊ฒฉ let impact = CHHapticEvent( eventType: .hapticTransient, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 1.0), CHHapticEventParameter(parameterID: .hapticSharpness, value: 1.0) ], relativeTime: 0 ) // ์ฌ์ง let rumble = CHHapticEvent( eventType: .hapticContinuous, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.5), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.3) ], relativeTime: 0.1, duration: 0.5 ) let pattern = try CHHapticPattern(events: [impact, rumble], parameters: []) let player = try engine.makePlayer(with: pattern) try player.start(atTime: 0) } catch { print("โ ํญ๋ฐ ํ ํฑ ์คํจ: \(error)") } } // ์์ง ์๋ฆฌ (์ง์์ ์ธ ์ง๋) func playEngineHaptic() { guard supportsHaptics, let engine = engine else { return } do { let event = CHHapticEvent( eventType: .hapticContinuous, parameters: [ CHHapticEventParameter(parameterID: .hapticIntensity, value: 0.6), CHHapticEventParameter(parameterID: .hapticSharpness, value: 0.2) ], relativeTime: 0, duration: 3.0 ) let pattern = try CHHapticPattern(events: [event], parameters: []) let player = try engine.makePlayer(with: pattern) try player.start(atTime: 0) } catch { print("โ ์์ง ํ ํฑ ์คํจ: \(error)") } } }
๐ฑ ์ข ํฉ ์์
import SwiftUI struct HapticDemoView: View { @State private var hapticManager = HapticManager() @State private var intensity: Float = 0.5 @State private var sharpness: Float = 0.5 var body: some View { NavigationStack { Form { Section("๊ธฐ๋ณธ ํ ํฑ") { Button("๋จ์ ํญ") { hapticManager.playSimpleTap() } Button("๋๋ธ ํญ") { hapticManager.playDoubleTap() } Button("์ฐ์ ์ง๋") { hapticManager.playContinuousHaptic() } } Section("์ปค์คํ ํ ํฑ") { VStack(alignment: .leading, spacing: 12) { Text("๊ฐ๋: \(String(format: "%.2f", intensity))") Slider(value: $intensity, in: 0...1) Text("์ ๋ช ๋: \(String(format: "%.2f", sharpness))") Slider(value: $sharpness, in: 0...1) Button("ํ ์คํธ") { hapticManager.playTap(intensity: intensity, sharpness: sharpness) } .frame(maxWidth: .infinity) .buttonStyle(.borderedProminent) } } Section("ํจํด") { Button("ํ์ด๋ ์์") { hapticManager.playFadingHaptic() } Button("ํ์ค ํจํด") { hapticManager.playPulsePattern() } } Section("๊ฒ์ ํจ๊ณผ") { Button("์ถฉ๋") { hapticManager.playCollisionHaptic(intensity: 0.8) } Button("ํญ๋ฐ") { hapticManager.playExplosionHaptic() } Button("์์ง ์๋ฆฌ") { hapticManager.playEngineHaptic() } } Section { Text("ํ ํฑ ์ง์: \(hapticManager.supportsHaptics ? "โ " : "โ")") .foregroundStyle(.secondary) } } .navigationTitle("ํ ํฑ ๋ฐ๋ชจ") } } }
๐ก HIG ๊ฐ์ด๋๋ผ์ธ
- ์ ์ ํ ์ฌ์ฉ: ์๋ฏธ ์๋ ์ธํฐ๋์ ์๋ง ํ ํฑ ์ฌ์ฉ
- ๊ณผ๋ํ ์ฌ์ฉ ๊ธ์ง: ๋๋ฌด ๋น๋ฒํ ํ ํฑ์ ์ฌ์ฉ์๋ฅผ ํผ๋กํ๊ฒ ํจ
- ๋๋ฐ์ด์ค ํ์ธ: ํ ํฑ ์ง์ ์ฌ๋ถ ํ์ธ ํ์
- ์ค์ ์กด์ค: ์์คํ ํ ํฑ ์ค์ ์กด์ค
- ์ค๋์ค์ ์กฐํ: ์ฌ์ด๋์ ํ ํฑ์ ํจ๊ป ์ฌ์ฉํ๋ฉด ๋ ๊ฐ๋ ฅํ ํจ๊ณผ
๐ฏ ์ค์ ํ์ฉ
- ๊ฒ์: ์ถฉ๋, ํญ๋ฐ, ๋ฐ์ฌ ๋ฑ์ ๋ฌผ๋ฆฌ์ ํผ๋๋ฐฑ
- UI ์ธํฐ๋์ : ๋ฒํผ ํญ, ์ค์์น ํ ๊ธ, ์ฌ๋ผ์ด๋ ์กฐ์
- ์๋ฆผ: ์ค์ํ ์ด๋ฒคํธ์ ๋ํ ์ด๊ฐ ์๋ฆผ
- ๋ฏธ๋์ด ์ฑ: ์์ ๋นํธ์ ๋ง์ถ ํ ํฑ
- ์ ๊ทผ์ฑ: ์๊ฐ ์ฅ์ ์ธ์ ์ํ ์ด๊ฐ ํผ๋๋ฐฑ
๐ ๋ ์์๋ณด๊ธฐ
resetHandler๋ฅผ ์ฌ์ฉํ์ฌ ์๋์ผ๋ก ์ฌ์์ํ๋๋ก ์ค์ ํ์ธ์. AHAP ํ์ผ์ ์ฌ์ฉํ๋ฉด ๋ณต์กํ ํจํด์ ํจ์จ์ ์ผ๋ก ๊ด๋ฆฌํ ์ ์์ต๋๋ค.