You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何借助AppAuth库实现SwiftUI中API请求的复用?

如何让AppAuth的API请求更通用可复用?

我刚接触SwiftUI开发,用AppAuth做了个带登录的简单应用,已经实现了登录登出,也能用令牌发请求,但所有请求都堆在单个类里,想拆分到不同文件。现在获取用户数据的请求是这么写的:

class AuthStore: ObservableObject {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    private var authState : OIDAuthState?
    
    @Published var isAuthenticated = false
    @Published var patron: User? // 补充原代码遗漏的patron属性

    func getUserData() {
        let userEndpoint = URL(string: "https://example.com/my-api/user/1")!
        
        self.authState?.performAction() { accessToken, idToken, error in
            if error != nil {
                print("Error fetching fresh tokens: \(error?.localizedDescription ?? "Unknown error")")
                return
            }
            
            guard let accessToken = accessToken else {
                return
            }
            
            // 添加Bearer令牌到请求头
            var urlRequest = URLRequest(url: userEndpoint)
            urlRequest.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
            
            let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
                
                print("Performing patron information request")
                if let error = error {
                    print("Error: \(error.localizedDescription)")
                    return
                }
                
                if let data = data {
                    do {
                        let decodedData = try JSONDecoder().decode(User.self, from: data)
                        DispatchQueue.main.async { // 原代码遗漏主线程更新UI,此处补充
                            self.patron = decodedData
                        }
                    } catch {
                        print("JSON Decoding error: \(error.localizedDescription)")
                    }
                }
            }
            
            task.resume()
        }
    }
}

我想让需要令牌的请求更通用可复用,想到用泛型,但写出来的代码有明显问题(异步请求无法直接返回泛型类型):

// 原代码的问题:异步操作不能直接返回T,需用闭包或Async/Await处理
func getData(url: URL) -> T {
        self.authState?.performAction() { accessToken, idToken, error in
            // ... 省略重复逻辑
            do {
                let decodedData = try JSONDecoder().decode(T.self, from: data)
                self.patron = decodedData
            } catch {
                // ...
            }
        }
        // 此处无法返回T,因为请求是异步的
}

另外考虑给AuthStore加个获取新鲜令牌的方法,在其他地方用令牌发请求,哪种方式更好?


解决方案1:用Async/Await重构泛型请求方法

Swift 5.5+的异步await可以解决异步返回的问题,把令牌获取、请求构建、JSON解码逻辑封装成通用泛型方法,避免重复代码:

class AuthStore: ObservableObject {
    private var authState: OIDAuthState?
    @Published var isAuthenticated = false
    @Published var patron: User?

    // 通用异步请求方法,支持任意Decodable类型
    func fetch<T: Decodable>(url: URL) async throws -> T {
        // 获取有效访问令牌
        let accessToken = try await withCheckedThrowingContinuation { continuation in
            authState?.performAction { token, _, error in
                if let error = error {
                    continuation.resume(throwing: error)
                    return
                }
                guard let token = token else {
                    continuation.resume(throwing: NSError(domain: "AuthError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无可用访问令牌"]))
                    return
                }
                continuation.resume(returning: token)
            }
        }

        // 构建带令牌的请求
        var request = URLRequest(url: url)
        request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")

        // 发起请求并处理响应
        let (data, response) = try await URLSession.shared.data(for: request)
        
        // 可选:校验HTTP状态码
        guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
            throw NSError(domain: "APIError", code: -1, userInfo: [NSLocalizedDescriptionKey: "响应状态无效"])
        }

        // 解码返回数据
        return try JSONDecoder().decode(T.self, from: data)
    }

    // 调用示例:获取用户数据
    func getUserData() async {
        do {
            let userEndpoint = URL(string: "https://example.com/my-api/user/1")!
            let user = try await fetch(url: userEndpoint)
            DispatchQueue.main.async {
                self.patron = user
            }
        } catch {
            print("获取用户数据失败:\(error.localizedDescription)")
        }
    }
}

之后其他API请求可以直接调用fetch<T>方法,无需重复编写令牌处理逻辑。

解决方案2:拆分API服务层,AuthStore仅负责令牌管理

如果想彻底拆分请求逻辑到不同文件,可以创建独立的API服务类,让AuthStore只提供令牌获取能力:

首先给AuthStore添加获取令牌的方法:

class AuthStore: ObservableObject {
    private var authState: OIDAuthState?
    @Published var isAuthenticated = false

    // 获取有效访问令牌的异步方法
    func getValidAccessToken() async throws -> String {
        try await withCheckedThrowingContinuation { continuation in
            authState?.performAction { token, _, error in
                if let error = error {
                    continuation.resume(throwing: error)
                    return
                }
                guard let token = token else {
                    continuation.resume(throwing: NSError(domain: "AuthError", code: -1, userInfo: [NSLocalizedDescriptionKey: "无可用访问令牌"]))
                    return
                }
                continuation.resume(returning: token)
            }
        }
    }
}

然后创建单独的UserAPIService.swift文件:

class UserAPIService {
    private let authStore: AuthStore

    init(authStore: AuthStore) {
        self.authStore = authStore
    }

    func fetchUser(userId: Int) async throws -> User {
        // 获取令牌
        let token = try await authStore.getValidAccessToken()
        let url = URL(string: "https://example.com/my-api/user/\(userId)")!
        
        // 构建请求
        var request = URLRequest(url: url)
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")

        // 发起请求
        let (data, response) = try await URLSession.shared.data(for: request)
        
        // 校验状态码
        guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else {
            throw NSError(domain: "APIError", code: -1, userInfo: [NSLocalizedDescriptionKey: "响应无效"])
        }

        // 解码数据
        return try JSONDecoder().decode(User.self, from: data)
    }
}

同理可以创建PostAPIService、CommentAPIService等,每个服务负责对应模块的API请求,结构更清晰,便于维护和测试。

解决方案3:封装URLSession扩展(进阶)

可以把令牌注入逻辑封装到URLSession扩展中,让所有请求自动带上令牌:

extension URLSession {
    func data(for request: URLRequest, authStore: AuthStore) async throws -> (Data, URLResponse) {
        let token = try await authStore.getValidAccessToken()
        var authenticatedRequest = request
        authenticatedRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        return try await data(for: authenticatedRequest)
    }
}

然后在API服务中直接使用该扩展方法:

class UserAPIService {
    private let authStore: AuthStore
    private let session: URLSession

    init(authStore: AuthStore, session: URLSession = .shared) {
        self.authStore = authStore
        self.session = session
    }

    func fetchUser(userId: Int) async throws -> User {
        let url = URL(string: "https://example.com/my-api/user/\(userId)")!
        let request = URLRequest(url: url)
        
        let (data, response) = try await session.data(for: request, authStore: authStore)
        
        // 状态码校验与解码...
        return try JSONDecoder().decode(User.self, from: data)
    }
}

总结建议

  • 若只想简化现有代码,方案1的泛型异步方法最直接,改动小且复用性强。
  • 若想做清晰的分层架构,方案2的API服务层拆分更适合,每个模块的请求逻辑独立,便于后期维护。
  • 方案3适合大型项目,进一步解耦请求与令牌逻辑,让代码更简洁。

另外注意原代码的两个问题:更新@Published属性时必须切换到主线程;异步请求不能直接返回泛型类型,必须用闭包或Async/Await处理异步流。

内容的提问来源于stack exchange,提问作者Pieter

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 14:04:53