
Teilen Sie:
Abdul ist ein Developer Advocate für Vonage. Er hat einen Hintergrund als iOS-Ingenieur im Bereich Verbraucherprodukte. In seiner Freizeit fährt er gerne Rad, hört Musik und berät diejenigen, die gerade ihre Reise in die Technologiebranche beginnen.
Erstellen einer Drop-in Audio App mit SwiftUI und Vapor - Teil 1
Lesedauer: 9 Minuten
Hinweis: Einige der in diesem Artikel beschriebenen Tools oder Methoden werden möglicherweise nicht mehr unterstützt oder sind nicht mehr aktuell. Für aktualisierte Inhalte oder Support, überprüfen Sie unsere neuesten Beiträge oder kontaktieren Sie uns auf dem Vonage Community Slack
Einführung
Drop-in-Audio-Apps werden immer beliebter: Clubhouse, Soapbox, Twitter Spaces und andere erfreuen sich großer Beliebtheit. In diesem Lernprogramm werden Sie die Conversation API mit dem Client SDK verwenden, um Ihre eigene Drop-in-Audio-Anwendung zu erstellen. Das Tutorial besteht aus zwei Teilen: Der erste Teil behandelt den Backend-Server, der der zweite Teil wird die iOS-Anwendung behandelt.
Vonage API-Konto
Um dieses Tutorial durchzuführen, benötigen Sie ein Vonage API-Konto. Wenn Sie noch keines haben, können Sie sich noch heute anmelden und mit einem kostenlosen Guthaben beginnen. Sobald Sie ein Konto haben, finden Sie Ihren API-Schlüssel und Ihr API-Geheimnis oben auf dem Vonage-API-Dashboard.
Voraussetzungen
Xcode 12 und Swift 5 oder höher
Vapor 4.0 auf Ihrem Rechner installiert
ngrok um Ihren lokalen Rechner dem Internet auszusetzen
Unser Command Line Interface, das Sie mit
npm install @vonage/cli -g.
Erstellen einer Vonage-Anwendung
Zur Erstellung der Anwendung verwenden Sie die Vonage-Befehlszeilenschnittstelle. Wenn Sie die CLI noch nicht eingerichtet haben, führen Sie vonage config:set --apiKey=API_KEY --apiSecret=API_SECRET in Ihrem Terminal aus, wobei Sie den API-Schlüssel und das Geheimnis durch die Werte ersetzen, die Sie auf Ihrer Einstellungen Ihres Accounts.
Erstellen Sie zunächst ein Verzeichnis mit mkdir vonageapian und navigieren Sie dann in dieses Verzeichnis mit cd vonageapi. Als nächstes erstellen Sie die Vonage-Anwendung mit vonage apps:create VaporConvAPI --rtc_event_url=https://example.com/. Dieser Befehl speichert den privaten Schlüssel Ihrer Anwendung in der Datei vaporconvapi.key Datei und gibt die ID Ihrer Anwendung aus. Sie benötigen beide Werte für die weiteren Schritte.
Ein Vapor-Projekt erstellen
Erstellen Sie ein Vapor-Projekt mit dem Befehl new project vapor new VaporConvAPI in Ihrem Terminal. Das Terminal wird Sie einige Male fragen, ob Sie Fluent verwenden möchten. Beantworten Sie diese Frage mit Ja und wählen Sie SQLite als Datenbank. Als nächstes werden Sie gefragt, ob Sie Leaf verwenden möchten, was Sie mit Nein beantworten.
Fluent ist ein objektrelationales Mapping-Framework, das wir zum Speichern von Benutzerinformationen in der Datenbank verwenden werden. Sobald der Befehl beendet ist, wechseln Sie in den Projektordner mit cd VaporConvAPI.

Als nächstes kopieren Sie Ihre vaporconvapi.key Datei aus dem Stammverzeichnis Ihres Projekts in das Verzeichnis des Vapor-Projekts Sources/App/ Ordner. Danach können Sie das Projekt in Xcode öffnen mit vapor xcode. Wenn Xcode geöffnet wird, werden die Abhängigkeiten, auf die Vapor angewiesen ist, mit dem Swift Package Manager (SPM) heruntergeladen. Um die Abhängigkeiten zu sehen, können Sie die Package.swift Datei öffnen.
Standardmäßig führt Xcode Ihre Anwendung aus einem zufällig ausgewählten lokalen Verzeichnis aus. Da Sie die Datei vaporconvapi.key Datei laden, müssen Sie ein benutzerdefiniertes Arbeitsverzeichnis festlegen. Gehen Sie zu Produkt > Schema > Schema bearbeiten... und setzen Sie das Arbeitsverzeichnis auf den Stammordner Ihres Projekts.

