Låt oss diskutera olika sätt att påskynda din produktutveckling.

Visst, då gör vi det
Betyg 4,9/5 på Clutch

MOP

  • Tjänster

  • Produkter

  • Människor

  • Blogg

  • Låt oss bygga

Mer

  • Fallstudier

  • Utmärkelser

  • Kundutlåtanden

  • Lediga tjänster

  • Teknik

Socialt

  • LinkedIn

  • X

  • Instagram

  • Facebook

  • Dribbble

© 2026 Ministry of Programming

  • Integritetspolicy
Tillbaka till bloggen

Bygg din första SwiftUI-app (del 6): Skapa API-hjälparklassen

Dino Trnka
20 januari 2023Programvaruarkitektur och -utvecklingLäsningstid: 9 minuter
Bygg din första SwiftUI-app (del 6): Skapa API-hjälparklassen

Grattis! Vi har redan implementerat många viktiga komponenter i vår första SwiftUI-app. I föregående del, tillämpade vi den nödvändiga logiken för vårt godkännande. I vår Inloggningsåtgärd klass, vi har allt vi behöver för att skicka en API-förfrågan till vår mock-server.

Låt oss fundera på skalbarhet ett ögonblick. Om vi ville skapa ytterligare en API-förfrågan skulle vi behöva upprepa det mesta av koden vi redan skrivit i vår InloggningsåtgärdDet är inte den typen av arkitektur vi vill ha.

