0

I am using Core Haptics in my application to implement custom vibrations which could not be done by using simple UIImpactFeedbackGenerator Then i do the following -

  1. Instantiate the engine
  2. Start the engine
  3. Instantiate the CHHapticPatternPlayer
  4. Start the player

Function to play vibration -

func playContinuousVibration() {
    do {
        let pattern = try continuousVibration()
        try hapticEngine.start()
        let player = try hapticEngine.makePlayer(with: pattern)
        try player.start(atTime: CHHapticTimeImmediate)
        hapticEngine.notifyWhenPlayersFinished { _ in
            return .stopEngine
        }
    } catch {}
}

Firebase Crash Stack Trace

Crashed: com.apple.root.default-qos EXC_BAD_ACCESS KERN_INVALID_ADDRESS 0x0000000000000010

CoreHaptics __31-[CHHapticEngine handleFinish:]_block_invoke.306 + 276

libdispatch.dylib _dispatch_call_block_and_release + 32

libsystem_pthread.dylib start_wqthread + 8

Edit 1 - Posting the entire class code here to give the readers more context

final class CustomHapticFeedback {
    private let duration: Int
    private let amplitude: SwFeedbackImpactType?
    private var coreHapticsManager: Any? //Purposely made as Any to avoid using #available keyword everywhere
    
    init(duration: Int, amplitude: String) {
        self.duration = duration
        self.amplitude = SwFeedbackImpactType(rawValue: amplitude)
    }
    
    //This function will perform either UIKit haptic feedback or Core Haptics feedback depending on device and OS compatibility
    func performVibrateAction() {
        if #available(iOS 13.0, *) {
            guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else {
                performUIKitHapticFeedback()
                return
            }
            performCoreHapticFeedback()
        } else {
            performUIKitHapticFeedback()
        }
    }
    
    private func performUIKitHapticFeedback() {
        if let amplitude = amplitude {
            SwHaptic.impact(amplitude.value).generate()
        }
    }
    
    private func performCoreHapticFeedback() {
        if #available(iOS 13.0, *) {
            /*
             Currently Core Haptics is not being used anywhere else apart from Gamification flow hence creating a new instance of
             CoreHapticsManager everytime. Once this is being used in multiple places we can think of persisting it
             */
            coreHapticsManager = CoreHapticsManager(duration: duration, amplitude: amplitude ?? .light)
            if let manager = coreHapticsManager as? CoreHapticsManager {
                manager.playContinuousVibration()
            }
        }
    }
}

@available(iOS 13.0, *)
final class CoreHapticsManager {
    private let hapticEngine: CHHapticEngine
    private let duration: Int
    private let amplitude: SwFeedbackImpactType
    
    init?(duration: Int, amplitude: SwFeedbackImpactType) {
        self.duration = duration
        self.amplitude = amplitude
        let hapticCapability = CHHapticEngine.capabilitiesForHardware()
        guard hapticCapability.supportsHaptics else {
            return nil
        }
        
        do {
            hapticEngine = try CHHapticEngine()
        } catch _ {
            return nil
        }
    }
    
    func playContinuousVibration() {
        do {
            let pattern = try continuousVibration()
            try hapticEngine.start()
            let player = try hapticEngine.makePlayer(with: pattern)
            try player.start(atTime: CHHapticTimeImmediate)
            hapticEngine.notifyWhenPlayersFinished { _ in
                return .stopEngine
            }
        } catch {}
    }
}

@available(iOS 13.0, *)
extension CoreHapticsManager {
    //Below function will play a continuous vibration for a constant period of time
    private func continuousVibration() throws -> CHHapticPattern {
        let hapticIntensity: Float
        switch amplitude {
        case .light:
            hapticIntensity = 0.7
        case .medium:
            hapticIntensity = 0.85
        case .heavy:
            hapticIntensity = 1.0
        }
        let continuousVibrationEvent = CHHapticEvent(
            eventType: .hapticContinuous,
            parameters: [
                CHHapticEventParameter(parameterID: .hapticIntensity, value: hapticIntensity)
            ],
            relativeTime: 0,
            duration: (Double(duration)/1000))
        return try CHHapticPattern(events: [continuousVibrationEvent], parameters: [])
    }
}

Edit 2:

enum SwFeedbackImpactType: String {
    case light, medium, heavy
    var value: UIImpactFeedbackGenerator.FeedbackStyle {
        switch self {
        case .light:
            return .light
        case .medium:
            return .medium
        case .heavy:
            return .heavy
        }
    }
}

Edit 3: Making Haptic Player a member of the class instead of instantiating inside the function

@available(iOS 13.0, *)
final class CoreHapticsManager {
    private let hapticEngine: CHHapticEngine
    private let duration: Int
    private let amplitude: SwFeedbackImpactType
    private var player: CHHapticPatternPlayer?
    
