Swift中如何为API Manager的服务器登录创建正确的Completion Handler?
解决Swift API Manager登录方法中Completion Handler提前执行的问题
我来帮你搞定这个问题!你遇到的核心问题是把completion handler的调用时机搞错了——大概率是把它放在了网络请求的同步代码块里,导致还没等服务器返回响应就触发了。下面给你一步步展示正确的实现方式:
1. 重构你的API Manager登录方法
首先,我们要确保completion handler只在服务器返回响应后才被调用,也就是把它放在URLSession的异步回调内部。这里我用Result类型来统一处理成功/失败的情况,让逻辑更清晰:
import Foundation // 先定义一个登录响应的模型(根据你的实际接口返回结构调整) struct LoginResponse: Codable { let token: String let userId: Int // 其他你需要的字段 } class APIManager { static let shared = APIManager() // 单例模式,方便全局调用 func login(username: String, password: String, completion: @escaping (Result<LoginResponse, Error>) -> Void) { guard let url = URL(string: "https://your-server-url.com/login") else { completion(.failure(NSError(domain: "APIManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的登录URL"]))) return } // 构建请求体 let parameters: [String: Any] = [ "username": username, "password": password ] var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.httpBody = try? JSONSerialization.data(withJSONObject: parameters) // 发起网络请求 let task = URLSession.shared.dataTask(with: request) { data, response, error in // 先处理网络层面的错误 if let error = error { completion(.failure(error)) return } // 验证HTTP响应状态码 guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1 let error = NSError(domain: "APIManager", code: statusCode, userInfo: [NSLocalizedDescriptionKey: "登录失败,状态码:\(statusCode)"]) completion(.failure(error)) return } // 解析响应数据 guard let data = data else { completion(.failure(NSError(domain: "APIManager", code: -2, userInfo: [NSLocalizedDescriptionKey: "服务器返回空数据"]))) return } do { let loginResponse = try JSONDecoder().decode(LoginResponse.self, from: data) completion(.success(loginResponse)) } catch { completion(.failure(error)) } } task.resume() // 注意:这里绝对不能调用completion!否则会提前执行 } }
2. 在ViewController中调用并处理Segue
在你的登录ViewController里,调用这个方法时,要记得切换到主线程更新UI(因为网络请求的回调是在后台线程),然后在成功的分支里执行segue:
import UIKit class LoginViewController: UIViewController { @IBOutlet weak var usernameTextField: UITextField! @IBOutlet weak var passwordTextField: UITextField! @IBAction func loginButtonTapped(_ sender: UIButton) { guard let username = usernameTextField.text, !username.isEmpty, let password = passwordTextField.text, !password.isEmpty else { // 提示用户输入账号密码 let alert = UIAlertController(title: "提示", message: "请输入完整的账号和密码", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "确定", style: .default)) present(alert, animated: true) return } // 调用API登录 APIManager.shared.login(username: username, password: password) { [weak self] result in DispatchQueue.main.async { // 必须切回主线程更新UI switch result { case .success(let response): // 登录成功,保存token等信息(比如存在UserDefaults或者Keychain) UserDefaults.standard.set(response.token, forKey: "authToken") // 执行segue跳转到主界面 self?.performSegue(withIdentifier: "LoginToMain", sender: self) case .failure(let error): // 登录失败,提示用户 let alert = UIAlertController(title: "登录失败", message: error.localizedDescription, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "重试", style: .default)) self?.present(alert, animated: true) } } } } }
为什么你的原来写法会提前执行?
大概率是你把completion()的调用放在了task.resume()之后的同步代码里,比如这样:
// ❌ 错误示例:completion在网络请求发起后立刻执行,根本没等响应 func login(username: String, password: String, completion: @escaping () -> Void) { // ...构建请求... let task = URLSession.shared.dataTask(with: request) { data, response, error in // ...处理响应,但没在这里调用completion... } task.resume() completion() // 这里会立刻执行! }
正确的做法是必须把completion的调用放在dataTask的闭包内部,确保只有服务器返回响应、我们处理完状态码和数据之后,才触发completion。
内容的提问来源于stack exchange,提问作者Pouya Kahfi
相关产品推荐
相关产品推荐