Enligt DRY -principen (don't repeat yourself) bör vi inte duplicera kod, och vi bör sträva efter att inkapsla så mycket gemensam kod som möjligt för att göra utvecklingen enklare för oss själva och alla andra som kan tänkas arbeta med projektet med oss i framtiden.

Den här klassen bör täcka alla API-användningsfall vi kan behöva. Det betyder att vi även vill hantera andra typer av förfrågningar utöver FÅVi vill att vår klass ska kunna skicka POSTA, SÄTTA, LAPPA, och RADERA förfrågningar också.

Och det är precis vad vi ska göra nu!

I grund och botten, när vi är klara med den här hjälpklassen, kommer den att vara generisk, vilket betyder att vi inte behöver ändra den – eller så kommer vi att göra det väldigt sällan. Våra åtgärder kommer istället att använda den genom att tillhandahålla de nödvändiga parametrarna till den och analysera API-svaren från den.

Dags för handling! 😁

Skapa APIRequest-klassen

Låt oss skapa en ny singleton-klass i vår Verksamhetsnyttiga tjänster mapp. Vi kommer att kalla den API-förfrågan.

Vi börjar forma vår hjälpklass genom att flytta relevant kod från vår Inloggningsåtgärd till API-förfråganDet kommer att se ut så här:

import Foundation

typealias CompletionHandler = (Data) -> Void

enum HTTPMethod: String {
    case get
    case put
    case delete
    case post
}

class APIRequest<Parameters: Encodable, Model: Decodable> {
    
    static func call(
        path: String,
        method: HTTPMethod,
        parameters: Parameters? = nil,
        completion: @escaping CompletionHandler
    ) {
        
        let scheme: String = "https"
        let host: String = "72a29288-615d-4ed4-8f9a-21b224056d7f.mock.pstmn.io" // Put your mock server url here
        
        var components = URLComponents()
        components.scheme = scheme
        components.host = host
        components.path = path
        
        guard let url = components.url else {
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue
        
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        request.addValue("application/json", forHTTPHeaderField: "Accept")
        request.addValue("true", forHTTPHeaderField: "x-mock-match-request-body")
        
        if let parameters = parameters {
            request.httpBody = try? JSONEncoder().encode(parameters)
        }
        
        let task = URLSession.shared.dataTask(with: request) { data, _, error in
            if let data = data {
                completion(data)
            } else {
                if let error = error {
                    print("Error: \(error.localizedDescription)")
                }
            }
        }
        task.resume()
    }
}

Hittills, allt bra! Nu ska vi modifiera vår Inloggningsåtgärd att använda den nyskapade API-förfrågan klass:

import Foundation

struct LoginAction {
    let path = "/login"
    let method: HTTPMethod = .post
    var parameters: LoginRequest
    
    func call(
        completion: @escaping (LoginResponse) -> Void
    ) {
        APIRequest<LoginRequest, LoginResponse>.call(
            path: path,
            method: .post,
            parameters: parameters
        ) { data in
            if let response = try? JSONDecoder().decode(
                LoginResponse.self,
                from: data
            ) {
                completion(response)
            } else {
                print("Unable to decode response JSON")
            }
        }
    }
}

Toppen! Nu vår Inloggningsåtgärd innehåller bara den logik som är specifik för, ja, inloggningsåtgärden. 😃 All generisk API-hanteringskod flyttas till API-förfrågan klass, som kan förbli oförändrad och vara användbar för oss när vi behöver skicka en API-förfrågan. Och vi har stöd för alla typer av API-förfrågningar, och inte bara förFÅ begäran.

Med den här konfigurationen på plats kan vi helt enkelt instansiera en ny om vi behöver göra en ny förfrågan. API-förfrågan och fyll den med parametrar som är specifika för det specifika API-anropet.

Detta är ett viktigt första steg. Men vår API-förfrågan klassen är inte tillräckligt kraftfull ännu. Vi kan utöka den ytterligare till verkligen få det att lysa.

Få det att glänsa

Först och främst vår schema och värd variabler hör inte hemma i API-förfrågan klass. Dessa variabler bör finnas i en separat konfigurationsfil. Så, låt oss skapa en ny fil i vår Verksamhetsnyttiga tjänster mapp och namnge den Konfiguration:

class Config {
    static let shared = Config()
    
    let scheme: String = "https"
    let host: String = "72a29288-615d-4ed4-8f9a-21b224056d7f.mock.pstmn.io" // Put your mock server url here
}

Härnäst kommer vi att modifiera API-förfrågan klass i enlighet därmed:

...

class APIRequest<Parameters: Encodable, Model: Decodable> {
    
    static func call(
        scheme: String = Config.shared.scheme,
        host: String = Config.shared.host,
        path: String,
        method: HTTPMethod,
        parameters: Parameters? = nil,
        completion: @escaping CompletionHandler
    ) {
        var components = URLComponents()
        components.scheme = scheme
        components.host = host
        components.path = path
        
...

Du kan se att vi håller saker flexibla genom att tillåta passeringar schema och värd variabler som parametrar. Ändå är de inställda på våra hårdkodade variabler i Konfiguration filen som standard så att vi kan utelämna dem från våra anrop.

Vår API-hanterare kan hantera lyckade fall, men hur är det med misslyckanden? För närvarande skriver vi bara ut felmeddelandet i API-förfrågan klass, och det är inte ett bra sätt att hantera misslyckanden. Låt oss förbättra detta nu.

Hanteringsfel

Vi skulle kunna använda Apples Fel klassen för våra felhanteringsändamål, men vi kommer att dra mer nytta av att skapa vår egen klass (som implementerar den vanliga Fel klass). I Verksamhetsnyttiga tjänster mapp, skapa en ny uppräkning och kalla det API-fel.

Vid det här laget kommer det att vara enkelt och endast innehålla två felfall:

enum APIError: String, Error {
    case jsonDecoding
    case response
}

Nu ska vi integrera felfall i vår API-förfrågan klass. Jag markerade de tillagda kodraderna med kommentarer, så att du kan se vad som lades till.

import Foundation

typealias CompletionHandler = (Data) -> Void
typealias FailureHandler = (APIError) -> Void // Added this

enum HTTPMethod: String {
    case get
    case put
    case delete
    case post
}

class APIRequest<Parameters: Encodable, Model: Decodable> {
    
    static func call(
        scheme: String = Config.shared.scheme,
        host: String = Config.shared.host,
        path: String,
        method: HTTPMethod,
        parameters: Parameters? = nil,
        completion: @escaping CompletionHandler,
        failure: @escaping FailureHandler // Added this
    ) {
        var components = URLComponents()
        components.scheme = scheme
        components.host = host
        components.path = path
        
        guard let url = components.url else {
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue
        
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        request.addValue("application/json", forHTTPHeaderField: "Accept")
        request.addValue("true", forHTTPHeaderField: "x-mock-match-request-body")
        
        if let parameters = parameters {
            request.httpBody = try? JSONEncoder().encode(parameters)
        }
        
        let task = URLSession.shared.dataTask(with: request) { data, _, error in
            if let data = data {
                completion(data)
            } else {
                if error != nil {
                    failure(APIError.response) // Added this
                }
            }
        }
        task.resume()
    }
}

Naturligtvis måste vi nu integrera våra anpassade fel i Inloggningsåtgärd klass också:

import Foundation

struct LoginAction {
    let path = "/login"
    let method: HTTPMethod = .post
    var parameters: LoginRequest
    
    func call(
        completion: @escaping (LoginResponse) -> Void,
        failure: @escaping (APIError) -> Void // Added this
    ) {
        APIRequest<LoginRequest, LoginResponse>.call(
            path: path,
            method: .post,
            parameters: parameters
        ) { data in
            if let response = try? JSONDecoder().decode(
                LoginResponse.self,
                from: data
            ) {
                completion(response)
            } else {
                failure(.jsonDecoding) // Added this
            }
        } failure: { error in
            failure(error) // Added this
        }
    }
}

Nästan klart! Nu korrigerar vi vårt Logga inVisaModell nu genom att lägga till ett felblock och en variabel som kommer att lagra våra fel.

import Foundation

class LoginViewModel: ObservableObject {

    @Published var username: String = ""
    @Published var password: String = ""

    @Published var error: APIError? // Added this
    
    func login() {
        LoginAction(
            parameters: LoginRequest(
                username: username,
                password: password
            )
        ).call { response in
            self.error = nil // Added this
            
            Auth.shared.setCredentials(
                accessToken: response.data.accessToken,
                refreshToken: response.data.refreshToken
            )
        } failure: { error in
            self.error = error // Added this
        }
    }
}

Slutligen måste vi visa en felmeddelande för användaren om inloggningsåtgärden misslyckas.

Detta kräver att man lägger till ett litet kodavsnitt till Inloggningsskärm som visar en feletikett om värdet för fel variabeln från vymodellen är inställd.

import SwiftUI

struct LoginScreen: View {
    
    @ObservedObject var viewModel: LoginViewModel = LoginViewModel()
    
    var body: some View {
        VStack {
            
            Spacer()
            
            VStack {
                TextField(
                    "Login.UsernameField.Title".localized,
                    text: $viewModel.username
                )
                .autocapitalization(.none)
                .disableAutocorrection(true)
                .padding(.top, 20)
                
                Divider()
                
                SecureField(
                    "Login.PasswordField.Title".localized,
                    text: $viewModel.password
                )
                .padding(.top, 20)
                
                Divider()
            }
            
            Spacer()
            
            if viewModel.error != nil { // Added this
                Text("Login error")
                    .fontWeight(.bold)
                    .foregroundColor(.red)
            }
            
            Spacer()
            
            Button(
                action: viewModel.login,
                label: {
                    Text("Login.LoginButton.Title".localized)
                        .modifier(MainButton())
                }
            )
        }
        .padding(30)
    }
}

struct LoginScreen_Previews: PreviewProvider {
    static var previews: some View {
        LoginScreen()
    }
}

Och det var allt! Vi ändrade inte mycket kod, men vi implementerade en solid grundlogik för felhantering i vår app. Nu kan du utöka API-fel klass med dina egna anpassade fall för att signalera alla potentiella felanvändningsfall i din app på ett detaljerat och exakt sätt.

Förbättra APIRequest

Vår hjälpklass är ganska kraftfull just nu! Vi kan dock lägga till ett par saker till vår API-förfrågan att förbättra den ytterligare.

Låt oss börja med att implementera nätverkstillgänglighetsdetektering. Vi behöver skapa en ny klass i Verksamhetsnyttiga tjänster mapp som heter Nätverksmonitor.

import Foundation
import Network

class NetworkMonitor {

    static let shared: NetworkMonitor = NetworkMonitor()

    let monitor = NWPathMonitor()
    private var status: NWPath.Status = .requiresConnection
    var isReachable: Bool { status == .satisfied }

    func startMonitoring() {
        monitor.pathUpdateHandler = { path in
            self.status = path.status
        }

        let queue = DispatchQueue(label: "Monitor")
        monitor.start(queue: queue)
    }

    func stopMonitoring() {
        monitor.cancel()
    }

}

Denna enkla hjälpklass kommer att övervaka nätverksstatusen och lägga resultatet i ärNåbar boolesk variabel. För att den ska fungera måste vi anropa dess startaÖvervakning() metod så snart som möjligt.

Därför är det bästa stället att lägga detta samtal på init() metod för App projektets klass. Vi behöver modifiera vår SwiftUIBlueprintApp, så ser det ut så här:

import SwiftUI

@main
struct SwiftUIBlueprintApp: App {
    
    init() {
        NetworkMonitor.shared.startMonitoring() // Added this
    }
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Toppen! Nu lägger vi till vårt nya felfall i API-fel vi skapade tidigare:

import Foundation

enum APIError: String, Error {
    case jsonDecoding
    case response
    case noInternet // Added this
}

Till slut är det bara att ringa ärNåbar() metod från API-förfrågan klass:

...

    static func call(
        scheme: String = Config.shared.scheme,
        host: String = Config.shared.host,
        path: String,
        method: HTTPMethod,
        parameters: Parameters? = nil,
        completion: @escaping CompletionHandler,
        failure: @escaping FailureHandler
    ) {
        if !NetworkMonitor.shared.isReachable { // Added this
            return failure(.noInternet)
        }
        
        var components = URLComponents()
        components.scheme = scheme
        components.host = host
        components.path = path
        
...

API-hjälpen är utrustad för att hantera nätverksanslutning! Om du vill testa detta kan du skriva något liknande detta i Inloggningsskärm fil:

... om viewModel.error == .noInternet { Text("Inget internet") .fontWeight(.bold) .foregroundColor(.red) } annars om viewModel.error != nil { Text("Inloggningsfel") .fontWeight(.bold) .foregroundColor(.red) } ...

Naturligtvis är det bättre att skapa en ViewModifier att sammanfatta dessa två rader med stylingkod som är duplicerade här, men jag lämnar det till dig 😉

Om du följde allt vi har gjort hittills och sedan försökte logga in utan internetanslutning, bör din inloggningsskärm se ut så här:

OBS! Använd inte simulatorn när du testar internetanslutningen, eftersom den inte fungerar korrekt och du inte kommer att kunna testa den korrekt. Använd en riktig iOS-enhet istället.

Nätverksdetekteringen är klar! Låt oss lägga till några fler praktiska funktioner i vår API-förfrågan klass och avsluta det.

Allt eftersom vårt projekt blir större kan vi stöta på användningsfall där API:et inte tar emot några parametrar eller inte producerar något svar. Vår API-hjälparklass måste kunna hantera dessa scenarier. Vi behöver också stöd för frågeobjekt som kan krävas i API-anrop (vanligtvis FÅ förfrågningsparametrar). Slutligen måste vi skilja mellan auktoriserade och icke-auktoriserade anrop så att vi kan lägga till vår Bearer-token som en header i alla auktoriserade förfrågningar.

Så här kan vi enkelt utöka vår API-förfrågan för att stödja alla dessa funktioner (som vanligt är tillagda kodrader markerade i kommentarerna):

import Foundation

typealias CompletionHandler = (Data) -> Void
typealias FailureHandler = (APIError) -> Void

struct EmptyRequest: Encodable {} // Added this
struct EmptyResponse: Decodable {} // Added this

enum HTTPMethod: String {
    case get
    case put
    case delete
    case post
}

class APIRequest<Parameters: Encodable, Model: Decodable> {
    
    static func call(
        scheme: String = Config.shared.scheme,
        host: String = Config.shared.host,
        path: String,
        method: HTTPMethod,
        authorized: Bool,                  // Added this
        queryItems: [URLQueryItem]? = nil, // Added this
        parameters: Parameters? = nil,
        completion: @escaping CompletionHandler,
        failure: @escaping FailureHandler
    ) {
        if !NetworkMonitor.shared.isReachable {
            return failure(.noInternet)
        }
        
        var components = URLComponents()
        components.scheme = scheme
        components.host = host
        components.path = path
        
        if let queryItems = queryItems { // Added this
            components.queryItems = queryItems
        }
        
        guard let url = components.url else {
            return
        }
        
        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue
        
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        request.addValue("application/json", forHTTPHeaderField: "Accept")
        request.addValue("true", forHTTPHeaderField: "x-mock-match-request-body")
        
        if let parameters = parameters {
            request.httpBody = try? JSONEncoder().encode(parameters)
        }
    
        if authorized, let token = Auth.shared.getAccessToken() { // Added this
            request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }
        
        let task = URLSession.shared.dataTask(with: request) { data, _, error in
            if let data = data {
                completion(data)
            } else {
                if error != nil {
                    failure(APIError.response)
                }
            }
        }
        task.resume()
    }
}

Det var allt! Vi har en fullt fungerande API-förfrågan hjälpklass som vi kan använda för alla möjliga typer av förfrågningar! Om vi vill stödja fler funktioner i framtiden kan vi utöka den här klassen, men vår API-hjälpare är nu mer än kapabel att hantera alla våra behov. Nu, när vi vill implementera en ny åtgärd, kan vi skapa en ny instans av API-förfrågan och ange nödvändiga parametrar och kompletteringshanterare.

Ingen duplicerad kod! Skalbarhet rakt igenom 😉

Demoprojekt ❤️️

Om du vill ladda ner ett projekt som innehåller allt vi har gjort hittills kan du göra det genom att klicka här .

” Bygg din första SwiftUI-app ” är en del av en artikelserie som är utformad för att hjälpa dig att komma igång med iOS-apputveckling.

Om du har några frågor kring någon av ovanstående tankar, dela dem gärna i kommentarsfältet.

Klicka här för att läsa del 1: Projektuppbyggnad
Klicka
här för att läsa del 2: Projektarkitektur
Klicka
här för att läsa del 3: Skapa inloggningsskärmen
Klicka
här för att läsa del 4: Konfigurera en simulerad server med Postman
Klicka
här för att läsa del 5: Hantering av auktorisering

Nästa del ⏭️

Vi har skapat en gedigen ritning för våra SwiftUI-appar! Vi vet dock fortfarande inte hur vi ska skicka våra appar till AppStore så att världen kan njuta av vårt fantastiska arbete. I nästa artikel kommer vi att lära oss vad som krävs för att ladda upp vår app till AppStore och hur man gör det. Spännande tider väntar! 🤩