https://d226lax1qjow5r.cloudfront.net/blog/blogposts/building-a-drop-in-audio-app-with-swiftui-and-vapor-part-2/voice_swift-vapor_p2_1200x600.png

Erstellen einer Drop-in Audio App mit SwiftUI und Vapor - Teil 2

Zuletzt aktualisiert am March 3, 2021

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 creationXcode 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.

  1. Führen Sie den pod init aus, um eine neue Poddatei für Ihr Projekt zu erstellen.

  2. Öffnen Sie die Poddatei in Xcode mit open -a Xcode Podfile.

  3. 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
  1. Installieren Sie das SDK mit pod install.

  2. Ö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, RoomListViewwenn 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.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.swiftund 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 flowGif 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:

https://a.storyblok.com/f/270183/400x400/19c02db2d3/abdul-ajetunmobi.png
Abdul AjetunmobiSenior Advocate für Entwickler

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.