SwiftUI 使用URLSession的URLSessionDownloadTask下載和播放音樂
xcode 13.4.1, swift 5.5, iOS 15.4
2022-09-09
現在絕大部分App都不會獨立的,一定會與後台或其他的服務器互相溝通,最簡單是從服務器獲取資料然後顯示出來。在
SwiftUI 使用URLSession的URLSessionDataTask下載JSON Data使用URLSessionDataTask演示了下載簡單的JSON資料,這次就使用URLSessionDownloadTask來下載一些大的檔案,我們會試試下載itune的試聽音樂檔案並進行播放。
1. 在itune上選一首自己喜歡的preview的歌,我這里選了這首
Light Year的歌:
import SwiftUI
struct SongDownloaderExample: View {
var body: some View {
VStack {
Button {
guard let songUrl = URL(string: "https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview125/v4/0c/94/76/0c9476bc-7ef0-c690-de28-c441a35c11e5/mzaf_11460274548565212191.plus.aac.p.m4a") else {
return
}
} label: {
Text("Download")
}
}
}
}
2. 創建一個SongDownloader來接受一個url然後使用URLSessionDataTask來進行下載:
import SwiftUI
class SongDownloader : ObservableObject {
func fetchSongAtUrl(_ item : URL) {
let configuration = URLSessionConfiguration.default
let urlSession = URLSession(configuration: configuration)
let downloadTask = urlSession.downloadTask(with: item)
downloadTask.resume()
}
}
struct SongDownloaderExample: View {
@ObservedObject var songDownloader = SongDownloader()
var body: some View {
VStack {
Button {
guard let songUrl = URL(string: "https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview125/v4/0c/94/76/0c9476bc-7ef0-c690-de28-c441a35c11e5/mzaf_11460274548565212191.plus.aac.p.m4a") else {
return
}
songDownloader.fetchSongAtUrl(songUrl)
} label: {
Text("Download")
}
}
}
}
以上實作按了Download就會執行下載程式,為了讓URLSessionDataTask完成下載後通知我們,需要在創建URLSession時,傳入一個URLSessionDownloadDelegate,而在下載完成後,URLSessionDownloadDelegate的urlSession將會被執行,這里我們就要機會做一些下載完成後的工作:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
3. 在下載完成後,我們把下載的臨時檔案搬到App的sandbox的目錄中,並把保存的檔案位置放到變量songFileLocation中,而這個變量使用了Published,當發生改生時就會通知View進行重畫,就會在Text中顯示其檔案位置:
import SwiftUI
class SongDownloader : NSObject, ObservableObject, URLSessionDownloadDelegate {
var songUrl : URL?
@Published var songFileLocation : URL?
func fetchSongAtUrl(_ songUrl : URL) {
self.songUrl = songUrl
let configuration = URLSessionConfiguration.default
let urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
let downloadTask = urlSession.downloadTask(with: songUrl)
downloadTask.resume()
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
let fileManager = FileManager.default
guard let documentsPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
fatalError()
}
guard let lastPathComponent = self.songUrl?.lastPathComponent else {
fatalError()
}
let songFileLocation = documentsPath.appendingPathComponent(lastPathComponent)
do {
if fileManager.fileExists(atPath: songFileLocation.path) {
try fileManager.removeItem(at: songFileLocation)
}
try fileManager.copyItem(at: location, to: songFileLocation)
DispatchQueue.main.async {
self.songFileLocation = songFileLocation
}
} catch {
print(error)
}
}
}
struct SongDownloaderExample: View {
@ObservedObject var songDownloader = SongDownloader()
var body: some View {
VStack {
Button {
guard let songUrl = URL(string: "https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview125/v4/0c/94/76/0c9476bc-7ef0-c690-de28-c441a35c11e5/mzaf_11460274548565212191.plus.aac.p.m4a") else {
return
}
songDownloader.fetchSongAtUrl(songUrl)
} label: {
Text("Download")
}
if songDownloader.songFileLocation != nil {
Text("\(songDownloader.songFileLocation!)")
}
}
}
}
4. 然後使用AVPlayerViewController來播放音樂,關於如何用UIViewControllerRepresentable來把UIKit的元件封裝成SwiftUI的元件,請參考
SwiftUI 通過UIViewControllerRepresentable protocol來封裝AVPlayerViewController,實作如下:
import SwiftUI
import AVKit
struct AudioPlayerView : UIViewControllerRepresentable {
let url : URL
func makeUIViewController(context: Context) -> AVPlayerViewController {
let player = AVPlayer(url: url)
let viewController = AVPlayerViewController()
viewController.player = player
viewController.entersFullScreenWhenPlaybackBegins = true
player.play()
return viewController
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
}
}
5. 當songFileLocation變化時,通過onChange 來改變playMusic,當playMusic變成true是,就會拉起AudioPlayerView進行播放:
import SwiftUI
class SongDownloader : NSObject, ObservableObject, URLSessionDownloadDelegate {
var songUrl : URL?
@Published var songFileLocation : URL?
func fetchSongAtUrl(_ songUrl : URL) {
self.songUrl = songUrl
let configuration = URLSessionConfiguration.default
let urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
let downloadTask = urlSession.downloadTask(with: songUrl)
downloadTask.resume()
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
let fileManager = FileManager.default
guard let documentsPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
fatalError()
}
guard let lastPathComponent = self.songUrl?.lastPathComponent else {
fatalError()
}
let songFileLocation = documentsPath.appendingPathComponent(lastPathComponent)
do {
if fileManager.fileExists(atPath: songFileLocation.path) {
try fileManager.removeItem(at: songFileLocation)
}
try fileManager.copyItem(at: location, to: songFileLocation)
DispatchQueue.main.async {
self.songFileLocation = songFileLocation
}
} catch {
print(error)
}
}
}
struct SongDownloaderExample: View {
@State private var playMusic = false
@ObservedObject var songDownloader = SongDownloader()
var body: some View {
VStack {
Button {
guard let songUrl = URL(string: "https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview125/v4/0c/94/76/0c9476bc-7ef0-c690-de28-c441a35c11e5/mzaf_11460274548565212191.plus.aac.p.m4a") else {
return
}
songDownloader.fetchSongAtUrl(songUrl)
} label: {
Text("Download")
}
if songDownloader.songFileLocation != nil {
Text("\(songDownloader.songFileLocation!)")
}
}
.onChange(of: songDownloader.songFileLocation) { newValue in
if newValue != nil {
playMusic = true
}
}
.sheet(isPresented: $playMusic) {
AudioPlayerView(url: self.songDownloader.songFileLocation!)
}
}
}
6. 另外URLSessionDownloadDelegate還提供了一個方法,可以跟蹤下載的過程func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64),並使用變量downloadPercent在計算已下載的百份比,並顯示出來,實作如下:
import SwiftUI
class SongDownloader : NSObject, ObservableObject, URLSessionDownloadDelegate {
var songUrl : URL?
@Published var songFileLocation : URL?
@Published var isDownloading = false
@Published var downloadPercent : Float = 0
func fetchSongAtUrl(_ songUrl : URL) {
self.songUrl = songUrl
let configuration = URLSessionConfiguration.default
let urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
let downloadTask = urlSession.downloadTask(with: songUrl)
downloadTask.resume()
self.isDownloading = true
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
let fileManager = FileManager.default
guard let documentsPath = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
fatalError()
}
guard let lastPathComponent = self.songUrl?.lastPathComponent else {
fatalError()
}
let songFileLocation = documentsPath.appendingPathComponent(lastPathComponent)
do {
if fileManager.fileExists(atPath: songFileLocation.path) {
try fileManager.removeItem(at: songFileLocation)
}
try fileManager.copyItem(at: location, to: songFileLocation)
DispatchQueue.main.async {
self.songFileLocation = songFileLocation
}
} catch {
print(error)
}
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
if let error = error {
print(error.localizedDescription)
}
self.isDownloading = false
}
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
self.downloadPercent = Float(totalBytesWritten*100)/Float(totalBytesExpectedToWrite)
}
}
struct SongDownloaderExample: View {
@State private var playMusic = false
@ObservedObject var songDownloader = SongDownloader()
var body: some View {
VStack {
Button {
guard let songUrl = URL(string: "https://audio-ssl.itunes.apple.com/itunes-assets/AudioPreview125/v4/0c/94/76/0c9476bc-7ef0-c690-de28-c441a35c11e5/mzaf_11460274548565212191.plus.aac.p.m4a") else {
return
}
songDownloader.fetchSongAtUrl(songUrl)
} label: {
Text("Download")
}
if songDownloader.isDownloading {
Text("\(songDownloader.downloadPercent)% downloaded")
}
if songDownloader.songFileLocation != nil {
Text("\(songDownloader.songFileLocation!)")
}
}
.onChange(of: songDownloader.songFileLocation) { newValue in
if newValue != nil {
playMusic = true
}
}
.sheet(isPresented: $playMusic) {
AudioPlayerView(url: self.songDownloader.songFileLocation!)
}
}
}