Updated for OS X 10.11 and Swift 2
Added more descriptive errors.
This commit is contained in:
committed by
Kim Wittenburg
parent
45f664cb10
commit
80f177807d
60
TagTunes/DescriptiveError.swift
Normal file
60
TagTunes/DescriptiveError.swift
Normal file
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// DescriptiveError.swift
|
||||
// TagTunes
|
||||
//
|
||||
// Created by Kim Wittenburg on 06.09.15.
|
||||
// Copyright © 2015 Kim Wittenburg. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// A custom error class that wraps another error to display a more descriptive
|
||||
/// error description than for example "The operation couldn't be completed."
|
||||
public class DescriptiveError: NSError {
|
||||
|
||||
/// Initializes the receiver with the specified `underlyingError`.
|
||||
/// The underlying error is added to the receiver's `userInfo` dictionary as
|
||||
/// is the specified dictionary.
|
||||
public init(underlyingError: NSError, userInfo: [NSString: AnyObject]?) {
|
||||
var actualUserInfo = userInfo ?? [NSString: AnyObject]()
|
||||
actualUserInfo[NSUnderlyingErrorKey] = underlyingError
|
||||
super.init(domain: underlyingError.domain, code: underlyingError.code, userInfo: actualUserInfo)
|
||||
}
|
||||
|
||||
public required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("DescriptiveError instances should not be encoded.")
|
||||
}
|
||||
|
||||
/// Returns the value for the NSUnderlyingErrorKey in the error's `userInfo`
|
||||
/// dictionary as an `NSError` instance.
|
||||
public var underlyingError: NSError {
|
||||
return userInfo[NSUnderlyingErrorKey] as! NSError
|
||||
}
|
||||
|
||||
override public var localizedDescription: String {
|
||||
if domain == NSURLErrorDomain {
|
||||
switch code {
|
||||
case NSURLErrorNotConnectedToInternet:
|
||||
return NSLocalizedString("You are not connected to the internet.", comment: "Error message informing the user that he is not connected to the internet.")
|
||||
case NSURLErrorTimedOut:
|
||||
return NSLocalizedString("The network request timed out.", comment: "Error message informing the user that the network request timed out.")
|
||||
default: break
|
||||
}
|
||||
}
|
||||
return super.localizedDescription
|
||||
}
|
||||
|
||||
override public var localizedRecoverySuggestion: String? {
|
||||
if domain == NSURLErrorDomain {
|
||||
switch code {
|
||||
case NSURLErrorNotConnectedToInternet:
|
||||
return NSLocalizedString("Please check your network connection and try again.", comment: "Error recovery suggestion for 'not connected to the internet' error.")
|
||||
case NSURLErrorTimedOut:
|
||||
return NSLocalizedString("Please check your network connection and try again. If that does not help it may be possible that Apple's Search API service is currently offline. Please try again later.", comment: "Error recovery suggestion for 'time out' error.")
|
||||
default: break
|
||||
}
|
||||
}
|
||||
return super.localizedRecoverySuggestion
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,11 +17,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<string>1.1</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>4242</string>
|
||||
<string>1</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.music</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
|
||||
@@ -57,7 +57,7 @@ internal class MainViewController: NSViewController {
|
||||
private let urlSession = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: nil, delegateQueue: NSOperationQueue.mainQueue())
|
||||
|
||||
/// The URL task currently loading the search results
|
||||
private var searchTask: NSURLSessionDataTask?
|
||||
private var searchTask: NSURLSessionTask?
|
||||
|
||||
/// If `true` the search section is displayed at the top of the
|
||||
/// `outlineView`.
|
||||
@@ -77,7 +77,7 @@ internal class MainViewController: NSViewController {
|
||||
/// Errors that occured during loading the tracks for the respective album.
|
||||
private var trackErrors = [Album: NSError]()
|
||||
|
||||
// MARK: Overrides
|
||||
// MARK: View Life Cycle
|
||||
|
||||
override internal func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
@@ -109,7 +109,7 @@ internal class MainViewController: NSViewController {
|
||||
if showsSearch {
|
||||
contents.append(OutlineViewConstants.Items.searchResultsHeaderItem)
|
||||
if !searchResults.isEmpty {
|
||||
contents.extend(searchResults as [AnyObject])
|
||||
contents.appendContentsOf(searchResults as [AnyObject])
|
||||
} else if searching {
|
||||
contents.append(OutlineViewConstants.Items.loadingItem)
|
||||
} else if let error = searchError {
|
||||
@@ -121,10 +121,10 @@ internal class MainViewController: NSViewController {
|
||||
contents.append(OutlineViewConstants.Items.albumsHeaderItem)
|
||||
}
|
||||
}
|
||||
contents.extend(albums as [AnyObject])
|
||||
contents.appendContentsOf(albums as [AnyObject])
|
||||
if !unsortedTracks.isEmpty {
|
||||
contents.append(OutlineViewConstants.Items.unsortedTracksHeaderItem)
|
||||
contents.extend(unsortedTracks as [AnyObject])
|
||||
contents.appendContentsOf(unsortedTracks as [AnyObject])
|
||||
}
|
||||
return contents
|
||||
}
|
||||
@@ -190,8 +190,7 @@ internal class MainViewController: NSViewController {
|
||||
|
||||
/// Starts a search for the specified search term. Calling this method
|
||||
internal func beginSearchForTerm(term: String) {
|
||||
searchTask?.cancel()
|
||||
searchResults.removeAll()
|
||||
cancelSearch()
|
||||
if let url = iTunesAPI.createAlbumSearchURLForTerm(term) {
|
||||
showsSearch = true
|
||||
searchTask = urlSession.dataTaskWithURL(url, completionHandler: processSearchResults)
|
||||
@@ -202,24 +201,39 @@ internal class MainViewController: NSViewController {
|
||||
outlineView.reloadData()
|
||||
}
|
||||
|
||||
/// Cancels the current search (if there is one). This also hides the search
|
||||
/// results.
|
||||
internal func cancelSearch() {
|
||||
searchTask?.cancel()
|
||||
searchResults.removeAll()
|
||||
showsSearch = false
|
||||
outlineView.reloadData()
|
||||
}
|
||||
|
||||
/// Processes the data returned from a network request into the
|
||||
/// `searchResults`.
|
||||
private func processSearchResults(data: NSData?, response: NSURLResponse?, error: NSError?) {
|
||||
private func processSearchResults(data: NSData?, response: NSURLResponse?, var error: NSError?) {
|
||||
searchTask = nil
|
||||
if let theError = error {
|
||||
searchError = theError
|
||||
} else if let theData = data {
|
||||
if let theData = data where error == nil {
|
||||
do {
|
||||
let searchResults = try iTunesAPI.parseAPIData(theData).map { SearchResult(representedAlbum: $0) }
|
||||
self.searchResults = searchResults
|
||||
} catch let error as NSError {
|
||||
searchError = error
|
||||
} catch let theError as NSError {
|
||||
error = theError
|
||||
}
|
||||
}
|
||||
if let theError = error {
|
||||
searchErrorOccured(theError)
|
||||
}
|
||||
showsSearch = true
|
||||
outlineView.reloadData()
|
||||
}
|
||||
|
||||
/// Called when an error occurs during searching.
|
||||
private func searchErrorOccured(error: NSError) {
|
||||
searchError = error
|
||||
}
|
||||
|
||||
/// Adds the search result at the specified `row` to the albums section and
|
||||
/// begins loading its tracks.
|
||||
internal func selectSearchResultAtRow(row: Int) {
|
||||
@@ -251,7 +265,7 @@ internal class MainViewController: NSViewController {
|
||||
let task = urlSession.dataTaskWithURL(url) { (data, response, var error) -> Void in
|
||||
self.trackTasks[album] = nil
|
||||
do {
|
||||
if let theData = data {
|
||||
if let theData = data where error == nil {
|
||||
let newAlbum = try iTunesAPI.parseAPIData(theData)[0]
|
||||
let index = self.albums.indexOf(album)!
|
||||
self.albums.removeAtIndex(index)
|
||||
@@ -262,6 +276,7 @@ internal class MainViewController: NSViewController {
|
||||
} catch _ {
|
||||
// Will never happen
|
||||
}
|
||||
|
||||
self.trackErrors[album] = error
|
||||
self.outlineView.reloadData()
|
||||
}
|
||||
@@ -273,6 +288,7 @@ internal class MainViewController: NSViewController {
|
||||
func cancelLoadingTracksForAlbum(album: Album) {
|
||||
trackTasks[album]?.cancel()
|
||||
trackTasks[album] = nil
|
||||
trackErrors[album] = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil)
|
||||
}
|
||||
|
||||
private func saveTracks(tracks: [Track: [iTunesTrack]]) {
|
||||
@@ -284,6 +300,9 @@ internal class MainViewController: NSViewController {
|
||||
NSProgress.currentProgress()?.localizedAdditionalDescription = progress.localizedAdditionalDescription
|
||||
for (parentTrack, targetTracks) in tracks {
|
||||
for track in targetTracks {
|
||||
if progress.cancelled {
|
||||
return
|
||||
}
|
||||
parentTrack.saveToTrack(track)
|
||||
++progress.completedUnitCount
|
||||
NSProgress.currentProgress()?.localizedAdditionalDescription = progress.localizedAdditionalDescription
|
||||
@@ -302,6 +321,9 @@ internal class MainViewController: NSViewController {
|
||||
NSProgress.currentProgress()?.localizedDescription = NSLocalizedString("Saving artworks…", comment: "Alert message indicating that the artworks for the selected tracks are currently being saved")
|
||||
NSProgress.currentProgress()?.localizedAdditionalDescription = progress.localizedAdditionalDescription
|
||||
for album in albums {
|
||||
if progress.cancelled {
|
||||
return
|
||||
}
|
||||
do {
|
||||
try album.saveArtwork()
|
||||
} catch _ {
|
||||
@@ -338,7 +360,7 @@ internal class MainViewController: NSViewController {
|
||||
alert.beginSheetModalForWindow(view.window!, completionHandler: nil)
|
||||
} else if let selection = iTunes.selection.get() as? [iTunesTrack] {
|
||||
let newTracks = Set(selection).subtract(allITunesTracks)
|
||||
unsortedTracks.extend(newTracks)
|
||||
unsortedTracks.appendContentsOf(newTracks)
|
||||
outlineView.reloadData()
|
||||
}
|
||||
}
|
||||
@@ -413,6 +435,7 @@ internal class MainViewController: NSViewController {
|
||||
if sectionOfRow(row)! != .SearchResults {
|
||||
if let album = item as? Album {
|
||||
cancelLoadingTracksForAlbum(album)
|
||||
trackErrors[album] = nil
|
||||
albums.removeElement(album)
|
||||
} else if let track = item as? Track {
|
||||
track.associatedTracks = []
|
||||
@@ -458,11 +481,47 @@ internal class MainViewController: NSViewController {
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Error Handling
|
||||
|
||||
extension MainViewController {
|
||||
|
||||
override internal func willPresentError(error: NSError) -> NSError {
|
||||
let recoveryOptions = [
|
||||
NSLocalizedString("OK", comment: "Button title"),
|
||||
NSLocalizedString("Try Again", comment: "Button title for error alerts offering the user to try again.")
|
||||
]
|
||||
return DescriptiveError(underlyingError: error, userInfo: [NSRecoveryAttempterErrorKey: self, NSLocalizedRecoveryOptionsErrorKey: recoveryOptions])
|
||||
}
|
||||
|
||||
override internal func attemptRecoveryFromError(error: NSError, optionIndex recoveryOptionIndex: Int, delegate: AnyObject?, didRecoverSelector: Selector, contextInfo: UnsafeMutablePointer<Void>) {
|
||||
let didRecover = attemptRecoveryFromError(error, optionIndex: recoveryOptionIndex)
|
||||
// TODO: Notify the delegate
|
||||
}
|
||||
|
||||
override internal func attemptRecoveryFromError(error: NSError, optionIndex recoveryOptionIndex: Int) -> Bool {
|
||||
if recoveryOptionIndex == 0 {
|
||||
return true
|
||||
}
|
||||
// TODO: Implementation
|
||||
if error == searchError {
|
||||
|
||||
} else {
|
||||
for (album, trackError) in trackErrors {
|
||||
if error == trackError {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - User Interface Validations
|
||||
|
||||
extension MainViewController: NSUserInterfaceValidations {
|
||||
|
||||
func validateUserInterfaceItem(anItem: NSValidatedUserInterfaceItem) -> Bool {
|
||||
internal func validateUserInterfaceItem(anItem: NSValidatedUserInterfaceItem) -> Bool {
|
||||
if anItem.action() == "performSave:" {
|
||||
for row in outlineView.selectedRowIndexes {
|
||||
return sectionOfRow(row) == .Albums
|
||||
@@ -488,7 +547,7 @@ extension MainViewController: NSUserInterfaceValidations {
|
||||
|
||||
extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
|
||||
func outlineView(outlineView: NSOutlineView, numberOfChildrenOfItem item: AnyObject?) -> Int {
|
||||
internal func outlineView(outlineView: NSOutlineView, numberOfChildrenOfItem item: AnyObject?) -> Int {
|
||||
if item == nil {
|
||||
return outlineViewContents.count
|
||||
} else if let album = item as? Album {
|
||||
@@ -500,7 +559,7 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func outlineView(outlineView: NSOutlineView, child index: Int, ofItem item: AnyObject?) -> AnyObject {
|
||||
internal func outlineView(outlineView: NSOutlineView, child index: Int, ofItem item: AnyObject?) -> AnyObject {
|
||||
if item == nil {
|
||||
return outlineViewContents[index]
|
||||
} else if let album = item as? Album {
|
||||
@@ -512,19 +571,19 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func outlineView(outlineView: NSOutlineView, isItemExpandable item: AnyObject) -> Bool {
|
||||
internal func outlineView(outlineView: NSOutlineView, isItemExpandable item: AnyObject) -> Bool {
|
||||
return self.outlineView(outlineView, numberOfChildrenOfItem: item) > 0
|
||||
}
|
||||
|
||||
func outlineView(outlineView: NSOutlineView, isGroupItem item: AnyObject) -> Bool {
|
||||
internal func outlineView(outlineView: NSOutlineView, isGroupItem item: AnyObject) -> Bool {
|
||||
return Section.isHeaderItem(item)
|
||||
}
|
||||
|
||||
func outlineView(outlineView: NSOutlineView, shouldSelectItem item: AnyObject) -> Bool {
|
||||
internal func outlineView(outlineView: NSOutlineView, shouldSelectItem item: AnyObject) -> Bool {
|
||||
return !(self.outlineView(outlineView, isGroupItem: item) || item === OutlineViewConstants.Items.loadingItem || item === OutlineViewConstants.Items.noResultsItem || item is NSError)
|
||||
}
|
||||
|
||||
func outlineView(outlineView: NSOutlineView, heightOfRowByItem item: AnyObject) -> CGFloat {
|
||||
internal func outlineView(outlineView: NSOutlineView, heightOfRowByItem item: AnyObject) -> CGFloat {
|
||||
if item is Album || item is SearchResult {
|
||||
return 39
|
||||
} else if let track = item as? Track {
|
||||
@@ -540,7 +599,7 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func outlineView(outlineView: NSOutlineView, viewForTableColumn tableColumn: NSTableColumn?, item: AnyObject) -> NSView? {
|
||||
internal func outlineView(outlineView: NSOutlineView, viewForTableColumn tableColumn: NSTableColumn?, item: AnyObject) -> NSView? {
|
||||
if item === OutlineViewConstants.Items.searchResultsHeaderItem {
|
||||
var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier, owner: nil) as? AdvancedTableCellView
|
||||
if view == nil {
|
||||
@@ -653,7 +712,7 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
return nil
|
||||
}
|
||||
|
||||
func outlineView(outlineView: NSOutlineView, writeItems items: [AnyObject], toPasteboard pasteboard: NSPasteboard) -> Bool {
|
||||
internal func outlineView(outlineView: NSOutlineView, writeItems items: [AnyObject], toPasteboard pasteboard: NSPasteboard) -> Bool {
|
||||
var rows = [Int]()
|
||||
var containsValidItems = false
|
||||
for item in items {
|
||||
@@ -672,7 +731,7 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
return true
|
||||
}
|
||||
|
||||
func outlineView(outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: AnyObject?, proposedChildIndex index: Int) -> NSDragOperation {
|
||||
internal func outlineView(outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: AnyObject?, proposedChildIndex index: Int) -> NSDragOperation {
|
||||
let firstUnsortedRow = outlineViewContents.count - (unsortedTracks.isEmpty ? 0 : unsortedTracks.count+1)
|
||||
// Drop in the 'unsorted' section
|
||||
if item == nil && index >= firstUnsortedRow || item === OutlineViewConstants.Items.unsortedTracksHeaderItem {
|
||||
@@ -698,7 +757,7 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
return .Every
|
||||
}
|
||||
|
||||
func outlineView(outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: AnyObject?, childIndex index: Int) -> Bool {
|
||||
internal func outlineView(outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: AnyObject?, childIndex index: Int) -> Bool {
|
||||
guard let data = info.draggingPasteboard().dataForType(OutlineViewConstants.pasteboardType), draggedRows = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? [Int] else {
|
||||
return false
|
||||
}
|
||||
@@ -728,7 +787,7 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
|
||||
// Add the dragged tracks to the new target
|
||||
if let targetTrack = item as? Track {
|
||||
targetTrack.associatedTracks.extend(draggedTracks)
|
||||
targetTrack.associatedTracks.appendContentsOf(draggedTracks)
|
||||
} else if let targetAlbum = item as? Album {
|
||||
for draggedTrack in draggedTracks {
|
||||
var inserted = false
|
||||
@@ -744,7 +803,7 @@ extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unsortedTracks.extend(draggedTracks)
|
||||
unsortedTracks.appendContentsOf(draggedTracks)
|
||||
}
|
||||
outlineView.reloadData()
|
||||
return true
|
||||
|
||||
@@ -125,7 +125,7 @@ public struct iTunesAPI {
|
||||
/// - returns: The query URL or `nil` if `term` is invalid.
|
||||
public static func createAlbumSearchURLForTerm(term: String) -> NSURL? {
|
||||
var searchTerm = term.stringByReplacingOccurrencesOfString(" ", withString: "+")
|
||||
searchTerm = CFURLCreateStringByAddingPercentEscapes(nil, searchTerm, nil, nil, CFStringBuiltInEncodings.UTF8.rawValue) as String
|
||||
searchTerm = searchTerm.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet())!
|
||||
if searchTerm.isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user