ios - Background mp3 download on iOS 14 in a swift ui app
问题描述
I'm creating a swift UI radio streaming app that has a library of past episodes that can be downloaded. I would like users to be able to begin a download and then lock the screen. Currently this suspends the download(s) in progress. My download function :
func downloadFile(withUrl url: URL, andFilePath filePath: URL) {
URLSession.shared
.downloadTaskPublisher(for: url)
.retry(4)
.map(\.0)
.receive(on: RunLoop.main)
.sink(receiveCompletion: { [self] _ in
downloading = false
downloaded = true
},
receiveValue: { data in
do {
self.downloading = true
try FileManager.default.moveItem(atPath: data.path,
toPath: filePath.path)
} catch {
self.downloaded = false
print("Error: \(error.localizedDescription)")
print("an error happened while downloading or saving the file")
}
})
.store(in: &networkSubscription)
}
Where .downloadTaskPublisher(for: url)
is :
import Combine
import Foundation
public extension URLSession {
/// Returns a publisher that wraps a URL session download task for a given
/// URL.
///
/// - Parameter url: The URL for which to create a download task.
/// - Returns: A publisher that wraps a download task for the URL.
func downloadTaskPublisher(for url: URL) -> DownloadTaskPublisher {
DownloadTaskPublisher(session: self, request: URLRequest(url: url))
}
/// Returns a publisher that wraps a URL session download task for a given
/// URL request.
///
/// - Parameter request: The URL request for which to create a download task.
/// - Returns: A publisher that wraps a download task for the URL request.
func downloadTaskPublisher(for request: URLRequest) -> DownloadTaskPublisher {
DownloadTaskPublisher(session: self, request: request)
}
}
public struct DownloadTaskPublisher {
fileprivate let session: URLSession
fileprivate let request: URLRequest
}
extension DownloadTaskPublisher: Publisher {
public typealias Output = (URL, URLResponse)
public typealias Failure = Error
public func receive<Subscriber>(subscriber: Subscriber)
where
Subscriber: Combine.Subscriber,
Subscriber.Failure == Failure,
Subscriber.Input == Output
{
let subscription = Subscription(subscriber: subscriber, session: session, request: request)
subscriber.receive(subscription: subscription)
}
}
private extension DownloadTaskPublisher {
final class Subscription {
private let downloadTask: URLSessionDownloadTask
init<Subscriber>(subscriber: Subscriber, session: URLSession, request: URLRequest)
where
Subscriber: Combine.Subscriber,
Subscriber.Input == Output,
Subscriber.Failure == Failure
{
downloadTask = session.downloadTask(with: request, completionHandler: { url, response, error in
guard let url = url, let response = response else {
subscriber.receive(completion: .failure(error!))
return
}
_ = subscriber.receive((url, response))
subscriber.receive(completion: .finished)
})
}
}
}
extension DownloadTaskPublisher.Subscription: Subscription {
fileprivate func request(_: Subscribers.Demand) {
downloadTask.resume()
}
fileprivate func cancel() {
downloadTask.cancel()
}
}
This download function writes the episode to disk but is canceled when the app is suspended.
I would like to write something like
func downloadFile(withUrl url: URL, andFilePath filePath: URL) {
URLSession.init(configuration: URLSessionConfiguration.background(withIdentifier: "background.download.session"))
.downloadTaskPublisher(for: url)
.retry(4)
.map(\.0)
.receive(on: RunLoop.main)
.sink(receiveCompletion: { [self] _ in
downloading = false
downloaded = true
},
receiveValue: { data in
do {
self.downloading = true
try FileManager.default.moveItem(atPath: data.path,
toPath: filePath.path)
} catch {
self.downloaded = false
print("Error: \(error.localizedDescription)")
print("an error happened while downloading or saving the file")
}
})
.store(in: &networkSubscription)
}
However, this throws a runtime exception: libc++abi.dylib: terminating with uncaught exception of type NSException *** Terminating app due to uncaught exception 'NSGenericException', reason: 'Completion handler blocks are not supported in background sessions. Use a delegate instead.' terminating with uncaught exception of type NSException
I would like to download files in the background using combine idioms and avoid have to attach an AppDelegate
to my @main struct: App
.
解决方案
As I understand the error throws by the compiler, I pretty sure you need to activate the Background Modes like this :
I did found a detailed tutorial by raywenderlich.com that follows almost exactly your use case. Maybe it can help you to dig even deeper on this feature
推荐阅读
- java - 如何将 SQL 查询从 JSP 发送到 servlet?
- arrays - 如何在 Swift 中将字符串数组转换为字符串
- java - 底部导航视图以打开新活动
- c# - 我可以使用 XmlReader.Create() 禁用命名空间支持吗?
- sql - 用于更新的嵌套替换
- c++ - 循环最后一次迭代的指针变化
- android - 我可以通过 adb shell 命令调用活动的完成()吗?
- azure - 从 Azure User Graph 获取唯一用户位置的方法
- node.js - 尽管在 express 中设置了 Access-Control-Allow-Origin,但请求标头字段 Authorization
- node.js - Node.js 没有找到 Axios 正文请求参数