아고라의 주요 제품은 초저지연(latency)으로 친구, 동료, 교사 등과 연결할 수 있는 오디오 및 비디오 스트리밍 서비스입니다. 그러나 아고라의 또 다른 제품은 네트워크 내 장치 간 메시지를 공유할 수 있는 아고라 RTM(실시간 메시징) SDK입니다. 이 튜토리얼에서는 RTM을 통해 수행할 수 있는 다양한 기능 중 일부를 배우게 됩니다.
이 튜토리얼에서는 텍스트와 사진과 같은 메시지를 다른 사용자에게 전송하는 방법, 오픈소스 iOS 프레임워크인 MessageKit을 사용하여 메시지를 표시하는 방법, RTM 채널에 있는 사용자를 감지하는 방법(주의가 필요한 사용자를 위해 손을 들어 표시하는 기능 포함), 채널과 파일을 공유하는 방법(공유된 파일은 확인 및 수출이 가능합니다)을 다룹니다.
필수 조건
- 아고라 개발자 계정(아고라 시작하기 참조)
- Xcode 12.0 이상
- iOS 14.0 이상을 실행하는 iOS 기기
- iOS 개발에 대한 기본적인 이해
- CocoaPods
iOS 14는 Agora RTM SDK의 요구 사항은 아니지만, UIDocumentPickerViewController
와 같은 다양한 Swift 클래스의 요구 사항입니다.
설치
이 리포지토리를 guide_start
브랜치에서 다운로드합니다.터미널에서 프로젝트 디렉토리를 열고 pod init
를 실행하여 AgoraRtm_iOS를 설치합니다. .xcworkspace 파일을 열어 시작합니다.
이 브랜치는 기본적으로 모든 UI가 이미 설정되어 있으며, Agora RTM SDK와의 연결 및 콜백만 추가하면 됩니다.
RTM 연결이 설정되기 전 프로젝트의 주요 4개 페이지입니다:

