更新於 2023 年 02 月 03 日

提供聲音和觸覺回饋(Haptic)

相信大家都有經歷過,自己點了一個按鈕,但是畫面完全沒有變化,你心裡想:現在是我沒有按到?還是正在跑?我應該要重按一次嗎?會不會打斷什麼?
這樣的使用者體驗是令人失望的,設計 app 的時候,提供明確的互動回饋是很重要的。最基本的、一定要有的就是視覺回饋的,也就是畫面的變化。
除此之外,我們還可以再對重要的場合加上聲音和觸覺的回饋,經典的例子就是 Apple Pay 付款的觸覺跟聽覺回饋。
這篇文章就介紹簡單介紹一些加入這兩種回饋的方式。

設計之前的考量

在開始介紹之前,我想提醒大家,這兩種回饋都不是絕對能產生的,使用者可能關閉音效或觸覺回饋、也可能他的裝置不支援觸覺回饋,所以你還是需要做好視覺回饋。
再來是要掌握使用的時機,這兩種回饋多了就會變成干擾。Human Interface Guidelines 中針對這兩項都有提供建議,設計前建議閱讀:
播放聲音
觸覺回饋

AudioToolbox

 內建音效

如果你需要的只是使用內建音效,那你只需要這個 framework。import 之後使用 AudioServicesPlaySystemSound 就可以播放內建音效。

// 播放通知音效
AudioServicesPlaySystemSound(SystemSoundID(1007))

這裡唯一的問題就是,這個魔法的 ID 1007 從哪裡來?
答案:直接到這個 repo 下方的 readme 找。

 自訂音效

除此之外,這個 SystemSoundID 其實是透過 URL 建立的,所以你也可以放自己的音效。

func registerSoundID(filename: String, ext: String) -> SystemSoundID {
    var systemID = SystemSoundID.zero
    
    // 取得檔案 URL,CFURL 是可以直接 bridge 的。
    let url = Bundle.main.url(forResource: filename, withExtension: ext)! as CFURL
    // ID 會用被寫入 systemID 的位置
    let result = AudioServicesCreateSystemSoundID(url, &systemID)
    // 確保沒有錯誤
    guard result == noErr else { return .zero }
    
    return systemID
}

// 假設要播放檔案 success.mp3
let id = registerSoundID(filename: "success", ext: "mp3")
AudioServicesPlayAlertSound(id)

 注意事項

注意這個 framework 是用來處理簡單、立即執行的短音效,彈性較低,下面是它的一些限制。

  • 使用 30 秒以內的音效。
  • 以系統音量播放,無法調整。
  • 只能馬上播放。
  • 無法循環、從音樂中間開始播放。
  • 無法指定輸出裝置。

看起來限制很多,但用來播放簡單的音效回饋已經足夠,如果你需要更多功能的話就要使用 AVFoundation 這個 framework 了。這個 framework 提供的功能非常豐富,網路上的資料也很多,這邊就不特別介紹。

UIFeedbackGenerator

想要提供簡單的觸覺回饋,UIKit 中的 UIFeedbackGenerator 是最簡單的方式。
UIKit 提供了三種實作的類型:

  • UIImpactFeedbackGenerator:適用可量化的反應,像是重力、壓力變化等等,你可以設定它的強度。
  • UISelectionFeedbackGenerator:提供選擇區域內容變化的觸覺回饋。
  • UINotificationFeedbackGenerator:提供成功、錯誤、警告的觸覺回饋。

這些差別要自己實際體驗才比較好理解,建議寫個簡單專案,先進手機按看看。
以下示範使用 UINotificationFeedbackGenerator。

let generator = UINotificationFeedbackGenerator()
// 成功的觸覺回饋。
generator.notificationOccurred(.success)

 用 prepare 降低延遲

UIFeedbackGenerator 使用上非常簡單,創好實例、呼叫方法就可以。不過有一個方法,可以讓這個觸覺回饋的延遲更低,建議在「可能發生觸覺回饋」的前幾秒先呼叫 prepare() 這個方法。
例如:在某個按鈕出現在畫面時呼叫 prepare,這樣在使用者點下按鈕時就可以更快收到觸覺回饋。
以下是幾個關於 prepare() 的使用須知:

  • 被呼叫後,觸覺回饋的引擎將進入「準備完畢(prepared)」的狀態,幾秒後沒有事件發生的話就會變回「待機(idle)」狀態。
  • 你可以重複呼叫多次 prepared,這並不會進行重複準備、產生效能問題,而是單純延長它「準備完畢」狀態的時間。

Core Haptics

如果你需要更加客製化的觸覺回饋,那你需要的是 Core Haptics 這個 framework。你可以定義觸覺回饋的 pattern,重複循環使用。在設計遊戲或音樂相關的 app 的時候,你可能會用到它。
我自己並沒有深入玩過它,如果你有興趣的話,可以參考 Paul Hudson 的這篇教學

將聲音和觸覺一起管理

這兩種回饋常常會是一起使用的,所以我個人喜歡把它們放在一起管理。
以下是簡單地在 SwiftUI 中實作內建音效 + 通知觸覺回饋的示範。

import UIKit
import AudioToolbox

// 內建音效
enum SystemSound: Int {
    case click         = 1104
    case error         = 1053
    case messageSent   = 1055
    case notificationDefault = 1007
}

extension SystemSound {
    func play() {
        AudioServicesPlaySystemSound(SystemSoundID(rawValue))
    }
}

// 呼叫的方法
extension UINotificationFeedbackGenerator {
    func play(sound: SystemSound, haptic: UINotificationFeedbackGenerator.FeedbackType) {
        sound.play()
        notificationOccurred(haptic)
    }
}

// 放到環境變數中,因為本身的變化不需要觀察,也不需要多個實例。
struct FeedbackGeneratorKey: EnvironmentKey {
    static var defaultValue = UINotificationFeedbackGenerator()
}

extension EnvironmentValues {
    var feedbackGenerator: UINotificationFeedbackGenerator {
        get { self[FeedbackGeneratorKey.self] }
        set { self[FeedbackGeneratorKey.self] = newValue }
    }
}

 在 SwiftUI 中呼叫的情況

import SwiftUI

struct ContentView: View {
    // 從環境變數中拿到共用的實例
    @Environment(\.feedbackGenerator) var feedbackGenerator
    
    var body: some View {
        Button("Error Feedback") {
            feedbackGenerator.play(sound: .error, haptic: .error)
            // 如果有可能繼續被點擊,就可以播放後直接 prepare。
            feedbackGenerator.prepare()
        }
        .onAppear { feedbackGenerator.prepare() }
    }
}

結語

在 app 之中,適當的使用音效和觸覺會讓使用者體驗大大加分,這篇文章主要介紹的 AudioToolbox 和 UIFeedbackGenerator 使用上也都很簡單,試著幫你的 app 新增看看吧 😊