Benutzerauthentifizierung
Wenn Sie Ihre Anwendung verwenden, müssen Sie Benutzer authentifizieren, um das Client SDK in der iOS-Anwendung zu nutzen. Die Conversation API hat ein Konzept von Benutzerein Objekt, das einen eindeutigen Vonage-Benutzer im Kontext Ihrer Vonage-Anwendung identifiziert. Ihr Backend-Server verfolgt auch die Benutzer, die eins-zu-eins mit dem Vonage-Benutzer übereinstimmen. Um zwischen den beiden zu unterscheiden, werden die Benutzer auf Ihrem Backend-Server in Zukunft als Datenbankbenutzer bezeichnet. Sobald Sie einen registrierten und gespeicherten Benutzer haben, wird der Server diesen verwenden, um ein JSON-Web-Token (JWT) für das Client SDK zur Anmeldung zu generieren.
Erstellen des Datenbank-Benutzermodells
Im Ordner Models Ordner löschen Sie die Datei Todo.swift Datei und erstellen Sie eine neue Datei mit dem Namen User.swift indem Sie zu Datei > Neu > Datei (CMD + N). Als nächstes erstellen Sie eine neue Klasse namens User die das Fluent-Modell für die Datenbankbenutzer sein wird:
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
}
}Die Eigenschaft schema Eigenschaft ist der Name der Tabelle in der Datenbank; die id und name Eigenschaften sind die Felder der Tabelle. Die Eigenschaft id ist optional und wird vom Benutzer erzeugt, da die Vonage-Benutzerkennung hier verwendet wird, aber noch nicht verfügbar ist.
Erstellen Sie die Datenbank-Benutzer-Migration
Um die Tabelle in der Datenbank zu erstellen, benötigen Sie eine Migration. Migrationen definieren Änderungen an der Datenbank, in diesem Fall die Erstellung der Tabelle User. Im Ordner Migrations Ordner, löschen Sie die CreateTodo.swift Datei und erstellen Sie eine neue Datei mit dem Namen CreateUser.swift. Dann erstellen Sie eine neue Struktur namens 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()
}
}
Beide Funktionen, prepare und revertsind für das Mirgration Protokoll benötigt. prepare wird aufgerufen, wenn die Migration ausgeführt wird; beachten Sie, dass das Schema und die Felder mit der User Klasse, die Sie gerade erstellt haben. Die id wird als Bezeichner festgelegt, der nicht automatisch erhöht wird, da die Vonage-Benutzer-ID wie bereits erwähnt verwendet wird.
Jetzt können Sie die Migrationen zu Ihrem Projekt hinzufügen, die configure.swift Datei und löschen Sie die app.migrations.add(CreateTodo()) Zeile und fügen Sie hinzu:
app.migrations.add(CreateUser())
try app.autoMigrate().wait()Dies führt die CreateUser Migration automatisch für Sie durch, wenn Ihr Server startet und nur bei Bedarf.
Erzeugen des JWT
Sowohl die Conversation API als auch die Vonage Client SDKs verwenden JWTs zur Authentifizierung. JWTs sind eine Methode zur sicheren Darstellung von Ansprüchen zwischen zwei Parteien. Mehr über JWTs erfahren Sie auf JWT.io oder über die Ansprüche, die die Conversation API unterstützt, in der Conversation API-Dokumentation. Öffnen Sie die Package.swift Datei und fügen Sie eine Abhängigkeit für Swift-JWT in das Top-Level dependencies Array als auch in das dependencies Array für das Ziel hinzu:
...
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))
]
),
...
Wenn Sie die Datei speichern, lädt SPM SwiftJWT. Um sie zu verwenden, erstellen Sie eine neue Datei im Ordner Models Ordner mit dem Namen 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)
}
}
Die jwtSigner Eigenschaft verwendet den privaten Schlüssel Ihrer Vonage-Anwendung, um Ihr JWT zu signieren. Sie wird in der makeJwt Funktion verwendet, die ein optionales Subjekt (sub) und eine Zugriffskontrollliste (ACL) annimmt. Admin JWTs werden erstellt, ohne dass ein Sub angegeben wird. Im Fall der Conversation API wäre ein Sub-Claim der Benutzername eines Vonage-Benutzers. Um die Ansprüche korrekt zu kodieren, SwiftJWT bietet ein Claim Protokoll, und wir erstellen eine neue Struktur, die mit dem Claim Protokoll in derselben Datei entspricht:
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/**"#: [:]
]
]
}Die Eigenschaften in der JwtClaim Struktur entsprechen den Ansprüchen, die von der Conversation API erwarteten Ansprüche. In einer Produktionsumgebung würden Sie eine kurze Verfallszeit für das JWT festlegen und nur die ACL-Pfade angeben, die benötigt werden.
Erstellen Sie nun eine Instanz der Auth Struktur in der Datei routes.swift Datei unter Verwendung Ihrer Vonage-Anwendungs-ID:
import Fluent
import Vapor
func routes(_ app: Application) throws {
var auth = Auth(applicationId: "APP_ID")
} Erstellen eines Vonage-Benutzers
Als nächstes können Sie mit der Erstellung der Endpunkte für die iOS-Anwendung beginnen. Der erste Endpunkt wird für die Authentifizierung sein. Der Server prüft zunächst, ob ein Datenbankbenutzer existiert, der mit dem eingehenden Benutzernamen übereinstimmt. Wenn dies der Fall ist, gibt der Server ein JWT zurück. Wenn der Datenbankbenutzer nicht existiert, erfolgt ein Aufruf an die Conversation API, um einen Vonage-Benutzer zu erstellen, die Details in der Datenbank zu speichern und dann ein JWT zurückzugeben.

Erstellen Sie zunächst eine neue Datei namens APIModels.swift im Verzeichnis Models Verzeichnis. In dieser Datei werden Sie alle für die Endpunkte benötigten Strukturen erstellen. Die erste Struktur, die Sie erstellen müssen, ist die AuthBody struct:
import Vapor
struct AuthBody: Content {
let name: String
}Dies ist das, was der iOS-Client an den Server senden wird. Die Struktur ist konform mit dem Content Protokoll von Vapor. Ein wesentlicher Vorteil der Verwendung von Vapor ist, dass Sie sich auf die Typsicherheit der Sprache Swift stützen können. Sie können Eingaben und Ausgaben an Ihren Server mit Structs modellieren, die dem Codable Protokoll entsprechen, wie Content das konform ist zu Codable.
Die folgenden Strukturen modellieren die erwartete Eingabe der Konversations-API, die Antwort der Konversations-API und die Antwort, die der Server an die iOS-Anwendung sendet:
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
}
}Es wurden Standardwerte geliefert für imageURL und displayName für den Zweck dieses Tutorials vorgegeben. Die Vonage APIs erwarten Felder in snake case, daher haben die structs das CodingKeys enum, um ihre Eigenschaftsnamen auf ihre Entsprechung in Großbuchstaben abzubilden.
Nun, da die Modelle vorhanden sind, können Sie die neue Route in die routes Funktion in der routes.swift Datei hinzufügen:
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 {
}
}
}
}
Diese Funktion definiert eine neue Route unter dem /auth Pfad des Servers, der eine zukünftige mit einem UserAuth.Response Typ zurückgibt - dem Typ, den die iOS-Anwendung erwartet.
Der Text der an den Server gesendeten Anfrage wird in die Variable authBody Variable dekodiert. Der Textkörper wird dann zum Filtern der Datenbankbenutzer verwendet. Da Sie nach einem Benutzer suchen (und Benutzernamen eindeutig sind), .first() auf die Antwort der Datenbankabfrage angewendet, die den Typ EventLoopFuture<User?>. Dieser wird dann in den erwarteten Typ von EventLoopFuture<UserAuth.Response> mit der flatMap Schließung.

Der zweite Teil des Flusses wird im flatMap Abschluss fort. Wenn die optionale Datenbankverwendung gleich Null ist, dann machen Sie einen Aufruf an /v0.1/users der Conversation API auf, um einen Benutzer zu erstellen:
...
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 }
}
}
}
...
Bei einer Anfrage an die Conversation API wird ein authorization Header zur Anfrage hinzugefügt, zusammen mit einer UserAuth.Body Struktur hinzugefügt, die als Körper der Anfrage kodiert ist. Die Antwort, die Vonage-Benutzer-ID des erstellten Benutzers, wird wiederum in einer flatMap Schließung in den erwarteten Typ von EventLoopFuture<UserAuth.Response>.
Diesmal kommt der Schritt hinzu, einen Datenbankbenutzer anzulegen und zu speichern. In einer Produktionsumgebung sollten Sie ein Kennwort verwenden, um den Zugang der Benutzer zu Ihrem System zu sichern, und Sie könnten noch einen Schritt weiter gehen und ein Authentifizierungs-Token für zukünftige Anfragen an Ihren Server zurückgeben.

Wenn die gesamte Route fertig ist, können Sie nun den Datenfluss von der Eingabe zum Server sehen, und durch eine Reihe von verketteten Transformationen erhalten Sie die gewünschte Ausgabe.
Listing-Gespräche
Sobald die iOS-Anwendung authentifiziert wurde, zeigt sie eine Liste von Audio-Räumen an, denen der Benutzer beitreten kann. Die Audio-Räume sind das Äquivalent der Unterhaltung Konzept der Conversation API. Um eine Liste der verfügbaren Konversationen für Ihre Vonage-Anwendung zu erhalten, können Sie /v0.2/conversations. Fügen Sie die benötigten Modelle in die APIModels Datei hinzu:
...
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"
}
}
}
}
...Erstellen Sie dann eine neue Route in der routes Funktion:
...
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
}
}
...
Ähnlich wie beim vorherigen Aufruf der Conversation API wird ein authorization Header zur Anfrage hinzugefügt. Die Antwort wird dann in den erwarteten Rückgabetyp für die Anwendung umgewandelt.
Eine Konversation führen
Die iOS-Anwendung muss neue Konversationen/Räume erstellen. Um ein neues Gespräch für Ihre Vonage-Anwendung zu erstellen, rufen Sie /v0.2/conversations. Fügen Sie eine Body struct zu der Conversation struct in der Datei APIModels Datei hinzu:
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"
}
}
}Für die Zwecke des Tutorials wurden wieder Standardwerte angegeben. Die Namen der Unterhaltungen in der Conversation API müssen eindeutig sein, daher wird eine zufällige UUID verwendet. Erstellen Sie dann eine neue Route in der routes Funktion:
...
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
}
}
...
Testen Sie den Server
Nun, da Ihre Routen definiert sind, können Sie sie erstellen und ausführen (CMD + R). Sobald Sie fertig sind, läuft Ihr Server lokal auf Port 8080. Um ihn für das Internet freizugeben, können Sie ngrok verwenden.
Führen Sie in Ihrem Terminal Folgendes aus ngrok http 8080. Ngrok generiert eine öffentliche URL, die Anrufe an Ihren lokalen Rechner weiterleitet.

Die ngrok-URL wird von der iOS-Anwendung für die Kommunikation mit dem Server verwendet. Sie können die von Ihnen erstellten Endpunkte mit einem API-Tool wie Postman, Rested oder Hoppscotch:
POST
/auth:

POST
/rooms:

GET
/rooms:

Wie geht es weiter?
Im zweiten Teil dieses Tutorials wird eine Drop-in-Audio-iOS-Anwendung mit SwiftUI und dem Client SDK erstellt, die den soeben erstellten Server nutzt.

Sie finden das fertige Projekt auf GitHub. Erfahren Sie mehr über die Conversation API auf developer.vonage.comund Vapor auf vapor.codes.
Teilen Sie:
Abdul ist ein Developer Advocate für Vonage. Er hat einen Hintergrund als iOS-Ingenieur im Bereich Verbraucherprodukte. In seiner Freizeit fährt er gerne Rad, hört Musik und berät diejenigen, die gerade ihre Reise in die Technologiebranche beginnen.
