// // AlbumCollection.swift // TagTunes // // Created by Kim Wittenburg on 08.09.15. // Copyright © 2015 Kim Wittenburg. All rights reserved. // import Foundation /// Manages a collection of albums. Managing includes support for deferred /// loading of an album's tracks as well as error support. public class AlbumCollection: CollectionType { // MARK: Types private enum AlbumState { case Normal case Error(NSError) case Loading(NSURLSessionTask) } /// Notifications posted by an album collection. The `userInfo` of these /// notifications contains all keys specified in `Keys`. public struct Notifications { /// Posted when an album is added to a collection. This notification is /// only posted if the album collection actually changed. public static let albumAdded = "AlbumAddedNotificationName" /// Posted when an album is removed from a collection. This notification /// is only posted if the album collection actually changed. public static let albumRemoved = "AlbumRemovedNotificationName" /// Posted when the album collection started a network request for an /// album's tracks. /// /// Note that the values for the keys `Album` and `AlbumIndex` for the /// corresponding `AlbumFinishedLoading` notification may both be /// different. public static let albumStartedLoading = "AlbumStartedLoadingNotificationName" /// Posted when an album collection finished loading the tracks for an /// album. Receiving this notification does not mean that the tracks were /// actually loaded successfully. It just means that the networ /// connection terminated. Use `errorForAlbum` to determine if an error /// occured while the tracks have been loaded. /// /// - note: Since the actual `Album` instance in the album collection may /// change during loading its tracks it is preferred that you use the /// `AlbumIndexKey` of the notification to determine which album finished /// loading its tracks. You can however use the `AlbumKey` as well to /// access the `Album` instance that is currently present in the /// collection at the respective index. public static let albumFinishedLoading = "AlbumFinishedLoadingNotificationName" /// These constants are available as keys in the `userInfo` dictionary /// for any notification an album collection may post. public struct Keys { /// The `Album` instance affected by the notification. public static let album = "AlbumKey" /// The index of the album affected by the notification. public static let albumIndex = "AlbumIndexKey" } } // MARK: Properties /// Access the userlying array of albums. public private(set) var albums = [Album]() private var albumStates = [Album: AlbumState]() /// The URL session used to load tracks for albums. private let urlSession = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: nil, delegateQueue: NSOperationQueue.mainQueue()) // MARK: Collection Type public init() {} public var startIndex: Int { return albums.startIndex } public var endIndex: Int { return albums.endIndex } public subscript (position: Int) -> Album { return albums[position] } // MARK: Album Collection /// Adds the specified album, if not already present, and begins to load its /// tracks. /// /// - parameters: /// - album: The album to be added. /// - flag: Specify `false` if the album collection should not begin to /// load the album's tracks immediately. public func addAlbum(album: Album, beginLoading flag: Bool = true) { if !albums.contains(album) { albums.append(album) let userInfo: [NSObject: AnyObject] = [AlbumCollection.Notifications.Keys.album: album, AlbumCollection.Notifications.Keys.albumIndex: albums.count-1] NSNotificationCenter.defaultCenter().postNotificationName(AlbumCollection.Notifications.albumAdded, object: self, userInfo: userInfo) if flag { beginLoadingTracksForAlbum(album) } } } /// Removes the specified album from the collection if it is present. public func removeAlbum(album: Album) { if let index = albums.indexOf(album) { removeAlbumAtIndex(index) } } /// Removes the album at the specified index from the collection. /// /// - requires: The specified index must be in the collection's range. public func removeAlbumAtIndex(index: Int) { let album = self[index] setAlbumState(nil, forAlbum: album) albums.removeAtIndex(index) let userInfo: [NSObject: AnyObject] = [AlbumCollection.Notifications.Keys.album: album, AlbumCollection.Notifications.Keys.albumIndex: index] NSNotificationCenter.defaultCenter().postNotificationName(AlbumCollection.Notifications.albumRemoved, object: self, userInfo: userInfo) } /// Begins to load the tracks for the specified album. If there is already a /// request for the specified album it is cancelled. When the tracks for the /// specified album have been loaded or an error occured, a /// `AlbumFinishedLoadingNotification` is posted. public func beginLoadingTracksForAlbum(album: Album) { guard let albumIndex = albums.indexOf(album) else { return } let url = iTunesAPI.createAlbumLookupURLForId(album.id) let task = urlSession.dataTaskWithURL(url) { (data, response, error) -> Void in var newAlbumIndex = self.albums.indexOf(album)! defer { let userInfo: [NSObject: AnyObject] = [AlbumCollection.Notifications.Keys.album: self.albums[albumIndex], AlbumCollection.Notifications.Keys.albumIndex: albumIndex] NSNotificationCenter.defaultCenter().postNotificationName(AlbumCollection.Notifications.albumFinishedLoading, object: self, userInfo: userInfo) } guard error == nil else { if error!.code != NSUserCancelledError { self.albumStates[album] = .Error(error!) } return } do { let newAlbum = try iTunesAPI.parseAPIData(data!)[0] self.albums.removeAtIndex(newAlbumIndex) self.albums.insert(newAlbum, atIndex: newAlbumIndex) self.setAlbumState(.Normal, forAlbum: album) } catch let error as NSError { self.setAlbumState(.Error(error), forAlbum: album) } catch _ { // Will never happen } } setAlbumState(.Loading(task), forAlbum: album) task.resume() let userInfo: [NSObject: AnyObject] = [AlbumCollection.Notifications.Keys.album: album, AlbumCollection.Notifications.Keys.albumIndex: albumIndex] NSNotificationCenter.defaultCenter().postNotificationName(AlbumCollection.Notifications.albumStartedLoading, object: self, userInfo: userInfo) } /// Cancels the request to load the tracks for the specified album and sets /// the error for the album to a `NSUserCancelledError` in the /// `NSCocoaErrorDomain`. public func cancelLoadingTracksForAlbum(album: Album) { let error = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil) setAlbumState(.Error(error), forAlbum: album) } /// Sets the state for the specified album. If the previous state was /// `Loading` the associated task is cancelled. private func setAlbumState(state: AlbumState?, forAlbum album: Album) { if case let .Some(.Loading(task)) = albumStates[album] { task.cancel() } albumStates[album] = state } public func isAlbumLoading(album: Album) -> Bool { if case .Some(.Loading) = albumStates[album] { return true } return false } public func errorForAlbum(album: Album) -> NSError? { if case let .Some(.Error(error)) = albumStates[album] { return error } return nil } }