Created
April 18, 2017 04:02
-
-
Save chriswebb09/dff9d5bd0964d817ac22d75d092733ba to your computer and use it in GitHub Desktop.
Monitoring Downloads
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
typealias JSON = [String: Any] | |
final class iTunesAPIClient: NSObject { | |
var activeDownloads: [String: Download]? | |
weak var defaultSession: URLSession? = URLSession(configuration: .default) | |
// MARK: - Main session used | |
weak var downloadsSession : URLSession? { | |
get { | |
let config = URLSessionConfiguration.background(withIdentifier: "background") | |
weak var queue = OperationQueue() | |
return URLSession(configuration: config, delegate: self, delegateQueue: queue) | |
} | |
} | |
func setup() { | |
activeDownloads = [String: Download]() | |
} | |
// MARK: - Main search functionality | |
static func search(for query: String, completion: @escaping (_ responseObject: JSON?, _ error: Error?) -> Void) { | |
if let encodedQuery = query.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), | |
let url = URL(string:"https://itunes.apple.com/search?media=music&entity=song&term=\(encodedQuery)") { | |
self.downloadData(url: url) { data, response, error in | |
if let error = error { | |
completion(nil, error) | |
} else { | |
if let data = data, | |
let responseObject = self.convertToJSON(with: data) { | |
DispatchQueue.main.async { | |
completion(responseObject, nil) | |
} | |
} else { | |
completion(nil, NSError.generalParsingError(domain: url.absoluteString)) | |
} | |
} | |
} | |
} | |
} | |
// MARK: - Transitory session | |
internal static func downloadData(url: URL, completion: @escaping (_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Void) { | |
URLSession(configuration: .ephemeral).dataTask(with: URLRequest(url: url)) { data, response, error in | |
completion(data, response, error) | |
}.resume() | |
} | |
// MARK: - Turns data into JSON - JSON is typealias | |
fileprivate static func convertToJSON(with data: Data) -> JSON? { | |
do { | |
return try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? JSON | |
} catch { | |
return nil | |
} | |
} | |
} | |
// MARK: - URLSessionDownloadDelegate | |
extension iTunesAPIClient: URLSessionDownloadDelegate { | |
func downloadTrackPreview(for download: Download?) { | |
if let download = download, | |
let urlString = download.url, | |
let url = URL(string: urlString) { | |
activeDownloads?[urlString] = download | |
download.downloadTask = downloadsSession?.downloadTask(with: url) | |
download.downloadTask?.resume() | |
} | |
} | |
func startDownload(_ download: Download?) { | |
if let download = download, let url = download.url { | |
activeDownloads?[url] = download | |
if let url = download.url { | |
if URL(string: url) != nil { | |
downloadTrackPreview(for: download) | |
} | |
} | |
} | |
} | |
// MARK: - Keeps track of download index - for collectionView | |
func trackIndexForDownloadTask(_ tracks: [iTrack], _ downloadTask: URLSessionDownloadTask) -> Int? { | |
if let url = downloadTask.originalRequest?.url?.absoluteString { | |
for (index, track) in tracks.enumerated() { | |
if url == track.previewUrl { | |
return index | |
} | |
} | |
} | |
return nil | |
} | |
} | |
// MARK: - URLSessionDelegate | |
extension iTunesAPIClient: URLSessionDelegate { | |
internal func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { | |
if let appDelegate = UIApplication.shared.delegate as? AppDelegate, | |
let completionHandler = appDelegate.backgroundSessionCompletionHandler { | |
appDelegate.backgroundSessionCompletionHandler = nil | |
DispatchQueue.main.async { | |
completionHandler() | |
} | |
} | |
} | |
internal func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64,totalBytesExpectedToWrite: Int64) { | |
if let downloadUrl = downloadTask.originalRequest?.url?.absoluteString, | |
let download = activeDownloads?[downloadUrl] { | |
download.progress = Float(totalBytesWritten)/Float(totalBytesExpectedToWrite) | |
} | |
} | |
} | |
extension iTunesAPIClient { | |
internal func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { | |
if let originalURL = downloadTask.originalRequest?.url?.absoluteString { | |
let destinationURL = LocalStorageManager.localFilePathForUrl(originalURL) | |
let fileManager = FileManager.default | |
do { | |
if let destinationURL = destinationURL { | |
try fileManager.copyItem(at: location, to: destinationURL) | |
} | |
} catch let error { | |
print("Could not copy file to disk: \(error.localizedDescription)") | |
} | |
} | |
} | |
func URLSessionDidFinishEventsForBackgroundURLSession(session: URLSession) { | |
print("Session: \(session)") | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
@UIApplicationMain | |
class AppDelegate: UIResponder, UIApplicationDelegate { | |
var window: UIWindow? | |
var backgroundSessionCompletionHandler: (() -> Void)? | |
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { | |
return true | |
} | |
func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void) { | |
backgroundSessionCompletionHandler = completionHandler | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
final class DataParser { | |
static func parseDataForTrack(json: JSON) -> iTrack?{ | |
if let track = iTrack(json: json) { | |
return track | |
} | |
return nil | |
} | |
static func parseDataForTracks(json: JSON) -> [iTrack]? { | |
var tracks = [iTrack]() | |
if let tracksJSON = json["results"] as? [[String : Any]] { | |
for trackJSON in tracksJSON { | |
if let track = parseDataForTrack(json: trackJSON) { | |
tracks.append(track) | |
} | |
} | |
} | |
return tracks | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
final class iTrackDataStore { | |
fileprivate weak var client: iTunesAPIClient? = iTunesAPIClient() | |
fileprivate var searchTerm: String? | |
init(searchTerm: String?) { | |
self.searchTerm = searchTerm | |
client?.setup() | |
} | |
func setSearch(string: String?) { | |
self.searchTerm = string | |
} | |
func downloadTrackPreview(for download: Download?) { | |
guard let download = download else { return } | |
if let client = client { | |
client.downloadTrackPreview(for: download) | |
} | |
} | |
func searchForTracks(completion: @escaping (_ downloads: [iTrack]?, _ error: Error?) -> Void) { | |
if let searchTerm = searchTerm { | |
iTunesAPIClient.search(for: searchTerm) { data, error in | |
if let error = error { | |
completion(nil, error) | |
} else if let data = data { | |
if let parsedData = DataParser.parseDataForTracks(json: data) { | |
completion(parsedData, nil) | |
} else { | |
completion(nil, NSError.generalParsingError(domain: "")) | |
} | |
} | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
protocol DownloadDelegate: class { | |
func downloadProgressUpdated(for progress: Float) | |
} | |
enum DownloadStatus { | |
case waiting, downloading, finished, cancelled | |
} | |
final class Download { | |
weak var delegate: DownloadDelegate? | |
var url: String? | |
var downloadTask: URLSessionDownloadTask? | |
var progress: Float = 0.0 { | |
didSet { | |
updateProgress() | |
} | |
} | |
// Gives float for download progress - for delegate | |
private func updateProgress() { | |
delegate?.downloadProgressUpdated(for: progress) | |
} | |
init(url: String) { | |
self.url = url | |
} | |
} | |
extension Download { | |
func getDownloadURL() -> URL? { | |
if let url = self.url { | |
return URL(string: url) | |
} | |
return nil | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
class ExampleViewController: UIViewController { | |
let client = iTunesAPIClient() | |
var store = iTrackDataStore(searchTerm: nil) | |
var tracks: [iTrack]? = nil | |
var downloads: [String: Download]? = nil | |
@IBOutlet weak var progressView: UIProgressView! { | |
didSet { | |
progressView.progress = 0 | |
} | |
} | |
@IBOutlet weak var downloadProgressLabel: UILabel! | |
@IBOutlet weak var downloadButton: UIButton! { | |
didSet { | |
downloadButton.layer.borderWidth = 1 | |
downloadButton.layer.borderColor = UIColor(red:0.00, green:0.48, blue:1.00, alpha:1.0).cgColor | |
downloadButton.layer.cornerRadius = 6 | |
} | |
} | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
client.setup() | |
store.setSearch(string: "blink") | |
store.searchForTracks { tracks, error in | |
self.tracks = tracks | |
guard let tracks = tracks else { return } | |
DispatchQueue.main.async { | |
self.downloadProgressLabel.text = tracks[0].previewUrl | |
} | |
} | |
} | |
@IBAction func downloadTapped(_ sender: Any) { | |
guard let tracks = tracks else { return } | |
let download = Download(url: tracks[0].previewUrl) | |
download.delegate = self | |
self.client.downloadTrackPreview(for: download) | |
} | |
} | |
extension ExampleViewController: DownloadDelegate { | |
func downloadProgressUpdated(for progress: Float) { | |
DispatchQueue.main.async { | |
self.progressView.progress += progress | |
self.downloadProgressLabel.text = String(format: "%.1f%%", progress * 100) | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
protocol iTrackDelegate: class { | |
func downloadIsComplete(downloaded: Bool) | |
} | |
enum Thumbs { | |
case up, down, none | |
} | |
struct iTrack { | |
weak var delegate: iTrackDelegate? | |
let trackName: String | |
let artistName: String | |
let artistId: Int | |
let previewUrl: String | |
let artworkUrl: String | |
let collectionName: String | |
var thumbs: Thumbs | |
var downloaded: Bool { | |
didSet { | |
delegate?.downloadIsComplete(downloaded: downloaded) | |
} | |
} | |
init?(json: [String : Any]) { | |
if let trackName = json["trackName"] as? String, | |
let artistName = json["artistName"] as? String, | |
let artistId = json["artistId"] as? Int, | |
let previewUrl = json["previewUrl"] as? String, | |
let artworkUrl = json["artworkUrl100"] as? String, | |
let collectionName = json["collectionName"] as? String { | |
self.trackName = trackName | |
self.artistName = artistName | |
self.artistId = artistId | |
self.previewUrl = previewUrl | |
self.artworkUrl = artworkUrl | |
self.collectionName = collectionName | |
self.downloaded = false | |
self.thumbs = .none | |
} else { | |
return nil | |
} | |
} | |
} | |
// MARK: - Hashable | |
extension iTrack: Hashable { | |
var hashValue: Int { | |
return trackName.hashValue | |
} | |
static func ==(lhs: iTrack, rhs: iTrack) -> Bool { | |
return lhs.previewUrl == rhs.previewUrl | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
final class LocalStorageManager { | |
static func localFilePathForUrl(_ previewUrl: String) -> URL? { | |
let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString | |
if let url = URL(string: previewUrl) { | |
let fullPath = documentsPath.appendingPathComponent(url.lastPathComponent) | |
return URL(fileURLWithPath: fullPath) | |
} | |
return nil | |
} | |
static func localFileExistsForImage(_ track: iTrack) -> Bool { | |
if let localUrl = LocalStorageManager.localFilePathForUrl(track.previewUrl) { | |
var isDir : ObjCBool = false | |
return FileManager.default.fileExists(atPath: localUrl.path , isDirectory: &isDir) | |
} | |
return false | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
extension NSError { | |
static func generalParsingError(domain: String) -> Error { | |
return NSError(domain: domain, code: 400, userInfo: [NSLocalizedDescriptionKey : NSLocalizedString("Error retrieving data", comment: "General Parsing Error Description")]) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment