관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 1. 전화걸기, 전화받기, VoIP(Voice over IP) - CallKit 본문

iOS 응용 (swift)

[iOS - swift] 1. 전화걸기, 전화받기, VoIP(Voice over IP) - CallKit

jake-kim 2022. 2. 11. 02:51

전화 받기

전화 받기 1
전화 받기 2

전화 걸기

1. 전화걸기, 전화받기, VoIP(Voice over IP) - CallKit

2. 전화걸기, 전화받기, VoIP(Voice over IP) - PushKit

iOS에서의 VoIP 개념?

  • VoIP(Voice over Internet Protocol): 셀룰러 서비스 대신 인터넷 연결을 사용하여 전화를 걸고 받을 수 있는 프로토콜
  • iOS 8이전에는 VoIP앱이 인터넷 연결을 사용하여 전화를 수신하기 위해 서버와 지속적인 연결이 필요했기 때문에 background에서 연결을 유지하면 배터리 소모, 앱이 충돌 등의 다양한 문제가 발생
  • iOS8부터 애플에서 PushKit을 만들고 기본 앱 Messenge에 도입하여 최적화 적용 (PushKit 내용은 다음 포스팅 글 참고)

CallKit

https://developer.apple.com/documentation/callkit

  • VoIP 서비스를 위해서 시스템에서 제공하는 통화 UI를 제공
  • 카톡의 보이스톡, 페이스톡처럼 앱을 이용해 전화가 걸려올 때 일반 전화 수신화면처럼 띄워줄 수 있도록 사용
  • 앱간의 calling 서비스 제공이 가능

시스템에 내장된 전화화면 UI 사용 가능

  • CallKit은 CXProvider, CXCallController로 구성

https://www.raywenderlich.com/1276414-callkit-tutorial-for-ios

  • CXProvider
    • CXCallUpdate 인스턴스를 통해 이름과 오디오 전용인지 화상통화인지 속성 정의 가능
    • 차례대로 앱에 이벤트를 알리려고 할 때마다 CXAction 인스턴스의 형태로 알림
    • CXAction인터페이스이고 구현체안 CXStartCallAction과 CXAnserCallAction 사용
  •  CXCallController
    • Call의 기능 사용 가능 - 전화걸기, 끝내기, mute, 일시정지 등
    • CXTransaction(action:) 인스턴스를 사용하여 request
      let callController = CXCallController()
      
      private func requestTransaction(with action: CXCallAction, completionHandler: (NSError? -> Void)?) {
          let transaction = CXTransaction(action: action)
          callController.request(transaction) { error in
              completionHandler?(error as NSError?)
          }
      }​

걸려오는 전화 받기 처리

  1. Call 이벤트가 발생하여 호출이 되면 CXCallUpdate를 생성, CXProvider에 의해 시스템에 전송
  2. 사용자가 호출에 응답하면 시스템이 CXAnsherCallAction 인스턴스를 CXProvider에 전달
  3. CXProviderDelegate를 준수하여 처리 가능

https://www.raywenderlich.com/1276414-callkit-tutorial-for-ios

 

 

걸려오는 전화 받기 구현

전화 받기 2

  • 수신 전화를 받도록 CXProvider객체를 마든 후 전역에 저장
  • 앱은 다음에 알아볼 PushKit에서 생성한 VoIP 푸시 알림과 같은 외부 알림에 대한 응답으로 CXProvider에게 호출을 알림
    // PushKit에 있는 VoIP 알림 수신 메소드
    
    // MARK: PKPushRegistryDelegate
    func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, forType type: PKPushType) {
        // report new incoming call
    }​
  • 위 메소드로 들어온 정보를 사용
    • uuid와 identifier 사용
  • CXProvider의 메소드인 reportNewIncomingCall(with:update:)를 통해 시스템에 calling 요청
    if let uuidString = payload.dictionaryPayload["UUID"] as? String,
        let identifier = payload.dictionaryPayload["identifier"] as? String,
        let uuid = UUID(uuidString: uuidString)
    {
        let update = CXCallUpdate()    
        update.callerIdentifier = identifier
        
        provider.reportNewIncomingCall(with: uuid, update: update) { error in
            // …
        }
    }​
  • reportNewIncomingCall(with:update:)가 호출되면 아래 `CXProviderDelegate`의 메소드가 호출 provider(_:perform:)
    • action.fail()이나 action.fultill()을 사용하여 통화 준비가 완료되었다는 메시지 전달 -> 수신 시작
      func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
        action.fulfill()
      }​

ex) PushKit 없이 전화받기 전체 코드
* CXHandle: phoneNumber나 email과 같이 전화를 받는 사람에게 연락할 수 있는 수단

https://developer.apple.com/documentation/callkit/cxhandle

import UIKit
import CallKit

class ViewController: UIViewController {
  private let provider = CXProvider(configuration: CXProviderConfiguration())
    
