Archived
1

Updated for OS X 10.11 and Swift 2

Added more descriptive errors.
This commit is contained in:
Kim Wittenburg
2015-09-11 14:45:48 +02:00
committed by Kim Wittenburg
parent 45f664cb10
commit 80f177807d
6 changed files with 172 additions and 33 deletions

View 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
}
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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
}