[iOS] - CallKit프레임워크 사용해보기 - 2

수킴·2022년 1월 7일
0

iOS 

목록 보기
7/12

개요

배웠던 CallKit개념을 활용하여 Raywenderlich에서 제공하는 CallKit 프로젝트를 따라 정리해보겠습니다.

과거에 VoIP앱들은 제한적이고, 알림을 전달하는 과정이 어려웠지만 CallKit의 도입으로 수월해졌다.

🚨 CallKit 기능은 시뮬레이터에서 작동하지 않습니다. (실제기기를 사용해야합니다.)

CallKit은 앱들이 기본전화UI와 통합하는 것을 허용함으로써 VoIP 환경을 개선하는 것을 목표로 하는 프레임워크

  • CallKit을 채택하면 앱은 다음과 같은 기능을 제공할 수 있습니다.
    1. 잠금 상태 및 잠금해제상태 모두에서 기본수신화면을 보여줄 수 있습니다.
    2. 기본전화앱의 연락처, 즐겨찾기, 최근화면에서 전화를 시작할 수 있습니다.
    3. 시스템에서 다른호출들과 함께 상호작용할 수 있습니다.

CXProvider

CXProvider는 시스템에 대역외(out-of-band)알림을 보고하기위해 사용됩니다. (일반적으로 수신전화와 같은 외부이벤트입니다.)

이벤트가 발생하면 CXProvider는 call update 를 생성하여 시스템에 알립니다. call update 는 새로운,변경된 전화와 관련된 정보를 캡슐화하고 CXCallUpdate 클래스로, 전화하는 사람이름이나 전화화가 비디오 또는 오디오 전용인지와 같은 속성을 표시합니다.

시스템이 앱에 이벤트를 알려줄 대는 CXAction의 인스턴스를 사용합니다. CXAction은 통신작업을 나타내는 추상클래스입니다. 각각의 작업을 위해, CallKit은 CXAction의 다른 구체적인 메서드를 제공합니다.

  • 예시) CXStartCallAction(발신전화시작), CXAnswerCallAction(수신작업을 응답)
  • 각각의 작업은 UUID식별자를 통해 실패하거나 실행할 수 있습니다.

앱은 CXProviderDelegate프로토콜을 사용하여 CXProvider와 통신할 수 있으며, 이 프로토콜은 provider생명주기이벤트 및 수신 액션에 대한 메서드를 정의합니다.

CXCallController

앱은 CXCallController를 사용하여 StartCallAction과 같은 사용자가 시작한 요청을 시스템에 알립니다.

  • 이것이 CXProvider와 CXCallController의 주요 차이점입니다.
  • provider는 시스템에 보고하는 반면, CXCallController는 사용자를 대신하여 시스템으로부터 요청을 합니다.

CXCallController는 이러한 요청을 하기 위해 트랜잭션(transactions)을 사용합니다. CXTransaction으로 표시되는 트랜잭션에는 하나 이상의 CXAction 인스턴스가 포함됩니다. CXCallController는 시스템에 트랜잭션(transactions)을 보냅니다. 모든 것이 정상이면 시스템은 provider에게 적절한 조치를 취하여 응답합니다.

Incoming Calls (수신 전화)

  1. 수신전화에 응답하여 앱은 CXCallUpdate를 구성하고 provider를 사용하여 시스템에 전송합니다.
  2. 시스템은 이를 모든 서비스에 수신전화로 게시합니다.
  3. 사용자가 전화에 응답하면 시스템은 CXAnswerCallAction 인스턴스를 provider에게 보냅니다.
  4. 앱은 적절한 CXProviderDelegate메서드를 구현하여 전화에 응답합니다.

ProviderDelegate

// ProvideDelegate.swift

import AVFoundation
import CallKit

class ProviderDelegate: NSObject {
 // 1.provider and the call controller를 참조할 변수 저장합니다.
 private let callManager: CallManager
 private let provider: CXProvider
 
 init(callManager: CallManager) {
   self.callManager = callManager
   // 2. static변수를 통해 적절한 CXProviderConfiguration와 함께 초기화
   provider = CXProvider(configuration: ProviderDelegate.providerConfiguration)
   
   super.init()
   // 3. provider가보내는 이벤트에 응답하도록 델리게이트 설정 (CXProviderDelegate를 채택해야함)
   provider.setDelegate(self, queue: nil)
 }
 
