1.0 으로 업그레이드
NUGU SDK iOS 를 0.x 에서 1.0 으로 업그레이드 하기위한 가이드입니다.
자세한 source code 변경사항은 github release note 를 참고해주세요.(https://github.com/nugu-developers/nugu-ios/releases/tag/1.0.

NotificationCenter 사용

NUGU SDK 의 구성요소에 대한 변경사항을 전달하기 위해 DelegateSet 대신 NotificationCenter 를 사용합니다.
DelegateSet 은 삭제되었으며, 기존에 DelegateSet 이 하던 역할을 delegate pattern 과 observer pattern 으로 구분하였습니다.

ASRAgent 변경사항 감지

이전코드
MainViewController.swift
NuguCentralManager.shared.client.asrAgent.add(delegate: self)
1.0 에서 변경된 코드
MainViewController.swift
asrResultObserver = object.observe(NuguAgentNotification.ASR.Result.self, queue: .main) { (notification) in
switch notification.result {
...
}
}

SystemAgent 변경사항 감지

이전코드
NuguCentralManager.swift
client.systemAgent.add(systemAgentDelegate: self)
1.0 에서 변경된 코드
NuguCentralManager.swift
systemAgentExceptionObserver = object.observe(NuguAgentNotification.System.Exception.self, queue: nil) { (notification) in
guard let self = self else { return }
switch notification.code {
...
}
}
systemAgentRevokeObserver = object.observe(NuguAgentNotification.System.RevokeDevice.self, queue: nil) { (notification) in
...
}

DialogStateAggregator 변경사항 감지

이전코드
MainViewController.swift
NuguCentralManager.shared.client.dialogStateAggregator.add(delegate: self)
1.0 에서 변경된 코드
MainViewController.swift
dialogStateObserver = object.observe(NuguClientNotification.DialogState.State.self, queue: nil) { (notification) in
switch notification.state {
...
}
}

NuguClient.Builder 추가

NuguClient 의 필수/선택 요소를 보다 효율적으로 생성하기 위해 builder pattern 이 적용되었습니다.
이전코드
NuguCentralManager.swift
lazy private(set) var client: NuguClient = {
let client = NuguClient(delegate: self)
...
return client
}()
1.0 에서 변경된 코드
NuguCentralManager.swift
lazy private(set) var client: NuguClient = {
let nuguBuilder = NuguClient.Builder()
...
// Set DataSource for SoundAgent
nuguBuilder.setDataSource(self)
// Set Delegate for PhoneCallAgent, MessageAgent, MediaPlayerAgent,
// ExtensionAgent, LocationAgent, PermissionAgent
nuguBuilder.setDelegate(self)
let client = nuguBuilder.build()
client.delegate = self
return client
}()

SpeechRecognizerAggregator 추가

NUGU SDK 의 SpeechRecognizerAggregatorMicInputProvider, ASRAgent, KeywordDetector 를 통합 관리합니다.
이전코드
NuguCentralManager.swift
private var startMicWorkItem: DispatchWorkItem?
// Audio input source
private let micQueue = DispatchQueue(label: "central_manager_mic_input_queue")
private let micInputProvider = MicInputProvider()
func enable() {
...
// Set Last WakeUp Keyword
// If you don't want to use saved wakeup-word, don't need to be implemented
if UserDefaults.Standard.useWakeUpDetector,
let keyword = Keyword(rawValue: UserDefaults.Standard.wakeUpWord) {
client.keywordDetector.keywordSource = keyword.keywordSource
startWakeUpDetector()
startMicWorkItem?.cancel()
startMicWorkItem = DispatchWorkItem(block: { [weak self] in
log.debug("startMicWorkItem start")
self?.startMicInputProvider(requestingFocus: false) { (success) in
guard success else {
log.debug("startMicWorkItem failed!")
return
}
}
})
guard let startMicWorkItem = startMicWorkItem else { return }
// When mic has been activated before interruption end notification has been fired,
// Option's .shouldResume factor never comes in. (even when it has to be)
// Giving small delay for starting mic can be a solution for this situation
DispatchQueue.global().asyncAfter(deadline: .now() + 0.5, execute: startMicWorkItem)
} else {
stopWakeUpDetector()
stopMicInputProvider()
}
}
func startMicInputProvider(requestingFocus: Bool, completion: @escaping (Bool) -> Void) {
startMicWorkItem?.cancel()
DispatchQueue.main.async {
guard UIApplication.shared.applicationState == .active else {
completion(false)
return
}
NuguAudioSessionManager.shared.requestRecordPermission { [unowned self] isGranted in
guard isGranted else {
log.error("Record permission denied")
completion(false)
return
}
self.micQueue.async { [unowned self] in
defer {
log.debug("addEngineConfigurationChangeNotification")
NuguAudioSessionManager.shared.registerAudioEngineConfigurationObserver()
}
self.micInputProvider.stop()
// Control center does not work properly when mixWithOthers option has been included.
// To avoid adding mixWithOthers option when audio player is in paused state,
// update audioSession should be done only when requesting focus
if requestingFocus {
NuguAudioSessionManager.shared.updateAudioSession(requestingFocus: requestingFocus)
}
do {
try self.micInputProvider.start()
completion(true)
} catch {
log.error(error)
completion(false)
}
}
}
}
}
func stopMicInputProvider() {
micQueue.sync {
startMicWorkItem?.cancel()
micInputProvider.stop()
NuguAudioSessionManager.shared.removeAudioEngineConfigurationObserver()
}
}
func startWakeUpDetector() {
client.keywordDetector.start()
}
func stopWakeUpDetector() {
client.keywordDetector.stop()
}
func startRecognition(initiator: ASRInitiator) {
client.asrAgent.startRecognition(initiator: initiator)
}
func stopRecognition() {
client.asrAgent.stopRecognition()
}
1.0 에서 변경된 코드
NuguCentralManager.swift
func startListening(initiator: ASRInitiator) {
client.speechRecognizerAggregator.startListening(initiator: initiator)
}
func startListeningWithTrigger() {
client.speechRecognizerAggregator.startListeningWithTrigger()
}
func stopListening() {
client.speechRecognizerAggregator.stopListening()
}
KeywordDetector, MicInputProvider , ASRAgent 시작/종료와 관련된 코드를 모두 제거하고SpeechRecognizerAggregatorstartListening(initiator:), startListeningWithTrigger, stopListening 함수 호출로 대체합니다.

Keyword 설정

이전코드
NuguCentralManager.swift
// Set Last WakeUp Keyword
// If you don't want to use saved wakeup-word, don't need to be implemented
if UserDefaults.Standard.useWakeUpDetector,
let keyword = Keyword(rawValue: UserDefaults.Standard.wakeUpWord) {
client.keywordDetector.keywordSource = keyword.keywordSource
}
1.0 에서 변경된 코드
NuguCentralManager.swift
// Set Last WakeUp Keyword
// If you don't want to use saved wakeup-word, don't need to be implemented.
// Because `aria` is set as a default keyword
if let keyword = Keyword(rawValue: UserDefaults.Standard.wakeUpWord) {
nuguBuilder.keywordDetector.keyword = keyword
}
// If you want to use built-in keyword detector, set this value as true
nuguBuilder.speechRecognizerAggregator.useKeywordDetector = UserDefaults.Standard.useWakeUpDetector
Keyword(e.g. "아리아") 관련 설정을 KeywordDetector 대신 SpeechRecognizerAggregator 에 적용합니다.

SDK 에서 AVAudioSessionManager 를 관리

NUGU SDK 를 사용하기 위해서는 시스템, Application, NUGU SDK 의 복합적인 상황에 맞춰 AVAudioSession 의 category, categoryOptionsAVAudioSession.interruptionNotification , AVAudioSession.routeChangeNotification 등을 적절하게 처리해 주어야 합니다.
SDK 1.0 에 추가된 AudioSessionManager 는 NUGU SDK 를 사용하기 위한 기본적인 AVAudioSession 처리를 구현하고 있습니다.
AVAudioSession 이 이미 Application 에서 충분히 복잡하게 관리되고 있다면, AudioSessionManager 를 사용하는 것보다 기존의 관리 로직에 NUGU SDK 를 위한 로직을 추가하는 방향이 더욱 효율적일 수 있습니다.
Custom AudioSessionManager 를 참고해주세요.

SDK 의 AudioSessionManager 사용

Application 의 NuguAudioSessionManager.swift 파일 및 관련 코드를 삭제합니다.
NuguCentralManager.swift
client.audioSessionManager?.enable()
AudioSessionManagerAVAudioSession.interruptionNotification , AVAudioSession.routeChangeNotification event 를 감지합니다.
NUGU SDK 를 사용하는 동안 event 를 감지하여 SDK 동작에 필요한 기능을 수행합니다.
NuguCentralManager.swift
client.audioSessionManager?.disable()
AudioSessionManagerAVAudioSession.interruptionNotification , AVAudioSession.routeChangeNotification event 감지를 중지합니다.
NUGU SDK 를 사용하지 않을 때 호출하여 SDK 가 불필요한 동작을 수행하지 않도록합니다.

Custom AudioSessionManager 사용

NUGU SDK 의 AudioSessionManager 를 사용하지 않고 Application 에서 AVAudioSession 을 직접 처리할 수 있습니다.
NuguCentralManager.swift
lazy private(set) var client: NuguClient = {
let nuguBuilder = NuguClient.Builder()
...
nuguBuilder.audioSessionManager = nil
...
let client = nuguBuilder.build()
client.delegate = self
...
return client
}()
NuguClient 생성시 audioSessionManager 를 nil 처리합니다.
NuguCentralManager.swift
extension NuguCentralManager: NuguClientDelegate {
func nuguClientWillUseMic() {
if requestingFocus {
NuguAudioSessionManager.shared.updateAudioSession(requestingFocus: true)
}
}
func nuguClientWillRequireAudioSession() -> Bool {
return NuguAudioSessionManager.shared.updateAudioSession(requestingFocus: true)
}
func nuguClientDidReleaseAudioSession() {
NuguAudioSessionManager.shared.notifyAudioSessionDeactivation()
}
...
}
NUGU SDK iOS 0.x 의 NuguAudioSessionManager.swift 를 참고하여 Application 에서 AudioSessionManager 를 직접 구현하고 NuguClientDelegate 의 일부 함수를 구현해 주어야 합니다.

VoiceChromePresenter 기능 추가

UITapGestureRecognizer 구현 이동

삭제해야 하는 코드
MainViewController.swift
@objc private extension MainViewController {
func didTapForStopRecognition() {
guard [.listening, .recognizing].contains(NuguCentralManager.shared.client.asrAgent.asrState) else { return }
NuguCentralManager.shared.client.asrAgent.stopRecognition()
}
}
func initializeNugu() {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapForStopRecognition))
tapGestureRecognizer.delegate = self
tapGestureRecognizer.cancelsTouchesInView = false
view.addGestureRecognizer(tapGestureRecognizer)
}
extension MainViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
let touchLocation = touch.location(in: gestureRecognizer.view)
return !nuguVoiceChrome.frame.contains(touchLocation)
}
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
}
UITapGestureRecognizer 관련 기능을 VoiceChromePresenter 에서 처리하기 때문에 Application 에서 관련 코드를 삭제합니다.

