// // 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) } // MARK: Constants /// Posted when an album is added to a collection. This notification is only /// posted if the album collection actually changed. public static let AlbumAddedNotificationName = "AlbumAddedNotificationName" /// Posted when an album is removed from a collection. This notification is /// only posted if the album collection actually changed. public static let AlbumRemovedNotificationName = "AlbumRemovedNotificationName" /// 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 network 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 AlbumFinishedLoadingNotificationName = "AlbumFinishedLoadingNotificationName" /// Key in the `userInfo` dictionary of a notification. The associated value /// is an `Int` indicating the index of the album that is affected. public static let AlbumIndexKey = "AlbumIndexKey" /// Key in the `userInfo` dictionary of a notification. The associated value /// is the `Album` instance that is affected. public static let AlbumKey = "AlbumKey" // 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) if flag { beginLoadingTracksForAlbum(album) } NSNotificationCenter.defaultCenter().postNotificationName(AlbumCollection.AlbumAddedNotificationName, object: self, userInfo: [AlbumCollection.AlbumIndexKey: albums.count-1]) } } /// 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) NSNotificationCenter.defaultCenter().postNotificationName(AlbumCollection.AlbumRemovedNotificationName, object: self, userInfo: [AlbumCollection.AlbumIndexKey: index]) } /// 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) { let url = iTunesAPI.createAlbumLookupURLForId(album.id) let task = urlSession.dataTaskWithURL(url) { (data, response, error) -> Void in var albumIndex = self.albums.indexOf(album)! defer { NSNotificationCenter.defaultCenter().postNotificationName(AlbumCollection.AlbumFinishedLoadingNotificationName, object: self, userInfo: [AlbumCollection.AlbumIndexKey: albumIndex]) } guard error == nil else { if error!.code != NSUserCancelledError { self.albumStates[album] = .Error(error!) } return } do { let newAlbum = try iTunesAPI.parseAPIData(data!)[0] albumIndex = self.albums.indexOf(album)! self.albums.removeAtIndex(albumIndex) self.albums.insert(newAlbum, atIndex: albumIndex) 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() } /// 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 } }