 // 4. 통화그룹수 1명 지정, 비디오오디오 지원, 전화번호 처리 지정
 static var providerConfiguration: CXProviderConfiguration = {
   let providerConfiguration = CXProviderConfiguration(localizedName: "Hotline")
   
   providerConfiguration.supportsVideo = true // 
   providerConfiguration.maximumCallsPerCallGroup = 1 // 통화그룹수 지정
   providerConfiguration.supportedHandleTypes = [.phoneNumber]
   
   return providerConfiguration
 }()
}

provider를 위한 delegate를 생성합니다.

  • CXProviderConfiguration를 사용하여 통화그룹수 ,비디오통화 허용여부, 처리할 타입(전화번호, 이메일주소)등등을 지정할 수 있습니다.

CXProvider API를 호출하여 수신 통화를 보고

// ProvideDelegate.swift
func reportIncomingCall(
  uuid: UUID, 
  handle: String, 
  hasVideo: Bool = false,
  completion: ((Error?) -> Void)?
) {
  // 1. (시스템에 알리기위해)전화관련데이터를 포함하는 call update생성 
  let update = CXCallUpdate()
  update.remoteHandle = CXHandle(type: .phoneNumber, value: handle)
  update.hasVideo = hasVideo
  
  // 2. 이 메서드를 사용하여 시스템에게 수신전화를 알립니다.
  provider.reportNewIncomingCall(with: uuid, update: update) { error in
    if error == nil {
      // 3. 시스템이 호출을 처리한 후 에러가 없다면 Call인스턴스 생성한 후 callManager에 추가
      let call = Call(uuid: uuid, handle: handle)
      self.callManager.add(call: call)
    }
    
    // 4. 에러발생
    completion?(error)
  }
}
  • 이 메서드를 통해 앱은 CXProvider API를 호출하여 수신전화를 보고할 수 있습니다.
  • 이 메서드를 통해 앱의 다른클래스에서 수신전화를 테스트할 수 있습니다.

CXProvicerDelegate채택

// ProviderDelegate.swift

extension ProviderDelegate: CXProviderDelegate {
  func providerDidReset(_ provider: CXProvider) {
    stopAudio()
    
    for call in callManager.calls {
      call.end()
    }
    
    callManager.removeAllCalls()
  }
}
  • CXProviderDelegate는 providerDidReset(_:)메서드를 필수로 작성해야합니다.
  • proovider는 reset하는 경우에 이 메서드를 호출하여 진행 중인 전화를 정리하고 정상상태(통화중이 아닌상태)로 되돌릴 수 있습니다.
  • 이 메서드에서는 진행 중인 오디오 세션을 종료하고 활성상태인 통화를 모두 삭제합니다.

ProviderDelegate사용

// AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

  var window: UIWindow?
  let callManager = CallManager()
  var providerDelegate: ProviderDelegate!
  
  class var shared: AppDelegate {
    return UIApplication.shared.delegate as! AppDelegate
  }
  
  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    providerDelegate = ProviderDelegate(callManager: callManager)
    return true
  }
  
  func displayIncomingCall(
    uuid: UUID,
    handle: String,
    hasVideo: Bool = false,
    completion: ((Error?) -> Void)?
  ) {
    providerDelegate.reportIncomingCall(
      uuid: uuid,
      handle: handle,
      hasVideo: hasVideo,
      completion: completion)
  }
}
  • ProviderDelegate는 수신전화를 보고하는 방법을 제공합니다.

위 과정들과 UI와 연결시키기

// CallsViewController.swift
@IBAction private func unwindForNewCall(_ segue: UIStoryboardSegue) {
		  // 1.
		guard 
		  let newCallController = segue.source as? NewCallViewController,
		  let handle = newCallController.handle 
		  else {
		    return
		}
		  
		let videoEnabled = newCallController.videoEnabled
		    
		// 2.
		let backgroundTaskIdentifier = 
		  UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
		
		DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
		  AppDelegate.shared.displayIncomingCall(
		    uuid: UUID(), 
		    handle: handle,
		    hasVideo: videoEnabled
		  ) { _ in
		    UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
		  }
		}
}
  • 메인 화면인 CallsViewController의 unwindForNewCall(_:)
  • CallsViewController → NewCallViewController의 세그과정에서 관련정보를 사용합니다.
  • 사용자는 작업이 완료되기 전에 앱을 일시 중단할 수 있으므로 백그라운드에서 작업을 사용해야 합니다.

