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
================================================
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" systemVersion="17A277" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="YES" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>
================================================
FILE: Example/PianoExample/Base.lproj/Main.storyboard
================================================
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14313.18" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="RZT-lr-wh0">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14283.14"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Navigation Controller-->
<scene sceneID="pSI-QR-7D9">
<objects>
<navigationController id="RZT-lr-wh0" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="XPs-ic-HN1">
<rect key="frame" x="0.0" y="20" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="BYZ-38-t0r" kind="relationship" relationship="rootViewController" id="TYX-4b-NQp"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="ucx-A2-xqN" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-196" y="132"/>
</scene>
<!--View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="ViewController" customModule="PianoExample" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" translatesAutoresizingMaskIntoConstraints="NO" id="HIm-Ye-lBa">
<rect key="frame" x="0.0" y="338" width="375" height="329"/>
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
<color key="sectionIndexBackgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
<sections/>
</tableView>
<toolbar opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Z3c-N2-ow6">
<rect key="frame" x="0.0" y="294" width="375" height="44"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="x3O-VQ-9IA"/>
</constraints>
<items>
<barButtonItem title="Item" id="efa-Dd-BH3"/>
</items>
</toolbar>
<label opaque="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumFontSize="5" translatesAutoresizingMaskIntoConstraints="NO" id="hyq-If-A8R" customClass="ResponsiveLabel" customModule="PianoExample" customModuleProvider="target">
<rect key="frame" x="20" y="84" width="335" height="190"/>
<constraints>
<constraint firstAttribute="height" constant="190" id="zm0-Op-tO3"/>
</constraints>
<fontDescription key="fontDescription" name="Menlo-Regular" family="Menlo" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="hyq-If-A8R" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" constant="20" id="09c-X9-aCL"/>
<constraint firstItem="HIm-Ye-lBa" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" id="4Vg-e3-JdB"/>
<constraint firstItem="Z3c-N2-ow6" firstAttribute="top" secondItem="hyq-If-A8R" secondAttribute="bottom" constant="20" id="Ijo-Sj-1Fl"/>
<constraint firstItem="Z3c-N2-ow6" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" id="NKi-uA-UVv"/>
<constraint firstItem="HIm-Ye-lBa" firstAttribute="top" secondItem="Z3c-N2-ow6" secondAttribute="bottom" id="WCL-8e-YzH"/>
<constraint firstItem="hyq-If-A8R" firstAttribute="top" secondItem="6Tk-OE-BBY" secondAttribute="top" constant="20" id="ZR4-L6-kg2"/>
<constraint firstItem="Z3c-N2-ow6" firstAttribute="trailing" secondItem="6Tk-OE-BBY" secondAttribute="trailing" id="fAd-0q-ad9"/>
<constraint firstItem="HIm-Ye-lBa" firstAttribute="trailing" secondItem="6Tk-OE-BBY" secondAttribute="trailing" id="iz5-dd-kzS"/>
<constraint firstItem="6Tk-OE-BBY" firstAttribute="trailing" secondItem="hyq-If-A8R" secondAttribute="trailing" constant="20" id="rFo-AP-YBA"/>
<constraint firstItem="HIm-Ye-lBa" firstAttribute="bottom" secondItem="8bC-Xf-vdC" secondAttribute="bottom" id="yfL-rw-1eo"/>
</constraints>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
</view>
<navigationItem key="navigationItem" id="ru4-i9-IcL"/>
<connections>
<outlet property="label" destination="hyq-If-A8R" id="cGk-B4-Hsk"/>
<outlet property="tableView" destination="HIm-Ye-lBa" id="lUh-el-D4J"/>
<outlet property="toolBar" destination="Z3c-N2-ow6" id="iYy-Fh-iJZ"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="501.60000000000002" y="131.78410794602701"/>
</scene>
</scenes>
</document>
================================================
FILE: Example/PianoExample/Info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Piano</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>
================================================
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..<sections.count {
switch i {
case 0:
// Wait
rows.append([
(".wait(text goes here)", .wait(0))
])
case 1:
// Wait Until Finished
rows.append([
(".waitUntilFinished", .waitUntilFinished)
])
case 2:
// Vibration
rows.append([
(".vibration(.default)", .vibration(.default)),
(".vibration(.alert)", .vibration(.alert))
])
case 3:
// Taptic Engine
rows.append([
(".tapticEngine(.peek)", .tapticEngine(.peek)),
(".tapticEngine(.pop)", .tapticEngine(.pop)),
(".tapticEngine(.cancelled)", .tapticEngine(.cancelled)),
(".tapticEngine(.tryAgain)", .tapticEngine(.tryAgain)),
(".tapticEngine(.failed)", .tapticEngine(.failed))
])
case 4:
// Haptic Feedback - Notification
rows.append([
(".hapticFeedback(.notification(.success))", .hapticFeedback(.notification(.success))),
(".hapticFeedback(.notification(.warning))", .hapticFeedback(.notification(.warning))),
(".hapticFeedback(.notification(.failure))", .hapticFeedback(.notification(.failure)))
])
case 5:
// Haptic Feedback - Impact
rows.append([
(".hapticFeedback(.impact(.light))", .hapticFeedback(.impact(.light))),
(".hapticFeedback(.impact(.medium))", .hapticFeedback(.impact(.medium))),
(".hapticFeedback(.impact(.heavy))", .hapticFeedback(.impact(.heavy)))
])
case 6:
// Haptic Feedback - Selection
rows.append([
(".hapticFeedback(.selection)", .hapticFeedback(.selection))
])
case 7:
// Sound - Assets Example
rows.append([
(".sound(.asset(name: \"heart\"))", .sound(.asset(name: "heart"))),
(".sound(.asset(name: \"kiss\"))", .sound(.asset(name: "kiss"))),
(".sound(.asset(name: \"wink\"))", .sound(.asset(name: "wink")))
// MARK:-
// MARK: Add your own sound assets here...
// MARK:-
])
case 8:
// Sound - File Example
rows.append([
(".sound(.asset(name: \"heart\"))", .sound(.file(name: "harp", extension: "wav")))
// MARK:-
// MARK: Add your own sound files here...
// MARK:-
])
case 9:
// Sound - URL Example
let joyFileUrl = Bundle.main.url(forResource: "joy", withExtension: "wav")!
rows.append([
(".sound(.url(joyFileUrl))", .sound(.url(joyFileUrl)))
])
case 10:
// Sound - System Predefined
rows.append([
(".sound(.system(.newMail))", .sound(.system(.newMail))),
(".sound(.system(.mailSent))", .sound(.system(.mailSent))),
(".sound(.system(.voicemail))", .sound(.system(.voicemail)))
])
// There's too many to manually code here, so let's use some Swift black magic
Piano.SystemSound.allCases.forEach {
rows[10].append((title: ".sound(.system(.\($0))", note: .sound(.system($0))))
}
/*
var z = 0
let sounds = AnyIterator {
let next = withUnsafeBytes(of: &z) { $0.load(as: 🎹.SystemSound.self) }
if next.hashValue != z { return nil }
z += 1
return next
} as AnyIterator<🎹.SystemSound>
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..<sections.count {
let section = sections[i]
let rows = rows[i]
data.append((title: section, rows: rows))
}
return data
}()
lazy var waitTextField: UITextField = {
let textField = UITextField()
textField.translatesAutoresizingMaskIntoConstraints = false
textField.keyboardType = .decimalPad
textField.delegate = self
textField.borderStyle = .none
return textField
}()
var waitValue: TimeInterval? = nil
var pianoString: String = "" {
didSet {
if pianoString.count == 0 {
label.textAlignment = .center
label.textColor = UIColor.gray
label.text = "Add some notes to your symphony"
} else {
label.textAlignment = .left
label.textColor = UIColor.black
label.text = pianoString
}
}
}
var notesToPlay = [🎹.Note]()
var notesToPlayAsStrings = [String]()
override func viewDidLoad() {
super.viewDidLoad()
title = "🎹 Piano"
let refreshButton = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(refreshButtonTapped))
navigationItem.setLeftBarButton(refreshButton, animated: false)
let undoButton = UIBarButtonItem(barButtonSystemItem: .undo, target: self, action: #selector(undoButtonTapped))
navigationItem.setRightBarButton(undoButton, animated: false)
pianoString = ""
let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
let playButton = UIBarButtonItem(barButtonSystemItem: .play, target: self, action: #selector(playButtonTapped))
toolBar.setItems([space, playButton, space], animated: false)
let shadow = UIView()
shadow.translatesAutoresizingMaskIntoConstraints = false
shadow.backgroundColor = UIColor.gray.withAlphaComponent(0.275)
toolBar.addSubview(shadow)
NSLayoutConstraint.activate([
shadow.leadingAnchor.constraint(equalTo: toolBar.leadingAnchor),
shadow.heightAnchor.constraint(equalToConstant: 0.75),
shadow.trailingAnchor.constraint(equalTo: toolBar.trailingAnchor),
shadow.bottomAnchor.constraint(equalTo: toolBar.bottomAnchor)
])
let toolBarTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(toolBarTapped))
toolBarTapGestureRecognizer.delegate = self
toolBar.addGestureRecognizer(toolBarTapGestureRecognizer)
let labelLongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(labelLongPressed))
labelLongPressGestureRecognizer.minimumPressDuration = 0.3
labelLongPressGestureRecognizer.delegate = self
label.addGestureRecognizer(labelLongPressGestureRecognizer)
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cellId")
tableView.dataSource = self
tableView.delegate = self
tableView.keyboardDismissMode = .onDrag
label.lineBreakMode = .byTruncatingMiddle
label.adjustsFontSizeToFitWidth = true
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
}
@objc func refreshButtonTapped() {
🎹.cancel()
waitTextField.resignFirstResponder()
notesToPlay.removeAll()
notesToPlayAsStrings.removeAll()
pianoString = ""
}
@objc func undoButtonTapped() {
waitTextField.resignFirstResponder()
if !notesToPlay.isEmpty {
notesToPlay.removeLast()
}
if !notesToPlayAsStrings.isEmpty {
notesToPlayAsStrings.removeLast()
}
setPianoStringToNotes()
}
@objc func toolBarTapped(sender: UITapGestureRecognizer) {
tableView.setContentOffset(.zero, animated: true)
waitTextField.resignFirstResponder()
}
@objc func labelLongPressed(sender: UILongPressGestureRecognizer) {
guard sender.state == .began && !notesToPlay.isEmpty, let senderView = sender.view, let superView = sender.view?.superview else { return }
senderView.becomeFirstResponder()
let copyItem = UIMenuItem(title: "Copy", action: #selector(labelMenuCopyTapped))
UIMenuController.shared.menuItems = [copyItem]
UIMenuController.shared.arrowDirection = .up
UIMenuController.shared.setTargetRect(senderView.frame, in: superView)
UIMenuController.shared.setMenuVisible(true, animated: true)
}
@objc func labelMenuCopyTapped() {
let text = label.text
UIPasteboard.general.string = text
label.resignFirstResponder()
}
@objc func playButtonTapped() {
waitTextField.resignFirstResponder()
if notesToPlay.count > 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..<notesToPlayAsStrings.count {
if i == 0 {
entireCommandAsString.append("Piano.play([\n")
numberOfLines += 1
}
if i != notesToPlayAsStrings.count - 1 {
let noteToPlayAsString = notesToPlayAsStrings[i]
entireCommandAsString.append(" " + noteToPlayAsString + ",\n")
numberOfLines += 1
} else {
let noteToPlayAsString = notesToPlayAsStrings[i]
entireCommandAsString.append(" " + noteToPlayAsString + "\n")
numberOfLines += 1
}
if i == notesToPlayAsStrings.count - 1 {
entireCommandAsString.append(" ])")
numberOfLines += 1
}
}
pianoString = entireCommandAsString
label.numberOfLines = numberOfLines
}
}
extension ViewController: UITableViewDataSource, UITableViewDelegate {
func numberOfSections(in tableView: UITableView) -> 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 = "<group>"; };
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 = "<group>"; };
106A373D1F66EE0200BF5BD1 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
106A37401F66EE0200BF5BD1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
106A37421F66EE0200BF5BD1 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
106A37451F66EE0200BF5BD1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
106A37471F66EE0200BF5BD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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 = "<group>"; };
10CCC0E51F6738010085294A /* harp.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = harp.wav; sourceTree = "<group>"; };
DA4BC02D2162AB77006C5ADF /* ResponsiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponsiveLabel.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
};
106A37391F66EE0200BF5BD1 /* Products */ = {
isa = PBXGroup;
children = (
106A37381F66EE0200BF5BD1 /* PianoExample.app */,
);
name = Products;
sourceTree = "<group>";
};
106A373A1F66EE0200BF5BD1 /* PianoExample */ = {
isa = PBXGroup;
children = (
10CCC0E21F67350C0085294A /* IGNORE */,
10CCC0E31F6737730085294A /* Sounds.xcassets */,
10CCC0E51F6738010085294A /* harp.wav */,
105A40E01F6756970078BAA6 /* joy.wav */,
106A373D1F66EE0200BF5BD1 /* ViewController.swift */,
);
path = PianoExample;
sourceTree = "<group>";
};
10CCC0C61F6712630085294A /* Frameworks */ = {
isa = PBXGroup;
children = (
10CCC0DF1F6733C70085294A /* Piano.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
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 = "<group>";
};
/* 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 = "<group>";
};
106A37441F66EE0200BF5BD1 /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
106A37451F66EE0200BF5BD1 /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* 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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:PianoExample.xcodeproj">
</FileRef>
</Workspace>
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2017 Saoud Rizwan <hello@saoudmr.com>
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 = "<group>"; };
106A36E21F66ECC300BF5BD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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 = "<group>"; };
106A36EE1F66ECC300BF5BD1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
106A36F81F66ECCF00BF5BD1 /* Piano.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Piano.swift; sourceTree = "<group>"; };
10CCC0E61F6747FA0085294A /* SystemSound.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemSound.swift; sourceTree = "<group>"; };
10CCC0E81F6748340085294A /* Audio.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Audio.swift; sourceTree = "<group>"; };
10CCC0EA1F67484C0085294A /* Vibration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vibration.swift; sourceTree = "<group>"; };
10CCC0EC1F6748600085294A /* TapticEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapticEngine.swift; sourceTree = "<group>"; };
10CCC0EE1F6748760085294A /* HapticFeedback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticFeedback.swift; sourceTree = "<group>"; };
10CCC0F01F67488D0085294A /* Note.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Note.swift; sourceTree = "<group>"; };
10CCC0F21F6748E20085294A /* Piano+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Piano+Error.swift"; sourceTree = "<group>"; };
10CCC0F41F6749FB0085294A /* UIDevice+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIDevice+Extension.swift"; sourceTree = "<group>"; };
/* 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 = "<group>";
};
106A36DF1F66ECC300BF5BD1 /* Products */ = {
isa = PBXGroup;
children = (
106A36DE1F66ECC300BF5BD1 /* Piano.framework */,
106A36E71F66ECC300BF5BD1 /* PianoTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
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 = "<group>";
};
106A36EB1F66ECC300BF5BD1 /* Tests */ = {
isa = PBXGroup;
children = (
106A36EC1F66ECC300BF5BD1 /* PianoTests.swift */,
106A36EE1F66ECC300BF5BD1 /* Info.plist */,
);
path = Tests;
sourceTree = "<group>";
};
/* 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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:Piano.xcodeproj">
</FileRef>
</Workspace>
================================================
FILE: Piano.xcodeproj/xcshareddata/xcschemes/Piano.xcscheme
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0900"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "106A36DD1F66ECC300BF5BD1"
BuildableName = "Piano.framework"
BlueprintName = "Piano"
ReferencedContainer = "container:Piano.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "106A36DD1F66ECC300BF5BD1"
BuildableName = "Piano.framework"
BlueprintName = "Piano"
ReferencedContainer = "container:Piano.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "106A36DD1F66ECC300BF5BD1"
BuildableName = "Piano.framework"
BlueprintName = "Piano"
ReferencedContainer = "container:Piano.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
================================================
FILE: Piano.xcworkspace/contents.xcworkspacedata
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Example/PianoExample.xcodeproj">
</FileRef>
<FileRef
location = "group:Piano.xcodeproj">
</FileRef>
<FileRef
location = "group:README.md">
</FileRef>
<FileRef
location = "group:LICENSE">
</FileRef>
<FileRef
location = "group:Piano.podspec">
</FileRef>
</Workspace>
================================================
FILE: README.md
================================================
<p align="center">
<img src="https://user-images.githubusercontent.com/7799382/30356431-dbba9920-97ed-11e7-8f2b-a5b5ba0e7682.png" alt="Piano" />
</p>
<p align="center">
<img src="https://user-images.githubusercontent.com/7799382/30309920-bcdb85ec-9742-11e7-96fc-af8155f4712d.png" alt="Platform: iOS 10.0+" />
<a href="https://developer.apple.com/swift" target="_blank"><img src="https://user-images.githubusercontent.com/7799382/30309908-ace5d886-9742-11e7-85ea-8d4e5f2af2ac.png" alt="Language: Swift 4" /></a>
<a href="https://cocoapods.org/pods/Piano" target="_blank"><img src="https://user-images.githubusercontent.com/7799382/33073452-cd78293e-ce77-11e7-8b39-8a1565616814.png" alt="CocoaPods compatible" /></a>
<a href="https://github.com/Carthage/Carthage" target="_blank"><img src="https://user-images.githubusercontent.com/7799382/30309900-9fc15d2e-9742-11e7-91fd-31bb1226db90.png" alt="Carthage compatible" /></a>
<img src="https://user-images.githubusercontent.com/7799382/30309910-adef2b38-9742-11e7-8140-d05534dd92a5.png" alt="License: MIT" />
</p>
<p align="center">
<a href="#installation">Installation</a>
• <a href="#usage">Usage</a>
• <a href="#documentation">Documentation</a>
• <a href="#why-i-built-piano">Why I Built Piano</a>
• <a href="#license">License</a>
• <a href="#contribute">Contribute</a>
• <a href="#questions">Questions?</a>
• <a href="#credits">Credits</a>
</p>
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 <a href="https://guides.cocoapods.org/using/using-cocoapods.html" target="_blank">CocoaPods</a>:
```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 <a href="https://github.com/Carthage/Carthage" target="_blank">Carthage</a>:
```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.|
<sub>See: [Apple's Guidelines for using Haptic Feedback](https://developer.apple.com/ios/human-interface-guidelines/user-interaction/feedback/)</sub>
#### `.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.
<p align="center">
<img src="https://user-images.githubusercontent.com/7799382/30370416-613f985a-982c-11e7-8646-33f1efb55d90.png" alt="Piano" width="300" height="500" />
</p>
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.
<img src="https://user-images.githubusercontent.com/7799382/30358465-97784ee0-97f9-11e7-9f12-75fa041cf556.png" alt="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 <a href="mailto:hello@saoudmr.com">hello@saoudmr.com</a>, or by twitter <a href="https://twitter.com/sdrzn" target="_blank">@sdrzn</a>. Please create an <a href="https://github.com/saoudrizwan/Piano/issues">issue</a> 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 <hello@saoudmr.com>
//
// 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 <hello@saoudmr.com>
//
// 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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.8</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>
================================================
FILE: Sources/Note.swift
================================================
// The MIT License (MIT)
//
// Copyright (c) 2018 Saoud Rizwan <hello@saoudmr.com>
//
// 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 <hello@saoudmr.com>
//
// 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 <UIKit/UIKit.h>
//! 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 <Piano/PublicHeader.h>
================================================
FILE: Sources/Piano.swift
================================================
// The MIT License (MIT)
//
// Copyright (c) 2018 Saoud Rizwan <hello@saoudmr.com>
//
// 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..<notes.count {
let note = notes[i]
var music: (() -> 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 <hello@saoudmr.com>
//
// 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 <hello@saoudmr.com>
//
// 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 <hello@saoudmr.com>
//
// 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..<commaIndex]
let afterCommaIndex = numbers.index(after: commaIndex)
let secondNumber = numbers[afterCommaIndex..<numbers.endIndex] // endIndex is an index after the last index
let generation = Int(firstNumber) ?? 0
let version = Int(secondNumber) ?? 0
return (generation, version)
} else {
return (0, 0)
}
}
// Returns a BOOL value representing whether the current device has a Taptic Engine or not
public var hasTapticEngine: Bool {
get {
let device = getDeviceGenerationVersion()
if device.generation == 8 {
if device.version == 4 { // SE
return false
} else {
return true
}
} else if device.generation > 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 <hello@saoudmr.com>
//
// 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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
================================================
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()
}
}
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
Condensed preview — 36 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (141K chars).
[
{
"path": ".gitignore",
"chars": 361,
"preview": "## OS X Finder\n.DS_Store\n\n## Build generated\nbuild/\nDerivedData\n\n## Various settings\n*.pbxuser\n!default.pbxuser\n*.mode1v"
},
{
"path": ".swift-version",
"chars": 4,
"preview": "4.2\n"
},
{
"path": "Example/PianoExample/AppDelegate.swift",
"chars": 2177,
"preview": "//\n// AppDelegate.swift\n// PianoExample\n//\n// Created by Saoud Rizwan on 9/11/17.\n// Copyright © 2017 Saoud Rizwan. "
},
{
"path": "Example/PianoExample/Assets.xcassets/AppIcon.appiconset/Contents.json",
"chars": 3273,
"preview": "{\n \"images\" : [\n {\n \"size\" : \"20x20\",\n \"idiom\" : \"iphone\",\n \"filename\" : \"Icon-App-20x20@2x.png\",\n "
},
{
"path": "Example/PianoExample/Assets.xcassets/Contents.json",
"chars": 62,
"preview": "{\n \"info\" : {\n \"version\" : 1,\n \"author\" : \"xcode\"\n }\n}"
},
{
"path": "Example/PianoExample/Base.lproj/LaunchScreen.storyboard",
"chars": 1719,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard"
},
{
"path": "Example/PianoExample/Base.lproj/Main.storyboard",
"chars": 7467,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<document type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\" version=\"3"
},
{
"path": "Example/PianoExample/Info.plist",
"chars": 1408,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Example/PianoExample/ResponsiveLabel.swift",
"chars": 273,
"preview": "//\n// ResponsiveLabel.swift\n// PianoExample\n//\n// Created by Saoud Rizwan on 10/1/18.\n// Copyright © 2018 Saoud Rizw"
},
{
"path": "Example/PianoExample/Sounds.xcassets/Contents.json",
"chars": 62,
"preview": "{\n \"info\" : {\n \"version\" : 1,\n \"author\" : \"xcode\"\n }\n}"
},
{
"path": "Example/PianoExample/Sounds.xcassets/heart.dataset/Contents.json",
"chars": 164,
"preview": "{\n \"info\" : {\n \"version\" : 1,\n \"author\" : \"xcode\"\n },\n \"data\" : [\n {\n \"idiom\" : \"universal\",\n \"fil"
},
{
"path": "Example/PianoExample/Sounds.xcassets/kiss.dataset/Contents.json",
"chars": 156,
"preview": "{\n \"info\" : {\n \"version\" : 1,\n \"author\" : \"xcode\"\n },\n \"data\" : [\n {\n \"idiom\" : \"universal\",\n \"fil"
},
{
"path": "Example/PianoExample/Sounds.xcassets/wink.dataset/Contents.json",
"chars": 159,
"preview": "{\n \"info\" : {\n \"version\" : 1,\n \"author\" : \"xcode\"\n },\n \"data\" : [\n {\n \"idiom\" : \"universal\",\n \"fil"
},
{
"path": "Example/PianoExample/ViewController.swift",
"chars": 19996,
"preview": "//\n// ViewController.swift\n// PianoExample\n//\n// Created by Saoud Rizwan on 9/11/17.\n// Copyright © 2017 Saoud Rizwa"
},
{
"path": "Example/PianoExample.xcodeproj/project.pbxproj",
"chars": 15443,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 48;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "Example/PianoExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"chars": 157,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"self:PianoExample.xc"
},
{
"path": "LICENSE",
"chars": 1099,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2017 Saoud Rizwan <hello@saoudmr.com>\n\nPermission is hereby granted, free of charge"
},
{
"path": "Piano.podspec",
"chars": 1023,
"preview": "Pod::Spec.new do |s|\n s.name = \"Piano\"\n s.version = \"1.8\"\n s.summary = \"Compose a symphony of sound"
},
{
"path": "Piano.xcodeproj/project.pbxproj",
"chars": 19394,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 48;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
},
{
"path": "Piano.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
"chars": 150,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"self:Piano.xcodeproj"
},
{
"path": "Piano.xcodeproj/xcshareddata/xcschemes/Piano.xcscheme",
"chars": 2872,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Scheme\n LastUpgradeVersion = \"0900\"\n version = \"1.3\">\n <BuildAction\n "
},
{
"path": "Piano.xcworkspace/contents.xcworkspacedata",
"chars": 422,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n version = \"1.0\">\n <FileRef\n location = \"group:Example/PianoE"
},
{
"path": "README.md",
"chars": 13381,
"preview": "<p align=\"center\">\n <img src=\"https://user-images.githubusercontent.com/7799382/30356431-dbba9920-97ed-11e7-8f2b-a5b5"
},
{
"path": "Sources/Audio.swift",
"chars": 1659,
"preview": "// The MIT License (MIT)\n//\n// Copyright (c) 2018 Saoud Rizwan <hello@saoudmr.com>\n//\n// Permission is hereby granted, f"
},
{
"path": "Sources/HapticFeedback.swift",
"chars": 3309,
"preview": "// The MIT License (MIT)\n//\n// Copyright (c) 2018 Saoud Rizwan <hello@saoudmr.com>\n//\n// Permission is hereby granted, f"
},
{
"path": "Sources/Info.plist",
"chars": 774,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Sources/Note.swift",
"chars": 1955,
"preview": "// The MIT License (MIT)\n//\n// Copyright (c) 2018 Saoud Rizwan <hello@saoudmr.com>\n//\n// Permission is hereby granted, f"
},
{
"path": "Sources/Piano+Error.swift",
"chars": 2226,
"preview": "// The MIT License (MIT)\n//\n// Copyright (c) 2018 Saoud Rizwan <hello@saoudmr.com>\n//\n// Permission is hereby granted, f"
},
{
"path": "Sources/Piano.h",
"chars": 469,
"preview": "//\n// Piano.h\n// Piano\n//\n// Created by Saoud Rizwan on 9/11/17.\n// Copyright © 2017 Saoud Rizwan. All rights reserv"
},
{
"path": "Sources/Piano.swift",
"chars": 16456,
"preview": "// The MIT License (MIT)\n//\n// Copyright (c) 2018 Saoud Rizwan <hello@saoudmr.com>\n//\n// Permission is hereby granted, f"
},
{
"path": "Sources/SystemSound.swift",
"chars": 4123,
"preview": "// The MIT License (MIT)\n//\n// Copyright (c) 2018 Saoud Rizwan <hello@saoudmr.com>\n//\n// Permission is hereby granted, f"
},
{
"path": "Sources/TapticEngine.swift",
"chars": 1652,
"preview": "// The MIT License (MIT)\n//\n// Copyright (c) 2018 Saoud Rizwan <hello@saoudmr.com>\n//\n// Permission is hereby granted, f"
},
{
"path": "Sources/UIDevice+Extension.swift",
"chars": 4268,
"preview": "// The MIT License (MIT)\n//\n// Copyright (c) 2018 Saoud Rizwan <hello@saoudmr.com>\n//\n// Permission is hereby granted, f"
},
{
"path": "Sources/Vibration.swift",
"chars": 1465,
"preview": "// The MIT License (MIT)\n//\n// Copyright (c) 2018 Saoud Rizwan <hello@saoudmr.com>\n//\n// Permission is hereby granted, f"
},
{
"path": "Tests/Info.plist",
"chars": 701,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Tests/PianoTests.swift",
"chars": 346,
"preview": "//\n// PianoTests.swift\n// PianoTests\n//\n// Created by Saoud Rizwan on 9/11/17.\n// Copyright © 2017 Saoud Rizwan. All"
}
]
About this extraction
This page contains the full source code of the saoudrizwan/Piano GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 36 files (127.6 KB), approximately 34.8k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.