Skip to content

Instantly share code, notes, and snippets.

@chriswebb09
Created April 18, 2017 04:02
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save chriswebb09/dff9d5bd0964d817ac22d75d092733ba to your computer and use it in GitHub Desktop.
Save chriswebb09/dff9d5bd0964d817ac22d75d092733ba to your computer and use it in GitHub Desktop.
Monitoring Downloads
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)")
}
}
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
}
}
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
}
}
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: ""))
}
}
}
}
}
}
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
}
}
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)
}
}
}
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
}
}
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
}
}
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