
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 1
Tempo de leitura: 10 minutos
Observação: algumas das ferramentas ou métodos descritos neste artigo podem não ter mais suporte ou estar desatualizados. Para obter conteúdo atualizado ou suporte, consulte nossas postagens mais recentes ou nossa Documentação
Introdução
Os aplicativos de áudio do tipo “drop-in” estão se tornando muito populares, com o Clubhouse, o Soapbox, o Twitter Spaces e outros ganhando bastante força. Neste tutorial, você utilizará a Conversation API com o Client SDK para criar seu próprio aplicativo de áudio do tipo “drop-in”. O tutorial é dividido em duas partes: esta primeira parte abordará o servidor de back-end, e a segunda parte abordará o aplicativo para iOS.
Pré-requisitos
Xcode 12 e Swift 5 ou versão posterior
Vapor 4.0 instalado no seu computador
ngrok para expor seu computador local à internet
Nossa interface de linha de comando, que você pode instalar com
npm install @vonage/cli -g.
Criação de uma aplicação da Vonage
Para criar o aplicativo, você utilizará a interface de linha de comando da Vonage. Caso ainda não tenha configurado a CLI, execute vonage config:set --apiKey=API_KEY --apiSecret=API_SECRET no seu terminal, substituindo “API Key” e “Secret” pelos valores encontrados na página de configurações da sua conta.
Primeiro, crie um diretório usando mkdir vonageapi, depois acesse o diretório com cd vonageapi. Em seguida, crie o aplicativo Vonage com vonage apps:create VaporConvAPI --rtc_event_url=https://example.com/. Esse comando salvará a chave privada do seu aplicativo no vaporconvapi.key arquivo e exibirá o ID do seu aplicativo. Você precisará desses dois valores para as etapas seguintes.
Criar um projeto de vapor
Crie um projeto Vapor usando o comando “novo projeto” vapor new VaporConvAPI no seu terminal. O terminal exibirá algumas mensagens, perguntando primeiro se você deseja usar o Fluent. Responda que sim e escolha o SQLite como banco de dados. Em seguida, será perguntado se você deseja usar o Leaf; responda que não.
Fluent é uma estrutura de mapeamento objeto-relacional que usaremos para armazenar informações do usuário no banco de dados. Assim que o comando for concluído, mude para a pasta do projeto usando cd VaporConvAPI.

Em seguida, copie seu vaporconvapi.key arquivo do diretório raiz do seu projeto para a pasta do projeto Vapor Sources/App/ . Feito isso, você pode abrir o projeto no Xcode usando vapor xcode. Quando o Xcode abrir, ele começará a baixar as dependências das quais o Vapor depende, usando o Swift Package Manager (SPM). Para visualizar as dependências, você pode abrir o Package.swift arquivo.
Por padrão, o Xcode executa seu aplicativo a partir de um diretório local aleatório. Como você vai carregar o vaporconvapi.key arquivo, é necessário definir um diretório de trabalho personalizado. Vá para Produto > Esquema > Editar Esquema... e defina o diretório de trabalho como a pasta raiz do seu projeto.