수신전화 응답처리하기 (전화 받기)

// ProviderDelegate.swift

func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
  // 1. callManager를 통해 call의 UUID를 참조합니다.
  guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
    action.fail()
    return
  }
  
  // 2. 앱이 통화에 대한 오디오 세션을 구성합니다. 시스템이 세션을 높은 우선순위로 활성화합니다
  configureAudioSession()
  // 3. answer메서드는 전화가 활성(active)상태인것을 나타냅니다.
  call.answer()
  
	// 4. 행동을 처리할 때 실패하거나 수행하는 것이 중요합니다. 
	// 프로세스 중에 오류가 없다고 가정하면 fulfill()을 호출하여 성공을 나타낼 수 있습니다.
  action.fulfill()
}

// 5. 시스템이 provider의 오디오세션을 활성화하면 delegate에게 알립니다 -> 전화오디오 시작
func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) {
 startAudio()
}

통화종료 처리하기

  • 현재 통화를 종료할 수 있는 방법이 없습니다.
  • 기본전화앱 화면에서 종료하는 방법(1a) 및 앱내에서 종료하는 방법(1b) 두가지를 사용할 수 있습니다
    • 1a의 경우 자동으로 시스템은 provider에게 CXEndCallAction를 보냅니다.
    • 1b의 경우 해당 작업을 트랜잭션으로 포장하여 시스템에 요청하는 것이 중요합니다. 시스템이 요청을 처리하면 CXEndCallAction을 provider에게 다시 보냅니다

앱 내부에서 통화종료 (UI 변경하기)

// ProviderDelegate.swift
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
  // 1. callManager로 call을 참조
  guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
    action.fail()
    return
  }
  
  // 2. 통화를 종료하기 위해 통화의 오디오를 중지합니다.
  stopAudio()
  // 3. 통화상태를 end로 변경하여 다른 클래스가 새로운 상태에 반응할 수 있습니다.
  call.end()
  // 4. action을 실행할 수 있습니다.
  action.fulfill()
  // 5. call이 더이상 필요하않으므로 callmanger는 call을 처리(dispose)할 수 있습니다.
  callManager.remove(call: call)
}

앱 내부에서 통화종료 (Requesting Transactions : 종료 action 요청)

// CallManager.swift

// CXCallController 통신하기위해 인스턴스 생성
private let callController = CXCallController()

func end(call: Call) {
  // 1. 통화종료작업을 생성. (나중에 식별하기위한 call의 uuid를 초기화할 때 전달)
  let endCallAction = CXEndCallAction(call: call.uuid)
  // 2. 액션을 트랜잭션으로 포장하여 시스템으로 보냅니다. 
  let transaction = CXTransaction(action: endCallAction)
  
  requestTransaction(transaction)
}

// 3. 시스템은 provider가 이 트랜잭션을 수행하도록 요청하고, 그러면 방금 구현한 메서드가 호출됩니다.
private func requestTransaction(_ transaction: CXTransaction) {
 callController.request(transaction) { error in
   if let error = error {
     print("Error requesting transaction: \(error)")
   } else {
     print("Requested transaction successfully")
   }
 }
}

Transaction처리한 결과 UI 적용시키기

// CallsViewController.swift
override func tableView(
  _ tableView: UITableView,
  commit editingStyle: UITableViewCell.EditingStyle,
  forRowAt indexPath: IndexPath
) {
  let call = callManager.calls[indexPath.row]
  callManager.end(call: call)
}

여러 Provider Actions(Other Provider Actions)

CXProviderDelegate 공식문서의 Handling Call Actions을 보면 여러 Action을 처리할 수 있습니다.

  • 통화보류, 음소거 호출, 그룹 호출 등등

통화 보류처리하기

// ProviderDelegate.swift