ASRBeepPlayer 구현 이동

삭제해야 하는 코드
NuguCentralManager.swift
lazy private(set) var asrBeepPlayer: ASRBeepPlayer = ASRBeepPlayer(focusManager: client.focusManager)
MainViewController.swift
extension MainViewController: DialogStateDelegate {
func dialogStateDidChange(_ state: DialogState, isMultiturn: Bool, chips: [ChipsAgentItem.Chip]?, sessionActivated: Bool) {
switch state {
case .listening:
DispatchQueue.main.async {
NuguCentralManager.shared.asrBeepPlayer.beep(type: .start)
}
case .thinking:
DispatchQueue.main.async { [weak self] in
self?.nuguButton.pauseDeactivateAnimation()
}
default:
break
}
}
}
extension MainViewController: ASRAgentDelegate {
func asrAgentDidReceive(result: ASRResult, dialogRequestId: String) {
switch result {
case .complete:
DispatchQueue.main.async {
NuguCentralManager.shared.asrBeepPlayer.beep(type: .success)
}
case .error(let error, _):
DispatchQueue.main.async {
switch error {
case ASRError.listenFailed:
NuguCentralManager.shared.asrBeepPlayer.beep(type: .fail)
case ASRError.recognizeFailed:
NuguCentralManager.shared.localTTSAgent.playLocalTTS(type: .deviceGatewayRequestUnacceptable)
default:
NuguCentralManager.shared.asrBeepPlayer.beep(type: .fail)
}
}
default: break
}
}
}
Beep 음 재생과 관련된 기능을 VoiceChromePresenter 에서 처리하기 때문에 Application 의 ASRBeepPlayer.swift 파일 및 asrBeepPlayer.beep(type:) 호출 코드를 삭제합니다.

ControlCenterManager 추가

AudioPlayerAgent 를 사용하여 음악을 재생하는 경우 SDK 의 ControlCenterManager 에서 MPNowPlayingInfoCenter 를 통해 재생정보를 iOS platform 으로 전달합니다.
삭제해야 하는 코드
NuguCentralManager.swift
let displayPlayerController = NuguDisplayPlayerController()
MainViewController.swift
extension MainViewController: AudioDisplayViewPresenterDelegate {
func displayControllerShouldUpdateTemplate(template: AudioPlayerDisplayTemplate) {
NuguCentralManager.shared.displayPlayerController.update(template)
}
func displayControllerShouldUpdateState(state: AudioPlayerState) {
NuguCentralManager.shared.displayPlayerController.update(state)
}
func displayControllerShouldUpdateDuration(duration: Int) {
NuguCentralManager.shared.displayPlayerController.update(duration)
}
func displayControllerShouldRemove() {
NuguCentralManager.shared.displayPlayerController.remove()
}
...
}
MPNowPlayingInfoCenter 관련 기능을 AudioDisplayViewPresenterControlCenterManager 에서 처리하기 때문에 Application 의 NuguDisplayPlayerController.swift 및 관련 코드를 삭제합니다.