Autenticação de usuário
Ao utilizar seu aplicativo, você precisará autenticar os usuários para usar o Client SDK no aplicativo para iOS. A Conversation API possui o conceito de Usuários, um objeto que identifica um usuário exclusivo da Vonage no contexto do seu aplicativo Vonage. Seu servidor de back-end também manterá o controle dos usuários, que terão um mapeamento um-para-um com o usuário da Vonage. Para diferenciar os dois, os usuários no seu servidor de back-end serão referidos daqui em diante como “usuários do banco de dados”. Assim que você tiver um usuário registrado e salvo, o servidor usará essas informações para gerar um JSON Web Token (JWT) para que o Client SDK faça o login.
Criar o modelo do usuário do banco de dados
Na Models pasta, exclua o Todo.swift arquivo e crie um novo arquivo chamado User.swift , acessando Arquivo > Novo > Arquivo (CMD + N). Em seguida, crie uma nova classe chamada User , que será o Modelo Fluent para os usuários do banco de dados:
import Fluent
final class User: Model {
static let schema = "users"
@ID(custom: "id", generatedBy: .user) var id: String?
@Field(key: "name") var name: String
init() {}
init(id: String?, name: String) {
self.id = id
self.name = name
}
}A schema propriedade será o nome da tabela no banco de dados; a id e name serão os campos da tabela. A id é opcional e gerada pelo usuário, pois o ID de usuário da Vonage será usado aqui, mas ainda não está disponível.
Criar a migração do usuário do banco de dados
Para criar a tabela no banco de dados, você precisará de uma migração. As migrações definem alterações no banco de dados; neste caso, a criação da tabela User. Na Migrations pasta, exclua o CreateTodo.swift arquivo e crie um novo arquivo chamado CreateUser.swift. Em seguida, crie uma nova estrutura chamada CreateUser:
import Fluent
struct CreateUser: Migration {
func prepare(on database: Database) -> EventLoopFuture<Void> {
database.schema(User.schema)
.field("id", .string, .identifier(auto: false))
.field("name", .string, .required)
.create()
}
func revert(on database: Database) -> EventLoopFuture<Void> {
database.schema(User.schema).delete()
}
}
Ambas as funções, prepare e revert, são exigidas pelo Mirgration protocolo. prepare é chamada quando a migração é executada; observe como o esquema e os campos correspondem à User classe que você acabou de criar. A id propriedade é definida como um identificador que não é autoincrementado, já que o ID de usuário da Vonage será usado, conforme mencionado anteriormente.
Agora você pode adicionar as migrações ao seu projeto, abrir o configure.swift arquivo, exclua a app.migrations.add(CreateTodo()) linha e adicione:
app.migrations.add(CreateUser())
try app.autoMigrate().wait()Isso executará a CreateUser execução automática da migração para você quando o servidor for iniciado e somente quando necessário.
Gerar o JWT
Tanto a Conversation API quanto os SDKs do Vonage Client utilizam JWTs para autenticação. Os JWTs são um método para representar reivindicações de forma segura entre duas partes. Você pode ler mais sobre JWTs em JWT.io ou sobre as reivindicações compatíveis com a Conversation API na documentação da Conversation API. Abra o Package.swift arquivo e adicione uma dependência para Swift-JWT no dependencies , bem como na dependencies matriz do destino:
...
dependencies: [
// 💧 A server-side Swift web framework.
.package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"),
.package(url: "https://github.com/vapor/fluent.git", from: "4.0.0"),
.package(url: "https://github.com/vapor/fluent-sqlite-driver.git", from: "4.0.0"),
.package(name: "SwiftJWT", url: "https://github.com/Kitura/Swift-JWT.git", from: "3.0.0")
],
targets: [
.target(
name: "App",
dependencies: [
.product(name: "Fluent", package: "fluent"),
.product(name: "FluentSQLiteDriver", package: "fluent-sqlite-driver"),
.product(name: "Vapor", package: "vapor"),
.product(name: "SwiftJWT", package: "SwiftJWT")
],
swiftSettings: [
// Enable better optimizations when building in Release configuration. Despite the use of
// the `.unsafeFlags` construct required by SwiftPM, this flag is recommended for Release
// builds. See <https://github.com/swift-server/guides#building-for-production> for details.
.unsafeFlags(["-cross-module-optimization"], .when(configuration: .release))
]
),
...
Ao salvar o arquivo, o SPM fará o download SwiftJWT. Para usá-lo, crie um novo arquivo na Models pasta chamada Auth.swift:
import Vapor
import SwiftJWT
struct Auth {
private let applicationId: String
lazy var adminJWT: String = {
return makeJwt()
}()
private let jwtSigner: JWTSigner = {
let privateKeyPath = URL(fileURLWithPath: "Sources/App/vaporconvapi.key")
let privateKey: Data = try! Data(contentsOf: privateKeyPath, options: .alwaysMapped)
return JWTSigner.rs256(privateKey: privateKey)
}()
init(applicationId: String) {
self.applicationId = applicationId
}
func makeJwt(sub: String? = nil, acl: JwtClaim.Paths? = nil) -> String {
let iat = Date().timeIntervalSince1970.rounded()
let exp = iat.advanced(by: 21600.0)
let claims = JwtClaim(applicationId: applicationId, iat: iat, jti: UUID(), exp: exp, sub: sub, acl: acl)
var jwt = JWT(claims: claims)
return try! jwt.sign(using: jwtSigner)
}
}
A jwtSigner propriedade usa a chave privada do seu aplicativo Vonage para assinar seu JWT. Ela é usada na makeJwt função, que recebe um sujeito (sub) opcional e uma lista de controle de acesso (ACL). Os JWTs de administrador são criados sem fornecer um sub; no caso da Conversation API, uma reivindicação sub seria o nome de usuário de um usuário da Vonage. Para codificar as reivindicações corretamente, SwiftJWT fornece um Claim protocolo, e criamos uma nova estrutura que está em conformidade com o Claim protocolo no mesmo arquivo:
struct JwtClaim: Claims {
typealias Paths = [String: [String: [String: String]]]
let applicationId: String
let iat: TimeInterval
let jti: UUID
let exp: TimeInterval
let sub: String?
let acl: Paths?
enum CodingKeys: String, CodingKey {
case iat, jti, exp, sub, acl
case applicationId = "application_id"
}
static let defaultPaths: Paths = ["paths":
[
#"/*/users/**"#: [:],
#"/*/conversations/**"#: [:],
#"/*/sessions/**"#: [:],
#"/*/devices/**"#: [:],
#"/*/image/**"#: [:],
#"/*/media/**"#: [:],
#"/*/push/**"#: [:],
#"/*/knocking/**"#: [:],
#"/*/legs/**"#: [:]
]
]
}As propriedades na JwtClaim estrutura correspondem às reivindicações esperadas pela Conversation API. Em um ambiente de produção, você definiria um prazo de validade curto para o JWT e forneceria apenas os caminhos da ACL necessários.
Agora crie uma instância da Auth estrutura no routes.swift arquivo usando seu ID de aplicativo da Vonage:
import Fluent
import Vapor
func routes(_ app: Application) throws {
var auth = Auth(applicationId: "APP_ID")
} Criação de um usuário da Vonage
Em seguida, você pode começar a criar os endpoints para o aplicativo iOS. O primeiro endpoint será para autenticação. O servidor verificará primeiro se existe um usuário no banco de dados que corresponda ao nome de usuário recebido. Se houver, o servidor retornará um JWT. Se o usuário não existir no banco de dados, ele fará uma chamada à Conversation API para criar um usuário da Vonage, salvará os detalhes no banco de dados e, em seguida, retornará um JWT.

Primeiro, crie um novo arquivo chamado APIModels.swift no Models diretório. Esse arquivo será onde você criará todas as estruturas necessárias para os endpoints. A primeira estrutura que você precisa criar é a AuthBody struct:
import Vapor
struct AuthBody: Content {
let name: String
}É isso que o cliente iOS enviará ao servidor. A estrutura está em conformidade com o Content protocolo do Vapor. Uma vantagem significativa de usar o Vapor é que você pode contar com a segurança de tipos da linguagem Swift. Você pode modelar entradas e saídas para o seu servidor usando estruturas que seguem o Codable protocolo, como Content que está em conformidade com Codable.
As estruturas a seguir modelam a entrada esperada da Conversation API, a resposta da Conversation API e a resposta que o servidor enviará ao aplicativo iOS:
struct IDResponse: Content {
let id: String
}
struct UserAuth: Content {
struct Body: Content {
let name: String
let displayName: String
let imageURL: String
init(name: String) {
self.name = name
self.displayName = name
self.imageURL = "https://example.com/image.png"
}
enum CodingKeys: String, CodingKey {
case name
case displayName = "display_name"
case imageURL = "image_url"
}
}
struct Response: Content {
let name: String
let jwt: String
}
}Foram fornecidos valores padrão para imageURL e displayName para os fins deste tutorial. As APIs da Vonage esperam campos em snake case, portanto, as estruturas possuem a CodingKeys enum para mapear os nomes de suas propriedades para seus equivalentes em snake case.
Agora que os modelos já estão definidos, você pode adicionar a nova rota à routes função no routes.swift arquivo:
func routes(_ app: Application) throws {
var auth = Auth(applicationId: "APP_ID")
app.post("auth") { req -> EventLoopFuture<UserAuth.Response> in
let authBody = try req.content.decode(AuthBody.self)
return User.query(on: req.db)
.filter(\.$name == authBody.name)
.first()
.flatMap { user -> EventLoopFuture<UserAuth.Response> in
if let user = user {
let userAuthResponse = UserAuth.Response(
name: user.name,
jwt: auth.makeJwt(sub: user.name, acl: JwtClaim.defaultPaths))
return req.eventLoop.makeSucceededFuture(userAuthResponse)
} else {
}
}
}
}
Essa função define uma nova rota na /auth trajeto do servidor, que retorna um future com um UserAuth.Response tipo — o tipo que o aplicativo iOS espera.
O corpo da solicitação enviada ao servidor é decodificado na authBody variável. O corpo é então usado para filtrar os usuários do banco de dados. Como você está procurando um usuário (e os nomes de usuário são únicos), .first() é aplicado à resposta da consulta ao banco de dados, que retorna o tipo EventLoopFuture<User?>. Isso, então, é transformado no tipo esperado de EventLoopFuture<UserAuth.Response> com o flatMap clausula de fechamento.

A segunda parte do fluxo continua no flatMap closure. Se o parâmetro opcional do banco de dados for nil, faça uma chamada à /v0.1/users da Conversation API para criar um usuário:
...
app.post("auth") { req -> EventLoopFuture<UserAuth.Response> in
...
.flatMap { user -> EventLoopFuture<UserAuth.Response> in
if let user = user {
...
} else {
return req.client.post(URI(scheme: "https", host: "api.nexmo.com", path: "v0.1/users")) { req in
req.headers.add(name: .authorization, value: "Bearer \(auth.adminJWT)")
try req.content.encode(UserAuth.Body(name: authBody.name), as: .json)
}.flatMap { response -> EventLoopFuture<UserAuth.Response> in
let responseBody = try! response.content.decode(IDResponse.self)
let user = User(id: responseBody.id, name: authBody.name)
let userAuthResponse = UserAuth.Response(
name: user.name,
jwt: auth.makeJwt(sub: user.name, acl: JwtClaim.defaultPaths))
return user.save(on: req.db).map { userAuthResponse }
}
}
}
...
Ao enviar uma solicitação à Conversation API, um authorization cabeçalho é adicionado à solicitação, juntamente com uma UserAuth.Body estrutura codificada como o corpo da solicitação. A resposta, o ID de usuário da Vonage do usuário criado, é novamente transformada em uma flatMap closure para o tipo esperado de EventLoopFuture<UserAuth.Response>.
Desta vez, há uma etapa adicional: criar um usuário do banco de dados e salvá-lo. Em um ambiente de produção, você deve usar uma senha para proteger o acesso dos usuários ao seu sistema e pode ir um pouco além, retornando um token de autenticação para futuras solicitações ao seu servidor.

Com todo o percurso concluído, agora você pode observar o fluxo de dados desde a entrada até o servidor e, por meio de uma série de transformações encadeadas, obter a saída desejada.
Conversas sobre listagens
Assim que o aplicativo para iOS for autenticado, ele exibirá uma lista de salas de áudio nas quais o usuário poderá entrar. As salas de áudio serão o equivalente à conceito de da Conversation API. Para obter uma lista das conversas disponíveis para seu aplicativo Vonage, você pode chamar /v0.2/conversations. Adicione os modelos necessários ao APIModels arquivo:
...
struct Conversation: Content {
struct Response: Content {
let embedded: Embedded
enum CodingKeys: String, CodingKey {
case embedded = "_embedded"
}
struct Embedded: Content {
let data: Conversation.Response.Data
}
struct Data: Content {
let conversations: [Conv]
}
struct Conv: Content {
let id: String
let displayName: String
enum CodingKeys: String, CodingKey {
case id
case displayName = "display_name"
}
}
}
}
...Em seguida, crie uma nova rota na routes função:
...
app.get("rooms") { req -> EventLoopFuture<[Conversation.Response.Conv]> in
return req.client.get(URI(scheme: "https", host: "api.nexmo.com", path: "v0.2/conversations")) { req in
req.headers.add(name: .authorization, value: "Bearer \(auth.adminJWT)")
}.map { response -> [Conversation.Response.Conv] in
let responseBody = try! response.content.decode(Conversation.Response.self)
return responseBody.embedded.data.conversations
}
}
...
Assim como na chamada anterior feita à Conversation API, um authorization cabeçalho é adicionado à solicitação. A resposta é então transformada no tipo de retorno esperado pelo aplicativo.
Iniciando uma conversa
O aplicativo para iOS precisa criar novas conversas/salas. Para criar uma nova conversa no seu aplicativo Vonage, ligue para /v0.2/conversations. Adicione uma Body estrutura à Conversation estrutura no APIModels arquivo:
struct Conversation: Content {
...
struct Body: Content {
let name: String = UUID().uuidString
let displayName: String
let imageURL: String = "https://example.com/image.png"
let properties: [String: Int] = ["ttl": 300]
enum CodingKeys: String, CodingKey {
case name, properties
case displayName = "display_name"
case imageURL = "image_url"
}
}
}Os valores padrão foram fornecidos novamente para fins do tutorial. Os nomes das conversas na Conversation API precisam ser únicos; por isso, utiliza-se um UUID aleatório. Em seguida, crie uma nova rota na routes função:
...
app.post("rooms") { req -> EventLoopFuture<IDResponse> in
let conversationBody = try req.content.decode(Conversation.Body.self)
return req.client.post(URI(scheme: "https", host: "api.nexmo.com", path: "v0.1/conversations")) { req in
req.headers.add(name: .authorization, value: "Bearer \(auth.adminJWT)")
try req.content.encode(conversationBody, as: .json)
}.map { response -> IDResponse in
let responseBody = try! response.content.decode(IDResponse.self)
return responseBody
}
}
...
Teste o servidor
Agora que suas rotas estão definidas, você pode compilar e executar (CMD + R). Quando terminar, seu servidor estará em execução localmente na porta 8080. Para disponibilizá-lo na internet, você pode usar o ngrok.
No seu terminal, execute ngrok http 8080. O ngrok gerará uma URL pública que redireciona as chamadas para sua máquina local.

A URL do ngrok é o que o aplicativo iOS usará para se comunicar com o servidor. Você pode testar os endpoints que criou usando uma ferramenta de API, como o Postman, o Rested ou Hoppscotch:
POST
/auth:

POST
/rooms:

OBTER
/rooms:

E agora?
A segunda parte deste tutorial irá criar um aplicativo de áudio para iOS com o SwiftUI e o Client SDK, que utiliza o servidor que você acabou de criar.

Você pode encontrar o projeto concluído no GitHub. Saiba mais sobre a Conversation API em developer.vonage.come sobre o Vapor em vapor.codes.
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