๐Ÿ“ณ Core Haptics

์ •๊ตํ•œ ์ด‰๊ฐ ํ”ผ๋“œ๋ฐฑ ๋””์ž์ธ ํ”„๋ ˆ์ž„์›Œํฌ

iOS 13+Taptic Engine

โœจ Core Haptics๋ž€?

Core Haptics๋Š” iPhone์˜ Taptic Engine์„ ํ™œ์šฉํ•˜์—ฌ ์ •๊ตํ•˜๊ณ  ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆ ๊ฐ€๋Šฅํ•œ ์ด‰๊ฐ ํ”ผ๋“œ๋ฐฑ์„ ๋งŒ๋“œ๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค. ๋‹จ์ˆœํ•œ ์ง„๋™์„ ๋„˜์–ด ๊ฐ•๋„, ์„ ๋ช…๋„, ์ง€์† ์‹œ๊ฐ„์„ ์กฐ์ ˆํ•˜์—ฌ ๊ฒŒ์ž„, ์•ฑ, ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜์— ๋ชฐ์ž…๊ฐ ์žˆ๋Š” ์ด‰๊ฐ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. AHAP ํŒŒ์ผ์„ ํ†ตํ•ด ๋ณต์žกํ•œ ํ–…ํ‹ฑ ํŒจํ„ด์„ ์ €์žฅํ•˜๊ณ  ์žฌ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ’ก ํ•ต์‹ฌ ๊ธฐ๋Šฅ: ์ปค์Šคํ…€ ํ–…ํ‹ฑ ํŒจํ„ด ยท ์—ฐ์†/์ˆœ๊ฐ„ ์ด๋ฒคํŠธ ยท ๊ฐ•๋„ ์กฐ์ ˆ ยท ์„ ๋ช…๋„ ์กฐ์ ˆ ยท AHAP ํŒŒ์ผ ์ง€์› ยท ์˜ค๋””์˜ค-ํ–…ํ‹ฑ ๋™๊ธฐํ™” ยท ๋™์  ๋งค๊ฐœ๋ณ€์ˆ˜

๐ŸŽฎ 1. CHHapticEngine ์ดˆ๊ธฐํ™”

CHHapticEngine์„ ์ƒ์„ฑํ•˜๊ณ  ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.

HapticManager.swift โ€” ์—”์ง„ ์ดˆ๊ธฐํ™”
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 ์ด๋ฒคํŠธ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

TransientHaptics.swift โ€” ์ˆœ๊ฐ„ ํ–…ํ‹ฑ
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 ์ด๋ฒคํŠธ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

ContinuousHaptics.swift โ€” ์—ฐ์† ํ–…ํ‹ฑ
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 ํŒŒ์ผ๋กœ ์ €์žฅํ•˜๊ณ  ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค.

AHAPLoader.swift โ€” 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. ๊ฒŒ์ž„์šฉ ํ–…ํ‹ฑ ํŒจํ„ด

๊ฒŒ์ž„์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋‹ค์–‘ํ•œ ํ–…ํ‹ฑ ํŒจํ„ด์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

GameHaptics.swift โ€” ๊ฒŒ์ž„์šฉ ํ–…ํ‹ฑ
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)")
        }
    }
}

๐Ÿ“ฑ ์ข…ํ•ฉ ์˜ˆ์ œ

HapticDemoView.swift โ€” ํ–…ํ‹ฑ ๋ฐ๋ชจ ์•ฑ
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 ๊ฐ€์ด๋“œ๋ผ์ธ

๐ŸŽฏ ์‹ค์ „ ํ™œ์šฉ

๐Ÿ“š ๋” ์•Œ์•„๋ณด๊ธฐ

โšก๏ธ ์„ฑ๋Šฅ ํŒ: ํ–…ํ‹ฑ ์—”์ง„์€ ์•ฑ์ด ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋กœ ๊ฐ€๋ฉด ์ž๋™์œผ๋กœ ์ค‘์ง€๋ฉ๋‹ˆ๋‹ค. ํฌ๊ทธ๋ผ์šด๋“œ๋กœ ๋Œ์•„์˜ฌ ๋•Œ resetHandler๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ž๋™์œผ๋กœ ์žฌ์‹œ์ž‘ํ•˜๋„๋ก ์„ค์ •ํ•˜์„ธ์š”. AHAP ํŒŒ์ผ์„ ์‚ฌ์šฉํ•˜๋ฉด ๋ณต์žกํ•œ ํŒจํ„ด์„ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.