
Share:
)
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 2
Lesedauer: 7 Minuten
Einführung
Die erste Teil dieses Lernprogramms wurde die Konversations-API verwendet, um einen Server für eine Drop-in-Audio-Anwendung zu erstellen. Der Server unterstützt das Erstellen neuer Benutzer, das Erstellen neuer Chaträume und das Auflisten aller offenen Chaträume. In diesem Tutorial werden Sie eine iOS-Anwendung erstellen, die das Vonage Client SDK verwendet, um Chats zu konsumieren und zu starten. Wenn Sie direkt in dieses Tutorial einsteigen möchten, können Sie den Anweisungen im GitHub-Repository für den Server folgen, um alles einzurichten.
Voraussetzungen
Zusätzlich zu den Voraussetzungen für den ersten Teil benötigen Sie Cocoapods um das Vonage Client SDK für iOS zu installieren.
Erstellen der iOS-Anwendung
Es wird Zeit, die iOS-Anwendung einzurichten. Sobald sie erstellt ist, installieren Sie das Client-SDK und fragen Sie nach Mikrofonberechtigungen.
Ein Xcode-Projekt erstellen
Um zu beginnen, öffnen Sie Xcode und erstellen Sie ein neues Projekt, indem Sie zu Datei > Neu > Projekt. Wählen Sie eine App-Vorlage und geben Sie ihr einen Namen. Wählen Sie SwiftUI für die Schnittstelle, SwiftUI App für den Lebenszyklusund Swift für die Sprache. Und schließlich ein Ort, an dem Sie Ihr Projekt speichern.
Xcode project creation
Installieren Sie das Client-SDK
Nachdem Sie nun das Projekt erstellt haben, können Sie das Vonage Client SDK als Abhängigkeit hinzufügen. Navigieren Sie in Ihrem Terminal zu dem Ort, an dem Sie das Projekt gespeichert haben, und führen Sie die folgenden Befehle aus.
Führen Sie den
pod init
aus, um eine neue Poddatei für Ihr Projekt zu erstellen.Öffnen Sie die Poddatei in Xcode mit
open -a Xcode Podfile
.Aktualisieren Sie das Podfile, damit es
NexmoClient
als eine Abhängigkeit.
# 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'
end
Installieren Sie das SDK mit
pod install
.Öffnen Sie den neuen xcworkspace Datei in Xcode mit
open SwiftUIDropin.xcworkspace
.
Berechtigungen für das Mikrofon
Da die Anwendung das Mikrofon zum Tätigen von Anrufen verwendet, müssen Sie explizit um Erlaubnis bitten, dies zu tun.
Der erste Schritt ist die Bearbeitung der Info.plist
Datei. Die Datei Info.plist
ist eine Datei, die alle für die Anwendung erforderlichen Metadaten enthält. Fügen Sie der Datei einen neuen Eintrag hinzu, indem Sie den Mauszeiger über den letzten Eintrag in der Liste bewegen und auf die kleine +
Schaltfläche, die erscheint. Wählen Sie in der Dropdown-Liste Privacy - Microphone Usage Description
und fügen Sie Microphone access required to take part in audio rooms
für seinen Wert.
Den zweiten Schritt für die Beantragung von Mikrofonberechtigungen werden Sie später im Lernprogramm durchführen.
Erstellen des Anmeldebildschirms
Das Client SDK benötigt ein JWT, um sich mit den Vonage-Servern zu verbinden. Die iOS-Anwendung muss einen Benutzernamen an den /auth
Endpunkt des Servers senden. Erstellen Sie eine neue Datei namens Models.swift
indem Sie zu Datei > Neu > Datei (CMD + N). Ähnlich wie im Backend gibt es eine Struktur für den Body der Anfrage und eine Struktur für die Serverantwort.
Fügen Sie die folgenden Strukturen in die Datei Models.swift
Datei hinzu:
struct Auth: Codable {
struct Body: Codable {
let name: String
}
struct Response: Codable {
let name: String
let jwt: String
}
}
Da die iOS-Anwendung drei verschiedene Endpunkte verwenden wird, werden Sie eine kleine Klasse erstellen, um den Netzwerkcode wiederzuverwenden. Erstellen Sie eine neue Datei namens RemoteLoader.swift
und fügen Sie die folgende Klasse hinzu:
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()
}
}
Die Klasse RemoteLoader
Klasse besteht aus einem Fehler-Enum und einer statischen load
Funktion. Die Ladefunktion ist generisch über zwei Typen, T
und U
, die mit dem Codeable
Protokoll entsprechen.
T
stellt die Struktur dar, die als Körper einer von dieser Funktion gesendeten Anfrage verwendet wird. Sie ist optional, da einige Anfragen keinen Body benötigen. U
steht für den Typ der Antwortstruktur.
Bei einer Netzwerkanfrage geben Sie die URL, den Body und den Antworttyp an, und die load
Funktion gibt ein Ergebnis zurück.
Bevor Sie mit der Erstellung der Benutzeroberfläche (UI) für die App beginnen, erstellen Sie zunächst eine Modellklasse. Diese Klasse wird verwendet, um die Logik der App vom Anzeigecode zu trennen. In diesem Fall wird die Modellklasse die Client-SDK-Delegate-Aufrufe verarbeiten und die Login-Netzwerkanforderung stellen.
Am Anfang der Datei ContentView.swift
Datei importieren Sie das Client SDK und AVFoundation:
import SwiftUI
import NexmoClient
import AVFoundation
Erstellen Sie dann am Ende der Datei eine neue Klasse namens AuthModel
.
final class AuthModel: NSObject, ObservableObject, NXMClientDelegate {
}
Definieren Sie innerhalb dieser Klasse die benötigten Eigenschaften:
final class AuthModel: NSObject, ObservableObject, NXMClientDelegate {
@Published var loading = false
@Published var connected = false
var name = ""
private let audioSession = AVAudioSession.sharedInstance()
}
Die @Published
Eigenschaft Wrapper ist, wie die UI wissen wird, wann sie auf Änderungen von der Modellklasse reagieren soll; dies wird alles für Sie erledigt, da die Klasse mit dem ObservedObject
Protokoll entspricht.
Die Eigenschaft audioSession
Eigenschaft wird verwendet, um die Mikrofonberechtigung anzufordern. Um die Abfrage der Mikrofonberechtigungen für die App abzuschließen, fügen Sie die folgende Funktion in die AuthModel
Klasse hinzu:
func requestPermissionsIfNeeded() {
if audioSession.recordPermission != .granted {
audioSession.requestRecordPermission { (isGranted) in
print("Microphone permissions \(isGranted)")
}
}
}
Diese Funktion prüft zunächst, ob die Berechtigungen bereits erteilt wurden; wenn nicht, fordert sie sie an und gibt das Ergebnis auf der Konsole aus. Als nächstes können Sie die Funktion hinzufügen, die die Anfrage an den Backend-Server stellt, indem Sie die RemoteLoader
Klasse:
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
}
}
}
Ersetzen Sie die urlString
durch Ihre ngrok-URL. Sobald die Antwort empfangen wurde, verwendet die Funktion das JWT, um sich beim Client-SDK anzumelden und den Delegaten des SDK auf diese Klasse zu setzen. In einer Produktionsumgebung sollte der Server auch Informationen über die TTL des JWT weitergeben und in der Anwendung weitere Prüfungen über die Gültigkeit des JWT durchführen, bevor Aktionen ausgeführt werden, die das Client-SDK erfordern.
Die NXMClientDelegate
ist die Art und Weise, wie das Client SDK Änderungen mit den Vonage-Servern zurück an Ihre Anwendung kommuniziert. Als nächstes implementieren Sie die erforderlichen Delegatenfunktionen in der AuthModel
Klasse:
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
}
Wenn sich der Status des SDK ändert oder ein Fehler auftritt, werden die Booleschen connected
und loading
geändert, was wiederum Änderungen an der Benutzeroberfläche zur Folge hat. Die letzte Funktion, die Sie der AuthModel
hinzufügen müssen, ruft auf. requestPermissionsIfNeeded
:
func setup() {
requestPermissionsIfNeeded()
}
Nachdem die Modellklasse fertig ist, können Sie nun die Benutzeroberfläche erstellen. Aktualisieren Sie die ContentView
struct:
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)
}
}
Die ContentView
struct hat eine Instanz der AuthModel
Klasse. Die Eigenschaft loading der authModel
bestimmt, ob die ContentView
einen Ladezustand oder die Eingabeansicht rendert, die ein Textfield
für einen einzugebenden Benutzernamen und eine Schaltfläche, die die login
Funktion von vorhin auslöst.
Die Eingabeansicht hat auch eine versteckte NavigationLink
der die nächste Ansicht anzeigt, RoomListView
wenn das Client SDK erfolgreich eine Verbindung herstellt. Wenn Sie die Zeile NavigationLink
Zeile auskommentieren und das Projekt ausführen (CMD + R), wird der Anmeldebildschirm angezeigt:
Two screenshots, the first the iOS app requesting permissions, the second the login screen.
Erstellen des Raumlistenbildes
Wenn das Client SDK erfolgreich eine Verbindung herstellt, möchten Sie den Backend-Server anfordern, um eine Liste aller offenen Räume zu erhalten. Ähnlich wie beim Anmeldebildschirm müssen Sie die Modellstrukturen hinzufügen und dann eine Modellklasse erstellen, um die Logik zu verarbeiten. Fügen Sie die Strukturen in die Models.swift
Datei hinzu:
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
}
}
Erstellen Sie eine neue Datei mit dem Namen RoomListView.swift
und fügen Sie die Modellklasse hinzu:
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
}
}
}
}
Diese Modellklasse kümmert sich um das Laden der Liste der Räume und das Senden der Anfrage zum Erstellen eines neuen Raums; ersetzen Sie den urlString
String durch Ihre URL von ngrok. Die UI wird die results
Eigenschaft.
Als nächstes erstellen Sie die Benutzeroberfläche, die diese Modellklasse beobachten wird. Fügen Sie die RoomListView
Struktur in dieselbe Datei ein:
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)
})
}
}
Die RoomListView
Struktur hat eine List
Komponente, die die Liste der offenen Räume anzeigt, und eine Aktualisierungsschaltfläche in der Navigationsleiste. Außerdem gibt es eine Button
Komponente, die einen Booleschen Wert umschaltet, der eine CreateRoomModal
Ansicht. Fügen Sie die Ansicht in dieselbe Datei ein:
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()
}
}
}
Diese Ansicht besteht aus einer TextField
für die Eingabe des Raumnamens, einem Ladezustand und einem Button
zum Auslösen der Funktion zum Anlegen des Raums an der RoomModel
Klasse. Die beiden NavigationLink
Komponenten in der RoomListView
verweisen auf a RoomView
. Die RoomView
nimmt eine Konversations-ID als Parameter, den das Client-SDK benötigt, um die Konversation zu laden.
Den Raumbildschirm erstellen
Auf diesem letzten Bildschirm werden die Benutzer Ihrer Anwendung an Unterhaltungen teilnehmen und miteinander sprechen. Ähnlich wie bei den vorherigen Bildschirmen fügen Sie zunächst eine Modellstruktur in die Models.swift
Datei hinzufügen:
struct Member: Hashable {
let id: String
let name: String
}
Diese Struktur wird darstellen, wie ein Mitglied eines Raums in der Benutzeroberfläche angezeigt wird. Als nächstes erstellen Sie eine neue Datei namens RoomView.swift
und erstellen Sie in dieser Datei eine ConversationModel
Klasse:
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)
}
}
Die ConversationModel
Klasse ist konform mit NXMConversationDelegate
zusätzlich zum ObservableObject
Protokoll wie die anderen Modellklassen. Die beiden Funktionen aus der NXMConversationDelegate
die Sie verwenden werden, sind didReceive:event
und didReceive:error
.
Die Funktion didReceive:event
Funktion benachrichtigt die iOS-Anwendung, wenn Benutzer die Unterhaltung verlassen oder ihr beitreten. Wenn dies geschieht, werden Mitglieder hinzugefügt und aus dem members
Array hinzugefügt und entfernt, was von der Benutzeroberfläche beobachtet wird.
Nun können Sie die Funktionen, die das Laden und Verlassen von Konversationen behandeln, der ConversationModel
Klasse hinzufügen:
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()
}
...
}
Die Funktion loadConversation
Funktion ruft getConversationWithUuid
im Client SDK auf und gibt das in einer lokalen Eigenschaft gespeicherte Gesprächsobjekt zurück. Jetzt, wo Sie das Konversationsobjekt haben, können Sie der Konversation beitreten. Sobald dies abgeschlossen ist, können Sie die Medien für den Benutzer aktivieren, so dass er sprechen und andere Benutzer in der Unterhaltung hören kann. leaveConversation
bewirkt das Gegenteil. Es deaktiviert die Medien, verlässt die Konversation und ruft einen Beendigungshandler auf, der an die Funktion übergeben wird.
Die Benutzeroberfläche für diesen Bildschirm ist in zwei Strukturen aufgeteilt, eine kleinere Ansicht für ein einzelnes Mitglied und eine größere Ansicht mit einem Raster von Mitgliedern und Griffen zur Navigation. Erstellen Sie die MemberView
Struktur in der gleichen Datei:
struct MemberView: View {
var memberName: String
var body: some View {
VStack {
Circle()
.fill(Color.gray)
.frame(width: 75, height: 75)
Text(memberName)
}
}
}
Dies ist ein Kreis mit Text
Komponente für den Namen des Raummitglieds. Als nächstes fügen Sie die 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)
})
}
}
Diese Ansicht hat eine presentationMode
Eigenschaft, die es ermöglicht, die Ansicht zu schließen, wenn der Benutzer das Gespräch/den Raum verlässt, sowie ein dreispaltiges Raster, in dem die Raummitglieder angezeigt werden.
Führen Sie Ihre Anwendung aus
Wenn Sie das Projekt ausführen (CMD + R), werden Sie zunächst aufgefordert, Mikrofonrechte zuzulassen, falls Sie dies noch nicht getan haben.
Loggen Sie sich mit einem Benutzernamen ein, und Sie gelangen zur Raumliste, wo Sie einen Raum erstellen können. Wenn Sie einen Raum erstellen, werden Sie zur Raumansicht weitergeleitet. Wiederholen Sie die gleichen Schritte mit einem anderen Benutzernamen und einem anderen Gerät, und Sie können in dem Raum chatten!
Gif of the app flow
Was kommt als nächstes?
Sie finden das fertige iOS-Anwendungsprojekt auf GitHub. Sie können noch viel mehr mit dem Client SDK machen, erfahren Sie mehr auf developer.vonage.com.
Share:
)
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.