본문 바로가기

개발/WebRTC

iOS CallKit

CallKit

iOS10부터 지원되기 시작한 신규 기능. 이때부터 부분적으로나마 iOS의 통화화면 기능을 제어할 수 있게 되었습니다.

소개

CallKit 이전의 Voip역사

Voip서비스를 제공하는 앱들이 예전부터 있었으나 이 앱들의 문제점은 전화를 수신했을때 사용자에게 알리는 것이 문제였습니다. 수신 전화를 알리는데 일반 푸시로는 백그라운드 상태의 앱을 깨울 수가 없어서 연결과정은 앱에 진입했을때만 가능했었습니다. 이런 불편한 점은 iOS8부터 PushKit(VoipPush)이 도입되어 해결이 되었는데 voip push인 경우 앱을 깨우고 일반 푸시일때는 불가능한 백그라운드 동작을 일부 허용해주는 식으로 개선이 되었습니다. 하지만 iOS의 전화화면에 대한 커스텀은 불가능하여 iOS9까지 전화가 와도 노티를 통해서만 사용자에게 알림을 줄 수 밖에 없었습니다.

iOS4 Viber IncomingCall

CallKit 이후

iOS10에 콜킷이 추가되고 난 뒤, 앞서 말했듯이 전화가 왔을때 수신전화를 iOS와 동일한 UI를 사용할 수 있어서 불편한 점을 최소화 할 수 있게 되었습니다.

CallKit의 기능

  • 잠금화면, 스프링보드에서 iOS의 전화UI를 사용할 수 있다
  • 전화번호부 참조를 통해 발신자를 식별하여 발신자명을 꾸며주거나, 수신 번호를 차단하는 기능을 제공한다 (Application Extension)

CallKit을 통한 전화UI 사용하기

CallKit은 전화 기능을 제공하는 것이 아니고, iOS 시스템상의 전화의 UI를 제공하는 개념으로 이해하면 됩니다. 또한 CallKit은 시뮬레이터에서 작동하지 않습니다.

시작

먼저 Target 설정에서 Background Mode - Voip 를 체크해줘야 합니다. CallKit은 해당 권한이 없으면 아예 동작하지를 않습니다.

콜킷을 사용하기 위해서 2개의 클래스를 생성해줘야 합니다.

CXProvider

앱이 수신 전화와 같은 외부의 이벤트를 받았을 때 CXProvider를 통해 시스템에 통지합니다. CXProvider는 CXCallUpdate라는 클래스를 통해 시스템에 통지합니다.

반대로, 시스템은 이벤트를 앱에 알리기 위해 CXAction라는 인스턴스를 사용하는데 해당 이벤트는 CXProvdierDelegate를 통해 받을 수 있습니다.

CXCallController

CXCallController는 전화걸기같은 사용자 시작요청을 시스템에 알립니다. CXCallController는 그런 요청들을 시스템에 알리기 위해 CXTransaction이라는 클래스를 사용하는데 이 트랜잭션은 하나 이상의 CXAction 인스턴스를 필요로 합니다.

CXProvider는 이벤트를 시스템에 보고하고, CXCallController는 사용자의 요청을 시스템에 요청하는 차이가 있습니다.

// 각각의 클래스 생성하는 방법
// 앱은 CXProviderConfiguration을 이용하여 해당 앱에서 어떤 종류의 전화를 허용하는지 
// Provider에게 알려줄 수 있습니다.
let configuration = CXProviderConfiguration(localizedName: "sample")   
configuration.supportsVideo = false // 비디오콜도 지원하는지
configuration.maximumCallsPerCallGroup = 1 // 그룹통화시 몇명까지 참여 가능한지
configuration.supportedHandleTypes = [.phoneNumber]
configuration.iconTemplateImageData = //Data() 콜킷UI에 아이콘 삽입

let provider = CXProvider(configuration: configuration)
provider.setDelegate(self, queue: nil)