    init?(duration: Int, amplitude: SwFeedbackImpactType) {
        self.duration = duration
        self.amplitude = amplitude
        let hapticCapability = CHHapticEngine.capabilitiesForHardware()
        guard hapticCapability.supportsHaptics else {
            return nil
        }
        
        do {
            hapticEngine = try CHHapticEngine()
        } catch _ {
            return nil
        }
    }
    
    func playContinuousVibration() {
        do {
            let pattern = try continuousVibration()
            try hapticEngine.start()
            player = try hapticEngine.makePlayer(with: pattern)
            try player?.start(atTime: CHHapticTimeImmediate)
            hapticEngine.notifyWhenPlayersFinished { _ in
                return .stopEngine
            }
        } catch {}
    }
}
kgupta073
  • 1
  • 3
  • Apple says: "Create a haptic engine instance. Maintain a strong reference to it so it doesn’t go out of scope while the haptic is playing." You need to keep your `player` alive. Make it a member of the class instead of declaring the variable in the method. – Eric Aya Dec 27 '21 at 13:02
  • Hi, try to upload all the code for testing. – Diego Jiménez Dec 27 '21 at 13:02
  • @EricAya Got it. Let me try that. – kgupta073 Dec 27 '21 at 13:09
  • @wazowski Have posted more code here for your understanding. Kindly look into it. – kgupta073 Dec 27 '21 at 13:09
  • Can you share SwFeedbackImpactType too please – Diego Jiménez Dec 27 '21 at 13:33
  • @wazowski Have added the SwFeedbackImpactType enum as well. Anyway that is used as a fallback in case custom haptics is not compatible with the device/OS so don't think that is coming in the picture here – kgupta073 Dec 28 '21 at 05:30
  • @EricAya So here i have already made the engine a member of the class. Are you suggesting i should do the same for the `player` as well? – kgupta073 Dec 28 '21 at 07:53
  • @kgupta073 Yes, this is what I'm suggesting. I quoted the part about the engine but it's the same for the player. – Eric Aya Dec 28 '21 at 12:56
  • @EricAya Ok. I have made the `player` an optional property of the class instead of instantiating it within the function itself. Can you please check EDIT 3 and tell me if that works? – kgupta073 Dec 30 '21 at 05:32

1 Answers1

0

I have managed to play the haptic like this. I have removed extra code for the test, but I think you can reproduce it in your full code, right? Because time, I have tested only for iOS 13 and above.

import Foundation

import CoreHaptics

@available(iOS 13.0, *)
class HapticManager {
  let hapticEngine: CHHapticEngine

  init?() {
    let hapticCapability = CHHapticEngine.capabilitiesForHardware()
    guard hapticCapability.supportsHaptics else {
      return nil
    }

    do {
      hapticEngine = try CHHapticEngine()
      hapticEngine.isAutoShutdownEnabled = true
    } catch let error {
      print("Haptic engine Creation Error: \(error)")
      return nil
    }
  }
  
  func playPattern() {
    do {
      let pattern = try continuousVibration()
      try hapticEngine.start()
      let player = try hapticEngine.makePlayer(with: pattern)
      try player.start(atTime: CHHapticTimeImmediate)
      hapticEngine.notifyWhenPlayersFinished { _ in
        return .stopEngine
      }
    } catch {
      print("Failed to play pattern: \(error)")
    }
  }
}

@available(iOS 13.0, *)
extension HapticManager {
  private func basicPattern() throws -> CHHapticPattern {

        let pattern = CHHapticEvent(
          eventType: .hapticTransient,
          parameters: [
            CHHapticEventParameter(parameterID: .hapticIntensity, value: 3.0),
            CHHapticEventParameter(parameterID: .hapticSharpness, value: 3.0)
          ],
          relativeTime: 1)

        return try CHHapticPattern(events: [pattern], parameters: [])
  }
    
    private func continuousVibration() throws -> CHHapticPattern {
        let duration = 1000 // ms suppose
        let hapticIntensity: Float
        hapticIntensity = 1.0
        let continuousVibrationEvent = CHHapticEvent(
            eventType: .hapticContinuous,
            parameters: [
                CHHapticEventParameter(parameterID: .hapticIntensity, value: hapticIntensity)
            ],
            relativeTime: 0,
            duration: (Double(duration)/1000))
        return try CHHapticPattern(events: [continuousVibrationEvent], parameters: [])
    }
}

So in order to test you can use playPattern and continuosVibration will be fired.

But I think if you want to keep it working you should put it in a while loop with a trigger like this:

func startHaptic() {
    sendHaptics = true
    while sendHaptics == true {
      queue.async {
        hapticManager?.playPattern()
      }
    }
  }

The sendHaptics property must be global, right? and toggle it when no more haptic is needed.

Diego Jiménez
  • 1,398
  • 1
  • 15
  • 26