";
};
/* 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
================================================
Installation
• Usage
• Documentation
• Why I Built Piano
• License
• Contribute
• Questions?
• 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.
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.
## 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()
}
}