let controller = CXCallController()

Outgoing Call

발신전화에서의 CallKit 처리 방법에 대해서 알아보겠습니다. 앱 내에서 전화를 걸때 CallKit의 처리과정은 다음과 같습니다.

  1. 사용자가 상대방에게 전화를 거는 이벤트를 발생시킴
  2. CXStartCallAction과 CXTransaction을 생성하고 CXCallController를 통해 이벤트를 요청함
  3. request가 성공적으로 끝나면 CXProvider의 reportOutgoingCall 함수를 호출하여 시스템에 현재 발신전화 중임을 알림
  4. CXStartCallAction이 제대로 실행되었으면 CXProvider와 연결된 Delegate의 provider(_ provider: CXProvider, perform action: CXStartCallAction) 함수가 호출됨

이 과정은 CallKit이 전화를 거는 것이 아니고, iOS에 현재 앱이 전화를 거는 상태임을 알려주는 과정입니다. 4번까지 진행되면 iOS15 기준 좌상단 시계에 통화 상태임을 알리는 인디케이터가 켜진 것을 볼 수 있습니다.

이때 주의해야할 점은, 사용자 입장에서는 전화를 거는 것이고, 이는 iOS의 시스템UI를 사용하는 것과는 별개의 일이므로 CallKit은 현재 통화상태임을 알리는 인디케이터만 사용자에게 보여줄뿐, 전화의 UI를 보여주지 않습니다. 통화중인 UI를 만들어야 하는 것은 개발자의 몫입니다.

CXHandle

A means by which a call recipient can be reached, such as a phone number or email address.
전화번호나 이메일 주소와 같이 전화를 받는 사람에게 연락할 수 있는 수단입니다. (구글번역)

전화걸기 예제코드

func reportOutgingCall(callee: String) {
    uuid = UUID()
    
	// Action 생성
    let handle = CXHandle(type: .phoneNumber, value: callee)
    let startCallAction = CXStartCallAction(call: uuid, handle: handle)
    startCallAction.contactIdentifier = callee;
    startCallAction.isVideo = false
    
	// Transaction 생성
    let transaction = CXTransaction()
    transaction.addAction(startCallAction)
    
	// 해당 트랜섹션을 시스템에 요청
    self.controller.request(transaction) { error in
        if let error = error {
            print("\\(#function):L\\(#line) StartCallAction transaction request failed: \\(error.localizedDescription)")
        } else {
            print("\\(#function):L\\(#line) StartCallAction transaction request successful")
            
            let callUpdate = CXCallUpdate()
            callUpdate.remoteHandle = handle
            callUpdate.supportsDTMF = true;
            callUpdate.supportsHolding = true;
            callUpdate.supportsGrouping = false;
            callUpdate.supportsUngrouping = false;
            callUpdate.hasVideo = false;
            
			self.provider.reportOutgoingCall(with: self.currentCallUUID!, startedConnectingAt: nil) // 전화를 건 시점부터
        }
    }
}

/// 발신전화 transaction이 성공하면 호출되는 delegate함수
func provider(_ provider: CXProvider, perform action: CXStartCallAction) {
    self.provider.reportOutgoingCall(with: currentCallUUID!, connectedAt: nil) // 통화과 연결되고 난 후
    
    action.fulfill() // 이 함수에서 action이 잘 수행되었음 알림
    // action.fail() // 이 함수에서 action 수행에 실패했음을 알림
}

Incoming Call

걸려온 전화를 받을때의 CallKit 처리방법에 대해서 알아보겠습니다.

  1. 소켓이나 Voip Push를 통해 전화가 왔음을 인지
  2. CXProvider의 reportNewIncomingCall 함수를 통해 시스템에 전화가 왔음을 알려줌
  3. 시스템은 전화UI를 띄워 사용자에게 전화가 왔음을 알려줌
  4. 전화는 사용자의 선택에 따라 받을지 말지가 결정되고 각각의 이벤트는 CXProviderDelegate로 전달됨
  5. 사용자가 전화수락 버튼을 눌러 전화를 받았을 경우
    • CXAnswerCallAction delegate가 호출됨
  6. 사용자가 전화거절 버튼을 눌러 전화를 거절했을 경우
    • CXEndCallAction delegate가 호출됨