  override func viewDidLoad() {
    super.viewDidLoad()
  }
  @IBAction func didTapButton(_ sender: Any) {
    provider.setDelegate(self, queue: nil)
    
    // TODO: UUID값과 update값은 PushKit에서 넘어온(pushRegistry 메소드) 정보를 이용하여 사용
    // PushKit 포스팅 글 참고: https://ios-development.tistory.com/875
    let update = CXCallUpdate()
    update.remoteHandle = CXHandle(type: .generic, value: "Jake")
    provider.reportNewIncomingCall(with: UUID(), update: update) { error in
      print(error)
    }
  }
}

extension ViewController: CXProviderDelegate {
  func providerDidReset(_ provider: CXProvider) {
  }
  
  func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
    action.fulfill()
  }
  
  func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
    action.fulfill()
  }
}

전화 걸기

  • 전화 걸기는 3가지가 존재하고, 이 중에서 CallKit은 첫 번째 케이스 사용
    • 앱 내에서 통화 요청하여 전화 걸기
    • URL 링크 열어서 전화 걸기
    • Siri를 사용하여 전화 걸기
  • 전화 걸기 구현 방법
    • UUID와 CXHandle 인스턴스를 이용하여 수신자 타겟을 정의
      * CXHandle: phoneNumber나 email과 같이 전화를 받는 사람에게 연락할 수 있는 수단
      let uuid = UUID()
      let handle = CXHandle(type: .emailAddress, value: "palatable77@gmail.com")​
      
      let startCallAction = CXStartCallAction(call: uuid, handle: handle)
    • 위 startCallAction을 CXTransaction인스턴스를 통해 전화 걸기 요청
      // callController는 전역에 존재
      private let callController = CXCallController()
      
      let transaction = CXTransaction(action: startCallAction)
      callController.request(transaction) { error in
          if let error = error {
              print("Error requesting transaction: \(error)")
          } else {
              print("Requested transaction successfully")
          }
      }​
       
    • 전화 받기와 마찬가지로 델리게이트에서 전화 걸기 시작
      func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
          // configure audio session
          action.fulfill()
      }​
    • 주의사항: 전화걸기에 CXProvider인스턴스는 사용하지 않지만, CXCallController()보다 먼저 전역에서 초기화 되어있지 않으면 오류 발생
      Error Domain=com.apple.CallKit.error.requesttransaction Code=2 "(null)"​

* 전화 받기 + 전화 걸기 전체 코드

//  ViewController.swift

import UIKit
import CallKit

class ViewController: UIViewController {
  private let callController = CXCallController()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    self.view.backgroundColor = .white
  }
  
  @IBAction func didTapButton(_ sender: Any) {
    self.receiving()
  }
  
  @IBAction func didTapOutgoingButton(_ sender: Any) {
    self.outgoing()
  }
  
  // 전화 받기
  private func receiving() {
    let provider = CXProvider(configuration: CXProviderConfiguration())
    provider.setDelegate(self, queue: nil)
    
    // TODO: UUID값과 update값은 PushKit에서 넘어온(pushRegistry 메소드) 정보를 이용하여 사용
    // PushKit 포스팅 글 참고: https://ios-development.tistory.com/875
    let update = CXCallUpdate()
    update.remoteHandle = CXHandle(type: .generic, value: "Jake")
    provider.reportNewIncomingCall(with: UUID(), update: update) { error in
      print(error ?? "")
    }
  }
  
  // 전화 하기
  private func outgoing() {
    let uuid = UUID()
    let handle = CXHandle(type: .emailAddress, value: "palatable77@gmail.com")
    
    let startCallAction = CXStartCallAction(call: uuid, handle: handle)
    let transaction = CXTransaction(action: startCallAction)
    self.callController.request(transaction) { error in
      print(error ?? "")
    }
  }
}

extension ViewController: CXProviderDelegate {
  func providerDidReset(_ provider: CXProvider) {
  }
  
  func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
    action.fulfill()
  }
  
  // 전화 받기 델리게이트 메소드
  func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
    action.fulfill()
  }
  
  // 전화 걸기 델리게이트 메소드
  func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
    action.fulfill()
  }
}

발신자 식별, 수신 전화 차단

  • iOS내에 정보를 저장하는 디렉토리인 Call Directory Extension이라는게 존재
  • 해당 디렉토리를 프로젝트에 추가하고 호스트 앱에서 Call Directory Extension을 호출하는 방식으로 동작
  • Call Directory Extension 추가
    • Xcode -> 현재 Target 선택 -> File -> New -> Target
    • Call Directory Extension 선택
       
    • 생성
    • 프로젝트에 추가된 것 확인