단계 1: 로그인
첫 번째 화면(ViewController)에는 두 개의 텍스트 필드와 연결 버튼이 있습니다. 연결 버튼을 누르면 두 텍스트 필드의 값을 가져와 MultiChatVC의 인스턴스를 생성한 후 기본 UI를 생성하고, 이 튜토리얼 후반부에 설명할 몇 가지 추가 요소를 생성합니다.
MultiChatVC의 viewDidLoad 메서드에는 rtmLogin() 호출이 있습니다. 이 메서드는 guide_start 분기에서 현재 빈 상태이지만, 여기에는 RTM 백엔드 및 채널에 연결하는 논리가 들어갈 것입니다.
먼저 App ID를 사용하여 AgoraRtmKit 인스턴스를 생성해야 합니다. App ID가 없는 경우 How to Get Started with Agora를 참고하세요.
MultiChatVC에는 RTM 인스턴스를 위한 선택적 값인 rtmKit이 이미 존재하므로, 이 값을 사용하고 이 MultiChatVC 인스턴스에 델리게이트를 할당합니다:
self.rtmKit = AgoraRtmKit(appId: <#Agora App ID#>, delegate: self)
이 후에는 RTM 백엔드에 연결하고 채널을 생성한 다음 해당 채널에 참여해야 합니다:
self.rtmKit?.login(
byToken: nil, user: self.localUser.userDetails.senderId,
completion: { loginCode in
if loginCode == .ok {
self.rtmChannel = self.rtmKit?.createChannel(
withId: self.channel, delegate: self
)
self.rtmChannel?.join(
completion: self.channelJoined(joinCode:)
)
}
}
)
이 예제에서는 nil
를 토큰으로 전달합니다. 이는 개발 프로젝트이기 때문이지만, 생산 환경에서는 보안상의 이유로 토큰을 사용하는 것이 강력히 권장됩니다. 토큰을 생성하는 방법에 대해 자세히 설명한 많은 기사들이 있으며, 이 기사는 Golang을 사용한 예시를 포함하고 있습니다.
위 예제에서 볼 수 있듯이, 우리는 멤버 self.localUser
, self.channel
및 메서드 self.channelJoined
에 접근할 수 있습니다. 이 코드 스니펫을 MultiChatVC.rtmLogin()
에 추가하면 채널에 가입된 후 수행할 작업을 확인할 수 있습니다.
우리 경우, localUser는 MultiChatVC의 초기화자에서 정의됩니다. localUser는 RTMUser의 인스턴스로, 다음과 같은 속성을 포함합니다:
- 유일한 ID와 초기 화면의 입력란에 입력된 사용자 이름 등 사용자 세부 정보
- 초기 상태는
.offline
- handRaised: 사용자의 손이 들어올려졌는지 여부를 표시하기 위해 나중에 사용됩니다
channelJoined 메서드는 현재 채널에 성공적으로 가입되었는지 확인하는 빈 if 문으로 구성되어 있습니다. 여기서 몇 가지 작업을 수행해야 합니다: 로컬 사용자의 상태를 .online
로 설정하고, 연결된 모든 사용자의 목록인 connectedUsers
에 추가하며, [senderId: userDetails]
형식의 사전(dictionary)에 추가해야 합니다. 이 사전은 연결된 사용자 전체 목록을 매번 검색하는 것보다 더 빠르게 사용자를 조회하기 위해 존재합니다.
channelJoined
메서드는 위의 단계가 추가된 후 다음과 같이 보입니다:
func channelJoined(joinCode: AgoraRtmJoinChannelErrorCode) {
if joinCode == .channelErrorOk {
print("connected to channel")
self.localUser.status = .online
self.connectedUsers.append(self.localUser)
self.usersLookup[
self.localUser.userDetails.senderId
] = self.localUser
}
}
디버깅에 도움이 되기 때문에 print 문장을 그대로 남겨두었습니다.
이제 채널에 연결되었으며 다음 단계로 진행할 준비가 되었습니다: MessageKit을 사용하여 다른 멤버들에게 메시지를 전송하는 것입니다.
단계 2: MessageKit 통합
MessageKit이란 무엇인가요?
MessageKit은 메시징 사용자 인터페이스를 만들기 위한 오픈소스, 커뮤니티 주도형 UI 라이브러리입니다. 이 유형의 UI 라이브러리 중 하나일 뿐이지만, iOS 생태계에서의 인기로 인해 이 튜토리얼에 선택되었습니다.
이 프로젝트에서 가장 복잡한 부분이기 때문에 시간을 충분히 투자해 주세요. 만약 길을 잃었다면, 모든 부분이 채워진 리포지토리의 기본 브랜치를 참고하세요.

MessageKit 설정
MessageKit의 GitHub 저장소를 확인하여 환경에 맞게 가장 적합하게 설정하는 방법에 대한 자세한 정보를 확인하세요. 이 게시물을 위해 제공된 코드 저장소에서는 이미 MultiChatVC.pages.messages
에 표시되도록 설정되어 있으며,RTMChatViewController
를 사용합니다.
이 저장소에서 MessageKit을 돕기 위해 생성된 일부 클래스는 다음과 같습니다:
Sender
는 MessageKit의 프로토콜SenderType
MessageKitMessage,
MessageKit의 프로토콜MessageType
를 사용하여 UI에 삽입된 모든 메시지를 처리합니다ImageMediaItem,
MessageKit에서 이미지 메시지를 표시하기 위해 사용됩니다
를 사용하여 메시지를 보낸 사람에 대한 정보를 처리합니다.
위 클래스 중 일부는 JSON으로 변환하는 메서드를 포함하고 있으며, 이는 기기에서 RTM 네트워크로, 그리고 채널 내 다른 기기로 정보를 전송하는 방식입니다.
MessageKit과 관련된 대부분의 코드는 MultiChatVC+Message.swift
에 있으며, 이는 우리 스타터 프로젝트에 포함되어 있습니다.
이 파일에서는 MultiChatVC의 기본 클래스를 확장하여 MessagesDataSource, MessagesLayoutDelegate, MessagesDisplayDelegate 프로토콜을 사용하도록 했습니다. 이 프로토콜은 MessageKit에 표시할 메시지를 알려주는 역할을 합니다. 이 애플리케이션의 MessageKit 구현은 MessageKit GitHub 저장소에서 찾은 일부 코드 샘플을 기반으로 했습니다. 이 부분에 대해 불분명한 점이 있다면 MessageKit 저장소의 문서를 참고하시기 바랍니다.
inputBar
및 ;insertMessages
메서드로 스크롤을 내려보면, 하단 텍스트 필드에 추가된 텍스트가 캡처되어 MessageKitMessage
객체로 형성되는 부분을 확인할 수 있습니다. 이 구현에서는 텍스트와 이미지 기반 메시지만 허용되지만, MessageKit은 비디오, 오디오, 연락처 등 더 많은 유형을 지원합니다. 이 예제는 해당 유형도 허용하도록 수정할 수 있습니다.
MessageKitMessage가 생성된 후 addNewMessage가 호출되어 메시지가 뷰에 삽입됩니다. 하지만 이제 이 메시지를 RTM 내 모든 다른 피어에게 배포해야 합니다.
현재 기본 UI는 다음과 같습니다:

메시지 인코딩
MessageKitMessage에는 generateMessageText
라는 메서드가 있습니다.이 메서드가 무엇을 하는지 살펴보겠습니다:
func generateMessageText() -> String {
var rtmMessage: [String: Any] = [
"sender": self.sender.toJSON(),
"message_id": self.messageId,
"timestamp": ISO8601DateFormatter().string(from: self.sentDate)
]
switch self.kind {
case .attributedText(let str):
rtmMessage["type"] = "text"
rtmMessage["body"] = str.string
case .text(let str):
rtmMessage["type"] = "text"
rtmMessage["body"] = str
case .photo(_):
rtmMessage["type"] = "photo"
default:
print("no conversion for type: \(kind)")
}
let jsonData = try! JSONSerialization.data(
withJSONObject: rtmMessage, options: []
)
let decoded = String(data: jsonData, encoding: .utf8)!
return decoded
}
이 스니펫에서 볼 수 있듯이, 우리는 MessageKitMessage 속성을 Swift 사전으로 변환한 후 JSON 데이터로 시리얼화하여 문자열로 변환하고 있습니다.
예를 들어, 다음과 같은 객체를 생성했다면:
let msg = MessageKitMessage(
sender: Sender(senderId: "my-sender-id", displayName: "Max"),
messageId: "random-message-id",
sentDate: .init(),
kind: .text("Hello World")
)
msg.generateMessageText()
의 출력은 다음과 같습니다:
{
"sender": {
"display_name": "Max",
"sender_id": "my-sender-id"
},
"message_id": "random-message-id",
"timestamp": "2021-03-12T09:05:16Z",
"type": "text",
"body": "Hello World"
}
출력은 위 내용이 문자열로 감싸진 형태가 됩니다
위 내용은 원격 장치에서 이 초기화자를 통해 수신되면 MessageKit 메시지로 다시 변환될 수 있습니다:
public init?(basicMessage: AgoraRtmMessage) {
let data = Data(basicMessage.text.utf8)
do {
guard let json = try JSONSerialization.jsonObject(
with: data, options: []) as? [String: Any],
let sender = json["sender"] as? [String: String],
let displayName = sender["display_name"],
let senderId = sender["sender_id"],
let messageId = json["message_id"] as? String,
let type = json["type"] as? String,
let timestamp = json["timestamp"] as? String,
let body = json["body"] as? String,
let sentDate = ISO8601DateFormatter().date(from: timestamp)
else {
return nil
}var messageKind: MessageKind!
if type == "text" {
messageKind = .text(body)
} else {
return nil
}
self = MessageKitMessage(
sender: Sender(senderId: senderId, displayName: displayName),
messageId: messageId, sentDate: sentDate, kind: messageKind
)
} catch let error as NSError {
print("Failed to load: \(error.localizedDescription)")
return nil
}
}
일반적으로 이 유형의 클래스를 인코딩 및 디코딩하려면 Swift의 Codable을 사용하는 것이 좋습니다. 그러나 MessageKind 속성은 UIImage를 포함해 여러 유형일 수 있으므로 명시적으로 인코딩해야 합니다. 이 방식으로 인코딩 및 디코딩하면 메시지의 형식을 정확히 알 수 있으므로, Android를 포함한 모든 플랫폼에서 동일하게 해석됩니다.
RTM을 통해 메시지 전송
이제 메시지 본문을 나타내는 문자열을 얻었으므로 이 문자열을 RTM을 통해 전송해야 합니다.
이를 위해 insertMessages
메서드가 있는 MultiChatVC+Messages.swift
로 이동하여 JSON 텍스트를 가져온 후 현재 채널 인스턴스(MultiChatVC.rtmChannel)를 사용하여 AgoraRtmChannel.send()
메서드를 호출합니다. 다음과 같이:
if let msgText = message?.generateMessageText() {
self.rtmChannel?.send(
AgoraRtmMessage(text: msgText),
completion: { sentCode in
if sentCode == .errorOk {
print("Message was sent!")
}
}
)
}
네트워크 문제로 인해 메시지가 채널에 성공적으로 전송되지 않을 경우 피드백을 생성하는 것이 좋을 수 있습니다. 하지만 단순성을 위해 해당 부분을 생략하고 Agora RTM 인프라에 의존하겠습니다.
하지만 이미지는 어떻게 될까요? RTM을 통해 이미지를 이렇게 간단한 방법으로 전송할 수 없습니다. 이는 RTM 메시지의 크기 제한이 32KB이기 때문이기도 하지만, RTM을 통해 이미지를 전송하는 특정 방법이 있기 때문입니다.
RTM을 통해 이미지를 전송하려면 먼저 해당 이미지가 장치에 파일로 존재해야 합니다. 이 예제에서는 FileManager.default.temporaryDirectory
에 임의의 UUID를 파일 이름으로 생성합니다.:
let fileURL = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
if let _ = try? img.pngData()?.write(to: fileURL) {
// Image saved to `fileURL`, now send it over RTM
}
첫 번째 단계는 AgoraRtmKit 인스턴스를 사용하여 이미지 메시지를 생성하는 것입니다. 이를 위해 createImageMessage(byUploading:,withRequest:)
를 사용합니다:
var requestID = Int64.random(in: Int64.min...Int64.max)
self.rtmKit?.createImageMessage(
byUploading: fileURL.path,
withRequest: &requestID,
completion: { (requestId, imageMsg, errorCode) in
if errorCode == .ok, let imageMsg = imageMsg {
// now send our imageMsg over RTM
}
}
)
많은 이미지를 전송하고 별도의 완료 메서드를 정의하며 다른 위치로 전송될 다른 업로드된 이미지를 구분하려면 requestID를 추적해야 할 수 있습니다. 하지만 이 경우 추적하지 않은 임의의 requestID를 사용하는 것이 문제 없습니다.
이제 imageMsg
객체는 AgoraRtmImageMessage
유형이며, 일반적인 AgoraRtmMessage와 동일한 방식으로 네트워크를 통해 전송될 수 있습니다. 이 객체는 mediaId와 fileName과 같은 추가 속성을 가지고 있습니다. 그러나 MessageKitMessage에 필요한 발신자와 타임스탬프와 같은 정보는 여전히 추가해야 합니다. 이 정보는 이전에 사용된 generateMessageText()
에서 생성되어 현재 사용되지 않은 AgoraRtmImageMessage의 text
속성에 할당할 수 있습니다. 그런 다음 메시지는 이전과 동일한 방식으로 안전하게 네트워크를 통해 전송될 수 있습니다:
imageMsg.text = message.generateMessageText()
self.rtmChannel?.send(imageMsg, completion: { (messageSent) in
if messageSent == .errorOk {
print("Message has been sent!")
}
})
RTM에서 메시지 수신
Agora RTM을 통해 메시지를 수신하는 것은 다양한 델리게이트 메서드를 통해 처리됩니다. 우리 경우 그룹 채팅의 델리게이트 메서드는 AgoraRtmChannelDelegate
프로토콜에 정의되어 있으며, 이 프로젝트에서는 모든 AgoraRtmChannelDelegate 및 AgoraRtmDelegate 메서드가 MultiChatVC+AgoraRtmDelegate.swift
에 정의되어 있으며, 채널 델리게이트는 채널 생성 시 할당됩니다.
채널 내 모든 사용자로부터 들어오는 메시지를 감시하기 위해 관심 있는 델리게이트 메서드는 channel(_:,messageReceived:from:)입니다. MultiChatVC+AgoraRtmDelegate.swift에서 이 메서드는 이미 정의되어 있습니다. 이 메서드는 handleMessageReceived()
메서드를 직접 호출하며, AgoraRtmMessage만 전달합니다.
여기서 우리가 해야 할 일은 이전에 정의한 선택적 초기화자를 사용하여 MessageKitMessage
인스턴스를 생성하는 것입니다. 이 메시지가 예상된 구조라면, 동일한 addNewMessage()
를 사용하여 MessageKit 뷰에 직접 메시지를 삽입할 수 있습니다. handleMessageReceived의 상단 코드는 다음과 같아야 합니다:
if let msgKitMessage = MessageKitMessage(basicMessage: message) {
self.addNewMessage(message: msgKitMessage)
return
}
// otherwise handle other types of incoming message
이 기능은 텍스트 기반 메시지에만 적용됩니다. 이제 들어오는 AgoraRtmImageMessage
객체를 살펴보겠습니다.
같은 파일에서 이미 정의된 channel(_:,imageMessageReceived:,from:)
델리게이트 메서드를 찾을 수 있으며, 이 메서드의 본체는 빈 상태입니다.
createImageMessage와 유사하게, AgoraRtmKit 클래스에 downloadMedia(toMemory:,withRequest:,completion:)
라는 메서드가 있습니다. 이 메서드를 사용하여 이미지를 다운로드한 후 MessageKitMessage에 전달하여 MessageKit으로 표시할 수 있습니다.
이미지를 다운로드하려면 메서드 본문에 다음 코드를 추가하세요:
var requestId = Int64.random(in: Int64.min...Int64.max)
channel.kit.downloadMedia(
toMemory: message.mediaId, withRequest: &requestId
) { (request, imageData, errcode) in
if let imgData = imageData, let img = UIImage(data: imgData) {
// send image to MessageKit
}
}
이제 UIImage 형식의 이미지가 있습니다. 이 이미지는 메모리에 저장하는 대신 기기에 직접 저장할 수 있습니다.
MessageKitMessage를 생성하려면 메시지의 text 속성에서 JSON으로 인코딩된 데이터를 가져와야 합니다. 이 프로젝트에 이미 해당 기능을 구현한 메서드가 있습니다. 이 메서드의 구조는 MessageKitMessage(basicMessage:)
와 매우 유사하며, 이름은 getProperties
입니다:
static func getProperties(
from text: String
) -> (sender: Sender, sentData: Date, messageId: String)? {
let data = Data(text.utf8)
do {
guard let json = try JSONSerialization.jsonObject(
with: data, options: []) as? [String: Any],
let sender = json["sender"] as? [String: String],
let displayName = sender["display_name"],
let senderId = sender["sender_id"],
let messageId = json["message_id"] as? String,
let timestamp = json["timestamp"] as? String,
let sentDate = ISO8601DateFormatter().date(from: timestamp)
else { return nil }
return (
Sender(senderId: senderId, displayName: displayName),
sentDate,
messageId
)
} catch let err as NSError {
print("ERROR COULD NOT DECODE \(err)")
print(text)
}
return nil
}
이제 위의 방법을 사용하여 MessageKitMessage를 거의 생성할 수 있습니다. 마지막으로 해야 할 일은 단계 2의 초반에 언급된 ImageMediaItem
을 사용하여 UIImage를 MediaItem 유형으로 변환하는 것입니다. 이 변환은 UIImage를 인수로 받는 초기화자를 사용하며, 이후 모든 값을 MessageKitMessage 초기화자에 전달합니다:
guard let (sender, sentDate, messageId) = MessageKitMessage.getProperties(
from: message.text
) else {
return
}
let mediaItem = ImageMediaItem(image: img)
let message = MessageKitMessage(
sender: sender,
messageId: messageId,
sentDate: sentDate,
kind: .photo(mediaItem)
)
이제 이 객체를 다시 한 번 addNewMessage()
에 전달하면 수신한 이미지가 MessageKit에 표시됩니다!

단계 3: 동료 표시
이 섹션에서는 채널에 있는 다른 모든 사용자를 표시하는 한 가지 방법을 살펴보겠습니다. 이는 최대 2개의 섹션으로 구성된 간단한 목록 뷰로, 첫 번째 섹션에는 현재 온라인 상태인 사용자가 표시되고, 두 번째 섹션에는 채널에 있었지만 현재 로그아웃한 사용자가 표시됩니다.
또한 사용자의 상태를 다른 모든 사용자로 변경하여 이름 옆에 손 들어올린 아이콘을 표시하는 간단한 버튼을 추가할 것입니다.
상태 업데이트 전송
채널 전체에 메시지를 전송하여 채널에 참여한 나머지 사용자에게 우리 세부 정보를 전달합니다. 이는 이전에 사용한 채널 가입 콜백을 활용합니다. 채널에 멤버가 가입할 때마다 해당 멤버에게 우리 세부 정보를 포함한 메시지를 전송하여 그들의 목록을 업데이트합니다. 채널에 멤버가 가입할 때마다 channel(_:,memberJoined:)
로 알림을 받습니다.AgoraRtmDelegate를 사용해야 합니다. 일부 메시지는 채널 전체가 아닌 특정 사용자에게 직접 전송되기 때문입니다.
먼저, 채널에 가입하는 최신 멤버들에게 상태를 전송합니다.
전송할 메시지의 본문은 MessageKit에서 전송한 메시지와 유사하지만, 필요한 정보의 양은 훨씬 적습니다. 사용자에게 전달해야 할 내용은 발신자 정보(디스플레이 이름 및 ID), 메시지 유형(이 경우 “status”
), 그리고 상태를 나타내는 열거형의 인코딩입니다.
로컬 사용자 데이터를 localUser
속성에 저장하며, 이 속성은 사용자 정의 유형 RTMUser
입니다. 따라서 이 구조체를 JSON 문자열로 변환하고 텍스트 기반 AgoraRtmMessage
로 변환하는 메서드를 추가했습니다. 메시지에 액세스하려면 self.localUser.statusRTMMessage
를 호출하면 됩니다. 이 구현은 RTMUser.swift
를 참고하세요. 결과는 다음과 같은 텍스트를 포함하는 AgoraRtmMessage를 생성합니다:
{
"sender":{
"sender_id":"B22663DD-3A30-4102-B4A1-5645AD144625",
"display_name":"ipad"
},
"body":"online",
"type":"status"
}
메시지를 수신하면 AgoraRtmKit을 사용하여 해당 메시지를 새로 가입한 사용자에게 전송합니다. 전체 메서드는 다음과 같이 보입니다:
func channel(_ channel: AgoraRtmChannel, memberJoined member: AgoraRtmMember) {
channel.kit.send(
self.localUser.statusRTMMessage,
toPeer: member.userId,
completion: { sentErr in
if sentErr == .ok {
print("Status message has sent")
}
})
}
channelJoined 이벤트에서 동료들에게 유사한 메시지를 전송해야 합니다. 하지만 특정 사용자에게 전송하는 대신 이 메시지는 채널 전체에 전송됩니다:
func channelJoined(joinCode: AgoraRtmJoinChannelErrorCode) {
if joinCode == .channelErrorOk {
print("connected to channel")
self.localUser.status = .online
self.connectedUsers.append(self.localUser)
self.usersLookup[self.localUser.userDetails.senderId] = self.localUser
// Status related code:
self.rtmChannel?.send(self.localUser.statusRTMMessage) { sentErr in
if sentErr != .errorOk {
print("status to channel send failed \(sentErr.rawValue)")
}
}
self.membersTable?.reloadData()
}
}
상태 업데이트 수신
단계 1에서 우리는 로컬 사용자를 connectedUsers 및 usersLookup 맵에 추가했습니다. 이제 채널에 가입하거나 탈퇴하는 모든 사용자에 대해 동일한 작업을 수행하려면 다시 한 번 채널 델리게이트 메서드를 사용합니다. 다른 멤버의 상태를 확인하기 위해 추가하는 델리게이트 메서드는 rtmKit(_:,messageReceived:,fromPeer:)
및 channel(_:,messageReceived:,from:)
입니다. 또한 channel(_:,memberLeft:)
를 사용하여 원격 사용자의 상태를 오프라인으로 업데이트할 것입니다.
첫 번째 두 메서드는 이미 정의되어 있으며, 이전에 다룬 함수 handleMessageReceived()
를 호출합니다.이 함수는 먼저 들어온 메시지가 MessageKit에서 사용되어야 하는 메시지인지 확인합니다. MessageKit에서 사용할 메시지를 찾지 못하면, 다음으로 확인해야 할 기능은 이 AgoraRtmMessage에 JSON 문자열 본문이 포함되어 있는지, 그리고 그 본문이 예상하는 구조와 일치하는지 여부입니다.
let data = Data(message.text.utf8)if message.type == .text, let json = try? JSONSerialization.jsonObject(
with: data, options: []
) as? [String: Any] {
guard let senderJson = json["sender"] as? [String: String],
let displayName = senderJson["display_name"],
let senderId = senderJson["sender_id"],
let type = json["type"] as? String,
let body = json["body"] as? String
else { return }
let sender = Sender(senderId: senderId, displayName: displayName)
if type == "status"
guard let userStatus = RTMUser.Status(rawValue: status) else {
print("could not set status to \(status)")
return
}
self.statusUpdate(from: sender, newStatus: userStatus)
}
}
유사하게, memberLeft 메서드는 동일한 statusUpdate 메서드를 호출해야 합니다:
func channel(_ channel: AgoraRtmChannel, memberLeft member: AgoraRtmMember) {
if let offlineUser = self.usersLookup[member.userId] {
self.statusUpdate(
from: offlineUser.userDetails,
newStatus: .offline
)
}
}
statusUpdate(from:newStatus:)
는 프로젝트에 정의되어 있으며, 그 흐름은 다음과 같습니다:
- 이 senderId의 사용자가 존재하지 않으면, 지정된 상태로 새로운 사용자가 생성됩니다.
- 사용자가 존재하면, 해당 사용자의 상태가 업데이트되고, 연결된 사용자 목록(connectedUsers) 또는 오프라인 사용자 목록(offlineUsers)으로 이동됩니다.
- 마지막으로 membersTable이 업데이트된 데이터로 재로드됩니다.
func statusUpdate(from sender: Sender, newStatus status: RTMUser.Status) {
if let senderObj = self.usersLookup[sender.senderId] {
if senderObj.status == status {
return
}
let oldStatus = senderObj.status senderObj.status = status
if status == .offline {
self.connectedUsers.removeAll(
where: { $0.userDetails.senderId == sender.senderId }
)
self.offlineUsers.append(senderObj)
} else if oldStatus == .offline {
self.offlineUsers.removeAll(
where: { $0.userDetails.senderId == sender.senderId }
)
self.connectedUsers.append(senderObj)
}
} else if status != .offline {
let newUser = RTMUser(
userDetails: sender,
handRaised: false,
status: status
)
self.usersLookup[sender.senderId] = newUser
self.connectedUsers.append(newUser)
} else {
return
}
self.membersTable?.reloadData()
}
이제 사용자가 채널에 가입하거나 탈퇴할 때마다 테이블이 자동으로 업데이트됩니다!

