Repository: saoudrizwan/Piano Branch: master Commit: c508659fea5a Files: 36 Total size: 127.6 KB Directory structure: gitextract_7isrf40a/ ├── .gitignore ├── .swift-version ├── Example/ │ ├── PianoExample/ │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Base.lproj/ │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ ├── ResponsiveLabel.swift │ │ ├── Sounds.xcassets/ │ │ │ ├── Contents.json │ │ │ ├── heart.dataset/ │ │ │ │ └── Contents.json │ │ │ ├── kiss.dataset/ │ │ │ │ └── Contents.json │ │ │ └── wink.dataset/ │ │ │ └── Contents.json │ │ └── ViewController.swift │ └── PianoExample.xcodeproj/ │ ├── project.pbxproj │ └── project.xcworkspace/ │ └── contents.xcworkspacedata ├── LICENSE ├── Piano.podspec ├── Piano.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ └── contents.xcworkspacedata │ └── xcshareddata/ │ └── xcschemes/ │ └── Piano.xcscheme ├── Piano.xcworkspace/ │ └── contents.xcworkspacedata ├── README.md ├── Sources/ │ ├── Audio.swift │ ├── HapticFeedback.swift │ ├── Info.plist │ ├── Note.swift │ ├── Piano+Error.swift │ ├── Piano.h │ ├── Piano.swift │ ├── SystemSound.swift │ ├── TapticEngine.swift │ ├── UIDevice+Extension.swift │ └── Vibration.swift └── Tests/ ├── Info.plist └── PianoTests.swift ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ ## OS X Finder .DS_Store ## Build generated build/ DerivedData ## Various settings *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata/ ## Other *.moved-aside *.xcuserstate *.xcscmblueprint ## Obj-C/Swift specific *.hmap *.ipa *.dSYM.zip *.dSYM # Swift Package Manager .build/ ================================================ FILE: .swift-version ================================================ 4.2 ================================================ FILE: Example/PianoExample/AppDelegate.swift ================================================ // // AppDelegate.swift // PianoExample // // Created by Saoud Rizwan on 9/11/17. // Copyright © 2017 Saoud Rizwan. All rights reserved. // import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. return true } func applicationWillResignActive(_ application: UIApplication) { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. } func applicationDidEnterBackground(_ application: UIApplication) { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. } func applicationWillEnterForeground(_ application: UIApplication) { // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. } func applicationDidBecomeActive(_ application: UIApplication) { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. } func applicationWillTerminate(_ application: UIApplication) { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } } ================================================ FILE: Example/PianoExample/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "20x20", "idiom" : "iphone", "filename" : "Icon-App-20x20@3x.png", "scale" : "3x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "iphone", "filename" : "Icon-App-29x29@3x.png", "scale" : "3x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "iphone", "filename" : "Icon-App-40x40@3x.png", "scale" : "3x" }, { "size" : "57x57", "idiom" : "iphone", "filename" : "Icon-App-57x57@1x.png", "scale" : "1x" }, { "size" : "57x57", "idiom" : "iphone", "filename" : "Icon-App-57x57@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@2x.png", "scale" : "2x" }, { "size" : "60x60", "idiom" : "iphone", "filename" : "Icon-App-60x60@3x.png", "scale" : "3x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@1x.png", "scale" : "1x" }, { "size" : "20x20", "idiom" : "ipad", "filename" : "Icon-App-20x20@2x.png", "scale" : "2x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@1x.png", "scale" : "1x" }, { "size" : "29x29", "idiom" : "ipad", "filename" : "Icon-App-29x29@2x.png", "scale" : "2x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@1x.png", "scale" : "1x" }, { "size" : "40x40", "idiom" : "ipad", "filename" : "Icon-App-40x40@2x.png", "scale" : "2x" }, { "size" : "50x50", "idiom" : "ipad", "filename" : "Icon-Small-50x50@1x.png", "scale" : "1x" }, { "size" : "50x50", "idiom" : "ipad", "filename" : "Icon-Small-50x50@2x.png", "scale" : "2x" }, { "size" : "72x72", "idiom" : "ipad", "filename" : "Icon-App-72x72@1x.png", "scale" : "1x" }, { "size" : "72x72", "idiom" : "ipad", "filename" : "Icon-App-72x72@2x.png", "scale" : "2x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@1x.png", "scale" : "1x" }, { "size" : "76x76", "idiom" : "ipad", "filename" : "Icon-App-76x76@2x.png", "scale" : "2x" }, { "size" : "83.5x83.5", "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", "scale" : "2x" }, { "size" : "1024x1024", "idiom" : "ios-marketing", "filename" : "PianoAppIcon.png", "scale" : "1x" } ], "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/PianoExample/Assets.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/PianoExample/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: Example/PianoExample/Base.lproj/Main.storyboard ================================================ ================================================ FILE: Example/PianoExample/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName Piano CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString 1.0 CFBundleVersion 1 LSRequiresIPhoneOS UILaunchStoryboardName LaunchScreen UIMainStoryboardFile Main UIRequiredDeviceCapabilities armv7 UISupportedInterfaceOrientations UIInterfaceOrientationPortrait UISupportedInterfaceOrientations~ipad UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight ================================================ FILE: Example/PianoExample/ResponsiveLabel.swift ================================================ // // ResponsiveLabel.swift // PianoExample // // Created by Saoud Rizwan on 10/1/18. // Copyright © 2018 Saoud Rizwan. All rights reserved. // import UIKit class ResponsiveLabel: UILabel { override var canBecomeFirstResponder: Bool { return true } } ================================================ FILE: Example/PianoExample/Sounds.xcassets/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" } } ================================================ FILE: Example/PianoExample/Sounds.xcassets/heart.dataset/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" }, "data" : [ { "idiom" : "universal", "filename" : "Heavy Black Heart.wav" } ] } ================================================ FILE: Example/PianoExample/Sounds.xcassets/kiss.dataset/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" }, "data" : [ { "idiom" : "universal", "filename" : "Kiss Mark.wav" } ] } ================================================ FILE: Example/PianoExample/Sounds.xcassets/wink.dataset/Contents.json ================================================ { "info" : { "version" : 1, "author" : "xcode" }, "data" : [ { "idiom" : "universal", "filename" : "Winking Face.wav" } ] } ================================================ FILE: Example/PianoExample/ViewController.swift ================================================ // // ViewController.swift // PianoExample // // Created by Saoud Rizwan on 9/11/17. // Copyright © 2017 Saoud Rizwan. All rights reserved. // import UIKit import Piano class ViewController: UIViewController { @IBOutlet weak var label: ResponsiveLabel! @IBOutlet weak var toolBar: UIToolbar! @IBOutlet weak var tableView: UITableView! let cellData: [(title: String, rows: [(title: String, note: 🎹.Note)])] = { let sections = ["", "", "Vibration", "Taptic Engine", "Haptic Feedback - Notification", "Haptic Feedback - Impact", "Haptic Feedback - Selection", "Sound - Assets Example", "Sound - File Example", "Sound - URL Example", "Sound - System Predefined"] var rows = [[(title: String, note: 🎹.Note)]]() for i in 0.. for sound in sounds { rows[10].append((title: ".sound(.system(.\(sound))", note: .sound(.system(sound)))) } */ rows[10].removeSubrange(0..<3) // remove the first three we created as an example default: break } } var data = [(title: String, rows: [(title: String, note: 🎹.Note)])]() for i in 0.. 0 { label.textColor = UIColor(red: 69.0/255.0, green: 241.0/255.0, blue: 126.0/255.0, alpha: 1.0) 🎹.play(notesToPlay) { self.label.textColor = UIColor.black } } } func cellTapped(indexPath: IndexPath) { let data = cellData[indexPath.section].rows[indexPath.row] switch data.note { case .wait, .waitUntilFinished: break default: 🎹.play([data.note]) } } @objc func cellAddButtonTapped(sender: UIButton) { waitTextField.resignFirstResponder() guard let cell = sender.superview as? UITableViewCell, let indexPath = tableView.indexPath(for: cell) else { return } let data = cellData[indexPath.section].rows[indexPath.row] switch data.note { case .wait: let waitNote = 🎹.Note.wait(waitValue ?? 0) let waitString = ".wait(\(waitValue ?? 0.0))" notesToPlay.append(waitNote) notesToPlayAsStrings.append(waitString) default: notesToPlay.append(data.note) notesToPlayAsStrings.append(data.title) } setPianoStringToNotes() } func setPianoStringToNotes() { var entireCommandAsString = "" var numberOfLines = 0 for i in 0.. Int { return cellData.count } func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { var title = cellData[section].title let unsupported = " (UNSUPPORTED)" switch section { case 3: // Taptic Engine if !UIDevice.current.hasTapticEngine { title.append(unsupported) } case 4, 5, 6: // Haptic Feedback if !UIDevice.current.hasHapticFeedback { title.append(unsupported) } default: break } return title } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return cellData[section].rows.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "cellId") ?? UITableViewCell(style: .value1, reuseIdentifier: "cellId") let data = cellData[indexPath.section].rows[indexPath.row] cell.textLabel?.text = data.title let addButton = UIButton(type: .contactAdd) addButton.addTarget(self, action: #selector(cellAddButtonTapped), for: .touchUpInside) cell.accessoryView = addButton if indexPath.section == 0 { // Wait cell.textLabel?.text = "" cell.contentView.addSubview(waitTextField) NSLayoutConstraint.activate([ waitTextField.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: 20), waitTextField.topAnchor.constraint(equalTo: cell.contentView.topAnchor), waitTextField.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -20), waitTextField.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor) ]) waitTextField.setContentHuggingPriority(UILayoutPriority.defaultLow, for: .vertical) let one = ".wait(" let two: String = (waitValue == nil) ? "tap to input seconds" : "\(waitValue!)" let three = ")" let attributedString = NSMutableAttributedString(string: one + two + three) attributedString.addAttributes([.foregroundColor: UIColor.black], range: NSRange(location: 0, length: one.count)) attributedString.addAttributes([.foregroundColor: UIColor.lightGray], range: NSRange(location: one.count, length: two.count)) attributedString.addAttributes([.foregroundColor: UIColor.black], range: NSRange(location: one.count + two.count, length: three.count)) waitTextField.attributedText = attributedString } else { if cell.contentView.subviews.contains(waitTextField) { waitTextField.removeFromSuperview() } } cell.selectionStyle = (indexPath.section == 0 || indexPath.section == 1) ? .none : .default return cell } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 45 } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { cellTapped(indexPath: indexPath) tableView.deselectRow(at: indexPath, animated: true) } } extension ViewController: UITextFieldDelegate { func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { if textField == waitTextField { let text = (textField.text ?? "") as NSString var newString = text.replacingCharacters(in: range, with: string) newString = newString.replacingOccurrences(of: ".wait(", with: "") newString = newString.replacingOccurrences(of: ".wait", with: "") newString = newString.replacingOccurrences(of: ")", with: "") newString = newString.replacingOccurrences(of: "tap to input seconds", with: "") newString = newString.replacingOccurrences(of: " ", with: "") waitValue = TimeInterval(newString) newString = ".wait(" + newString + ")" let attributedString = NSMutableAttributedString(string: newString) attributedString.addAttributes([.foregroundColor: UIColor.black], range: NSRange(location: 0, length: newString.count)) waitTextField.attributedText = attributedString if let newPosition = textField.position(from: textField.endOfDocument, offset: -1) { textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition) } return false } else { return true } } func textFieldDidBeginEditing(_ textField: UITextField) { if textField == waitTextField { let string = ".wait(" + ((waitValue == nil) ? "" : "\(waitValue!)") + ")" let attributedString = NSMutableAttributedString(string: string) attributedString.addAttributes([.foregroundColor: UIColor.black], range: NSRange(location: 0, length: string.count)) waitTextField.attributedText = attributedString if let newPosition = textField.position(from: textField.endOfDocument, offset: -1) { textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition) } } } func textFieldDidEndEditing(_ textField: UITextField) { if textField == waitTextField { let one = ".wait(" let two: String = (waitValue == nil) ? "tap to input seconds" : "\(waitValue!)" let three = ")" let attributedString = NSMutableAttributedString(string: one + two + three) attributedString.addAttributes([.foregroundColor: UIColor.black], range: NSRange(location: 0, length: one.count)) attributedString.addAttributes([.foregroundColor: UIColor.gray], range: NSRange(location: one.count, length: two.count)) attributedString.addAttributes([.foregroundColor: UIColor.black], range: NSRange(location: one.count + two.count, length: three.count)) waitTextField.attributedText = attributedString } } } extension ViewController: UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { if let touchedView = touch.view, touchedView.isKind(of: UIControl.self) { return false } else { return true } } } extension ViewController { @objc private func keyboardWillShow(notification: NSNotification) { let waitCellIndexPath = IndexPath(row: 0, section: 0) if let visibleIndexPaths = tableView.indexPathsForVisibleRows, visibleIndexPaths.contains(waitCellIndexPath) { tableView.scrollToRow(at: waitCellIndexPath, at: UITableViewScrollPosition.middle, animated: true) } } } ================================================ FILE: Example/PianoExample.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 48; objects = { /* Begin PBXBuildFile section */ 105A40E11F675C690078BAA6 /* harp.wav in Resources */ = {isa = PBXBuildFile; fileRef = 10CCC0E51F6738010085294A /* harp.wav */; }; 105A40E21F675C690078BAA6 /* joy.wav in Resources */ = {isa = PBXBuildFile; fileRef = 105A40E01F6756970078BAA6 /* joy.wav */; }; 106A373C1F66EE0200BF5BD1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 106A373B1F66EE0200BF5BD1 /* AppDelegate.swift */; }; 106A373E1F66EE0200BF5BD1 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 106A373D1F66EE0200BF5BD1 /* ViewController.swift */; }; 106A37411F66EE0200BF5BD1 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 106A373F1F66EE0200BF5BD1 /* Main.storyboard */; }; 106A37431F66EE0200BF5BD1 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 106A37421F66EE0200BF5BD1 /* Assets.xcassets */; }; 106A37461F66EE0200BF5BD1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 106A37441F66EE0200BF5BD1 /* LaunchScreen.storyboard */; }; 10CCC0E01F6733CD0085294A /* Piano.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 10CCC0DF1F6733C70085294A /* Piano.framework */; }; 10CCC0E11F6733CD0085294A /* Piano.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 10CCC0DF1F6733C70085294A /* Piano.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 10CCC0E41F6737730085294A /* Sounds.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 10CCC0E31F6737730085294A /* Sounds.xcassets */; }; DA4BC02E2162AB77006C5ADF /* ResponsiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4BC02D2162AB77006C5ADF /* ResponsiveLabel.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ 10CCC0D01F6728FA0085294A /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 10; files = ( 10CCC0E11F6733CD0085294A /* Piano.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 105A40E01F6756970078BAA6 /* joy.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = joy.wav; sourceTree = ""; }; 106A37381F66EE0200BF5BD1 /* PianoExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PianoExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; 106A373B1F66EE0200BF5BD1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 106A373D1F66EE0200BF5BD1 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 106A37401F66EE0200BF5BD1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 106A37421F66EE0200BF5BD1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 106A37451F66EE0200BF5BD1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 106A37471F66EE0200BF5BD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 10CCC0DF1F6733C70085294A /* Piano.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Piano.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 10CCC0E31F6737730085294A /* Sounds.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Sounds.xcassets; sourceTree = ""; }; 10CCC0E51F6738010085294A /* harp.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = harp.wav; sourceTree = ""; }; DA4BC02D2162AB77006C5ADF /* ResponsiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponsiveLabel.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 106A37351F66EE0200BF5BD1 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 10CCC0E01F6733CD0085294A /* Piano.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 106A372F1F66EE0200BF5BD1 = { isa = PBXGroup; children = ( 106A373A1F66EE0200BF5BD1 /* PianoExample */, 106A37391F66EE0200BF5BD1 /* Products */, 10CCC0C61F6712630085294A /* Frameworks */, ); sourceTree = ""; }; 106A37391F66EE0200BF5BD1 /* Products */ = { isa = PBXGroup; children = ( 106A37381F66EE0200BF5BD1 /* PianoExample.app */, ); name = Products; sourceTree = ""; }; 106A373A1F66EE0200BF5BD1 /* PianoExample */ = { isa = PBXGroup; children = ( 10CCC0E21F67350C0085294A /* IGNORE */, 10CCC0E31F6737730085294A /* Sounds.xcassets */, 10CCC0E51F6738010085294A /* harp.wav */, 105A40E01F6756970078BAA6 /* joy.wav */, 106A373D1F66EE0200BF5BD1 /* ViewController.swift */, ); path = PianoExample; sourceTree = ""; }; 10CCC0C61F6712630085294A /* Frameworks */ = { isa = PBXGroup; children = ( 10CCC0DF1F6733C70085294A /* Piano.framework */, ); name = Frameworks; sourceTree = ""; }; 10CCC0E21F67350C0085294A /* IGNORE */ = { isa = PBXGroup; children = ( 106A37421F66EE0200BF5BD1 /* Assets.xcassets */, 106A373B1F66EE0200BF5BD1 /* AppDelegate.swift */, 106A373F1F66EE0200BF5BD1 /* Main.storyboard */, 106A37441F66EE0200BF5BD1 /* LaunchScreen.storyboard */, 106A37471F66EE0200BF5BD1 /* Info.plist */, DA4BC02D2162AB77006C5ADF /* ResponsiveLabel.swift */, ); name = IGNORE; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 106A37371F66EE0200BF5BD1 /* PianoExample */ = { isa = PBXNativeTarget; buildConfigurationList = 106A374A1F66EE0200BF5BD1 /* Build configuration list for PBXNativeTarget "PianoExample" */; buildPhases = ( 106A37341F66EE0200BF5BD1 /* Sources */, 106A37351F66EE0200BF5BD1 /* Frameworks */, 106A37361F66EE0200BF5BD1 /* Resources */, 10CCC0D01F6728FA0085294A /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( ); name = PianoExample; productName = PianoExample; productReference = 106A37381F66EE0200BF5BD1 /* PianoExample.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 106A37301F66EE0200BF5BD1 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0900; LastUpgradeCheck = 0900; ORGANIZATIONNAME = "Saoud Rizwan"; TargetAttributes = { 106A37371F66EE0200BF5BD1 = { CreatedOnToolsVersion = 9.0; }; }; }; buildConfigurationList = 106A37331F66EE0200BF5BD1 /* Build configuration list for PBXProject "PianoExample" */; compatibilityVersion = "Xcode 8.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 106A372F1F66EE0200BF5BD1; productRefGroup = 106A37391F66EE0200BF5BD1 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 106A37371F66EE0200BF5BD1 /* PianoExample */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 106A37361F66EE0200BF5BD1 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 105A40E11F675C690078BAA6 /* harp.wav in Resources */, 105A40E21F675C690078BAA6 /* joy.wav in Resources */, 106A37461F66EE0200BF5BD1 /* LaunchScreen.storyboard in Resources */, 106A37431F66EE0200BF5BD1 /* Assets.xcassets in Resources */, 106A37411F66EE0200BF5BD1 /* Main.storyboard in Resources */, 10CCC0E41F6737730085294A /* Sounds.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 106A37341F66EE0200BF5BD1 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 106A373E1F66EE0200BF5BD1 /* ViewController.swift in Sources */, 106A373C1F66EE0200BF5BD1 /* AppDelegate.swift in Sources */, DA4BC02E2162AB77006C5ADF /* ResponsiveLabel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXVariantGroup section */ 106A373F1F66EE0200BF5BD1 /* Main.storyboard */ = { isa = PBXVariantGroup; children = ( 106A37401F66EE0200BF5BD1 /* Base */, ); name = Main.storyboard; sourceTree = ""; }; 106A37441F66EE0200BF5BD1 /* LaunchScreen.storyboard */ = { isa = PBXVariantGroup; children = ( 106A37451F66EE0200BF5BD1 /* Base */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ 106A37481F66EE0200BF5BD1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/**"; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; 106A37491F66EE0200BF5BD1 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/**"; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; VALIDATE_PRODUCT = YES; }; name = Release; }; 106A374B1F66EE0200BF5BD1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = LR7NY5NPR9; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/**"; INFOPLIST_FILE = PianoExample/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = saoudrizwan.PianoExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 106A374C1F66EE0200BF5BD1 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; DEVELOPMENT_TEAM = LR7NY5NPR9; FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/**"; INFOPLIST_FILE = PianoExample/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = saoudrizwan.PianoExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 106A37331F66EE0200BF5BD1 /* Build configuration list for PBXProject "PianoExample" */ = { isa = XCConfigurationList; buildConfigurations = ( 106A37481F66EE0200BF5BD1 /* Debug */, 106A37491F66EE0200BF5BD1 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 106A374A1F66EE0200BF5BD1 /* Build configuration list for PBXNativeTarget "PianoExample" */ = { isa = XCConfigurationList; buildConfigurations = ( 106A374B1F66EE0200BF5BD1 /* Debug */, 106A374C1F66EE0200BF5BD1 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 106A37301F66EE0200BF5BD1 /* Project object */; } ================================================ FILE: Example/PianoExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2017 Saoud Rizwan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Piano.podspec ================================================ Pod::Spec.new do |s| s.name = "Piano" s.version = "1.8" s.summary = "Compose a symphony of sounds and vibrations with Taptic Engine" s.description = <<-DESC Piano is a delightful and easy-to-use wrapper around the AVFoundation and UIHapticFeedback classes, leveraging the full capabilities of the Taptic Engine, while following strict Apple guidelines to preserve battery life. Ultimately, Piano allows you, the composer, to conduct masterful symphonies of sounds and vibrations, and create a more immersive, usable and meaningful user experience in your app or game. DESC s.homepage = "https://github.com/saoudrizwan/Piano" s.license = { :type => "MIT", :file => "LICENSE" } s.author = { "Saoud Rizwan" => "hello@saoudmr.com" } s.social_media_url = "https://twitter.com/sdrzn" s.platform = :ios, "10.0" s.source = { :git => "https://github.com/saoudrizwan/Piano.git", :tag => "#{s.version}" } s.source_files = "Sources/**/*.{h,m,swift}" end ================================================ FILE: Piano.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 48; objects = { /* Begin PBXBuildFile section */ 106A36E81F66ECC300BF5BD1 /* Piano.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 106A36DE1F66ECC300BF5BD1 /* Piano.framework */; }; 106A36ED1F66ECC300BF5BD1 /* PianoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 106A36EC1F66ECC300BF5BD1 /* PianoTests.swift */; }; 106A36EF1F66ECC300BF5BD1 /* Piano.h in Headers */ = {isa = PBXBuildFile; fileRef = 106A36E11F66ECC300BF5BD1 /* Piano.h */; settings = {ATTRIBUTES = (Public, ); }; }; 106A36F91F66ECCF00BF5BD1 /* Piano.swift in Sources */ = {isa = PBXBuildFile; fileRef = 106A36F81F66ECCF00BF5BD1 /* Piano.swift */; }; 10CCC0E71F6747FA0085294A /* SystemSound.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CCC0E61F6747FA0085294A /* SystemSound.swift */; }; 10CCC0E91F6748340085294A /* Audio.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CCC0E81F6748340085294A /* Audio.swift */; }; 10CCC0EB1F67484C0085294A /* Vibration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CCC0EA1F67484C0085294A /* Vibration.swift */; }; 10CCC0ED1F6748600085294A /* TapticEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CCC0EC1F6748600085294A /* TapticEngine.swift */; }; 10CCC0EF1F6748760085294A /* HapticFeedback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CCC0EE1F6748760085294A /* HapticFeedback.swift */; }; 10CCC0F11F67488D0085294A /* Note.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CCC0F01F67488D0085294A /* Note.swift */; }; 10CCC0F31F6748E20085294A /* Piano+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CCC0F21F6748E20085294A /* Piano+Error.swift */; }; 10CCC0F51F6749FB0085294A /* UIDevice+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10CCC0F41F6749FB0085294A /* UIDevice+Extension.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 106A36E91F66ECC300BF5BD1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 106A36D51F66ECC300BF5BD1 /* Project object */; proxyType = 1; remoteGlobalIDString = 106A36DD1F66ECC300BF5BD1; remoteInfo = Piano; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 106A36DE1F66ECC300BF5BD1 /* Piano.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Piano.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 106A36E11F66ECC300BF5BD1 /* Piano.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Piano.h; sourceTree = ""; }; 106A36E21F66ECC300BF5BD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 106A36E71F66ECC300BF5BD1 /* PianoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PianoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 106A36EC1F66ECC300BF5BD1 /* PianoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PianoTests.swift; sourceTree = ""; }; 106A36EE1F66ECC300BF5BD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 106A36F81F66ECCF00BF5BD1 /* Piano.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Piano.swift; sourceTree = ""; }; 10CCC0E61F6747FA0085294A /* SystemSound.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemSound.swift; sourceTree = ""; }; 10CCC0E81F6748340085294A /* Audio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Audio.swift; sourceTree = ""; }; 10CCC0EA1F67484C0085294A /* Vibration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vibration.swift; sourceTree = ""; }; 10CCC0EC1F6748600085294A /* TapticEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapticEngine.swift; sourceTree = ""; }; 10CCC0EE1F6748760085294A /* HapticFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticFeedback.swift; sourceTree = ""; }; 10CCC0F01F67488D0085294A /* Note.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Note.swift; sourceTree = ""; }; 10CCC0F21F6748E20085294A /* Piano+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Piano+Error.swift"; sourceTree = ""; }; 10CCC0F41F6749FB0085294A /* UIDevice+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extension.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 106A36DA1F66ECC300BF5BD1 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 106A36E41F66ECC300BF5BD1 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 106A36E81F66ECC300BF5BD1 /* Piano.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 106A36D41F66ECC300BF5BD1 = { isa = PBXGroup; children = ( 106A36E01F66ECC300BF5BD1 /* Sources */, 106A36EB1F66ECC300BF5BD1 /* Tests */, 106A36DF1F66ECC300BF5BD1 /* Products */, ); sourceTree = ""; }; 106A36DF1F66ECC300BF5BD1 /* Products */ = { isa = PBXGroup; children = ( 106A36DE1F66ECC300BF5BD1 /* Piano.framework */, 106A36E71F66ECC300BF5BD1 /* PianoTests.xctest */, ); name = Products; sourceTree = ""; }; 106A36E01F66ECC300BF5BD1 /* Sources */ = { isa = PBXGroup; children = ( 106A36E21F66ECC300BF5BD1 /* Info.plist */, 106A36E11F66ECC300BF5BD1 /* Piano.h */, 106A36F81F66ECCF00BF5BD1 /* Piano.swift */, 10CCC0F21F6748E20085294A /* Piano+Error.swift */, 10CCC0F01F67488D0085294A /* Note.swift */, 10CCC0E61F6747FA0085294A /* SystemSound.swift */, 10CCC0E81F6748340085294A /* Audio.swift */, 10CCC0EA1F67484C0085294A /* Vibration.swift */, 10CCC0EC1F6748600085294A /* TapticEngine.swift */, 10CCC0EE1F6748760085294A /* HapticFeedback.swift */, 10CCC0F41F6749FB0085294A /* UIDevice+Extension.swift */, ); path = Sources; sourceTree = ""; }; 106A36EB1F66ECC300BF5BD1 /* Tests */ = { isa = PBXGroup; children = ( 106A36EC1F66ECC300BF5BD1 /* PianoTests.swift */, 106A36EE1F66ECC300BF5BD1 /* Info.plist */, ); path = Tests; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ 106A36DB1F66ECC300BF5BD1 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 106A36EF1F66ECC300BF5BD1 /* Piano.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ 106A36DD1F66ECC300BF5BD1 /* Piano */ = { isa = PBXNativeTarget; buildConfigurationList = 106A36F21F66ECC300BF5BD1 /* Build configuration list for PBXNativeTarget "Piano" */; buildPhases = ( 106A36D91F66ECC300BF5BD1 /* Sources */, 106A36DA1F66ECC300BF5BD1 /* Frameworks */, 106A36DB1F66ECC300BF5BD1 /* Headers */, 106A36DC1F66ECC300BF5BD1 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = Piano; productName = Piano; productReference = 106A36DE1F66ECC300BF5BD1 /* Piano.framework */; productType = "com.apple.product-type.framework"; }; 106A36E61F66ECC300BF5BD1 /* PianoTests */ = { isa = PBXNativeTarget; buildConfigurationList = 106A36F51F66ECC300BF5BD1 /* Build configuration list for PBXNativeTarget "PianoTests" */; buildPhases = ( 106A36E31F66ECC300BF5BD1 /* Sources */, 106A36E41F66ECC300BF5BD1 /* Frameworks */, 106A36E51F66ECC300BF5BD1 /* Resources */, ); buildRules = ( ); dependencies = ( 106A36EA1F66ECC300BF5BD1 /* PBXTargetDependency */, ); name = PianoTests; productName = PianoTests; productReference = 106A36E71F66ECC300BF5BD1 /* PianoTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 106A36D51F66ECC300BF5BD1 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0900; LastUpgradeCheck = 0900; ORGANIZATIONNAME = "Saoud Rizwan"; TargetAttributes = { 106A36DD1F66ECC300BF5BD1 = { CreatedOnToolsVersion = 9.0; LastSwiftMigration = 0900; }; 106A36E61F66ECC300BF5BD1 = { CreatedOnToolsVersion = 9.0; ProvisioningStyle = Manual; }; }; }; buildConfigurationList = 106A36D81F66ECC300BF5BD1 /* Build configuration list for PBXProject "Piano" */; compatibilityVersion = "Xcode 8.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, ); mainGroup = 106A36D41F66ECC300BF5BD1; productRefGroup = 106A36DF1F66ECC300BF5BD1 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 106A36DD1F66ECC300BF5BD1 /* Piano */, 106A36E61F66ECC300BF5BD1 /* PianoTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 106A36DC1F66ECC300BF5BD1 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 106A36E51F66ECC300BF5BD1 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 106A36D91F66ECC300BF5BD1 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 10CCC0F31F6748E20085294A /* Piano+Error.swift in Sources */, 10CCC0EB1F67484C0085294A /* Vibration.swift in Sources */, 10CCC0E91F6748340085294A /* Audio.swift in Sources */, 10CCC0F11F67488D0085294A /* Note.swift in Sources */, 10CCC0F51F6749FB0085294A /* UIDevice+Extension.swift in Sources */, 10CCC0ED1F6748600085294A /* TapticEngine.swift in Sources */, 10CCC0E71F6747FA0085294A /* SystemSound.swift in Sources */, 106A36F91F66ECCF00BF5BD1 /* Piano.swift in Sources */, 10CCC0EF1F6748760085294A /* HapticFeedback.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 106A36E31F66ECC300BF5BD1 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 106A36ED1F66ECC300BF5BD1 /* PianoTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 106A36EA1F66ECC300BF5BD1 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 106A36DD1F66ECC300BF5BD1 /* Piano */; targetProxy = 106A36E91F66ECC300BF5BD1 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ 106A36F01F66ECC300BF5BD1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; 106A36F11F66ECC300BF5BD1 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; 106A36F31F66ECC300BF5BD1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = saoudrizwan.Piano; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 106A36F41F66ECC300BF5BD1 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = ""; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = ""; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/Sources/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = saoudrizwan.Piano; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; 106A36F61F66ECC300BF5BD1 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = 5M795QY47C; INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = saoudrizwan.PianoTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 106A36F71F66ECC300BF5BD1 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Manual; DEVELOPMENT_TEAM = 5M795QY47C; INFOPLIST_FILE = Tests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = saoudrizwan.PianoTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 106A36D81F66ECC300BF5BD1 /* Build configuration list for PBXProject "Piano" */ = { isa = XCConfigurationList; buildConfigurations = ( 106A36F01F66ECC300BF5BD1 /* Debug */, 106A36F11F66ECC300BF5BD1 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 106A36F21F66ECC300BF5BD1 /* Build configuration list for PBXNativeTarget "Piano" */ = { isa = XCConfigurationList; buildConfigurations = ( 106A36F31F66ECC300BF5BD1 /* Debug */, 106A36F41F66ECC300BF5BD1 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 106A36F51F66ECC300BF5BD1 /* Build configuration list for PBXNativeTarget "PianoTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 106A36F61F66ECC300BF5BD1 /* Debug */, 106A36F71F66ECC300BF5BD1 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 106A36D51F66ECC300BF5BD1 /* Project object */; } ================================================ FILE: Piano.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Piano.xcodeproj/xcshareddata/xcschemes/Piano.xcscheme ================================================ ================================================ FILE: Piano.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: README.md ================================================

