210 lines
8.4 KiB
Swift
210 lines
8.4 KiB
Swift
//
|
|
// 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
|
|
}
|
|
|
|
}
|