손을 들어보세요
이 섹션의 마지막 부분은 “손을 들어보세요” 버튼입니다.
상태 업데이트를 보내는 것과 유사하게, 손 들어보기 업데이트의 JSON을 포함한 AgoraRtmMessage를 빠르게 가져올 수 있는 속성을 추가했습니다. 이 속성은 raiseHandRTMMessage이며, 메시지의 텍스트는 다음과 같습니다:
1{
2 "type": "raise_hand",
3 "sender": {
4 "display_name": "Max",
5 "sender_id": "1D83826A-7586-40C8-A002-767766E4FB76"
6},
7 "body": "true"
8}
“손 들어올리기” 버튼은 이미 raiseHandPressed()
메서드를 호출합니다. 따라서 이 메서드에서는 localUser의 handRaised 속성을 변경하고, raiseHandRTMMessage로 메시지를 가져온 후 채널의 나머지 부분에 전송하며, 버튼 텍스트를 업데이트하고 membersTable을 다시 한 번 재로드해야 합니다:
@objc func raiseHandPressed(sender: UIButton) {
self.localUser.handRaised.toggle()
self.rtmChannel?.send(self.localUser.raiseHandRTMMessage)
sender.setTitle(
(self.localUser.handRaised ? "Lower" : "Raise") + " Hand" ,
for: .normal
)
self.membersTable?.reloadData()
}
UITableViewDelegate 메서드 tableView(_:,cellForRowAt:)
에서, handRaised의 값이 true인 경우 사용자에게 어떤 일이 발생하는지 확인할 수 있습니다:
if user.handRaised {
cell.accessoryView = UIImageView(
image: UIImage(systemName: "hand.raised.fill")
)
} else {
cell.accessoryView = nil
}
따라서 우리를 포함해 몇 명의 사용자가 손을 들어올린 상태라면, 다음과 같은 상황을 확인할 수 있습니다:

단계 4: 파일 공유
네트워크상의 다른 사용자와 소량의 데이터를 공유하는 것 외에도, 더 큰 파일을 전송할 수 있습니다.
MessageKit 통합을 통해 이미지를 전송하는 예제를 이미 살펴보았기 때문에, 이 부분에서는 PDF 및 USDZ와 같은 3D 파일 등 다른 유형의 파일을 전송하는 방법을 알아보겠습니다.
프로젝트는 이미 다음과 같은 구조를 갖추고 있습니다:

“파일 업로드” 버튼을 선택하면 UIDocumentPickerViewController가 열리며, 여기서 파일을 선택하여 업로드할 수 있습니다. 다음과 같이 표시됩니다:

항목을 선택하면 피커 뷰가 닫히고 UIDocumentPickerDelegate
메서드인 didPickDocumentsAt
가 호출되어 선택한 문서의 URL을 알려줍니다.
func documentPicker(
_ controller: UIDocumentPickerViewController,
didPickDocumentsAt urls: [URL]
) {
guard let url = urls.first,
url.startAccessingSecurityScopedResource()
else { return }
// send the file to RTM
}
이 방법에서 우리는 AgoraRtmFileMessage를 생성한 후 채널 내 모든 사용자에게 전송해야 합니다.
이 튜토리얼에서 이전에 createImageMessage
를 사용하여 AgoraRtmImageMessage를 생성하는 방법을 보았습니다. 이와 유사한 메서드인 createFileMessage
를 사용하여 AgoraRtmFileMessage를 생성할 것입니다.
이 메서드는 파일 메시지를 생성하기 위해 URL 경로와 요청 ID 포인터를 입력으로 받습니다. 그 후, 성공 여부를 확인하기 위한 완료 콜백을 포함합니다.
var requestID: Int64 = Int64.random(in: Int64.min...Int64.max)
self.rtmKit?.createFileMessage(
byUploading: url.path, withRequest: &requestID,
completion: { (requestId, fileMsg, errorCode) in
if errorCode == .ok, let fileMsg = fileMsg {
// send the file to our channel
} else {
print(errorCode)
}
DispatchQueue.main.async {
url.stopAccessingSecurityScopedResource()
}
})
채널에 파일을 전송하는 방법은 이전과 매우 유사합니다. 이번 경우 사용자를 추적하지 않기 때문에, 이전 섹션에서처럼 발신자 정보 등을 추가할 필요가 없습니다. 그러나 컬렉션 뷰에서 항목에 대한 정보를 표시하는 데 유용하기 때문에 파일 이름을 명시적으로 설정합니다:
fileMsg.fileName = url.lastPathComponent
self.rtmChannel?.send(fileMsg, completion: { sentStatus in
if sentStatus == .errorOk {
print("File message sent")
}
})
이 파일을 테이블에 표시해야 합니다. 테이블은 downloadsTable
이라는 속성을 통해 찾을 수 있습니다. 테이블의 데이터 소스는 downloadFiles
라는 배열입니다. downloadFiles
는 DownloadableFileData
라는 사용자 정의 유형의 배열입니다. DownloadableFileData
는 파일 이름과 Agora RTM에서 사용되는 다운로드 ID만 포함하는 기본 구조체입니다.
self.downloadFiles.append(
DownloadableFileData(
filename: url.lastPathComponent,
downloadID: fileMsg.mediaId
)
)
self.downloadsTable?.reloadData()
문서 선택기 델리게이트 메서드는 이제 다음과 같이 되어야 합니다:
func documentPicker(
_ controller: UIDocumentPickerViewController,
didPickDocumentsAt urls: [URL]
) {
guard let url = urls.first,
url.startAccessingSecurityScopedResource()
else { return }
var requestID: Int64 = Int64.random(in: Int64.min...Int64.max)
self.rtmKit?.createFileMessage(
byUploading: url.path, withRequest: &requestID,
completion: { (requestId, fileMsg, errorCode) in
if errorCode == .ok, let fileMsg = fileMsg {
fileMsg.fileName = url.lastPathComponent
self.rtmChannel?.send(fileMsg, completion: { sentStatus in
if sentStatus == .errorOk { print("File message sent") }
})
self.downloadFiles.append(
DownloadableFileData(
filename: url.lastPathComponent,
downloadID: fileMsg.mediaId
)
)
self.downloadsTable?.reloadData()
} else {
print(errorCode)
}
DispatchQueue.main.async {
url.stopAccessingSecurityScopedResource()
}
}
)
}
다른 장치에 수신된 메시지를 표시하는 것은 현재 매우 간단합니다. 우리가 해야 할 일은 수신되는 메시지를 캡처하고 downloadFiles 배열에 다른 항목을 추가하는 것입니다:
func channel(
_ channel: AgoraRtmChannel,
fileMessageReceived message: AgoraRtmFileMessage,
from member: AgoraRtmMember
) {
self.downloadFiles.append(
DownloadableFileData(filename: message.fileName, downloadID: message.mediaId)
)
self.downloadsTable?.reloadData()
}
이제 채널 내 모든 기기에서 자신의 기기에서 전송된 파일과 채널 내에서 전송 중인 모든 다른 파일을 확인할 수 있습니다. 다운로드 페이지의 모습은 다음과 같습니다:

이제 클릭 시 다운로드하고 열어야 할 파일들이 필요합니다. 이 기능은 UITableViewDelegate 메서드 didSelectItemAt
에 구현됩니다.
파일을 다운로드하려면 이전에 이미지를 다운로드할 때 사용한 방법과 거의 동일한 방법을 사용합니다. 하지만 메모리에 다운로드하는 대신 파일로 다운로드합니다: downloadMedia(_:toFile:,withRequest:,completion:)
. 파일이 다운로드되면 showFile()
메서드를 호출합니다.
func collectionView(
_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath
) {
let docsData = self.downloadFiles[indexPath.row]
let downloadFileURL = FileManager.default.temporaryDirectory
.appendingPathComponent(docsData.filename)var requestId = Int64.random(in: Int64.min...Int64.max)
self.rtmKit?.downloadMedia(
docsData.downloadID,
toFile: downloadFileURL.path,
withRequest: &requestId,
completion: { _, errcode in
if errcode == .ok {
self.showFile(at: downloadFileURL)
} else {
print(errcode)
}
}
)
}
showFile는 단순히 QLPreviewController
를 표시합니다. 이 컨트롤러는 Files 앱과 동일한 방식으로 거의 모든 유형의 파일을 표시하는 데 사용됩니다.
다음은 파일을 업로드한 후 그 중 하나를 표시하는 흐름입니다:

그게 전부입니다. 이제 로컬 네트워크를 통해 공유된 모든 종류의 파일을 쉽게 확인할 수 있습니다.
테스트
완성된 프로젝트를 확인하려면 리포지토리를 다운로드하고 메인 브랜치를 체크아웃하세요.

Conclusion
Now you can add messaging to your application, including an app that uses voice or video messaging, see all online users, and even send files across the network.
There’s many more things that can be done with Agora RTM, and hopefully this post can help you achieve some of those things. These messages work between all supported platforms too, so a message can go from iOS to Android, to macOS, and even a Unity project.
Other Resources
For more information about building applications using Agora SDKs, take a look at the Agora Video Call Quickstart Guide and Agora API Reference.
I also invite you to join the Agora Developer Slack community.