Piano

Platform: iOS 10.0+ Language: Swift 4 CocoaPods compatible Carthage compatible License: MIT

InstallationUsageDocumentationWhy I Built PianoLicenseContributeQuestions?Credits

Piano is a **convenient** and **easy-to-use** wrapper around the `AVFoundation` and `UIHapticFeedback` frameworks, leveraging the full capabilities of the **Taptic Engine**, while following strict Apple guidelines to **preserve battery life**. Ultimately, Piano allows you, the composer, to conduct masterful symphonies of sounds and vibrations, and create a more immersive, usable and meaningful user experience in your app or game. ## Compatibility Piano requires **iOS 10+** and is compatible with **Swift 4.2** projects. ## Installation * Installation for CocoaPods: ```ruby platform :ios, '10.0' target 'ProjectName' do use_frameworks! pod 'Piano', '~> 1.8' end ``` *(if you run into problems, `pod repo update` and try again)* * Installation for Carthage: ```ruby github "saoudrizwan/Piano" ``` *(make sure Xcode 10 is [set as your system's default Xcode](https://stackoverflow.com/a/28901378/3502608) before using CocoaPods or Carthage with Swift 4 frameworks)* * Or embed the Piano framework into your project And `import Piano` in the files you'd like to use it. ## Usage Using Piano is simple. ```swift let symphony: [Piano.Note] = [ .sound(.asset(name: "acapella")), .hapticFeedback(.impact(.light)), .waitUntilFinished, .hapticFeedback(.impact(.heavy)), .wait(0.2), .sound(.system(.chooChoo)) ] Piano.play(symphony) ``` ... or better yet: ```swift 🎹.play([ .sound(.asset(name: "acapella")) ]) ``` Optionally add a completion block to be called when all the notes are finished playing: ```swift 🎹.play([ .sound(.asset(name: "acapella")) ]) { // ... } ``` Or cancel the currently playing symphony: ```swift 🎹.cancel() ``` In the background, each note has an internal completion block, so you can add a `.waitUntilFinished` note that tells Piano to not play the next note until the previous note is done playing. This is useful for creating patterns of custom haptic feedback, besides the ones Apple predefined. This is also great for creating complex combinations of sound effects and vibrations. ### Notes #### `.sound(Audio)` Plays an audio file. |Audio | | |------------ | ------------- | |`.asset(name: String)` | Name of asset in any .xcassets catalogs. It's recommended to add your sound files to Asset Catalogs instead of as standalone files to your main bundle.| |`.file(name: String, extension: String)` | Retrieves a file from the main bundle. For example a file named `Beep.wav` would be accessed with `.file(name: "Beep", extension: "wav")`.| |`.url(URL)` | This only works for file URLs, not network URLs.| |`.system(SystemSound)` | Predefined system sounds in every iPhone. [See all available options here](https://github.com/saoudrizwan/Piano/blob/master/Sources/SystemSound.swift). | #### `.vibration(Vibration)` Plays standard vibrations available on all models of the iPhone. |Vibration | | |------------ | -------------| |`.default` | Basic 1-second vibration | |`.alert` | Two short consecutive vibrations | #### `.tapticEngine(TapticEngine)` Plays Taptic Engine vibrations available on the iPhone 6S and above. |TapticEngine | | | ------------ | ------------- | |`.peek` | One weak boom | |`.pop` | One strong boom | |`.cancelled` | Three sequential weak booms | |`.tryAgain` | One weak boom then one strong boom | |`.failed` | Three sequential strong booms | #### `.hapticFeedback(HapticFeedback)` Plays Taptic Engine Haptic Feedback available on the iPhone 7 and above. |HapticFeedback | | | |------------ | ------------- |------------- | |`.notification(Notification)` | **Notification** | Communicate that a task or action has succeeded, failed, or produced a warning of some kind. | | | `.success` | Indicates that a task or action has completed successfully. | | | `.warning` | Indicates that a task or action has produced a warning. | | | `.failure` | Indicates that a task or action has failed. | |`.impact(Impact)` | **Impact** | Indicates that an impact has occurred. For example, you might trigger impact feedback when a user interface object collides with something or snaps into place. | | | `.light` | Provides a physical metaphor representing a collision between small, light user interface elements.| | | `.medium` | Provides a physical metaphor representing a collision between moderately sized user interface elements.| | | `.heavy` | Provides a physical metaphor representing a collision between large, heavy user interface elements.| |`.selection` | | Indicates that the selection is actively changing. For example, the user feels light taps while scrolling a picker wheel.| See: [Apple's Guidelines for using Haptic Feedback](https://developer.apple.com/ios/human-interface-guidelines/user-interaction/feedback/) #### `.waitUntilFinished` Tells Piano to wait until the previous note is done playing before playing the next note. #### `.wait(TimeInterval)` Tells Piano to wait a given duration before playing the next note. ### Device Capabilities * The iPhone 6S and 6S Plus carry the first generation of Taptic Engine which has a few "haptic" vibration patterns, which you can play with Piano using the `.tapticEngine()` notes. * The iPhone 7 and above carry the latest version of the Taptic Engine which supports the iOS 10 Haptic Feedback frameworks, allowing you to select from many more vibration types. You can play these vibrations using the `.hapticFeedback()` notes. * All versions of the iPhone can play the `.vibration()` notes. Piano also includes a useful extension for `UIDevice` to check if the user's device has a Taptic Engine and if it supports Haptic Feedback. This extension is especially useful for creating symphonies for all devices: ```swift if UIDevice.current.hasHapticFeedback { // use .hapticFeedback(HapticFeedback) notes } else if UIDevice.current.hasTapticEngine { // use .tapticEngine(TapticEngine) notes } else { // use .vibration(Vibration) notes } ``` **Note:** This extension does not work on simulators, it will always return false. ### Taptic Engine Guide Apple's [guide over the Haptic Feedback framework](https://developer.apple.com/documentation/uikit/uifeedbackgenerator) is very clear about using the Taptic Engine appropriately in order to prevent draining the user's device's battery life. Piano was built with this in mind, and handles most cases as efficiently as possible. But you can help preserve battery life and reduce latency further by calling these helper methods based on your specific needs. #### 1. Wake up the Taptic Engine ```swift Piano.wakeTapticEngine() ``` This initializes and allocates the Haptic Feedback framework and essentially "wakes up" the Taptic Engine, as it is normally in an idle state. A good place to put this is at the begin state of a gesture or action, in anticipation of playing a `.hapticFeedback()` note. #### 2. Prepare the Taptic Engine ```swift Piano.prepareTapticEngine() ``` This tells the Taptic Engine to prepare itself before creating any feedback to reduce latency when triggering feedback. From Apple's [documentation](https://developer.apple.com/documentation/uikit/uifeedbackgenerator): > This is particularly important when trying to match feedback to sound or visual cues. To preserve power, the Taptic Engine stays in this prepared state for only a short period of time (on the order of seconds), or until you next trigger feedback. Think about when and where you can best prepare your generators. If you call prepare and then immediately trigger feedback, the system won’t have enough time to get the Taptic Engine into the prepared state, and you may not see a reduction in latency. On the other hand, if you call prepare too early, the Taptic Engine may become idle again before you trigger feedback. tl;dr A good place to put this is right after calling `.wakeTapticEngine()`, usually at the beginning of a gesture or action, in anticipation of playing a `.hapticFeedback()` note. #### 3. Put the Taptic Engine back to Sleep ```swift Piano.putTapticEngineToSleep() ``` Once we know we're done using the Taptic Engine, we can deallocate the Haptic Feedback framework, returning the Taptic Engine to its idle state. A good place to put this is at the end of a finished, cancelled, or failed gesture or action. #### But you don't have to. Piano automatically wakes and prepares the Taptic Engine when you call `.play([ ... ])` if it includes a `.hapticFeedback()` note, and returns the Taptic Engine back to sleep when the notes are done playing. ### The Example App The [example app](https://github.com/saoudrizwan/Piano/tree/master/Example) is a great place to get started. It's designed as a playground for you to compose and test out your own symphonies of sounds and vibrations.