func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) {
  guard let call = callManager.callWithUUID(uuid: action.callUUID) else {
    action.fail()
    return
  }
  
  // 1. call에 대한 참조를 한 후 action의 isOnHold속성에 따라 상태를 업데이트합니다.
  call.state = action.isOnHold ? .held : .active
  
  // 2. 상태에 따라 통화 오디오처리를 시작하거나 중지합니다.
  if call.state == .held {
    stopAudio()
  } else {
    startAudio()
  }
  
  // 3. action 실행
  action.fulfill()
}
  • 사용자가 통화를 보류상태로 설정하기 위해서는 provider에게 CXSetHeldCallAction를 보냅니다.
  • 이 작업도 사용자가 시작한 작업이므로 CallManager 클래스도 확장해야 합니다. (end 종료작업과 같은 방식)
// CallManager.swift

func setHeld(call: Call, onHold: Bool) {
  let setHeldCallAction = CXSetHeldCallAction(call: call.uuid, onHold: onHold)
  let transaction = CXTransaction()
  transaction.addAction(setHeldCallAction)
  
  requestTransaction(transaction)
}
// CallsViewController.swift
override func tableView(
  _ tableView: UITableView,
  didSelectRowAt indexPath: IndexPath
) {
  let call = callManager.calls[indexPath.row]
  call.state = call.state == .held ? .active : .held
  callManager.setHeld(call: call, onHold: call.state == .held)
  
  tableView.reloadData()
}

Outgoing Calls(발신 전화)

발신전화 처리하기 - Handling Outgoing Calls

// ProviderDelegate.swift

// provider는 발신전화요청이 올 때 이 메서드를 호출합니다.
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
  let call = Call(uuid: action.callUUID, outgoing: true, 
                  handle: action.handle.value)
  // 1. 오디오세션을 구성합니다. 단지 configuration(구성)만 한 후, 실제 호출은 provider(_:didActivate)
  configureAudioSession()
  

	// 2. 델리게이트는 call의 생명주기를 관리합니다. 처음에 발신전화가 연결되었다고 보고된 후
	// cll 연결되면, provider delegate는 연결또한 보고합니다.
  call.connectedStateChanged = { [weak self, weak call] in
    guard 
      let self = self, 
      let call = call 
      else { 
        return
      }

    if call.connectedState == .pending {
      self.provider.reportOutgoingCall(with: call.uuid, startedConnectingAt: nil)
    } else if call.connectedState == .complete {
      self.provider.reportOutgoingCall(with: call.uuid, connectedAt: nil)
    }
  }

  // 3. start()메서드가 실행되면 생명주기를 변경 된 후 성공적으로 연결되면 action을 실행합니다.
  call.start { [weak self, weak call] success in
    guard 
      let self = self, 
      let call = call 
      else { 
        return 
      }

    if success {
      action.fulfill()
      self.callManager.add(call: call)
    } else {
      action.fail()
    }
  }
}

전화 시작하기 - 발신전화 생성

// CallManager.swift
func startCall(handle: String, videoEnabled: Bool) {

  // 1 CXHandle로 표시되는 핸들은 핸들 유형과 값을 지정할 수 있습니다.
  let handle = CXHandle(type: .phoneNumber, value: handle)

  // 2 CXStartCallAction은 고유한 UUID와 핸들을 매개변수로 받습니다.
  let startCallAction = CXStartCallAction(call: UUID(), handle: handle)

  // 3 시스템에 요청하기위해 action을 transaction으로 랩핑
	// isVideo 속성을 설정하여 통화가 오디오 전용인지 화상 통화인지 지정합니다.
  startCallAction.isVideo = videoEnabled
  let transaction = CXTransaction(action: startCallAction)
  
  requestTransaction(transaction)
}

provider delegate가 발신전화를 처리할 준비가 되면 앱에서 발신전화를 만드는 방법을 처리합니다.

수신 및 발신전화 분기처리