발신자 식별 준비

  • 전화가 걸려오면 시스템에서 먼저 사용자의 연락처를 참조하여 일치하는 전화번호 탐색
    • 일치하는 항목이 없는 경우, 시스템은 앱의 CXCallDirectoryProvider에 정의된 beginRequest(with:)메소드를 참고하여 식별하는 일치 목록 탐색
    • -> sns와 같이 시스템 연락처와 별개인 사용자의 연락처 목록을 관리하는 서비스 앱 or 배달 알림과 같이 앱 내에서 시작될 수 있느 수신 전화를 식별하는데 사용
    • sns앱에서 Jake와 친구이지만 연락처에 전화번호가 없는 경우, CallDirectory에 명시하여 "(App name) 발신자 ID: Jake Applessed"와 같이 표출
  • 위에서 만들어진 `CallDirectoryHandler`안에서 biginRequest()메소드 안에 addIdentificationEntry 메소드 호출
    • addIdentificationEntry에 정보가 담기면 전화받을 때 지정된 label로 표출
    • 주의 사항: 전화번호에 국가 코드를 반드시 붙여야하고, 숫자 타입의 오름차순으로 배열에 들어가지 않으면 오류 발생
      class CustomCallDirectoryProvider: CXCallDirectoryProvider {
          override func beginRequest(with context: CXCallDirectoryExtensionContext) {
          let labelsKeyedByPhoneNumber: [CXCallDirectoryPhoneNumber: String] = [821011112222: "발신자 테스트 Jake"] // 01011112222로 전화 오면 "발신자 테스트 Jake"라고 표출
          for (phoneNumber, label) in labelsKeyedByPhoneNumber.sorted(by: <) {
            context.addIdentificationEntry(withNextSequentialPhoneNumber: phoneNumber, label: label)
          }
      
              context.completeRequest()
          }
      }​

수신 전화 차단 준비

  • 전화를 받으면 시스템은 먼저 사용자의 차단 목록을 참조하여 차단해야 하는지 여부를 결정
  • 전화 번호가 없거나 차단 목록에 없는 경우, 앱의 beginRequest(with:) 메소드를 참고하여 수신 차단 결정
  • 위에서 만들어진 `CallDirectoryHandler`안에서 biginRequest()메소드 안에 addBlockingEntry 메소드 호출
    class CallDirectoryHandler: CXCallDirectoryProvider {
      override func beginRequest(with context: CXCallDirectoryExtensionContext) {
        context.delegate = self
    
        // 수신 차단
        let blockedPhoneNumbers: [CXCallDirectoryPhoneNumber] = [821011113333]
        for phoneNumber in blockedPhoneNumbers.sorted(by: <) {
          context.addBlockingEntry(withNextSequentialPhoneNumber: phoneNumber)
        }
        
        context.completeRequest()
      }
      
      ...
      
    }

ex) 발신자 식별, 수신 차단 코드

class CallDirectoryHandler: CXCallDirectoryProvider {
  override func beginRequest(with context: CXCallDirectoryExtensionContext) {
    context.delegate = self
    
    let labelsKeyedByPhoneNumber: [CXCallDirectoryPhoneNumber: String] = [821011112222: "발신자 테스트 Jake"] // 01011112222로 전화 오면 "발신자 테스트 Jake"라고 표출
    // 발신자 식별
    for (phoneNumber, label) in labelsKeyedByPhoneNumber.sorted(by: <) {
      context.addIdentificationEntry(withNextSequentialPhoneNumber: phoneNumber, label: label)
    }
    
    // 수신 차단
    let blockedPhoneNumbers: [CXCallDirectoryPhoneNumber] = [821011113333]
    for phoneNumber in blockedPhoneNumbers.sorted(by: <) {
      context.addBlockingEntry(withNextSequentialPhoneNumber: phoneNumber)
    }
    
    context.completeRequest()
  }
  
  ...
  
}

발신자 식별, 수진 전화 차단 활성화

  • Call Directory Extension의 번들 ID 확인: "com.ExVoIP.CallDirectory"
  • CXCallDirectoryManager.sharedInstance.reloadExtension을 통해 활성화
    • withIdentifier에 위에서 확인한 Call Directory의 Bundle ID 입력
      import UIKit
      import CallKit
      
      class ViewController: UIViewController {
        private let callController = CXCallController()
        
        override func viewDidLoad() {
          super.viewDidLoad()
          self.view.backgroundColor = .white
          
          // 발신자 표시, 수신 차단 기능 활성화
          CXCallDirectoryManager.sharedInstance
            .reloadExtension(withIdentifier: "com.ExVoIP.CallDirectory") { error in
              print(error ?? "")
            }
        }
       
       ...
       
       }​

* 전체 코드: https://github.com/JK0369/ExCallKit

 

* 참고

https://medium.com/mindful-engineering/voice-over-internet-protocol-voip-801ee15c3722

https://stackoverflow.com/questions/53619230/callkit-error-com-apple-callkit-error-requesttransaction-error-7

https://developer.apple.com/documentation/callkit/cxhandle

https://developer.apple.com/library/archive/documentation/Performance/Conceptual/EnergyGuide-iOS/OptimizeVoIP.html#//apple_ref/doc/uid/TP40015243-CH30

https://developer.apple.com/documentation/callkit

https://www.raywenderlich.com/1276414-callkit-tutorial-for-ios

Comments