
Compartilhar:
Abdul é um Developer Advocate da Vonage. Ele tem experiência profissional na área de produtos de consumo como engenheiro de iOS. Em seu tempo livre, ele gosta de andar de bicicleta, ouvir música e orientar quem está dando os primeiros passos na área de tecnologia
Criação de um aplicativo de áudio “drop-in” com SwiftUI e Vapor – Parte 2
Tempo de leitura: 8 minutos
Introdução
A primeira parte deste tutorial utilizou a Conversation API para criar um servidor para um aplicativo de áudio instantâneo. O servidor permite criar novos usuários, criar novas salas de bate-papo e listar todas as salas de bate-papo abertas. Neste tutorial, você vai desenvolver um aplicativo para iOS que usa o Vonage Client SDK para acessar o serviço e iniciar um bate-papo. Se quiser ir direto para este tutorial, siga as instruções no repositório do GitHub do servidor para configurar tudo.
Pré-requisitos
Além disso, de acordo com os pré-requisitos da primeira parte, você precisará de o Cocoapods para instalar o Vonage Client SDK para iOS.
Criação do aplicativo para iOS
É hora de configurar o aplicativo para iOS. Depois de criá-lo, você instalará o Client SDK e solicitará permissões para o microfone.
Criar um projeto no Xcode
Para começar, abra o Xcode e crie um novo projeto acessando Arquivo > Novo > Projeto. Selecione um modelo de aplicativo e dê um nome a ele. Selecione SwiftUI como interface, “Aplicativo SwiftUI” para o ciclo de vidae Swift para a linguagem. Por fim, um local para salvar seu projeto.