수신전화일때의 CallKit의 동작은 앱이 실행중일때와, 디바이스가 잠금화면이거나 앱이 백그라운드에 있을 경우 다르게 동작합니다.

전화받기 예제 코드

/// provider에게 전화가 걸려왔음을 보고하는 함수
func reportIncomingCall(caller: String) {
	// 누구에게 전화가 왔는지 정의하는 handle 정의
    let callHandle = CXHandle(type: .phoneNumber, value: caller)
    uuid = UUID()

    let callUpdate = CXCallUpdate()
    callUpdate.remoteHandle = callHandle
    callUpdate.supportsDTMF = false
    callUpdate.supportsHolding = false
    callUpdate.supportsGrouping = false
    callUpdate.supportsUngrouping = false
    callUpdate.hasVideo = false

    provider.reportNewIncomingCall(with: uuid, update: callUpdate) { error in
        if let error = error {
            NSLog("new incoming call report fail : \\(error.localizedDescription)")
            return
        }
        NSLog("new incoming call report success")
    }
}

/// 수신전화를 수락하면 호출되는 delegate 함수
func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) {
	print("\\(#function):L\\(#line) action: CXAnswerCallAction")

	action.fulfill()
	// action.fail()
}

/// 전화를 끊으면 호출되는 함수
func provider(_ provider: CXProvider, perform action: CXEndCallAction) {
	print("\\(#function):L\\(#line) action: CXEndCallAction")

	uuid = nil
	action.fulfill()
	// action.fail()
	// provider.invalidate()
}

Voip Push(PushKit)과의 관계

Voip Push가 일반적인 상황이 아닌 특수한 목적용으로만 제공되는 푸시여서 iOS는 voip push의 경우 suspend나 background상태의 앱을 일시적으로 active 상태로 바꾸고, 짧은 시간이나마 네트워크 사용 가능 등 다양한 기능을 제공했었습니다. 하지만 이를 악용하는 앱들이 나와서 iOS13부터 voip push로 앱을 깨우는 경우 CXProvider의 reportCall을 호출하지않으면 앱이 죽도록 변경되어 voip push와 callkit은 뗄래야 뗄 수 없는 사이로 만들어놨습니다.

그런 여파때문인지 제가 가지고 있는 테스트폰 13.4.1에 백그라운드로 가면 먹통이 되는 현상이 있는데 한참 헤매다가 이게 애플버그로 13.3.1부터 먹통이라는 SO글을 봤습니다 OTL

마무리

아주 간단하게 Callkit의 기능중 전화에 대한 라이프 사이클에 대해 알아보았는데, 실제의 Voip 서비스의 앱의 CallKit로직은 저렇게 단순하지 않습니다. 여기서는 CallKit이 전화 이벤트를 어떻게 다루는지만 보았는데 실제 Voip 서비스앱은 전화의 라이프 사이클과 함께 CallKit을 제어하기 때문에 관련 로직이 복잡합니다. 여기서는 얘기하지 않았지만 앱에서 사용할 전화화면도 구성해야하고 고려할게 매우 많습니다.

어쨌든, CallKit은 전화를 거는 기능이 아닌 앱에서 voip 전화 상황에서 UI도움을 주는 프레임워크로 이해하면 이 글의 의도를 잘 파악하신 겁니다.

그리고 또 다른 CallKit의 기능인 Extension을 활용한 방법은 다음 포스팅에서 다루도록 하겠습니다.

'개발 > WebRTC' 카테고리의 다른 글

WebRTC란?  (2) 2023.05.07
WebRTC remind  (0) 2020.11.02