iOS 14 在 swift ui 应用程序中下载背景 mp3

Background mp3 download on iOS 14 in a swift ui app

我正在创建一个 swift UI 广播流应用程序,它有一个可以下载的过去剧集的库。我希望用户能够开始下载然后锁定屏幕。目前,这会暂停正在进行的下载。我的下载功能:

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)
    }

其中 .downloadTaskPublisher(for: url) 是:

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()
    }
}

此下载功能会将剧集写入磁盘,但会在应用暂停时被取消。

我想写点像

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)
    }

但是,这会引发运行时异常: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

我想使用 combine idioms 在后台下载文件,避免必须将 AppDelegate 附加到我的 @main struct: App

据我了解编译器抛出的错误,我很确定您需要像这样激活后台模式:

我确实找到了 raywenderlich.com 的详细教程,它几乎完全符合您的用例。也许它可以帮助您更深入地挖掘此功能