Instale o Client SDK
Agora que você criou o projeto, pode adicionar o Vonage Client SDK como dependência. Navegue até o local onde salvou o projeto no seu terminal e execute os seguintes comandos.
Execute o
pod initpara criar um novo Podfile para o seu projeto.Abra o Podfile no Xcode usando
open -a Xcode Podfile.Atualize o Podfile para incluir
NexmoClientcomo dependência.
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
target 'SwiftUIDropin' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for SwiftUIDropin
pod 'NexmoClient'
endInstale o SDK usando
pod install.Abra o novo arquivo xcworkspace no Xcode usando
open SwiftUIDropin.xcworkspace.
Permissões do microfone
Como o aplicativo utilizará o microfone para fazer chamadas, é necessário solicitar permissão para isso de forma explícita.
O primeiro passo é editar o Info.plist arquivo. O Info.plist é um arquivo que contém todos os metadados necessários para o aplicativo. Adicione uma nova entrada ao arquivo passando o mouse sobre a última entrada da lista e clicando no pequeno + que aparece. Na lista suspensa, selecione Privacy - Microphone Usage Description e adicione Microphone access required to take part in audio rooms como valor.
Você realizará a segunda etapa para solicitar permissões de microfone mais adiante neste tutorial.
Criar a tela de login
O Client SDK precisa de um JWT para se conectar aos servidores da Vonage. O aplicativo para iOS precisa enviar um nome de usuário para o /auth ponto de extremidade do servidor. Crie um novo arquivo chamado Models.swift acessando Arquivo > Novo > Arquivo (CMD + N). Assim como no backend, há uma estrutura para o corpo da solicitação e outra para a resposta do servidor.
Adicione as seguintes estruturas ao Models.swift arquivo:
struct Auth: Codable {
struct Body: Codable {
let name: String
}
struct Response: Codable {
let name: String
let jwt: String
}
}Como o aplicativo para iOS utilizará três endpoints diferentes, você criará uma pequena classe para reutilizar o código de rede. Crie um novo arquivo chamado RemoteLoader.swift e adicione a seguinte classe:
import Foundation
final class RemoteLoader {
enum RemoteLoaderError: Error {
case url
case data
}
static func load<T: Codable, U: Codable>(urlString: String, body: T?, responseType: U.Type, completion: @escaping ((Result<U, RemoteLoaderError>) -> Void)) {
guard let url = URL(string: urlString) else {
completion(.failure(.url))
return
}
var request = URLRequest(url: url)
if let body = body, let encodedBody = try? JSONEncoder().encode(body) {
request.httpMethod = "POST"
request.httpBody = encodedBody
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
}
URLSession.shared.dataTask(with: request) { data, response, error in
if let data = data {
if let response = try? JSONDecoder().decode(U.self, from: data) {
completion(.success(response))
return
}
}
completion(.failure(.data))
}.resume()
}
}A RemoteLoader classe consiste em uma enumeração de erros e uma função estática load . A função load é genérica para dois tipos, T e U, que estão em conformidade com o Codeable protocolo.
T representa a estrutura que será usada como corpo de uma solicitação enviada por esta função. É opcional, pois algumas solicitações podem não exigir um corpo. U representa o tipo da estrutura de resposta.
Ao fazer uma solicitação de rede, você fornece a URL, o corpo da solicitação e o tipo de resposta, e a load função retorna um resultado.
Antes de começar a criar a interface do usuário (UI) do aplicativo, você criará primeiro uma classe de modelo. Essa classe é usada para separar a lógica do aplicativo do código da visualização. Nesse caso, a classe de modelo irá lidar com as chamadas de delegado do Client SDK e fazer a solicitação de rede para o login.
No início do ContentView.swift arquivo, importe o Client SDK e o AVFoundation:
import SwiftUI
import NexmoClient
import AVFoundationEm seguida, no final do arquivo, crie uma nova classe chamada AuthModel.
final class AuthModel: NSObject, ObservableObject, NXMClientDelegate {
}Nessa classe, defina as propriedades necessárias:
final class AuthModel: NSObject, ObservableObject, NXMClientDelegate {
@Published var loading = false
@Published var connected = false
var name = ""
private let audioSession = AVAudioSession.sharedInstance()
}O @Published wrapper de propriedade é o que permite que a interface do usuário saiba quando reagir às alterações da classe do modelo; tudo isso é tratado automaticamente para você, já que a classe está em conformidade com o ObservedObject protocolo.
A audioSession propriedade é usada para solicitar permissões de microfone. Para concluir a solicitação de permissões de microfone para o aplicativo, adicione a seguinte função à AuthModel classe:
func requestPermissionsIfNeeded() {
if audioSession.recordPermission != .granted {
audioSession.requestRecordPermission { (isGranted) in
print("Microphone permissions \(isGranted)")
}
}
}Essa função verificará primeiro se as permissões já foram concedidas; caso contrário, ela as solicitará e exibirá o resultado no console. Em seguida, você pode adicionar a função que faz a solicitação ao servidor backend usando a RemoteLoader classe:
func login() {
loading = true
RemoteLoader.load(urlString: "https://URL.ngrok.io/auth", body: Auth.Body(name: self.name), responseType: Auth.Response.self) { result in
switch result {
case .success(let response):
DispatchQueue.main.async {
NXMClient.shared.setDelegate(self)
NXMClient.shared.login(withAuthToken: response.jwt)
}
default:
break
}
}
}Substitua a urlString string pela sua URL do ngrok. Assim que a resposta for recebida, a função usará o JWT para fazer login no Client SDK e definirá o delegado do Client SDK para esta classe. Em um ambiente de produção, é recomendável que o servidor também transmita informações sobre o TTL do JWT e que o aplicativo realize verificações adicionais sobre a validade do JWT antes de executar ações que exijam o Client SDK.
É NXMClientDelegate é assim que o Client SDK comunica as alterações dos servidores da Vonage de volta ao seu aplicativo. Em seguida, implemente as funções delegadas necessárias na AuthModel classe:
func client(_ client: NXMClient, didChange status: NXMConnectionStatus, reason: NXMConnectionStatusReason) {
switch status {
case .connected:
self.connected = true
self.loading = false
default:
self.connected = false
self.loading = false
}
}
func client(_ client: NXMClient, didReceiveError error: Error) {
self.loading = false
self.connected = false
}Quando ocorre uma alteração no status do SDK ou um erro, os valores booleanos connected e loading serão alteradas, o que provocará mudanças na interface do usuário. A função final que você precisa adicionar ao AuthModel chamadas requestPermissionsIfNeeded:
func setup() {
requestPermissionsIfNeeded()
}Com a classe do modelo pronta, agora você pode criar a interface do usuário. Atualize a ContentView estrutura:
struct ContentView: View {
@ObservedObject var authModel = AuthModel()
var body: some View {
NavigationView {
VStack {
if authModel.loading {
ProgressView()
Text("Loading").padding(20)
} else {
TextField("Name", text: $authModel.name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.multilineTextAlignment(.center)
.padding(20)
Button("Log in") {
authModel.login()
}
NavigationLink("", destination: RoomListView(),
isActive: $authModel.connected).hidden()
}
}.navigationTitle("VonageHouse 👋")
.navigationBarBackButtonHidden(true)
}.onAppear(perform: authModel.setup)
}
}A ContentView estrutura possui uma instância da AuthModel classe. A propriedade loading da authModel determinará se o ContentView ela exibirá um estado de carregamento ou a visualização de entrada, que possui um Textfield campo para a inserção de um nome de usuário e um botão que aciona a login função mencionada anteriormente.
A visualização de entrada também possui um NavigationLink que exibirá a próxima visualização, RoomListView, quando o Client SDK se conectar com sucesso. Se você comentar a NavigationLink linha e executar o projeto (CMD + R), você verá a tela de login:

Criar a tela da lista de salas
Quando o Client SDK se conectar com sucesso, você deverá solicitar ao servidor backend uma lista de todas as salas abertas. Da mesma forma que na tela de login, você deverá adicionar as estruturas do modelo e, em seguida, criar uma classe de modelo para lidar com a lógica. Adicione as estruturas ao Models.swift arquivo:
struct RoomResponse: Codable {
let id: String
let displayName: String
enum CodingKeys: String, CodingKey {
case id
case displayName = "display_name"
}
}
struct CreateRoom: Codable {
struct Body: Codable {
let displayName: String
enum CodingKeys: String, CodingKey {
case displayName = "display_name"
}
}
struct Response: Codable {
let id: String
}
}Crie um novo arquivo chamado RoomListView.swift e adicione a classe do modelo:
import SwiftUI
final class RoomModel: ObservableObject {
@Published var results = [RoomResponse]()
@Published var loading = false
@Published var showingCreateModal = false
@Published var hasConv = false
var convID: String? = nil
var roomName: String = ""
func loadRooms() {
RemoteLoader.load(urlString: "https://URL.ngrok.io/rooms", body: Optional<String>.none, responseType: [RoomResponse].self) { result in
switch result {
case .success(let response):
DispatchQueue.main.async {
self.results = response
}
default:
break
}
}
}
func createRoom() {
RemoteLoader.load(urlString: "https://URL.ngrok.io/rooms", body: CreateRoom.Body(displayName: self.roomName), responseType: CreateRoom.Response.self) { result in
switch result {
case .success(let response):
self.convID = response.id
DispatchQueue.main.async {
self.hasConv = true
self.loading = false
self.showingCreateModal = false
}
default:
break
}
}
}
}Essa classe de modelo é responsável por carregar a lista de salas e enviar a solicitação para criar uma nova sala; substitua a urlString string pela sua URL do ngrok. A interface do usuário observará a results propriedade.
Em seguida, crie a interface do usuário que observará essa classe de modelo. Adicione a RoomListView estrutura ao mesmo arquivo:
struct RoomListView: View {
@ObservedObject var roomModel = RoomModel()
var body: some View {
VStack {
List(roomModel.results, id: \.id) { item in
VStack(alignment: .leading) {
NavigationLink(destination: RoomView(convID: item.id, convName: item.displayName)) {
Text(item.displayName)
}
}
}.onAppear(perform: roomModel.loadRooms)
Button("Create room") {
roomModel.showingCreateModal.toggle()
}
NavigationLink("", destination: RoomView(convID: roomModel.convID ?? "", convName: roomModel.roomName),
isActive: $roomModel.hasConv).hidden()
}
.navigationTitle("VonageCottage 👋")
.navigationBarBackButtonHidden(true)
.navigationBarItems(trailing:
Button("Refresh") {
roomModel.loadRooms()
}
)
.sheet(isPresented: $roomModel.showingCreateModal, content: {
CreateRoomModal(roomModel: roomModel)
})
}
}A RoomListView estrutura possui um List componente que exibirá a lista de salas abertas e um botão de atualização na barra de navegação. Há também um Button componente que alterna um valor booleano que exibe uma CreateRoomModal visualização. Adicione a visualização ao mesmo arquivo:
struct CreateRoomModal: View {
@ObservedObject var roomModel: RoomModel
var body: some View {
if !roomModel.loading {
VStack {
TextField("Enter the room name", text: $roomModel.roomName)
.textFieldStyle(RoundedBorderTextFieldStyle())
.multilineTextAlignment(.center)
.padding(20)
Button("Create room") {
roomModel.loading = true
roomModel.createRoom()
}
}
} else {
ProgressView()
}
}
}Essa visualização consiste em um TextField campo para inserção do nome da sala, um indicador de carregamento e um Button para acionar a função de criação de sala na RoomModel classe. Os dois NavigationLink componentes na RoomListView fazem referência a um RoomView. O RoomView recebe um ID de conversa como parâmetro, necessário para que o Client SDK carregue a conversa.
Criar a tela “Sala”
Esta tela final é onde os usuários do seu aplicativo participarão de conversas e se comunicarão entre si. Assim como nas telas anteriores, você começará adicionando uma estrutura de modelo ao Models.swift arquivo:
struct Member: Hashable {
let id: String
let name: String
}Essa estrutura representará como um membro de uma sala é exibido na interface do usuário. Em seguida, crie um novo arquivo chamado RoomView.swift, e dentro desse arquivo, crie uma ConversationModel classe:
import SwiftUI
import NexmoClient
final class ConversationModel: NSObject, ObservableObject, NXMConversationDelegate {
@Published var loading = false
@Published var members = [Member]()
private var conversation: NXMConversation?
private let currentUsername: String? = NXMClient.shared.user?.name
func memberFrom(_ event: NXMMemberEvent) -> Member {
return Member(id: event.fromMemberId, name: event.embeddedInfo?.user.name ?? "")
}
func memberFrom(_ nxmMemberSummary: NXMMemberSummary) -> Member {
return Member(id: nxmMemberSummary.memberUuid, name: nxmMemberSummary.user.name)
}
func conversation(_ conversation: NXMConversation, didReceive event: NXMMemberEvent) {
let member = memberFrom(event)
switch event.state {
case .joined:
guard !self.members.contains(member),
self.currentUsername != member.name else { break }
self.members.append(member)
case .left:
guard self.members.contains(member),
let memberIndex = self.members.firstIndex(of: member) else { break }
self.members.remove(at: memberIndex)
default:
break
}
}
func conversation(_ conversation: NXMConversation, didReceive error: Error) {
print(error.localizedDescription)
}
}
A ConversationModel classe está em conformidade com NXMConversationDelegate além do ObservableObject protocolo, assim como as outras classes de modelo. As duas funções da NXMConversationDelegate que você utilizará são didReceive:event e didReceive:error.
A didReceive:event função é a forma como o aplicativo iOS será notificado sobre a saída e a entrada de usuários na conversa. Quando isso ocorre, os membros são adicionados e removidos da members array, o que a interface do usuário monitora.
Agora você pode adicionar as funções que lidam com o carregamento e o encerramento de conversas à ConversationModel classe:
final class ConversationModel: NSObject, ObservableObject, NXMConversationDelegate {
...
func loadConversation(convID: String) {
guard conversation == nil else { return }
loading = true
NXMClient.shared.getConversationWithUuid(convID) { error, conversation in
self.conversation = conversation
self.conversation?.delegate = self
self.conversation?.join { [weak self] error, memberId in
guard let self = self else { return }
self.conversation?.getMembersPage(withPageSize: 100, order: .asc) { error, membersPage in
DispatchQueue.main.async {
guard let membersPage = membersPage else { return }
self.members = membersPage.memberSummaries.map { self.memberFrom($0) }
if !self.members.contains(where: { $0.name == self.currentUsername }) {
if let id = memberId, let name = self.currentUsername {
self.members.append(Member(id: id, name: name))
}
}
self.loading = false
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.conversation?.enableMedia()
}
}
}
}
}
func leaveConversation(completion: () -> Void) {
self.conversation?.disableMedia()
self.conversation?.leave(nil)
completion()
}
...
}
As loadConversation chamadas de função getConversationWithUuid no Client SDK, retornando o objeto de conversa armazenado em uma propriedade local. Agora que você tem o objeto de conversa, pode entrar na conversa. Assim que isso for concluído, você pode habilitar a mídia para o usuário, permitindo que ele fale e ouça os outros usuários na conversa. leaveConversation faz o contrário. Ela desativa a mídia, sai da conversa e chama um manipulador de conclusão passado para a função.
A interface do usuário desta tela está dividida em duas estruturas: uma visualização menor para um único membro e uma visualização maior com uma grade de membros, que controla a navegação. Crie a MemberView estrutura no mesmo arquivo:
struct MemberView: View {
var memberName: String
var body: some View {
VStack {
Circle()
.fill(Color.gray)
.frame(width: 75, height: 75)
Text(memberName)
}
}
}Este é um círculo com Text um campo abaixo para o nome do membro da sala. Em seguida, adicione o RoomView:
struct RoomView: View {
@StateObject var conversationModel = ConversationModel()
@Environment(\.presentationMode) var presentationMode
var convID: String
var convName: String
let columns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
VStack {
if conversationModel.loading {
ProgressView()
Text("Loading").padding(20)
} else {
VStack {
ScrollView {
LazyVGrid(columns: columns, spacing: 75) {
ForEach(conversationModel.members, id: \.self) { member in
MemberView(memberName: member.name)
}
}
}
Button("Leave room") {
conversationModel.leaveConversation(completion: { presentationMode.wrappedValue.dismiss() })
}
}
}
}.navigationTitle(convName)
.navigationBarBackButtonHidden(true)
.onAppear(perform: {
conversationModel.loadConversation(convID: convID)
})
}
}Essa visualização possui uma presentationMode propriedade que permitirá que a visualização seja fechada quando o usuário sair da conversa/sala, e uma grade com três colunas onde os membros da sala serão exibidos.
Execute seu aplicativo
Se você executar o projeto (CMD + R), será solicitado, primeiro, que conceda permissões para o microfone, caso ainda não tenha feito isso.
Faça login com um nome de usuário e você será direcionado para a tela da lista de salas, onde poderá criar uma sala. Ao criar uma sala, você será direcionado para a tela da sala. Repita os mesmos passos com um nome de usuário e um dispositivo diferentes, e você poderá conversar na sala!

E agora?
Você pode encontrar o projeto completo do aplicativo para iOS em GitHub. É possível fazer muito mais com o Client SDK; saiba mais em developer.vonage.com.
Compartilhar:
Abdul é um Developer Advocate da Vonage. Ele tem experiência profissional na área de produtos de consumo como engenheiro de iOS. Em seu tempo livre, ele gosta de andar de bicicleta, ouvir música e orientar quem está dando os primeiros passos na área de tecnologia