-
URL Session Tutorial: HalfTunesiOS/iOS 2020. 5. 31. 10:04
https://www.raywenderlich.com/3244963-urlsession-tutorial-getting-started 를 공부한 내용입니다.
안녕하세요 😁
오늘은 URLSession의 DownloadTask를 활용해서 iTunes 비슷한 HalfTunes를 만들어 볼 것입니다.초기 프로젝트를 받으면 위와 같이 SearchBar만 있는 모습을 볼 수 있는데요!
검색도 안 되는 상태랍니다. 그래서 검색 기능부터 차근차근해볼게요 :)검색 기능
우선, 뷰 컨트롤러의 검색창에서 검색어를 입력하면 URL 통신을 통해 검색된 리스트들을 받아와야 하기 때문에
QueryService.swift에서 URLSession과 URLSessionDataTask를 만들어줘야 합니다.// URLSession을 생성하고 기본 세션 구성을 default로 초기화 let defaultSession = URLSession(configuration: .default) // 새로운 검색 문자열이 들어올 때마다 다시 초기화될 것 var dataTask: URLSessionDataTask?
위와 같이 설정해준 후에 getSearchResult(searchTerm:completion:) 함수 안에서 네트워크 연결을 해주겠습니다.
// MARK:- 검색 결과 받아오는 함수 func getSearchResults(searchTerm: String, completion: @escaping QueryResult) { // 진행되고 있는 datatask가 있다면 취소함 dataTask?.cancel() // query url안에 유저의 검색 문자열을 포함하기 위해 URLComponents를 만든 후 쿼리 스트링을 설정한다. if var urlComponents = URLComponents(string: "https://itunes.apple.com/search"){ urlComponents.query = "media=music&entity=song&term=\(searchTerm)" guard let url = urlComponents.url else { return } dataTask = defaultSession.dataTask(with: url) { data, response, error in defer { self.dataTask = nil } if let error = error { self.errorMessage += "DataTask error: " + error.localizedDescription + "\n" } else if let data = data, let response = response as? HTTPURLResponse, response.statusCode == 200 { self.updateSearchResults(data) // completion handler에 tracks를 전달하기 위해 main queue로 전환 DispatchQueue.main.async { completion(self.tracks, self.errorMessage) } } } dataTask?.resume() } }
그 후에 searchBarSearchButtonClicked(_:) 함수를 보면 네트워크 표시기를 숨긴 후 searchResult의 results를 저장한 후 테이블 뷰를 업데이트하는 것을 볼 수 있다.
// 네트워크 프로세스가 실행중임을 알리기 위해 상태 표시창에 네트워크 // 표시기를 활성화한다. func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { dismissKeyboard() guard let searchText = searchBar.text, !searchText.isEmpty else { return } UIApplication.shared.isNetworkActivityIndicatorVisible = true queryService.getSearchResults(searchTerm: searchText) { results, errorMessage in // NetworkActivityIndicator를 숨긴다 UIApplication.shared.isNetworkActivityIndicatorVisible = false // 테이블뷰 업데이트 if let results = results { self.searchResults = results self.tableView.reloadData() self.tableView.setContentOffset(CGPoint.zero, animated: false) } if !errorMessage.isEmpty { print("Search error: " + errorMessage) } } }
이렇게 설정한 후 검색을 해주면 아래와 같이 검색 기능이 잘 동작하는 것을 볼 수 있다.
하지만 다운로드 버튼을 누르면 아무런 동작도 하지 않는다.
그래서 다음으로는 다운로드를 구현해줄 것이다.노래 다운로드 기능
Download Class
우선 다운로드받을 Model을 설정해주어야 한다.
class Download { var track: Track init(track: Track) { self.track = track } var task: URLSessionDownloadTask? var isDownloading = false // 사용자가 다운로드를 중지했을 때 여태까지 생성된 Data를 저장한다. (다운로드 재개를 위해) var resumeData: Data? // 0.0 ~ 1.0 사이의 다운로드 진행도를 보여주는 변수 var progress: Float = 0 }
설정한 후 DownloadService.swift에 activeDownloads 딕셔너리를 설정해주는데, 이 딕셔너리는 URL과 Download를 연결해주는 딕셔너리이다.
// URL과 진행중인 Download를 매핑 var activeDownloads: [URL: Download] = [:]
URLSessionDelegate
다운로드의 진행상태를 업데이트하고 다운로드를 cancel, pause, resume 하기 위해 delegate 설정을 해야 한다.
URLSessionDownloadDeleagte를 설정해줄 것인데, URLSessionDownloadDeleagte는 download task에 대한 이벤트 처리를 한다.// 다운로드가 끝났을 때 (required method) func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { print("Finished downloading to \(location).") }
Downloading Track
download task를 다루는 session을 뷰 컨트롤러에 생성해준다.
// 다운로드 세션을 만들고 딜리게이트 처리까지 해준다. lazy var downloadsSession: URLSession = { let configuration = URLSessionConfiguration.background(withIdentifier: "bgSessionConfiguration") return URLSession(configuration: configuration, delegate: self, delegateQueue: nil) }()
task의 진행상황을 모니터링하는 downloadsSession 변수이다.
큐는 nil로 설정하면 serial operation queue를 만들어서 completion handler와 delegate 메소드를 모두 수행한다.뷰 컨트롤러의 viewDidLoad()에서 다운로드 서비스 세션을 지정해준다.
// 다운로드 세션을 위에 만든 lazy 변수로 설정 downloadService.downloadsSession = downloadsSession
설정된 Delegate와 session으로 사용자가 트랙의 다운로드를 원할 때 download task를 만든다.
그 후에 DownloadService.swift에서 startDownload(_:) 함수를 설정해준다.
다운로드가 시작될 때 실행될 함수이다.// MARK:- Download Start func startDownload(_ track: Track) { // 다운로드 객체 생성 let download = Download(track: track) // downloadTask 생성 download.task = downloadsSession.downloadTask(with: track.previewURL) download.task!.resume() // 다운로드 진행상황을 true로 설정 download.isDownloading = true // activeDownloads 설정 activeDownloads[download.track.previewURL] = download }
이렇게 한 후 앱을 빌드하게 된다면 출력문으로 다운로드된 파일의 경로가 나온다.
Saving Track
위에까지 진행하게 되면 download task가 끝났을 때, urlSession(_:downloadTask:didFinishDownloadingTo:) 함수는 URL을 임시 파일 저장 영역에 저장한다.
이것을 영구적인 영역에 옮기는 작업을 할 것이다.위에서 설정해줬던 urlSession(_:downloadTask:didFinishDownloadingTo:) 함수를 변경할 것이다.
// 다운로드가 끝났을 때 (required method) func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { // downloadTask의 원본 요청 url을 가져온다 guard let sourceURL = downloadTask.originalRequest?.url else { return } // downloadService의 해당 url의 activeDownloads를 가져와서 지워준다. let download = downloadService.activeDownloads[sourceURL] downloadService.activeDownloads[sourceURL] = nil // 로컬 파일로 저장하기 위해 destinationURL을 설정해준다. let destinationURL = localFilePath(for: sourceURL) print(destinationURL) // fileManager로 다운로드된 파일 위치를 적절한 위치로 옮겨준다. let fileManager = FileManager.default try? fileManager.removeItem(at: destinationURL) do { try fileManager.copyItem(at: location, to: destinationURL) download?.track.downloaded = true } catch let error { print("Could not copy file to disk: \(error.localizedDescription)") } // 다운로드 트랙과 일치하는 cell를 reload 해준다. if let index = download?.track.index { DispatchQueue.main.async { self.tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .none) } } }
이제 프로젝트를 빌드하고 트랙 다운로드를 완료했을 때 파일의 위치를 볼 수 있다.
또한 트랙을 탭하면 AVPlayerViewController를 통해 음악을 들을 수 있다.Canceling Downloads
다운로드를 취소하는 기능을 구현할 것이다.
다운로드를 취소하기 위해서 activeDownloads 딕셔너리레서 일치하는 Download를 가져온 후 cancel 작업을 실행한다.// MARK:- Download Cancle func cancelDownload(_ track: Track) { // activeDownloads 딕셔너리에서 해당 download를 가져와서 cancle if let download = activeDownloads[track.previewURL] { download.task?.cancel() activeDownloads[track.previewURL] = nil } }
Pausing Downloads
다운로드를 일시정지하는 기능을 구현할 것이다.
다운로드를 일시정지하는 것은 취소하는 것과 비슷하지만, cancel 작업을 수행할 때 resume data를 넘겨준다.
// MARK:- Download Pause func pauseDownload(_ track: Track) { // activeDownloads 딕셔너리에서 해당 download를 가져옴 guard let download = activeDownloads[track.previewURL] else { return } // download를 cancle해주는 데 파라미터로 resume data를 저장하도록 설정 if download.isDownloading { download.task?.cancel(byProducingResumeData: { data in download.resumeData = data }) // isDownloading 변수를 false로 바꿈 download.isDownloading = false } }
Resuming Downloads
다운로드를 재개하는 기능을 구현할 것이다.
사용자가 다운로드를 재개했을 때 Download resumeData의 존재 여부를 검사하고 다운로드를 재개한다.// MARK:- Download Resume func resumeDownload(_ track: Track) { // activeDownloads 딕셔너리에서 해당 download를 가져옴 guard let download = activeDownloads[track.previewURL] else { return } // resumeData가 있다면 resumeData를 이용해 다운로드 if let resumeData = download.resumeData { download.task = downloadsSession.downloadTask(withResumeData: resumeData) } else { // 없다면 다시 생성 후 다운로드 download.task = downloadsSession.downloadTask(with: download.track.previewURL) } download.task!.resume() // isDownloading 변수를 다시 true로 바꿔줌 download.isDownloading = true }
최종 결과 화면
이 기능들을 모두 구현하면 아래와 같은 앱을 만들 수 있다.
'iOS > iOS' 카테고리의 다른 글
[iOS] Cell LifeCycle (UITableView, UICollectionView) (1) 2020.08.28 iOS의 데이터베이스 비교 (SQLite, Core Data, Realm) (0) 2020.08.07 CoreLocation 적용하기 (0) 2020.06.20 UIGraphics를 사용한 간단한 스케치 어플 (0) 2020.05.16 델리게이트 패턴 실습 (0) 2020.03.28