diff --git a/GridNotes/GridNotes.xcodeproj/project.pbxproj b/GridNotes/GridNotes.xcodeproj/project.pbxproj index 68e99db..e3b96fc 100644 --- a/GridNotes/GridNotes.xcodeproj/project.pbxproj +++ b/GridNotes/GridNotes.xcodeproj/project.pbxproj @@ -31,7 +31,12 @@ 2B45543725AB735000A95718 /* Yamaha Grand Piano.sf2 in Resources */ = {isa = PBXBuildFile; fileRef = 2B45542725AB735000A95718 /* Yamaha Grand Piano.sf2 */; }; 2B45543F25AB8D6C00A95718 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B45543E25AB8D6C00A95718 /* SettingsViewController.swift */; }; 2B45544425ABDEA800A95718 /* KeyRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B45544325ABDEA800A95718 /* KeyRowView.swift */; }; - 2B45544925ACC62E00A95718 /* Note.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B45544825ACC62E00A95718 /* Note.swift */; }; + 2B45544925ACC62E00A95718 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B45544825ACC62E00A95718 /* AppState.swift */; }; + 2B6D2B9D25B510FC003AE01F /* RingKeyboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B6D2B9C25B510FC003AE01F /* RingKeyboardViewController.swift */; }; + 2B6D2BA225B51140003AE01F /* CGRect+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B6D2BA125B51140003AE01F /* CGRect+.swift */; }; + 2B6D2BA725B5116C003AE01F /* UIColor+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B6D2BA625B5116C003AE01F /* UIColor+.swift */; }; + 2B6D2BAC25B511B6003AE01F /* Bundle+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B6D2BAB25B511B6003AE01F /* Bundle+.swift */; }; + 2B6D2BB425B51BF2003AE01F /* KeyboardContainerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B6D2BB325B51BF2003AE01F /* KeyboardContainerController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -82,7 +87,12 @@ 2B45542725AB735000A95718 /* Yamaha Grand Piano.sf2 */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Yamaha Grand Piano.sf2"; sourceTree = ""; }; 2B45543E25AB8D6C00A95718 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; 2B45544325ABDEA800A95718 /* KeyRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyRowView.swift; sourceTree = ""; }; - 2B45544825ACC62E00A95718 /* Note.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Note.swift; sourceTree = ""; }; + 2B45544825ACC62E00A95718 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; + 2B6D2B9C25B510FC003AE01F /* RingKeyboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RingKeyboardViewController.swift; sourceTree = ""; }; + 2B6D2BA125B51140003AE01F /* CGRect+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+.swift"; sourceTree = ""; }; + 2B6D2BA625B5116C003AE01F /* UIColor+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+.swift"; sourceTree = ""; }; + 2B6D2BAB25B511B6003AE01F /* Bundle+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+.swift"; sourceTree = ""; }; + 2B6D2BB325B51BF2003AE01F /* KeyboardContainerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardContainerController.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -134,11 +144,16 @@ isa = PBXGroup; children = ( 2B45541225AB722300A95718 /* Midi.swift */, - 2B45544825ACC62E00A95718 /* Note.swift */, + 2B45544825ACC62E00A95718 /* AppState.swift */, 2B45540D25AB71CD00A95718 /* GridKeyboardViewController.swift */, 2B45544325ABDEA800A95718 /* KeyRowView.swift */, + 2B6D2B9C25B510FC003AE01F /* RingKeyboardViewController.swift */, 2B45543E25AB8D6C00A95718 /* SettingsViewController.swift */, 2B4553D125AB706500A95718 /* AppDelegate.swift */, + 2B6D2BB325B51BF2003AE01F /* KeyboardContainerController.swift */, + 2B6D2BA125B51140003AE01F /* CGRect+.swift */, + 2B6D2BA625B5116C003AE01F /* UIColor+.swift */, + 2B6D2BAB25B511B6003AE01F /* Bundle+.swift */, 2B4553DA25AB706700A95718 /* Assets.xcassets */, 2B4553DC25AB706700A95718 /* LaunchScreen.storyboard */, 2B4553DF25AB706700A95718 /* Info.plist */, @@ -331,12 +346,17 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2B6D2B9D25B510FC003AE01F /* RingKeyboardViewController.swift in Sources */, 2B45540E25AB71CD00A95718 /* GridKeyboardViewController.swift in Sources */, 2B45541325AB722300A95718 /* Midi.swift in Sources */, + 2B6D2BA225B51140003AE01F /* CGRect+.swift in Sources */, 2B4553D225AB706500A95718 /* AppDelegate.swift in Sources */, 2B45544425ABDEA800A95718 /* KeyRowView.swift in Sources */, + 2B6D2BB425B51BF2003AE01F /* KeyboardContainerController.swift in Sources */, 2B45543F25AB8D6C00A95718 /* SettingsViewController.swift in Sources */, - 2B45544925ACC62E00A95718 /* Note.swift in Sources */, + 2B45544925ACC62E00A95718 /* AppState.swift in Sources */, + 2B6D2BA725B5116C003AE01F /* UIColor+.swift in Sources */, + 2B6D2BAC25B511B6003AE01F /* Bundle+.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -505,6 +525,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = CPE62329FB; INFOPLIST_FILE = GridNotes/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.4; @@ -525,6 +546,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_TEAM = CPE62329FB; INFOPLIST_FILE = GridNotes/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.4; diff --git a/GridNotes/GridNotes/AppDelegate.swift b/GridNotes/GridNotes/AppDelegate.swift index cf4b001..46224f6 100644 --- a/GridNotes/GridNotes/AppDelegate.swift +++ b/GridNotes/GridNotes/AppDelegate.swift @@ -13,11 +13,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + initAudio() window = UIWindow() window?.makeKeyAndVisible() - window?.rootViewController = GridKeyboardViewController() + window?.rootViewController = KeyboardContainerController(initialState: AppState.defaultState) return true } } diff --git a/GridNotes/GridNotes/Note.swift b/GridNotes/GridNotes/AppState.swift similarity index 68% rename from GridNotes/GridNotes/Note.swift rename to GridNotes/GridNotes/AppState.swift index 73c910b..484e959 100644 --- a/GridNotes/GridNotes/Note.swift +++ b/GridNotes/GridNotes/AppState.swift @@ -5,7 +5,49 @@ // Created by Jason Pepas on 1/11/21. // -import Foundation +import UIKit + + +struct AppState { + var interface: Interface = .grid + var tonicNote: Note = .C + var scale: Scale = .major + var octaves: [Octave] + var keysPerOctave: KeysPerOctave + var nonScaleStyle: NonDiatonicKeyStyle = .disabled + var stickyKeys: Bool = false + var stuckKeys: Set = [] + + static var defaultState: AppState { + switch UIDevice.current.userInterfaceIdiom { + case .phone: + return AppState( + octaves: Octave.octavesForPhone, + keysPerOctave: .diatonicKeys + ) + case .pad: + return AppState( + octaves: Octave.octavesForPad, + keysPerOctave: .chromaticKeys + ) + default: + fatalError() + } + } +} + + +enum Interface: String, CaseIterable { + case grid + case ring + + var name: String { + switch self { + case .grid: return "Grid" + case .ring: return "Ring" + } + } +} enum Note: String, CaseIterable, Hashable { @@ -110,6 +152,24 @@ struct AbsoluteNote: Hashable { } } + var buttonText: String { + switch note { + case .A, .B, .C, .D, .E, .F, .G: + return "\(note.rawValue)\(octave.rawValue)" + case .AsBb: + return "A\(octave.rawValue)♯\nB\(octave.rawValue)♭" + case .CsDb: + return "C\(octave.rawValue)♯\nD\(octave.rawValue)♭" + case .DsEb: + return "D\(octave.rawValue)♯\nE\(octave.rawValue)♭" + case .FsGb: + return "F\(octave.rawValue)♯\nG\(octave.rawValue)♭" + case .GsAb: + return "G\(octave.rawValue)♯\nA\(octave.rawValue)♭" + } + } + + var next: AbsoluteNote? { switch note { case .GsAb: @@ -196,6 +256,20 @@ enum Scale: String, CaseIterable { } } + func sparseAbsoluteNotes(fromTonic tonic: AbsoluteNote) -> [AbsoluteNote?] { + var notes: [AbsoluteNote?] = [] + var note: AbsoluteNote? = tonic + for i in 0..<12 { + if semitoneIndices.contains(i) { + notes.append(note) + } else { + notes.append(nil) + } + note = note?.next + } + return notes + } + func absoluteNotes(fromTonic tonic: AbsoluteNote) -> [AbsoluteNote?] { var notes: [AbsoluteNote?] = [] var note: AbsoluteNote? = tonic @@ -208,3 +282,33 @@ enum Scale: String, CaseIterable { return notes } } + + +enum KeysPerOctave: String, CaseIterable { + case chromaticKeys + case diatonicKeys + + var name: String { + switch self { + case .chromaticKeys: + return "Chromatic (all 12 keys)" + case .diatonicKeys: + return "Diatonic (only in-scale keys)" + } + } +} + + +enum NonDiatonicKeyStyle: String, CaseIterable { + case shaded + case disabled + + var name: String { + switch self { + case .shaded: + return "Shaded, but Enabled" + case .disabled: + return "Shaded and Disabled" + } + } +} diff --git a/GridNotes/GridNotes/Bundle+.swift b/GridNotes/GridNotes/Bundle+.swift new file mode 100644 index 0000000..6b1e79b --- /dev/null +++ b/GridNotes/GridNotes/Bundle+.swift @@ -0,0 +1,15 @@ +// +// Bundle+.swift +// GridNotes +// +// Created by Jason Pepas on 1/17/21. +// + +import UIKit + + +extension Bundle { + var marketingVersion: String { + return infoDictionary!["CFBundleShortVersionString"] as! String + } +} diff --git a/GridNotes/GridNotes/CGRect+.swift b/GridNotes/GridNotes/CGRect+.swift new file mode 100644 index 0000000..d015bb3 --- /dev/null +++ b/GridNotes/GridNotes/CGRect+.swift @@ -0,0 +1,22 @@ +// +// CGRect+.swift +// GridNotes +// +// Created by Jason Pepas on 1/17/21. +// + +import UIKit + + +extension CGRect { + static func square(dimension: CGFloat) -> CGRect { + return CGRect(x: 0, y: 0, width: dimension, height: dimension) + } + + func centered(within containingRect: CGRect) -> CGRect { + var centeredRect = self + centeredRect.origin.x = (containingRect.width - width) / 2 + centeredRect.origin.y = (containingRect.height - height) / 2 + return centeredRect + } +} diff --git a/GridNotes/GridNotes/GridKeyboardViewController.swift b/GridNotes/GridNotes/GridKeyboardViewController.swift index 4a7dfa3..1032626 100644 --- a/GridNotes/GridNotes/GridKeyboardViewController.swift +++ b/GridNotes/GridNotes/GridKeyboardViewController.swift @@ -8,89 +8,35 @@ import UIKit -/// The main piano view controller. -class GridKeyboardViewController: UIViewController { - - enum KeysPerOctave: String, CaseIterable { - case chromaticKeys - case diatonicKeys - - var name: String { - switch self { - case .chromaticKeys: - return "Chromatic (all 12 keys)" - case .diatonicKeys: - return "Diatonic (only in-scale keys)" - } - } - } - - enum NonDiatonicKeyStyle: String, CaseIterable { - case shaded - case disabled - - var name: String { - switch self { - case .shaded: - return "Shaded, but Enabled" - case .disabled: - return "Shaded and Disabled" - } - } - } - - struct Model { - var tonicNote: Note - var scale: Scale - var octaves: [Octave] - var keysPerOctave: KeysPerOctave - var nonScaleStyle: NonDiatonicKeyStyle - var stickyKeys: Bool - var stuckKeys: Set - - static var defaultModel: Model { - switch UIDevice.current.userInterfaceIdiom { - case .phone: - return Model( - tonicNote: .C, - scale: .major, - octaves: Octave.octavesForPhone, - keysPerOctave: .diatonicKeys, - nonScaleStyle: .disabled, - stickyKeys: false, - stuckKeys: [] - ) - case .pad: - return Model( - tonicNote: .C, - scale: .major, - octaves: Octave.octavesForPad, - keysPerOctave: .chromaticKeys, - nonScaleStyle: .disabled, - stickyKeys: false, - stuckKeys: [] - ) - default: - fatalError() - } - } - } +/// The grid-layout piano view controller. +class GridKeyboardViewController: UIViewController, InterfaceDelegating { - private(set) var model: Model = Model.defaultModel + private(set) var state: AppState = AppState.defaultState - func set(model: Model) { - self.model = model + func set(state: AppState) { + self.state = state if isViewLoaded { _reconfigureToolbar() _reconfigureKeyRows() } } + public var interfaceDelegate: InterfaceChanging? = nil + + init(state: AppState) { + self.state = state + super.init(nibName: nil, bundle: nil) + } + // MARK: - Internals private var _rows: [KeyRowView] = [] private var _toolbar: UIToolbar! + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func viewDidLoad() { super.viewDidLoad() @@ -115,7 +61,7 @@ class GridKeyboardViewController: UIViewController { _toolbar.bottomAnchor.constraint(equalTo: _rows.first!.topAnchor).isActive = true // stack the key rows vertically. - for i in 0..<(model.octaves.count-1) { + for i in 0..<(state.octaves.count-1) { _rows[i].bottomAnchor.constraint(equalTo: _rows[i+1].topAnchor).isActive = true } @@ -149,7 +95,7 @@ class GridKeyboardViewController: UIViewController { } } - for _ in model.octaves { + for _ in state.octaves { _rows.append(KeyRowView()) } @@ -175,14 +121,14 @@ class GridKeyboardViewController: UIViewController { action: nil ) titleItem.isEnabled = false - titleItem.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.black], for: .disabled) + titleItem.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.darkGray], for: .disabled) items.append(titleItem) items.append( UIBarButtonItem.init(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) ) - if model.stuckKeys.count > 0 { + if state.stuckKeys.count > 0 { items.append( UIBarButtonItem.init( title: "Clear", @@ -199,7 +145,7 @@ class GridKeyboardViewController: UIViewController { items.append( UIBarButtonItem.init( - title: "\(model.tonicNote.name) \(model.scale.name)", + title: "\(state.tonicNote.name) \(state.scale.name)", style: .done, target: self, action: #selector(didPressSettings) @@ -213,20 +159,19 @@ class GridKeyboardViewController: UIViewController { private func _reconfigureKeyRows() { func reconfigureKeyRow(index: Int, octave: Octave) { - let firstNote = AbsoluteNote(note: model.tonicNote, octave: octave) - let allNotes = AbsoluteNote.chromaticScale(from: firstNote) - let scaleIndices = model.scale.semitoneIndices - - let styledNotes: [(AbsoluteNote, KeyRowView.KeyStyle)?] - switch model.keysPerOctave { + let tonicNote = AbsoluteNote(note: state.tonicNote, octave: octave) + let styledNotes: [(AbsoluteNote, KeyStyle)?] + switch state.keysPerOctave { case .chromaticKeys: + let allNotes = AbsoluteNote.chromaticScale(from: tonicNote) + let scaleIndices = state.scale.semitoneIndices styledNotes = allNotes.enumerated().map { (index, note) in if let note = note { if scaleIndices.contains(index) { return (note, .normal) } else { - let keyStyle = KeyRowView.KeyStyle(rawValue: model.nonScaleStyle.rawValue)! + let keyStyle = KeyStyle(rawValue: state.nonScaleStyle.rawValue)! return (note, keyStyle) } } else { @@ -236,21 +181,21 @@ class GridKeyboardViewController: UIViewController { } case .diatonicKeys: - styledNotes = model.scale.absoluteNotes(fromTonic: firstNote).map { note in + styledNotes = state.scale.absoluteNotes(fromTonic: tonicNote).map { note in guard let note = note else { return nil } - return (note, KeyRowView.KeyStyle.normal) + return (note, KeyStyle.normal) } } - let rowModel = KeyRowView.Model(styledNotes: styledNotes, stickyKeys: model.stickyKeys) + let rowModel = KeyRowView.Model(styledNotes: styledNotes, stickyKeys: state.stickyKeys) _rows[index].set(model: rowModel) } - self.model.stuckKeys = [] + self.state.stuckKeys = [] stopPlayingAllNotes() - for (i, octave) in model.octaves.reversed().enumerated() { + for (i, octave) in state.octaves.reversed().enumerated() { reconfigureKeyRow(index: i, octave: octave) } for row in _rows { @@ -271,13 +216,18 @@ class GridKeyboardViewController: UIViewController { /// Action to present the settings screen. @objc func didPressSettings() { - let settingsVC = SettingsViewController() - settingsVC.set(model: model) + didPressClear() + + let settingsVC = SettingsViewController(style: .grouped) + settingsVC.set(state: state) - settingsVC.modelDidChange = { [weak self] model in + settingsVC.appStateDidChange = { [weak self] state in guard let self = self else { return } - self.set(model: model) + self.set(state: state) self.dismissSettings() + if state.interface != .grid { + self.interfaceDelegate?.interfaceDidGetSelected(interface: state.interface, state: state) + } } settingsVC.navigationItem.rightBarButtonItem = UIBarButtonItem( @@ -295,35 +245,28 @@ class GridKeyboardViewController: UIViewController { /// Action for the button which clears all of the stuck keys. @objc func didPressClear() { - for note in model.stuckKeys { + for note in state.stuckKeys { keyDidGetReleased(absoluteNote: note) } _reconfigureKeyRows() } } -extension GridKeyboardViewController: KeyRowDelegate { +extension GridKeyboardViewController: KeyDelegate { func keyDidGetPressed(absoluteNote: AbsoluteNote) { startPlaying(absoluteNote: absoluteNote) - if model.stickyKeys { - model.stuckKeys.insert(absoluteNote) + if state.stickyKeys { + state.stuckKeys.insert(absoluteNote) } _reconfigureToolbar() } func keyDidGetReleased(absoluteNote: AbsoluteNote) { stopPlaying(absoluteNote: absoluteNote) - if model.stickyKeys { - model.stuckKeys.remove(absoluteNote) + if state.stickyKeys { + state.stuckKeys.remove(absoluteNote) } _reconfigureToolbar() } } - - -extension Bundle { - var marketingVersion: String { - return infoDictionary!["CFBundleShortVersionString"] as! String - } -} diff --git a/GridNotes/GridNotes/Info.plist b/GridNotes/GridNotes/Info.plist index 90accbc..faed662 100644 --- a/GridNotes/GridNotes/Info.plist +++ b/GridNotes/GridNotes/Info.plist @@ -17,7 +17,7 @@ CFBundleShortVersionString 1.1 CFBundleVersion - 3 + $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS UIApplicationSupportsIndirectInputEvents @@ -31,7 +31,7 @@ UIRequiresFullScreen UIStatusBarHidden - + UISupportedInterfaceOrientations UIInterfaceOrientationLandscapeLeft diff --git a/GridNotes/GridNotes/KeyRowView.swift b/GridNotes/GridNotes/KeyRowView.swift index 8f455c0..0219c2a 100644 --- a/GridNotes/GridNotes/KeyRowView.swift +++ b/GridNotes/GridNotes/KeyRowView.swift @@ -8,19 +8,20 @@ import UIKit -protocol KeyRowDelegate { +protocol KeyDelegate { func keyDidGetPressed(absoluteNote: AbsoluteNote) func keyDidGetReleased(absoluteNote: AbsoluteNote) } -class KeyRowView: UIView { +enum KeyStyle: String { + case normal + case shaded + case disabled +} - enum KeyStyle: String { - case normal - case shaded - case disabled - } + +class KeyRowView: UIView { struct Model { var styledNotes: [(AbsoluteNote, KeyStyle)?] = [] @@ -32,21 +33,19 @@ class KeyRowView: UIView { func set(model: Model) { self.model = model - _reloadViews() + _apply(model: model) } - var delegate: KeyRowDelegate? = nil + var delegate: KeyDelegate? = nil override init(frame: CGRect) { super.init(frame: frame) translatesAutoresizingMaskIntoConstraints = false - _reloadViews() + _apply(model: model) } // MARK: - Internals - private static let _shadedGray: UIColor = UIColor(white: 0.85, alpha: 1) - private var _keys: [UIButton] = [] required init?(coder: NSCoder) { @@ -92,27 +91,8 @@ class KeyRowView: UIView { private var _hasSetUpConstraints: Bool = false /// (Re)Construct the views according to the model. - private func _reloadViews() { - - func buttonText(absoluteNote: AbsoluteNote) -> String { - let note = absoluteNote.note - let octave = absoluteNote.octave - switch note { - case .A, .B, .C, .D, .E, .F, .G: - return "\(note.rawValue)\(octave.rawValue)" - case .AsBb: - return "A\(octave.rawValue)♯\nB\(octave.rawValue)♭" - case .CsDb: - return "C\(octave.rawValue)♯\nD\(octave.rawValue)♭" - case .DsEb: - return "D\(octave.rawValue)♯\nE\(octave.rawValue)♭" - case .FsGb: - return "F\(octave.rawValue)♯\nG\(octave.rawValue)♭" - case .GsAb: - return "G\(octave.rawValue)♯\nA\(octave.rawValue)♭" - } - } - + private func _apply(model: Model) { + for subview in subviews { subview.removeFromSuperview() } @@ -131,7 +111,7 @@ class KeyRowView: UIView { key.tag = i if let (absoluteNote, keyStyle) = styledNote { - let title = buttonText(absoluteNote: absoluteNote) + let title = absoluteNote.buttonText key.setTitle(title, for: .normal) switch keyStyle { @@ -140,10 +120,10 @@ class KeyRowView: UIView { key.backgroundColor = UIColor.white case .shaded: key.isEnabled = true - key.backgroundColor = KeyRowView._shadedGray + key.backgroundColor = UIColor.shadedKeyGray case .disabled: key.isEnabled = false - key.backgroundColor = KeyRowView._shadedGray + key.backgroundColor = UIColor.shadedKeyGray } if model.stickyKeys { @@ -157,7 +137,7 @@ class KeyRowView: UIView { } else { key.isEnabled = false - key.backgroundColor = KeyRowView._shadedGray + key.backgroundColor = UIColor.shadedKeyGray } _keys.append(key) @@ -170,22 +150,20 @@ class KeyRowView: UIView { // MARK: - Target/Action @objc func keyDidGetPressed(key: UIButton) { - if let (absoluteNote, _) = model.styledNotes[key.tag] { - key.backgroundColor = UIColor.yellow - delegate?.keyDidGetPressed(absoluteNote: absoluteNote) - } + guard let (absoluteNote, _) = model.styledNotes[key.tag] else { return } + key.backgroundColor = UIColor.yellow + delegate?.keyDidGetPressed(absoluteNote: absoluteNote) } @objc func keyDidGetReleased(key: UIButton) { - if let (absoluteNote, keyStyle) = model.styledNotes[key.tag] { - switch keyStyle { - case .normal: - key.backgroundColor = UIColor.white - case .shaded, .disabled: - key.backgroundColor = KeyRowView._shadedGray - } - delegate?.keyDidGetReleased(absoluteNote: absoluteNote) + guard let (absoluteNote, keyStyle) = model.styledNotes[key.tag] else { return } + switch keyStyle { + case .normal: + key.backgroundColor = UIColor.white + case .shaded, .disabled: + key.backgroundColor = UIColor.shadedKeyGray } + delegate?.keyDidGetReleased(absoluteNote: absoluteNote) } @objc func keyDidGetToggled(key: UIButton) { @@ -196,7 +174,7 @@ class KeyRowView: UIView { case .normal: key.backgroundColor = UIColor.white case .shaded, .disabled: - key.backgroundColor = KeyRowView._shadedGray + key.backgroundColor = UIColor.shadedKeyGray } delegate?.keyDidGetReleased(absoluteNote: absoluteNote) } else { diff --git a/GridNotes/GridNotes/KeyboardContainerController.swift b/GridNotes/GridNotes/KeyboardContainerController.swift new file mode 100644 index 0000000..d195596 --- /dev/null +++ b/GridNotes/GridNotes/KeyboardContainerController.swift @@ -0,0 +1,82 @@ +// +// KeyboardContainerController.swift +// GridNotes +// +// Created by Jason Pepas on 1/17/21. +// + +import UIKit + + +protocol InterfaceChanging { + func interfaceDidGetSelected(interface: Interface, state: AppState) +} + +protocol InterfaceDelegating { + var interfaceDelegate: InterfaceChanging? { get set } +} + + +class KeyboardContainerController: UIViewController, InterfaceChanging { + + var childController: (UIViewController & InterfaceDelegating) + + func interfaceDidGetSelected(interface: Interface, state: AppState) { + switch interface { + case .grid: + if type(of: childController) != GridKeyboardViewController.self { + let child = GridKeyboardViewController(state: state) + _swapToNewChildVC(child: child) + } + case .ring: + if type(of: childController) != RingKeyboardViewController.self { + let child = RingKeyboardViewController(state: state) + _swapToNewChildVC(child: child) + } + } + } + + init(initialState: AppState) { + switch initialState.interface { + case .grid: + childController = GridKeyboardViewController(state: initialState) + case .ring: + childController = RingKeyboardViewController(state: initialState) + } + super.init(nibName: nil, bundle: nil) + childController.interfaceDelegate = self + } + + // MARK: - Internals + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.addSubview(childController.view) + addChild(childController) + childController.didMove(toParent: self) + } + + private func _swapToNewChildVC(child newChild: UIViewController & InterfaceDelegating) { + childController.willMove(toParent: nil) + childController.removeFromParent() + childController.view.removeFromSuperview() + + view.addSubview(newChild.view) + view.topAnchor.constraint(equalTo: newChild.view.topAnchor).isActive = true + view.leadingAnchor.constraint(equalTo: newChild.view.leadingAnchor).isActive = true + view.trailingAnchor.constraint(equalTo: newChild.view.trailingAnchor).isActive = true + view.bottomAnchor.constraint(equalTo: newChild.view.bottomAnchor).isActive = true + addChild(newChild) + newChild.didMove(toParent: self) + childController = newChild + childController.interfaceDelegate = self + } + + override var prefersStatusBarHidden: Bool { + return true + } +} diff --git a/GridNotes/GridNotes/RingKeyboardViewController.swift b/GridNotes/GridNotes/RingKeyboardViewController.swift new file mode 100644 index 0000000..745b85c --- /dev/null +++ b/GridNotes/GridNotes/RingKeyboardViewController.swift @@ -0,0 +1,398 @@ +// +// RingKeyboardViewController.swift +// GridNotes +// +// Created by Jason Pepas on 1/17/21. +// + +import UIKit + + +/// The ring-layout piano view controller. +class RingKeyboardViewController: UIViewController, InterfaceDelegating { + + var state: AppState + + func set(state: AppState) { + self.state = state + _apply(state: state) + } + + var interfaceDelegate: InterfaceChanging? = nil + + init(state: AppState) { + self.state = state + super.init(nibName: nil, bundle: nil) + } + + // MARK: - Internals + + private let _ringView: RingKeyboardView = RingKeyboardView() + private let _settingsButton: UIButton = UIButton(type: .system) + private let _clearButton: UIButton = UIButton(type: .system) + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = UIColor.white + + // configure the ring view. + _ringView.delegate = self + _ringView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(_ringView) + view.topAnchor.constraint(equalTo: _ringView.topAnchor).isActive = true + view.leadingAnchor.constraint(equalTo: _ringView.leadingAnchor).isActive = true + view.trailingAnchor.constraint(equalTo: _ringView.trailingAnchor).isActive = true + view.bottomAnchor.constraint(equalTo: _ringView.bottomAnchor).isActive = true + + // configure the app label. + let appNameLabel: UILabel = UILabel() + appNameLabel.textColor = UIColor.darkGray + appNameLabel.text = "GridNotes \(Bundle.main.marketingVersion)" + appNameLabel.font = UIFont.boldSystemFont(ofSize: appNameLabel.font.pointSize) + appNameLabel.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(appNameLabel) + // pin to upper left of screen. + appNameLabel.topAnchor.constraint(equalToSystemSpacingBelow: view.topAnchor, multiplier: 1).isActive = true + appNameLabel.leadingAnchor.constraint(equalToSystemSpacingAfter: view.leadingAnchor, multiplier: 1).isActive = true + + // configure the settings button. + _settingsButton.titleLabel!.font = UIFont.boldSystemFont(ofSize: _settingsButton.titleLabel!.font.pointSize) + _settingsButton.addTarget(self, action: #selector(didPressSettings), for: .touchUpInside) + _settingsButton.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(_settingsButton) + // pin to upper right of screen. + _settingsButton.topAnchor.constraint(equalToSystemSpacingBelow: view.topAnchor, multiplier: 1).isActive = true + view.trailingAnchor.constraint(equalToSystemSpacingAfter: _settingsButton.trailingAnchor, multiplier: 1).isActive = true + + // configure the clear button. + _clearButton.setTitle("Clear", for: .normal) + _clearButton.titleLabel!.font = UIFont.boldSystemFont(ofSize: _clearButton.titleLabel!.font.pointSize) + _clearButton.addTarget(self, action: #selector(didPressClear), for: .touchUpInside) + _clearButton.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(_clearButton) + // pin to lower left of screen. + view.bottomAnchor.constraint(equalToSystemSpacingBelow: _clearButton.bottomAnchor, multiplier: 1).isActive = true + _clearButton.leadingAnchor.constraint(equalToSystemSpacingAfter: view.leadingAnchor, multiplier: 1).isActive = true + + _apply(state: state) + } + + override var prefersStatusBarHidden: Bool { + return true + } + + private func _apply(state: AppState) { + _settingsButton.setTitle("\(state.tonicNote.name) \(state.scale.name)", for: .normal) + _reconfigureClearButton() + + let tonicNote = AbsoluteNote(note: state.tonicNote, octave: .four) + let styledNotes: [(AbsoluteNote, KeyStyle)?] + switch state.keysPerOctave { + + case .chromaticKeys: + let allNotes = AbsoluteNote.chromaticScale(from: tonicNote) + let scaleIndices = state.scale.semitoneIndices + styledNotes = allNotes.enumerated().map { (index, note) in + if let note = note { + if scaleIndices.contains(index) { + return (note, .normal) + } else { + let keyStyle = KeyStyle(rawValue: state.nonScaleStyle.rawValue)! + return (note, keyStyle) + } + } else { + return nil + } + + } + + case .diatonicKeys: + styledNotes = state.scale.sparseAbsoluteNotes(fromTonic: tonicNote).map { note in + guard let note = note else { return nil } + return (note, KeyStyle.normal) + } + + } + + let model: RingKeyboardView.Model = RingKeyboardView.Model( + styledNotes: styledNotes, + stickyKeys: state.stickyKeys, + stuckKeys: state.stuckKeys + ) + _ringView.set(model: model) + } + + private func _reconfigureClearButton() { + let shouldShowClearButton = state.stickyKeys && state.stuckKeys.count > 0 + _clearButton.isHidden = !shouldShowClearButton + } + + // MARK: - Target/Action + + @objc func didPressSettings() { + didPressClear() + + let settingsVC = SettingsViewController(style: .grouped) + settingsVC.set(state: state) + + settingsVC.appStateDidChange = { [weak self] state in + guard let self = self else { return } + self.set(state: state) + self.dismissSettings() + if state.interface != .ring { + self.interfaceDelegate?.interfaceDidGetSelected(interface: state.interface, state: state) + } + } + + settingsVC.navigationItem.rightBarButtonItem = UIBarButtonItem( + barButtonSystemItem: .done, + target: self, + action: #selector(dismissSettings) + ) + let nav = UINavigationController(rootViewController: settingsVC) + present(nav, animated: true) + } + + @objc func dismissSettings() { + dismiss(animated: true, completion: nil) + } + + @objc func didPressClear() { + for note in state.stuckKeys { + keyDidGetReleased(absoluteNote: note) + } + _apply(state: state) + } +} + +extension RingKeyboardViewController: KeyDelegate { + + func keyDidGetPressed(absoluteNote: AbsoluteNote) { + startPlaying(absoluteNote: absoluteNote) + if state.stickyKeys { + state.stuckKeys.insert(absoluteNote) + } + _reconfigureClearButton() + } + + func keyDidGetReleased(absoluteNote: AbsoluteNote) { + stopPlaying(absoluteNote: absoluteNote) + if state.stickyKeys { + state.stuckKeys.remove(absoluteNote) + } + _reconfigureClearButton() + } +} + + +class RingKeyboardView: UIView { + + struct Model { + var styledNotes: [(AbsoluteNote, KeyStyle)?] = [] + var stickyKeys: Bool = false + var stuckKeys: Set = [] + } + + var model: Model = Model(styledNotes: []) + + func set(model: Model) { + self.model = model + _apply(model: model) + } + + var delegate: KeyDelegate? = nil + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = UIColor.white + _apply(model: model) + } + + // MARK: - Internals + + private var _keys: [UIButton] = [] + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private var _buttonRadius: CGFloat { + return min(bounds.width, bounds.height) * 0.085 + } + + private var _buttonFontSize: CGFloat { + return round(min(bounds.width, bounds.height) * 0.03) + } + + private var _buttonCenterlineDiameter: CGFloat { + let padding: CGFloat = 16 + return min(bounds.width, bounds.height) - (padding * 2) - (_buttonRadius * 2) + } + + private var _semitoneTickmarkLength: CGFloat { + return min(bounds.width, bounds.height) * 0.075 + } + + private func _tickmarkAngle(index: Int) -> CGFloat { + return CGFloat.pi / 6 * CGFloat(-index) + CGFloat.pi + } + + override func draw(_ rect: CGRect) { + + func drawButtonCenterlineCircle() { + let circleRect: CGRect = CGRect + .square(dimension: _buttonCenterlineDiameter) + .centered(within: rect) + let path = UIBezierPath(ovalIn: circleRect) + path.lineWidth = round(_semitoneTickmarkLength * 0.1) + UIColor.darkGray.setStroke() + path.stroke() + } + + func drawSemitoneTickmarks() { + + func tickmarkStart(index: Int) -> CGPoint { + let centerToTickmarkStartRadius: CGFloat = (_buttonCenterlineDiameter / 2) - (_semitoneTickmarkLength / 2) + let x: CGFloat = center.x + sin(_tickmarkAngle(index: index)) * centerToTickmarkStartRadius + let y: CGFloat = center.y + cos(_tickmarkAngle(index: index)) * centerToTickmarkStartRadius + return CGPoint(x: x, y: y) + } + + func tickmarkEnd(index: Int) -> CGPoint { + let centerToTickmarkEndRadius: CGFloat = (_buttonCenterlineDiameter / 2) + (_semitoneTickmarkLength / 2) + let x: CGFloat = center.x + sin(_tickmarkAngle(index: index)) * centerToTickmarkEndRadius + let y: CGFloat = center.y + cos(_tickmarkAngle(index: index)) * centerToTickmarkEndRadius + return CGPoint(x: x, y: y) + } + + for i in 0..<12 { + let path = UIBezierPath() + path.lineWidth = round(_semitoneTickmarkLength * 0.05) + path.move(to: tickmarkStart(index: i)) + path.addLine(to: tickmarkEnd(index: i)) + UIColor.darkGray.setStroke() + path.stroke() + } + } + + drawButtonCenterlineCircle() + drawSemitoneTickmarks() + } + + override func layoutSubviews() { + super.layoutSubviews() + + func buttonCenter(index: Int) -> CGPoint { + let centerToButtonCenterRadius: CGFloat = _buttonCenterlineDiameter / 2 + let x: CGFloat = center.x + sin(_tickmarkAngle(index: index)) * centerToButtonCenterRadius + let y: CGFloat = center.y + cos(_tickmarkAngle(index: index)) * centerToButtonCenterRadius + return CGPoint(x: x, y: y) + } + + for k in _keys { + k.frame = CGRect(x: 0, y: 0, width: _buttonRadius * 2, height: _buttonRadius * 2) + k.center = buttonCenter(index: k.tag) + k.layer.cornerRadius = _buttonRadius + k.layer.borderWidth = round(_semitoneTickmarkLength * 0.05) + if UIDevice.current.userInterfaceIdiom == .pad { + k.titleLabel?.font = UIFont.systemFont(ofSize: _buttonFontSize) + } + } + } + + private func _apply(model: Model) { + for k in _keys { + k.removeFromSuperview() + } + _keys.removeAll() + + for (i, pair) in model.styledNotes.enumerated() { + let key = UIButton(type: .system) + key.translatesAutoresizingMaskIntoConstraints = false + key.backgroundColor = UIColor.white + key.layer.borderColor = UIColor.darkGray.cgColor + key.titleLabel?.adjustsFontSizeToFitWidth = true + key.contentEdgeInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4) + key.titleLabel?.lineBreakMode = .byWordWrapping + key.titleLabel?.textAlignment = .center + key.tag = i + + switch pair { + + case let .some((absoluteNote, style)): + key.setTitle(absoluteNote.buttonText, for: .normal) + + switch style { + case .normal: + key.isEnabled = true + key.backgroundColor = UIColor.white + case .shaded: + key.isEnabled = true + key.backgroundColor = UIColor.shadedKeyGray + case .disabled: + key.isEnabled = false + key.backgroundColor = UIColor.shadedKeyGray + } + addSubview(key) + _keys.append(key) + + if model.stickyKeys { + key.addTarget(self, action: #selector(keyDidGetToggled(key:)), for: .touchDown) + } else { + key.addTarget(self, action: #selector(keyDidGetPressed(key:)), for: .touchDown) + key.addTarget(self, action: #selector(keyDidGetReleased(key:)), for: .touchUpInside) + key.addTarget(self, action: #selector(keyDidGetReleased(key:)), for: .touchDragExit) + key.addTarget(self, action: #selector(keyDidGetReleased(key:)), for: .touchCancel) + } + + case .none: + key.isEnabled = false + key.backgroundColor = UIColor.shadedKeyGray + } + } + + setNeedsLayout() + } + + // MARK: - Target/Action + + @objc func keyDidGetPressed(key: UIButton) { + guard let (absoluteNote, _) = model.styledNotes[key.tag] else { return } + key.backgroundColor = UIColor.yellow + delegate?.keyDidGetPressed(absoluteNote: absoluteNote) + } + + @objc func keyDidGetReleased(key: UIButton) { + guard let (absoluteNote, keyStyle) = model.styledNotes[key.tag] else { return } + switch keyStyle { + case .normal: + key.backgroundColor = UIColor.white + case .shaded, .disabled: + key.backgroundColor = UIColor.shadedKeyGray + } + delegate?.keyDidGetReleased(absoluteNote: absoluteNote) + } + + @objc func keyDidGetToggled(key: UIButton) { + if let (absoluteNote, keyStyle) = model.styledNotes[key.tag] { + if model.stuckKeys.contains(absoluteNote) { + model.stuckKeys.remove(absoluteNote) + switch keyStyle { + case .normal: + key.backgroundColor = UIColor.white + case .shaded, .disabled: + key.backgroundColor = UIColor.shadedKeyGray + } + delegate?.keyDidGetReleased(absoluteNote: absoluteNote) + } else { + model.stuckKeys.insert(absoluteNote) + key.backgroundColor = UIColor.yellow + delegate?.keyDidGetPressed(absoluteNote: absoluteNote) + } + } + } +} diff --git a/GridNotes/GridNotes/SettingsViewController.swift b/GridNotes/GridNotes/SettingsViewController.swift index cd94450..e0be965 100644 --- a/GridNotes/GridNotes/SettingsViewController.swift +++ b/GridNotes/GridNotes/SettingsViewController.swift @@ -10,16 +10,16 @@ import UIKit class SettingsViewController: UITableViewController { - private(set) var model: GridKeyboardViewController.Model = GridKeyboardViewController.Model.defaultModel + private(set) var state: AppState = AppState.defaultState - func set(model: GridKeyboardViewController.Model) { - self.model = model + func set(state: AppState) { + self.state = state if isViewLoaded { tableView.reloadData() } } - var modelDidChange: ((GridKeyboardViewController.Model) -> ())? = nil + var appStateDidChange: ((AppState) -> ())? = nil // MARK: - Internals @@ -29,6 +29,7 @@ class SettingsViewController: UITableViewController { private let _octaveKeysSection: Int = 3 private let _stickySection: Int = 4 private let _instrumentSection: Int = 5 + private let _interfaceSection: Int = 6 override func viewDidLoad() { super.viewDidLoad() @@ -40,7 +41,7 @@ class SettingsViewController: UITableViewController { // MARK: - UITableViewDelegate / UITableViewDataSource override func numberOfSections(in tableView: UITableView) -> Int { - return 6 + return 7 } override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { @@ -57,6 +58,8 @@ class SettingsViewController: UITableViewController { return "Sticky Keys" case _instrumentSection: return "Instrument (Fluid R3 SoundFont)" + case _interfaceSection: + return "Interface" default: fatalError() } @@ -69,13 +72,15 @@ class SettingsViewController: UITableViewController { case _scaleSection: return Scale.allCases.count case _nonDiatonicSection: - return GridKeyboardViewController.NonDiatonicKeyStyle.allCases.count + return NonDiatonicKeyStyle.allCases.count case _octaveKeysSection: - return GridKeyboardViewController.KeysPerOctave.allCases.count + return KeysPerOctave.allCases.count case _stickySection: return 2 case _instrumentSection: return Instrument.allCases.count + case _interfaceSection: + return Interface.allCases.count default: fatalError() } @@ -84,7 +89,8 @@ class SettingsViewController: UITableViewController { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath) switch indexPath.section { - case _tonicSection, _scaleSection, _nonDiatonicSection, _octaveKeysSection, _stickySection, _instrumentSection: + case _tonicSection, _scaleSection, _nonDiatonicSection, _octaveKeysSection, _stickySection, + _instrumentSection, _interfaceSection: _style(cell: cell, indexPath: indexPath) default: fatalError() @@ -96,19 +102,21 @@ class SettingsViewController: UITableViewController { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { switch indexPath.section { case _tonicSection: - model.tonicNote = Note.allCases[indexPath.row] + state.tonicNote = Note.allCases[indexPath.row] case _scaleSection: - model.scale = Scale.allCases[indexPath.row] + state.scale = Scale.allCases[indexPath.row] case _nonDiatonicSection: - model.nonScaleStyle = GridKeyboardViewController.NonDiatonicKeyStyle.allCases[indexPath.row] + state.nonScaleStyle = NonDiatonicKeyStyle.allCases[indexPath.row] case _octaveKeysSection: - model.keysPerOctave = GridKeyboardViewController.KeysPerOctave.allCases[indexPath.row] + state.keysPerOctave = KeysPerOctave.allCases[indexPath.row] case _stickySection: - model.stickyKeys = (indexPath.row == 0) + state.stickyKeys = (indexPath.row == 0) case _instrumentSection: deinitAudio() g_instrument = Instrument.allCases[indexPath.row] initAudio() + case _interfaceSection: + state.interface = Interface.allCases[indexPath.row] default: fatalError() } @@ -117,7 +125,7 @@ class SettingsViewController: UITableViewController { for iterated in tableView.indexPathsForSelectedRows ?? [] { tableView.deselectRow(at: iterated, animated: true) } - modelDidChange?(model) + appStateDidChange?(state) } private func _style(cell: UITableViewCell, indexPath: IndexPath) { @@ -126,31 +134,36 @@ class SettingsViewController: UITableViewController { case _tonicSection: let note = Note.allCases[indexPath.row] - isSelected = model.tonicNote == note + isSelected = state.tonicNote == note cell.textLabel?.text = note.name case _scaleSection: let scale = Scale.allCases[indexPath.row] - isSelected = model.scale == scale + isSelected = state.scale == scale cell.textLabel?.text = scale.name case _nonDiatonicSection: - let style = GridKeyboardViewController.NonDiatonicKeyStyle.allCases[indexPath.row] - isSelected = model.nonScaleStyle == style + let style = NonDiatonicKeyStyle.allCases[indexPath.row] + isSelected = state.nonScaleStyle == style cell.textLabel?.text = style.name case _octaveKeysSection: - let keyCount = GridKeyboardViewController.KeysPerOctave.allCases[indexPath.row] - isSelected = model.keysPerOctave == keyCount + let keyCount = KeysPerOctave.allCases[indexPath.row] + isSelected = state.keysPerOctave == keyCount cell.textLabel?.text = keyCount.name case _stickySection: - isSelected = (model.stickyKeys && indexPath.row == 0) || (!model.stickyKeys && indexPath.row == 1) + isSelected = (state.stickyKeys && indexPath.row == 0) || (!state.stickyKeys && indexPath.row == 1) cell.textLabel?.text = (indexPath.row == 0) ? "Enabled" : "Disabled" case _instrumentSection: isSelected = g_instrument == Instrument.allCases[indexPath.row] cell.textLabel?.text = Instrument.allCases[indexPath.row].displayName + + case _interfaceSection: + let interface = Interface.allCases[indexPath.row] + isSelected = state.interface == interface + cell.textLabel?.text = interface.name default: fatalError() @@ -169,7 +182,8 @@ class SettingsViewController: UITableViewController { for indexPath in tableView.indexPathsForVisibleRows ?? [] { guard let cell = tableView.cellForRow(at: indexPath) else { continue } switch indexPath.section { - case _tonicSection, _scaleSection, _nonDiatonicSection, _octaveKeysSection, _stickySection, _instrumentSection: + case _tonicSection, _scaleSection, _nonDiatonicSection, _octaveKeysSection, _stickySection, + _instrumentSection, _interfaceSection: _style(cell: cell, indexPath: indexPath) default: fatalError() diff --git a/GridNotes/GridNotes/UIColor+.swift b/GridNotes/GridNotes/UIColor+.swift new file mode 100644 index 0000000..789a0e9 --- /dev/null +++ b/GridNotes/GridNotes/UIColor+.swift @@ -0,0 +1,15 @@ +// +// UIColor+.swift +// GridNotes +// +// Created by Jason Pepas on 1/17/21. +// + +import UIKit + + +extension UIColor { + static var shadedKeyGray: UIColor { + return UIColor(white: 0.85, alpha: 1) + } +} diff --git a/README.md b/README.md index 74da3ee..1ad07ea 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A piano with a grid layout (iOS) ### 1.1 (unreleased) +- Implement "Ring" keyboard layout. - Use AVAudioSession `.mixWithOthers` option, which allows the piano to play over the audio of other apps. This allows GridNotes to be used in conjunction with e.g. a metronome app or while listening to a backing track on YouTube. - Remove Sine Wave sound font (all of the notes were at the wrong pitch).