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()