// CallsViewController.swift
@IBAction private func unwindForNewCall(_ segue: UIStoryboardSegue) {
    guard
      let newCallController = segue.source as? NewCallViewController,
      let handle = newCallController.handle
      else {
        return
    }
    
    let videoEnabled = newCallController.videoEnabled
    let incoming = newCallController.incoming
    
    if incoming {
      let backgroundTaskIdentifier =
        UIApplication.shared.beginBackgroundTask(expirationHandler: nil)

      DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
        AppDelegate.shared.displayIncomingCall(
          uuid: UUID(),
          handle: handle,
          hasVideo: videoEnabled
        ) { _ in
          UIApplication.shared.endBackgroundTask(backgroundTaskIdentifier)
        }
      }
    } 
	
		// 수신 전화가 아니라면, callManager는 발신전화를 시작하도록 요청합니다.
		else {
      callManager.startCall(handle: handle, videoEnabled: videoEnabled)
    }
  }

통화중인 경우 처리하기 - Managing Multiple Calls

사용자의 선택에 따라 여러 동작을 CXTransaction으로 결합합니다.

  • 예를 들어, 사용자가 진행 중인 통화를 종료하고 새 통화를 응답하도록 선택하는 경우라면 시스템은 전자의 경우 CXEndCallAction, 후자의 경우 CXStartCallAction을 생성합니다. 두 작업은 트랜잭션으로 포장되어 provider에게 전송되며, provider는 이를 개별적으로 처리합니다.

  • 앱은 한 번에 하나의 오디오 세션만 처리합니다. 통화 재개를 선택하면 다른 사람은 자동으로 보류됩니다.

전화목록 Extension생성하기 - Creating a Call Directory Extension

The directory extension는 CallKit에 의해 제공되는 새로운 extension point입니다.

  • 시스템 차단목록에 전화번호를 추가할 수 있습니다.
  • 전화번호 또는 이메일주소와 같은 기타 고유 식별 정보로 수신 전화를 식별할 수 있습니다.

시스템이 전화를 받으면 주소록에서 일치하는 항목을 확인합니다. 찾을 수 없는 경우 app-specific directory extensions을 확인할 수 있습니다.

타겟 extension 추가

  1. extension을 File → New → Target (Call Directory Extension 추가)

XCode는 자동으로 CallDirectoryHandler.swift 파일을 생성합니다.

  • beginRequest(with:)는 extension이 초기화될때 호출됩니다.
  • requestFailed(for:withError:)는 오류가 발생하면 extension에서 호스트앱에 extension요청을 취소하라는 메시지를 표시합니다.

차단처리하기

// CallDirectoryHandler.swift

private func addAllBlockingPhoneNumbers(to context: CXCallDirectoryExtensionContext) {
    let phoneNumbers: [CXCallDirectoryPhoneNumber] = [ 1234 ]
    for phoneNumber in phoneNumbers {
      context.addBlockingEntry(withNextSequentialPhoneNumber: phoneNumber)
    }
  }

addAllBlockingPhoneNumbers(to:)는 차단해야 하는 모든 전화 번호를 수집합니다.

  • 지정된 전화 번호로 addBlockingEntry(withNextSequentialPhoneNumber:)를 호출하면 차단 목록에 추가됩니다. 시스템은 provider는 차단번호를 표시하지 않습니다.

식별자 처리하기

// CallDirectoryHandler.swift

private func addAllIdentificationPhoneNumbers(to context: CXCallDirectoryExtensionContext) {
    let phoneNumbers: [CXCallDirectoryPhoneNumber] = [ 1111 ]
    let labels = [ "RW Tutorial Team" ]
    
    for (phoneNumber, label) in zip(phoneNumbers, labels) {
      context.addIdentificationEntry(
        withNextSequentialPhoneNumber: phoneNumber,
        label: label
      )
    }
  }

addAllIdentificationPhoneNumbers(to:)는 전화번호를 식별합니다.

  • addIdentificationEntry(withNextSequentialPhoneNumber:label:)를 지정한 전화번호와 label로 호출하면 새로운 식별정보를 생성합니다.

권한 설정

Settings → Phone(전화) → Call Blocking & Identification → 허용

차단번호 오고 있는지 디버깅 하는 방법

  • ProviderDelegate에서 reportIncomingCall(uuid:handle:hasVideo:completion:)에서 알 수 있습니다.
    • reportNewIncomingCall(withupdate:completion:)

참고링크

profile
iOS 공부 중 🧑🏻‍💻

0개의 댓글