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

RxSwift中服务器关闭时请求超时弹窗重复、页面空白问题求助

RxSwift+Alamofire 服务器关闭时的异常处理问题排查与修复

问题现象

当服务器关闭时,App抛出Urlsessiontask failed with error: the request timed out错误,页面空白;已添加错误弹窗,但需多次点击才能关闭。

代码问题分析

  1. 错误事件重复发送
    • 全局ErrorViewModel使用PublishSubject,若多次触发请求或页面重复订阅,会导致错误事件被多次接收,弹窗重复弹出。
    • LoginViewModelV的doLogin方法无请求防抖,用户重复触发登录会发起多个并行请求,每个请求失败都会发送错误事件。
  2. 强制解包引发潜在崩溃
    • response.response!.statusCode强制解包response.response,请求超时(服务器关闭)时该值为nil,会触发崩溃,导致后续错误处理逻辑失效,页面停留在空白状态。
  3. 请求参数与语法错误
    • 请求URL格式错误(htt://应为http:///https://),导致请求无效。
    • headers参数传入字符串,不符合Alamofire的HTTPHeaders类型要求,编译不通过。
    • LoginDto的CodingKeys枚举和init(from:)方法未闭合,doLogin参数名使用Swift关键字var,均为语法错误。
  4. 页面状态缺失处理
    • 请求失败后仅隐藏进度条、弹出弹窗,未在页面添加错误提示或重新请求入口,弹窗关闭后页面保持空白。

修复方案

1. 修正错误事件发送逻辑,避免重复弹窗

将ErrorViewModel的PublishSubject替换为PublishRelay(RxSwift推荐用于事件发送,避免序列因onError终止),并私有化初始化确保单例唯一:

import RxSwift
import RxCocoa

class ErrorViewModel {
    static let shared = ErrorViewModel()
    let errorRelay = PublishRelay<ErrorResponse>()
    var errorResponse: Observable<ErrorResponse> {
        return errorRelay.asObservable()
    }

    private init() {}
}

发送错误事件时改用accept:

// LoginViewModelV中修改
ErrorViewModel.shared.errorRelay.accept(ex)

2. 添加请求防抖,避免重复请求

在LoginViewModelV中添加请求状态控制,防止重复发起请求:

import RxSwift
import Alamofire

class LoginViewModelV{
    private let loginSubject = PublishSubject<LoginDto>()
    var LoginResponse: Observable<LoginDto> {
        return loginSubject.asObservable()
    }
    private let isRequesting = BehaviorRelay(value: false)

    func doLogin(parameters: [String: Any]) {
        guard !isRequesting.value else { return }
        isRequesting.accept(true)
        
        AF.request("https://myurl/login", method: .post, parameters: parameters,
                   encoding: JSONEncoding.default,
                   headers: HTTPHeaders(["Header-Key": "myheader"]))
            .validate(statusCode: 200..<500)
            .responseDecodable(of: LoginDto.self) { [weak self] response in
                defer { self?.isRequesting.accept(false) }
                
                switch response.result {
                case .success:
                    guard let statusCode = response.response?.statusCode else {
                        let ex = ErrorHandling.exception(statusCode: nil)
                        ErrorViewModel.shared.errorRelay.accept(ex)
                        return
                    }
                    switch statusCode {
                    case 200:
                        if let v_response = response.value {
                              self?.loginSubject.onNext(v_response)
                        }
                    default:
                        let ex = ErrorHandling.exception(statusCode: statusCode)
                        ErrorViewModel.shared.errorRelay.accept(ex)
                    }
                case .failure(let error):
                    let ex = ErrorHandling.exception(error: error, statusCode: response.response?.statusCode)
                    ErrorViewModel.shared.errorRelay.accept(ex)
                }
            }
    }
}

3. 修复语法与强制解包问题

修正LoginDto的语法错误:

struct LoginDto : Decodable {
    let outCode: String
    let outMessage: String

    enum CodingKeys: String, CodingKey {
        case outCode, outMessage
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        outCode = try values.decodeIfPresent(String.self, forKey: .outCode) ?? ""
        outMessage = try values.decodeIfPresent(String.self, forKey: .outMessage) ?? ""
    }
}

修正doLogin参数名,去掉关键字var,同时修正URL和headers格式。

4. 优化页面状态处理,解决空白问题

在LoginViewController中添加错误提示UI,弹窗关闭后页面显示错误信息:

import UIKit
import RxSwift

class LoginViewController: UIViewController {
    var loginViewModel = LoginViewModelV()    
    private let disposeBag = DisposeBag()
    private let errorLabel = UILabel()

    override func viewDidLoad() {
        super.viewDidLoad()
        setupErrorLabel()
        doLogin()
        observeEvents()
    }

    func setupErrorLabel() {
        errorLabel.text = "登录失败,请检查网络或服务器状态"
        errorLabel.textColor = .red
        errorLabel.isHidden = true
        errorLabel.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(errorLabel)
        NSLayoutConstraint.activate([
            errorLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            errorLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        ])
    }

    func observeEvents(){
        self.loginViewModel.LoginResponse
            .subscribe(onNext: { [weak self] _ in
                self?.errorLabel.isHidden = true
                // 处理登录成功逻辑
            })
            .disposed(by: self.disposeBag)

        ErrorViewModel.shared.errorResponse.subscribe(onNext: { [weak self] err in
            Custom_Progress_bar.hide()
            self?.errorLabel.isHidden = false
            Custom_Sweet_alert.show_error_message(messsage: err.message)
        }).disposed(by: self.disposeBag)
    }

    func doLogin(){
        let parameters: [String: Any] = [
            "userId": "myuserId",
            "passowrd": "mypasswor"
        ]
        loginViewModel.doLogin(parameters: parameters)
     }
}

5. 统一超时错误处理

在ErrorHandling中针对超时错误返回明确提示:

struct ErrorResponse {
    let message: String
}

enum ErrorHandling {
    static func exception(error: Error? = nil, statusCode: Int?) -> ErrorResponse {
        if let afError = error as? AFError, 
           let urlError = afError.underlyingError as? URLError, 
           urlError.code == .timedOut {
            return ErrorResponse(message: "服务器连接超时,请稍后重试")
        }
        let message = statusCode != nil ? "请求失败,状态码:\(statusCode!)" : error?.localizedDescription ?? "未知错误"
        return ErrorResponse(message: message)
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 12:55:00