diff --git a/Podfile.lock b/Podfile.lock index d22ece6..c5d39c4 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -31,4 +31,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 00721ff03a3007b0c070a96d5c58c16f92ab8027 -COCOAPODS: 1.5.2 +COCOAPODS: 1.7.0 diff --git a/Stitcher/Info.plist b/Stitcher/Info.plist index 1b741df..dc9c715 100644 --- a/Stitcher/Info.plist +++ b/Stitcher/Info.plist @@ -37,8 +37,8 @@ Spotify Client Id ${SPOTIFY_CLIENT_ID} - Spotify Consumer Secret - ${SPOTIFY_CONSUMER_SECRET} + Spotify Client Secret + ${SPOTIFY_CLIENT_SECRET} UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities @@ -47,10 +47,6 @@ UIStatusBarStyle UIStatusBarStyleLightContent - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait @@ -58,6 +54,10 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + UISupportedInterfaceOrientations~iphone + + UIInterfaceOrientationPortrait + UIViewControllerBasedStatusBarAppearance diff --git a/Stitcher/Resources/BuildConfig.swift b/Stitcher/Resources/BuildConfig.swift index 98328a3..79d56c9 100644 --- a/Stitcher/Resources/BuildConfig.swift +++ b/Stitcher/Resources/BuildConfig.swift @@ -23,6 +23,6 @@ struct BuildConfig { private enum Key: String { case clientId = "Spotify Client Id" - case clientSecret = "Spotify Consumer Secret" + case clientSecret = "Spotify Client Secret" } } diff --git a/Stitcher/UI/Common/BaseTableViewController.swift b/Stitcher/UI/Common/BaseTableViewController.swift index 3dee323..9fdad18 100644 --- a/Stitcher/UI/Common/BaseTableViewController.swift +++ b/Stitcher/UI/Common/BaseTableViewController.swift @@ -14,6 +14,14 @@ class BaseTableViewController: UITableViewController, DZNEmptyDataSetSource, DZNEmptyDataSetDelegate, BasePresenterDelegate where T: BasePresenter { // MARK: - Properties + public var childView: KeyboardTableView! { return tableView as! KeyboardTableView } + + override func loadView() { + tableView = KeyboardTableView(frame: UIScreen.main.bounds, style: .plain) + tableView.delegate = self + tableView.dataSource = self + view = tableView + } let presenter: T diff --git a/Stitcher/UI/Common/KeyboardSearchBar.swift b/Stitcher/UI/Common/KeyboardSearchBar.swift new file mode 100644 index 0000000..60f9f5a --- /dev/null +++ b/Stitcher/UI/Common/KeyboardSearchBar.swift @@ -0,0 +1,45 @@ +// +// KeyboardSearchBar.swift +// Stitcher +// +// Created by Giulio Montagner on 22/06/2019. +// Copyright © 2019 Meaningless. All rights reserved. +// + +import UIKit + +class KeyboardSearchBar: UISearchBar { + + var selectBelowDiscoverabilityTitle = "Select First Result" + + override var keyCommands: [UIKeyCommand]? { + get { + var commands = super.keyCommands ?? [] + + commands.append(UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [], action: #selector(downArrowPressed), discoverabilityTitle: selectBelowDiscoverabilityTitle)) + return commands + } + } + + @objc private func downArrowPressed() { + resignFirstResponder() + if(arrowKeyDelegate != nil) { + arrowKeyDelegate?.downArrowButtonPressed() + } + } + + weak var arrowKeyDelegate: KeyboardSearchBarDelegate? + override weak var delegate: UISearchBarDelegate? { + didSet { + arrowKeyDelegate = delegate as? KeyboardSearchBarDelegate + } + } + +} + + +protocol KeyboardSearchBarDelegate: UISearchBarDelegate { + + func downArrowButtonPressed() + +} diff --git a/Stitcher/UI/Common/KeyboardSearchController.swift b/Stitcher/UI/Common/KeyboardSearchController.swift new file mode 100644 index 0000000..3978b50 --- /dev/null +++ b/Stitcher/UI/Common/KeyboardSearchController.swift @@ -0,0 +1,40 @@ +// +// KeyboardSearchController.swift +// Stitcher +// +// Created by Giulio Montagner on 22/06/2019. +// Copyright © 2019 Meaningless. All rights reserved. +// +import UIKit + +class KeyboardSearchController: UISearchController, KeyboardSearchBarDelegate { + + var mySearchBar = KeyboardSearchBar() + + override var searchBar: KeyboardSearchBar { + get{ + mySearchBar.delegate = self + return mySearchBar; + } + } + + weak var keyboardSearchControllerDelegate: KeyboardSearchControllerDelegate? + override weak var delegate: UISearchControllerDelegate? { + didSet { + keyboardSearchControllerDelegate = delegate as? KeyboardSearchControllerDelegate + } + } + + func downArrowButtonPressed() { + if(keyboardSearchControllerDelegate != nil) { + keyboardSearchControllerDelegate?.didStartResultSelectionFromKeyboard(searchController: self) + } + } + +} + +protocol KeyboardSearchControllerDelegate: UISearchControllerDelegate { + + func didStartResultSelectionFromKeyboard(searchController: UISearchController) + +} diff --git a/Stitcher/UI/Common/KeyboardTableView.swift b/Stitcher/UI/Common/KeyboardTableView.swift new file mode 100644 index 0000000..1a6b6d2 --- /dev/null +++ b/Stitcher/UI/Common/KeyboardTableView.swift @@ -0,0 +1,114 @@ +// Douglas Hill, December 2018 +// Made for https://douglashill.co/reading-app/ +import UIKit + +/// A table view that allows navigation and selection using a hardware keyboard. +/// Only supports a single section. +class KeyboardTableView: UITableView { + // These properties may be set or overridden to provide discoverability titles for key commands. + var selectAboveDiscoverabilityTitle: String? + var selectBelowDiscoverabilityTitle: String? + var selectTopDiscoverabilityTitle: String? + var selectBottomDiscoverabilityTitle: String? + var clearSelectionDiscoverabilityTitle: String? + var activateSelectionDiscoverabilityTitle: String? + + override var canBecomeFirstResponder: Bool { + return true + } + + override var keyCommands: [UIKeyCommand]? { + var commands = super.keyCommands ?? [] + + commands.append(UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: [], action: #selector(selectAbove), maybeDiscoverabilityTitle: selectAboveDiscoverabilityTitle)) + commands.append(UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: [], action: #selector(selectBelow), maybeDiscoverabilityTitle: selectBelowDiscoverabilityTitle)) + commands.append(UIKeyCommand(input: UIKeyCommand.inputUpArrow, modifierFlags: .command, action: #selector(selectTop), maybeDiscoverabilityTitle: selectTopDiscoverabilityTitle)) + commands.append(UIKeyCommand(input: UIKeyCommand.inputDownArrow, modifierFlags: .command, action: #selector(selectBottom), maybeDiscoverabilityTitle: selectBottomDiscoverabilityTitle)) + + commands.append(UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(clearSelection), maybeDiscoverabilityTitle: clearSelectionDiscoverabilityTitle)) + + commands.append(UIKeyCommand(input: " ", modifierFlags: [], action: #selector(activateSelection))) + commands.append(UIKeyCommand(input: "\r", modifierFlags: [], action: #selector(activateSelection), maybeDiscoverabilityTitle: activateSelectionDiscoverabilityTitle)) + + return commands + } + + @objc func selectAbove() { + if let oldSelectedIndexPath = indexPathForSelectedRow { + selectRowAtIndex(oldSelectedIndexPath.row - 1) + } else { + selectBottom() + } + } + + @objc func selectBelow() { + if let oldSelectedIndexPath = indexPathForSelectedRow { + selectRowAtIndex(oldSelectedIndexPath.row + 1) + } else { + selectTop() + } + } + + @objc func selectTop() { + selectRowAtIndex(0) + } + + @objc func selectBottom() { + selectRowAtIndex(numberOfRows(inSection: 0) - 1) + } + + /// Tries to select and scroll to the row at the given index in section 0. + /// Does not require the index to be in bounds. Does nothing if out of bounds. + private func selectRowAtIndex(_ rowIndex: Int) { + guard rowIndex >= 0 && rowIndex < numberOfRows(inSection: 0) else { + return + } + + let indexPath = IndexPath(row: rowIndex, section: 0) + + switch cellVisibility(atIndexPath: indexPath) { + case .fullyVisible: + selectRow(at: indexPath, animated: false, scrollPosition: .none) + case .notFullyVisible(let scrollPosition): + // Looks better and feel more responsive if the selection updates without animation. + selectRow(at: indexPath, animated: false, scrollPosition: .none) + scrollToRow(at: indexPath, at: scrollPosition, animated: true) + flashScrollIndicators() + } + } + + /// Whether a row is fully visible, or if not if it’s above or below the viewport. + enum CellVisibility { case fullyVisible; case notFullyVisible(ScrollPosition); } + + /// Whether the given row is fully visible, or if not if it’s above or below the viewport. + private func cellVisibility(atIndexPath indexPath: IndexPath) -> CellVisibility { + let rowRect = rectForRow(at: indexPath) + if bounds.inset(by: adjustedContentInset).contains(rowRect) { + return .fullyVisible + } + + let position: ScrollPosition = rowRect.midY < bounds.midY ? .top : .bottom + return .notFullyVisible(position) + } + + @objc func clearSelection() { + selectRow(at: nil, animated: false, scrollPosition: .none) + } + + @objc func activateSelection() { + guard let indexPathForSelectedRow = indexPathForSelectedRow else { + return + } + delegate?.tableView?(self, didSelectRowAt: indexPathForSelectedRow) + } +} + +private extension UIKeyCommand { + convenience init(input: String, modifierFlags: UIKeyModifierFlags, action: Selector, maybeDiscoverabilityTitle: String?) { + if let discoverabilityTitle = maybeDiscoverabilityTitle { + self.init(input: input, modifierFlags: modifierFlags, action: action, discoverabilityTitle: discoverabilityTitle) + } else { + self.init(input: input, modifierFlags: modifierFlags, action: action) + } + } +} diff --git a/Stitcher/UI/Playlist/PlaylistViewController.swift b/Stitcher/UI/Playlist/PlaylistViewController.swift index 246cdc9..b8a9d8d 100644 --- a/Stitcher/UI/Playlist/PlaylistViewController.swift +++ b/Stitcher/UI/Playlist/PlaylistViewController.swift @@ -14,7 +14,7 @@ import DZNEmptyDataSet /** The user interface for adding, re-ordering, and remove tacks in a playlist. */ -final class PlaylistViewController: BaseTableViewController, UISearchControllerDelegate, UISearchResultsUpdating { +final class PlaylistViewController: BaseTableViewController, KeyboardSearchControllerDelegate, UISearchResultsUpdating { // MARK: - Properties @@ -26,12 +26,28 @@ final class PlaylistViewController: BaseTableViewController, return isSearchActive && !isSearchTextEmpty } + var startSearchDiscoverabilityTitle = "Search Track" + + override var keyCommands: [UIKeyCommand]? { + get { + var commands = super.keyCommands ?? [] + + commands.append(UIKeyCommand(input: "F", modifierFlags: [UIKeyModifierFlags.command], action: #selector(startSearch), discoverabilityTitle: startSearchDiscoverabilityTitle)) + return commands + } + } + // MARK: - Lifecycle override init(presenter: PlaylistPresenter) { super.init(presenter: presenter) presenter.playlistDelegate = self + + childView.selectAboveDiscoverabilityTitle = "Select Search Result Above" + childView.selectBelowDiscoverabilityTitle = "Select Search Result Below" + childView.selectTopDiscoverabilityTitle = "Select First Search Result" + childView.selectBottomDiscoverabilityTitle = "Select Last Search Result" } required init?(coder aDecoder: NSCoder) { @@ -218,7 +234,7 @@ final class PlaylistViewController: BaseTableViewController, navigationItem.hidesSearchBarWhenScrolling = false // Setup search - let searchController = UISearchController(searchResultsController: nil) + let searchController = KeyboardSearchController(searchResultsController: nil) searchController.delegate = self searchController.searchResultsUpdater = self searchController.obscuresBackgroundDuringPresentation = false @@ -293,6 +309,16 @@ final class PlaylistViewController: BaseTableViewController, isReorderControlHidden = presenter.tracksDataSource.items.count == 0 } + // MARK: - KEYBOARD TRIGGERS + + @objc func startSearch() { + navigationItem.searchController?.searchBar.becomeFirstResponder() + } + + func didStartResultSelectionFromKeyboard(searchController: UISearchController) { + childView.becomeFirstResponder() + childView.selectBelow() + } // MARK: - Base Presenter Delegate diff --git a/Stitcher/UI/Playlists/PlaylistsViewController.swift b/Stitcher/UI/Playlists/PlaylistsViewController.swift index a11b786..c9acc61 100644 --- a/Stitcher/UI/Playlists/PlaylistsViewController.swift +++ b/Stitcher/UI/Playlists/PlaylistsViewController.swift @@ -17,12 +17,27 @@ final class PlaylistsViewController: BaseTableViewController private let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: nil, action: nil) + var newPlaylistDiscoverabilityTitle = "New Playlist" + + override var keyCommands: [UIKeyCommand]? { + get { + var commands = super.keyCommands ?? [] + + commands.append(UIKeyCommand(input: "N", modifierFlags: [UIKeyModifierFlags.command], action: #selector(newPlaylistTriggered), discoverabilityTitle: newPlaylistDiscoverabilityTitle)) + return commands + } + } // MARK: - Lifecycle override init(presenter: PlaylistsPresenter) { super.init(presenter: presenter) presenter.playlistsDelegate = self + + childView.selectAboveDiscoverabilityTitle = "Select Playlist Above" + childView.selectBelowDiscoverabilityTitle = "Select Playlist Below" + childView.selectTopDiscoverabilityTitle = "Select First Playlist" + childView.selectBottomDiscoverabilityTitle = "Select Last Playlist" } required init?(coder aDecoder: NSCoder) { @@ -64,6 +79,10 @@ final class PlaylistsViewController: BaseTableViewController openPlaylist(playlist: nil) } + @objc func newPlaylistTriggered() { + openPlaylist(playlist: nil) + } + @objc private func didRefreshTable(_ refreshControl: UIRefreshControl) { presenter.playlistsDataSource.refresh()