Archived
1
This repository has been archived on 2020-06-04. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tagtunes/TagTunes/AlbumCollection.swift
2015-09-11 16:38:17 +02:00

186 lines
7.1 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)
}
// 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
}
}