Piano

You can even drag and drop your own sound files into the project and tweak the code a bit to see how your own sounds can work alongside the Taptic Engine. To add your own sound file, simply drag it into `Sounds.xcassets`, name it accordingly, then edit the `cellData` property in `ViewController.swift` (Scroll down to `case 7` in `cellData`, or look for "Add your own sound assets here..." in the Jump Bar using `Ctrl + 6`). ## Documentation Option + click on any of Piano's methods or notes for detailed documentation. documentation ## Why I Built Piano With the new iPhone 8 and iPhone X, we are going to see many new Augmented Reality apps, and one of the keypoints in the [Human Interface Guidelines for AR](https://developer.apple.com/ios/human-interface-guidelines/technologies/augmented-reality/) is to not clutter the AR view, allowing as much content from the augmented reality to be displayed as possible. Besides AR, Apple has spent tremendous time and manpower giving the iPhone an interface beyond our vision with the Taptic Engine and Siri. Apple even had a [session during WWDC 2017](https://developer.apple.com/videos/play/wwdc2017/803/) talking about the importance of sound design and the impact it can have on a user experience. It's obvious that the future of technology is not visual interfaces, but augmenting our connection with the real world. By using our physical, auditory, and most importantly visual senses, we can see the world in a whole new light. That's why I built Piano and [ARLogger](https://github.com/saoudrizwan/ARLogger), frameworks I hope will help developers create immersive and uncluttered interfaces, while keeping the user aware of the technology's state and purpose. If you'd like my help on an AR project, or just want to chat about the future of technology, don't hesitate to reach out to me on Twitter [@sdrzn](http://twitter.com/sdrzn). ## License Piano uses the MIT license. Please file an issue if you have any questions or if you'd like to share how you're using Piano. ## Contribute Please feel free to create issues for feature requests or send pull requests of any additions you think would complement Piano and its philosophy. ## Questions? Contact me by email hello@saoudmr.com, or by twitter @sdrzn. Please create an issue if you come across a bug or would like a feature to be added. ## Credits * Example app sound files from [Icons 8 UI Sounds](https://icons8.com/sounds) * Music notes in README header image from [LSE Design on the Noun Project](https://thenounproject.com/LSEdesigns/collection/music-notes/) ================================================ FILE: Sources/Audio.swift ================================================ // The MIT License (MIT) // // Copyright (c) 2018 Saoud Rizwan // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation @available(iOS 10.0, *) extension Piano { /// Audio file to play public enum Audio { /// Name of asset in any .xcassets catalogs case asset(name: String) /// Searches main bundle for file with given name and extension case file(name: String, extension: String) /// URL of audio file case url(URL) /// Predefined system sound included in all iPhones case system(SystemSound) } } ================================================ FILE: Sources/HapticFeedback.swift ================================================ // The MIT License (MIT) // // Copyright (c) 2018 Saoud Rizwan // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation @available(iOS 10.0, *) extension Piano { /// Second Generation Taptic Engine vibration options public enum HapticFeedback { /// Use notification feedback to communicate that a task or action has succeeded, failed, or produced a warning of some kind. case notification(Notification) public enum Notification { /// Indicates that a task or action, such as depositing a check or unlocking a vehicle, has completed. case success /// Indicates that a task or action, such as depositing a check or unlocking a vehicle, has produced a warning of some kind. case warning /// Indicates that a task or action, such as depositing a check or unlocking a vehicle, has failed. case failure } /// Use impact feedback generators to indicate that an impact has occurred. For example, you might trigger impact feedback when a user interface object collides with something or snaps into place. case impact(Impact) public enum Impact { /// Provides a physical metaphor representing a collision between small, light user interface elements. For example, the user might feel a thud when a view slides into place or two objects collide. case light /// Provides a physical metaphor representing a collision between moderately sized user interface elements. For example, the user might feel a thud when a view slides into place or two objects collide. case medium /// Provides a physical metaphor representing a collision between large, heavy user interface elements. For example, the user might feel a thud when a view slides into place or two objects collide. case heavy } /// Indicates that the selection is actively changing. For example, the user feels light taps while scrolling a picker wheel. This feedback is intended for communicating movement through a series of discrete values, not making or confirming a selection. case selection } } ================================================ FILE: Sources/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType FMWK CFBundleShortVersionString 1.8 CFBundleVersion $(CURRENT_PROJECT_VERSION) NSPrincipalClass ================================================ FILE: Sources/Note.swift ================================================ // The MIT License (MIT) // // Copyright (c) 2018 Saoud Rizwan // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation @available(iOS 10.0, *) extension Piano { /// Sound, feedback, vibration, or pause for Piano to play public enum Note { /// Audio file to play case sound(Audio) /// Standard vibrations available on all models of the iPhone case vibration(Vibration) /// First generation Taptic Engine vibrations case tapticEngine(TapticEngine) /// Second Generation Taptic Engine vibrations case hapticFeedback(HapticFeedback) /// Tells Piano to wait until the previous note is done playing before playing the next note case waitUntilFinished /// Tells Piano to wait a given duration before playing the next note case wait(TimeInterval) } } ================================================ FILE: Sources/Piano+Error.swift ================================================ // The MIT License (MIT) // // Copyright (c) 2018 Saoud Rizwan // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation @available(iOS 10.0, *) extension Piano { /// Possible errors when trying to play notes public enum PianoError: Error { case notFound(String) case couldNotPlay(String) } /// Currently, printing the errors in console is the most friendly way to handle them func handle(error: Error) { if let error = error as? PianoError { switch error { case .notFound(let name): print("🎹 Piano could not find \(name)!") case .couldNotPlay(let name): print("🎹 Piano could not play \(name)!") } } else { let error = error as NSError print(""" 🎹 Piano encountered an error! Domain: \(error.domain) Code: \(error.code) Description: \(error.localizedDescription) Failure Reason: \(error.localizedFailureReason ?? "") Suggestions: \(error.localizedRecoverySuggestion ?? "") """) } } } ================================================ FILE: Sources/Piano.h ================================================ // // Piano.h // Piano // // Created by Saoud Rizwan on 9/11/17. // Copyright © 2017 Saoud Rizwan. All rights reserved. // #import //! Project version number for Piano. FOUNDATION_EXPORT double PianoVersionNumber; //! Project version string for Piano. FOUNDATION_EXPORT const unsigned char PianoVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import ================================================ FILE: Sources/Piano.swift ================================================ // The MIT License (MIT) // // Copyright (c) 2018 Saoud Rizwan // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation import AudioToolbox.AudioServices import AVFoundation @available(iOS 10.0, *) public typealias 🎹 = Piano /// Piano /// /// Compose a symphony of sounds and vibrations using Taptic Engine @available(iOS 10.0, *) public class Piano { /// Internal instance of Piano to manage shared feedback generators and symphony trackers private static let `default` = Piano() /// Allocatable/deallocatable tuple of UIFeedbackGenerators (Apple recommended) private var feedbackGenerator: (notification: UINotificationFeedbackGenerator?, impact: (light: UIImpactFeedbackGenerator?, medium: UIImpactFeedbackGenerator?, heavy: UIImpactFeedbackGenerator?), selection: UISelectionFeedbackGenerator?) = (nil, (nil, nil, nil), nil) private var player: AVAudioPlayer? /// Keeps track of multiple symphonies, preventing multiple symphonies from being played at once private var symphonyCounter = 0 /// Holds all the scheduled Timers with music private var timers = [Timer]() private init() { } /// Wakes the Taptic Engine up from an idle state public static func wakeTapticEngine() { if Piano.default.feedbackGenerator.notification == nil { Piano.default.feedbackGenerator = (notification: UINotificationFeedbackGenerator(), impact: (light: UIImpactFeedbackGenerator(style: .light), medium: UIImpactFeedbackGenerator(style: .medium), heavy: UIImpactFeedbackGenerator(style: .heavy)), selection: UISelectionFeedbackGenerator()) } } /// This tells the Taptic Engine to prepare itself before creating any feedback to reduce latency when triggering feedback. You can call this as many times as you want, preferrably right before playing a .hapticFeedback note. /// /// Apple docs: /// When you call this method, the generator is placed into a prepared state for a short period of time. While the generator is prepared, you can trigger feedback with lower latency. /// Think about when you can best prepare your generators. Call prepare() before the event that triggers feedback. The system needs time to prepare the Taptic Engine for minimal latency. Calling prepare() and then immediately triggering feedback (without any time in between) does not improve latency. /// To conserve power, the Taptic Engine returns to an idle state after any of the following events: /// - You trigger feedback on the generator. /// - A short period of time passes (typically seconds). /// - The generator is deallocated. /// /// After feedback is triggered, the Taptic Engine returns to its idle state. If you might trigger additional feedback within the next few seconds, immediately call prepare() to keep the Taptic Engine in the prepared state. /// You can also extend the prepared state by repeatedly calling the prepare() method. However, if you continue calling prepare() without ever triggering feedback, the system may eventually place the Taptic Engine back in an idle state and ignore any further prepare() calls until after you trigger feedback at least once. public static func prepareTapticEngine() { if Piano.default.feedbackGenerator.notification == nil { Piano.wakeTapticEngine() } Piano.default.feedbackGenerator.selection?.prepare() Piano.default.feedbackGenerator.notification?.prepare() Piano.default.feedbackGenerator.impact.light?.prepare() Piano.default.feedbackGenerator.impact.medium?.prepare() Piano.default.feedbackGenerator.impact.heavy?.prepare() } /// Returns the Taptic Engine to an idle state public static func putTapticEngineToSleep() { Piano.default.feedbackGenerator = (nil, (nil, nil, nil), nil) } /// Plays the audio asset with the given name /// /// - Parameters: /// - assetName: name of asset as per in its respective .xcassets catalog /// - completion: completion handler private func playAudio(from assetName: String, completion: (() -> Void)?) { guard let asset = NSDataAsset(name: assetName) else { handle(error: PianoError.notFound(assetName)) completion?() return } do { try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default) try AVAudioSession.sharedInstance().setActive(true) player = try AVAudioPlayer(data: asset.data, fileTypeHint: nil) if let player = player { player.play() DispatchQueue.main.asyncAfter(deadline: .now() + player.duration, execute: { completion?() }) } else { handle(error: PianoError.couldNotPlay(assetName)) completion?() } } catch { handle(error: error) completion?() } } /// Plays the audio file with the given name and extension /// /// - Parameters: /// - file: name of file (Sound.mp4 -> ("Sound", "mp4") /// - completion: completion handler private func playAudio(from file: (name: String, extension: String), completion: (() -> Void)?) { guard let url = Bundle.main.url(forResource: file.name, withExtension: file.extension) else { handle(error: PianoError.notFound("\(file.name + "." + file.extension)")) completion?() return } do { try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default) try AVAudioSession.sharedInstance().setActive(true) player = try AVAudioPlayer(contentsOf: url) if let player = player { player.play() DispatchQueue.main.asyncAfter(deadline: .now() + player.duration, execute: { completion?() }) } else { handle(error: PianoError.couldNotPlay("\(file.name + "." + file.extension)")) completion?() } } catch { handle(error: error) completion?() } } /// Plays the audio from the specified URL /// /// - Parameters: /// - url: file URL of audio file /// - completion: completion handler private func playAudio(from url: URL, completion: (() -> Void)?) { do { try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default) try AVAudioSession.sharedInstance().setActive(true) player = try AVAudioPlayer(contentsOf: url) if let player = player { player.play() DispatchQueue.main.asyncAfter(deadline: .now() + player.duration, execute: { completion?() }) } else { handle(error: PianoError.couldNotPlay(url.absoluteString)) completion?() } } catch { handle(error: error) completion?() } } /// Plays system sound using Audio Services /// /// - Parameters: /// - soundId: System Sound ID of sound /// - completion: completion handler private func playSystemSound(with soundId: Int, completion: (() -> Void)?) { AudioServicesPlaySystemSoundWithCompletion(SystemSoundID(soundId)) { DispatchQueue.main.async { completion?() } } } /// Plays the specified haptic feedback, calling the specified completion handler after a time manually calculated from Apple's website /// /// - Parameters: /// - feedback: type of feedback to generate /// - completion: completion handler private func playHapticFeedback(_ feedback: HapticFeedback, completion: (() -> Void)?) { let duration: TimeInterval // value is calculated from https://developer.apple.com/ios/human-interface-guidelines/interaction/feedback/ switch feedback { case .notification(let notification): switch notification { case .success: Piano.default.feedbackGenerator.notification?.notificationOccurred(.success) duration = 0.2 case .warning: Piano.default.feedbackGenerator.notification?.notificationOccurred(.warning) duration = 0.25 case .failure: Piano.default.feedbackGenerator.notification?.notificationOccurred(.error) duration = 0.5 } case .impact(let impact): switch impact { case .light: Piano.default.feedbackGenerator.impact.light?.impactOccurred() case .medium: Piano.default.feedbackGenerator.impact.medium?.impactOccurred() case .heavy: Piano.default.feedbackGenerator.impact.heavy?.impactOccurred() } duration = 0.1 case .selection: Piano.default.feedbackGenerator.selection?.selectionChanged() duration = 0.05 } DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: { completion?() }) } /// Cancels the currently playing symphony public static func cancel() { for timer in Piano.default.timers { timer.invalidate() } Piano.default.timers.removeAll() } /// Play a symphony of notes /// /// Note: This method automatically cancels any previously playing symphonies public static func play(_ notes: [Note], completion: (() -> Void)? = nil) { cancel() Piano.default.symphonyCounter += 1 var pauseDurationBeforeNextNote: TimeInterval = 0 let notes = Piano.default.removeUnnecessaryNotes(from: notes) var completion = completion if notes.contains(where: { (note) -> Bool in switch note { case .hapticFeedback: return true default: return false } }) { prepareTapticEngine() if let definedCompletion = completion { let newCompletion: (() -> Void) = { definedCompletion() putTapticEngineToSleep() } completion = newCompletion } else { completion = { putTapticEngineToSleep() } } } notesLoop: for i in 0.. Void)? = nil var iterationCompletion: (() -> Void)? = nil if (i < notes.count - 2) { let nextNote = notes[i + 1] switch nextNote { case .waitUntilFinished: let afterNextNoteIndex = i + 2 let finalNoteIndex = notes.count - 1 let restOfNotes = Array(notes[afterNextNoteIndex...finalNoteIndex]) let capturedCounter = Piano.default.symphonyCounter iterationCompletion = { if Piano.default.symphonyCounter == capturedCounter { play(restOfNotes, completion: completion) } } default: break } } else if (i < notes.count - 1) { let nextNote = notes[i + 1] switch nextNote { case .waitUntilFinished: iterationCompletion = completion default: break } } else if i == notes.count - 1 { iterationCompletion = completion } switch note { case .sound(let audio): switch audio { case .asset(let name): music = { Piano.default.playAudio(from: name, completion: iterationCompletion) } case .file(let name, let type): music = { Piano.default.playAudio(from: (name, type), completion: iterationCompletion) } case .url(let url): music = { Piano.default.playAudio(from: url, completion: iterationCompletion) } case .system(let sound): music = { Piano.default.playSystemSound(with: sound.rawValue, completion: iterationCompletion) } } case .vibration(let vibration): music = { Piano.default.playSystemSound(with: vibration.rawValue, completion: iterationCompletion) } case .tapticEngine(let engine): music = { Piano.default.playSystemSound(with: engine.rawValue, completion: iterationCompletion) } case .hapticFeedback(let feedback): music = { Piano.default.playHapticFeedback(feedback, completion: iterationCompletion) } case .waitUntilFinished: if i != 0 { break notesLoop } case .wait(let interval): pauseDurationBeforeNextNote += interval if i == notes.count - 1 { music = { iterationCompletion?() } } } if let music = music { let timer = Timer(timeInterval: pauseDurationBeforeNextNote, repeats: false) { (_) in music() } RunLoop.main.add(timer, forMode: .common) Piano.default.timers.append(timer) } } if notes.count == 0 { completion?() } } /// Helper method for .play() to remove unnecessary .waitUntileFinisheds private func removeUnnecessaryNotes(from notes: [Note]) -> [Note] { var results = [Note]() for note in notes { if results.count == 0 { results.append(note) } else if let last = results.last { switch note { case .waitUntilFinished: switch last { case .waitUntilFinished: break default: results.append(note) } default: results.append(note) } } } if results.count == 1 { let onlyNote = results[0] switch onlyNote { case .waitUntilFinished: return [] default: break } } else { var removedFirstWaits = false var removedLastWaits = false while !removedFirstWaits || !removedLastWaits { switch results.first! { case .waitUntilFinished: results.removeFirst() default: removedFirstWaits = true } switch results.last! { case .waitUntilFinished: results.removeLast() default: removedLastWaits = true } } } return results } } ================================================ FILE: Sources/SystemSound.swift ================================================ // The MIT License (MIT) // // Copyright (c) 2018 Saoud Rizwan // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation @available(iOS 10.0, *) extension Piano { /// Default system sounds predefined and available on all iPhones /// Source: http://iphonedevwiki.net/index.php/AudioServices public enum SystemSound: Int, CaseIterable { case newMail = 1000 case mailSent = 1001 case voicemail = 1002 case receivedMessage = 1003 case sentMessage = 1004 case alarm = 1005 case lowPower = 1006 case smsReceived1 = 1007 case smsReceived2 = 1008 case smsReceived3 = 1009 case smsReceived4 = 1010 case smsReceived7 = 1012 case smsReceived5 = 1013 case smsReceived6 = 1014 case tweetSent = 1016 case anticipate = 1020 case bloom = 1021 case calypso = 1022 case chooChoo = 1023 case descent = 1024 case fanfare = 1025 case ladder = 1026 case minuet = 1027 case newsFlash = 1028 case noir = 1029 case sherwhoodForest = 1030 case spell = 1031 case suspense = 1032 case telegraph = 1033 case tiptoes = 1034 case typewriters = 1035 case update = 1036 case ussd = 1050 case simToolkitCallDropped = 1051 case simToolkitGeneralBeep = 1052 case simToolkitNegativeAck = 1053 case simToolkitPositiveAck = 1054 case simToolkitSms = 1055 case tinkQuiet = 1057 case ctBusy = 1070 case ctCongestion = 1071 case ctPathAck = 1072 case ctError = 1073 case ctCallWaiting = 1074 case ctKeyTone2 = 1075 case lock = 1100 case unlockFailed = 1102 case tink = 1103 case tock = 1104 case beepBeep = 1106 case ringerChanged = 1107 case photoShutter = 1108 case shake = 1109 case jblBegin = 1110 case jblConfirm = 1111 case jblCancel = 1112 case beginRecord = 1113 case endRecord = 1114 case jblAmbiguous = 1115 case jblNoMatch = 1116 case beginVideoRecord = 1117 case endVideoRecord = 1118 case vcInvitationAccepted = 1150 case vcRinging = 1151 case vcEnded = 1152 case ctCallWaiting2 = 1153 case vcRingingQuiet = 1154 case touchTone0 = 1200 case touchTone1 = 1201 case touchTone2 = 1202 case touchTone3 = 1203 case touchTone4 = 1204 case touchTone5 = 1205 case touchTone6 = 1206 case touchTone7 = 1207 case touchTone8 = 1208 case touchTone9 = 1209 case touchToneStar = 1210 case touchTonePound = 1211 case headsetStartCall = 1254 case headsetRedial = 1255 case headsetAnswerCall = 1256 case headsetEndCall = 1257 case headsetWait = 1258 case headsetTransitionEnd = 1259 case tockQuiet = 1306 } } ================================================ FILE: Sources/TapticEngine.swift ================================================ // The MIT License (MIT) // // Copyright (c) 2018 Saoud Rizwan // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation @available(iOS 10.0, *) extension Piano { /// First generation Taptic Engine vibrations public enum TapticEngine: Int { /// Weak boom case peek = 1519 /// Strong boom case pop = 1520 /// Three sequential weak booms case cancelled = 1521 /// Weak boom then strong boom case tryAgain = 1102 /// Three sequential strong booms case failed = 1107 } } ================================================ FILE: Sources/UIDevice+Extension.swift ================================================ // The MIT License (MIT) // // Copyright (c) 2018 Saoud Rizwan // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// Device extension to check whether user's device supports Taptic Engine and/or Haptic Feedback /// Be sure to use with UIDevice.current public extension UIDevice { /// In order to check if the iPhone has Taptic Engine and/or Haptic Feedback support, we need to check the device's model version. This function returns the generation and version of the current device. /// Note: Simulators will return a result of (0, 0), resulting in the hasTapticEngine and hasHapticFeedback BOOLs returning false /* Example: "iPhone7,1" on iPhone 6 Plus -> (7, 1) "iPhone7,2" on iPhone 6 -> (7, 2) "iPhone8,1" on iPhone 6S -> (8, 1) "iPhone8,2" on iPhone 6S Plus -> (8, 2) "iPhone8,4" on iPhone SE -> (8, 4) "iPhone9,1" on iPhone 7 (CDMA) -> (9, 1) "iPhone9,3" on iPhone 7 (GSM) -> (9, 3) "iPhone9,2" on iPhone 7 Plus (CDMA) -> (9, 2) "iPhone9,4" on iPhone 7 Plus (GSM) -> (9, 4) iPhone 8, 8S, and X will likely use a generation of 10 or greater, and will support Haptic Feedback, so this extension will work for those devices as well. iPhone X -> iPhone10,6 */ private func getDeviceGenerationVersion() -> (generation: Int, version: Int) { var sysinfo = utsname() uname(&sysinfo) let platform = String(bytes: Data(bytes: &sysinfo.machine, count: Int(_SYS_NAMELEN)), encoding: .ascii)!.trimmingCharacters(in: .controlCharacters) if platform.lowercased().prefix("iPhone".count) != "iPhone".lowercased() { // Not an iPhone (probably simulator) return (0, 0) } let numbers = platform.filter { "0123456789,".contains($0) } if let commaIndex = numbers.index(of: ",") { let firstNumber = numbers[numbers.startIndex.. 8 { return true } else { return false } } } // Returns a BOOL value representing whether the current device has a Taptic Engine with Haptic Feedback support public var hasHapticFeedback: Bool { get { let device = getDeviceGenerationVersion() if device.generation >= 9 { return true } else { return false } } } } ================================================ FILE: Sources/Vibration.swift ================================================ // The MIT License (MIT) // // Copyright (c) 2018 Saoud Rizwan // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation @available(iOS 10.0, *) extension Piano { /// Standard vibrations available on all models of the iPhone public enum Vibration: Int { /// Basic 1-second vibration case `default` = 4095 /// Two short consecutive vibrations case alert = 1011 } } ================================================ FILE: Tests/Info.plist ================================================ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType BNDL CFBundleShortVersionString 1.0 CFBundleVersion 1 ================================================ FILE: Tests/PianoTests.swift ================================================ // // PianoTests.swift // PianoTests // // Created by Saoud Rizwan on 9/11/17. // Copyright © 2017 Saoud Rizwan. All rights reserved. // import XCTest @testable import Piano class PianoTests: XCTestCase { override func setUp() { super.setUp() } override func tearDown() { super.tearDown() } }