URL 应用在后台时请求超时

URL request times out when app is in the background

我有一个可以调用 API 请求的应用程序。一些用户遇到了一个错误,当他们在获取数据时关闭应用程序并稍后打开它时,该应用程序会抛出超时错误。

我正在使用标准的 URLSession 数据任务,如下例所示:

var session = URLSession(configuration: .ephemeral, delegate: self, delegateQueue: queue)

private func loadModels -> AnyPublisher<[Model], LoadModelsUseCaseError> {
    guard let keyID = keyAdapter.getKeyID() else {
        return Fail<[Model], LoadModelsUseCaseError>(error: .keyIDNotFound).eraseToAnyPublisher()
    }

    let url = Environment.loadModelsURL(for: keyID)

    return apiAdapter.session
        .dataTaskPublisher(for: url)
        .decode(type: [Model].self, decoder: decoder)
        .mapError(LoadModelsUseCaseError.init)
        .eraseToAnyPublisher()
}

一种解决方法是在我调用该方法的视图模型中调用 .retry(1),但该解决方案有明显的缺陷。

另一种解决方法是捕获超时错误并再次调用加载方法。这也不完美,因为请求永远不会超时(即使在相关情况下)。

有什么处理这种情况的建议吗?非常感谢

好的,所以我已经解决了将以下代码放入我的 APIAdapter / APIManager 组件中的问题:

// MARK: - Configuration

private func configureNewSession() {
    session?.invalidateAndCancel()
    backgroundSession?.invalidateAndCancel()

    let configuration = URLSessionConfiguration.default
    configuration.isDiscretionary = true
    configuration.sessionSendsLaunchEvents = true
    session = URLSession(configuration: configuration, delegate: self, delegateQueue: queue)

    let backgroundSessionConfiguration = URLSessionConfiguration.background(withIdentifier: "background")
    backgroundSessionConfiguration.isDiscretionary = true
    backgroundSessionConfiguration.sessionSendsLaunchEvents = true
    backgroundSession = URLSession(configuration: backgroundSessionConfiguration, delegate: self, delegateQueue: queue)
}

private func subscribeToApplicationStateNotifications() {
    NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)
        .sink { _ in
            self.moveTasksToForeground()
        }
        .store(in: &subscriptions)

    NotificationCenter.default.publisher(for: UIApplication.willResignActiveNotification)
        .sink { _ in
            self.moveTasksToBackground()
        }
        .store(in: &subscriptions)
}

// MARK: - App Lifecycle

/// The method currently doesn't move tasks in the background (as only download / upload tasks can be resumed),
/// but cancels them. Cancelled tasks doesn't produce errors, so they doesn't need to be catched in the View Models.
public func moveTasksToBackground() {
    guard case .foreground = state else {
        return
    }

    // Arguments in completion handlers are: data tasks, download tasks and upload tasks respectively.
    session.getTasksWithCompletionHandler { dataTasks, _, _ in
        for dataTask in dataTasks {
            dataTask.suspend()
            // NOTE: - Download tasks can produce resume data that can be resumed by standard url session in rhe
            // foreground.
            //
            // Example:
            //
            // guard let downloadTask = downloadTask as? URLSessionDownloadTask else {
            //     continue
            // }
            // downloadTask.cancel(byProducingResumeData: { [self] resumeData in
            //     var downloadTask: URLSessionDownloadTask? = nil
            //     if let resumeData = resumeData {
            //         downloadTask = backgroundSession.downloadTask(withResumeData: resumeData)
            //     }
            //     downloadTask?.resume()
            // })
        }
    }
    state = .background
}

/// The method currently doesn't move tasks in the background (as only download / upload tasks can be resumed),
/// but cancels them. Cancelled tasks doesn't produce errors, so they doesn't need to be catched in the View Models.
public func moveTasksToForeground() {
    guard case .background = state else {
        return
    }

    // Arguments in completion handlers are: data tasks, download tasks and upload tasks respectively.
    backgroundSession.getTasksWithCompletionHandler { dataTasks, _, _ in
        for dataTask in dataTasks {
            dataTask.suspend()
            // NOTE: - Download tasks can produce resume data that can be resumed by standard url session in rhe
            // foreground.
            //
            // Example:
            //
            // guard let downloadTask = downloadTask as? URLSessionDownloadTask else {
            //     continue
            // }
            // downloadTask.cancel(byProducingResumeData: { [self] resumeData in
            //     var downloadTask: URLSessionDownloadTask? = nil
            //     if let resumeData = resumeData {
            //         downloadTask = urlSession.downloadTask(withResumeData: resumeData)
            //     }
            //     downloadTask?.resume()
            // })
        }
    }
    state = .foreground
}

当您暂停数据任务时,会话不会产生错误,因此无需在视图模型/视图/用例/服务/您调用 API 调用的任何地方过滤取消.您所要做的就是在用户打开应用程序/进入屏幕时刷新远程数据。