Skip to content

Commit

Permalink
Release 0.1.1
Browse files Browse the repository at this point in the history
  • Loading branch information
alexruperez committed Mar 6, 2018
1 parent 2ab7b9f commit 1846cc8
Show file tree
Hide file tree
Showing 11 changed files with 171 additions and 68 deletions.
6 changes: 6 additions & 0 deletions .travis.yml
@@ -0,0 +1,6 @@
language: swift
osx_image: xcode9
script:
- xcodebuild -workspace SpeechRecognizerButton.xcworkspace -scheme Example -destination "platform=iOS Simulator,name=iPhone 7,OS=11.0" -configuration Debug -enableCodeCoverage YES clean build test
after_success:
- bash <(curl -s https://codecov.io/bash)
5 changes: 5 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,8 @@
# Release 0.1.1

- [x] Sounds and vibration options.
- [x] SFButton inspectable properties.

# Release 0.1.0

- [x] First release.
2 changes: 1 addition & 1 deletion Example/ViewController.swift
Expand Up @@ -17,7 +17,7 @@ class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()

button.authorizationErrorHandling = .openSettings(completion: nil)
//button.authorizationErrorHandling = .openSettings(completion: nil)
button.resultHandler = {
self.label.text = $1?.bestTranscription.formattedString
self.button.play()
Expand Down
15 changes: 12 additions & 3 deletions ExampleUITests/ExampleUITests.swift
Expand Up @@ -9,15 +9,24 @@
import XCTest

class ExampleUITests: XCTestCase {

let app = XCUIApplication()
let duration: TimeInterval = 3

override func setUp() {
super.setUp()
continueAfterFailure = false
XCUIApplication().launch()
app.launch()
}

func testSFButton() {
XCTFail()
func testButton() {
app.buttons["Button"].press(forDuration: duration)
sleep(UInt32(duration))
}

func testButtonThenDrag() {
app.buttons["Button"].press(forDuration: duration, thenDragTo: app.otherElements.firstMatch)
sleep(UInt32(duration))
}

}
18 changes: 14 additions & 4 deletions README.md
Expand Up @@ -37,6 +37,14 @@ dependencies: [

## 🐒 Usage

### Required configuration:

Add `NSMicrophoneUsageDescription` key to your `Info.plist` file containing a description of how your app will use the voice recording.

### Optional configuration:

Add `NSSpeechRecognitionUsageDescription` key to your `Info.plist` file containing a description of how your app will use the transcription only if you want to use this functionality.

### Handling authorization:

#### Automatically opening Settings when denying permission:
Expand Down Expand Up @@ -75,19 +83,21 @@ Just set `weak var waveformView: SFWaveformView?` property or use the Interface

### Customizing SFButton configuration:

Just set the following properties by code.
Just set the following properties by code or use the Interface Builder inspectables.

```swift
button.audioSession...
button.recordURL = ...
button.audioFormatSettings [AV...Key: ...]
button.maxDuration = TimeInterval(...)
button.audioFormatSettings = [AV...Key: ...]
button.maxDuration = ...
button.locale = Locale....
button.taskHint = SFSpeechRecognitionTaskHint....
button.queue = OperationQueue....
button.contextualStrings = ["..."]
button.interactionIdentifier = "..."
button.animationDuration = TimeInterval(...)
button.animationDuration = ...
button.shouldVibrate = ...
button.shouldSound = ...
```

### Customizing SFWaveformView configuration:
Expand Down
2 changes: 1 addition & 1 deletion SpeechRecognizerButton.podspec
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'SpeechRecognizerButton'
s.version = '0.1.0'
s.version = '0.1.1'
s.summary = 'UIButton subclass with push to talk recording, speech recognition and Siri-style waveform view'

s.homepage = 'https://github.com/alexruperez/SpeechRecognizerButton'
Expand Down
16 changes: 10 additions & 6 deletions SpeechRecognizerButton.xcodeproj/project.pbxproj
Expand Up @@ -8,7 +8,7 @@

/* Begin PBXBuildFile section */
814DED3F20442E4100DCDDE0 /* SpeechRecognizerButton.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 814DED3520442E4000DCDDE0 /* SpeechRecognizerButton.framework */; };
814DED4420442E4100DCDDE0 /* SpeechRecognizerButtonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 814DED4320442E4100DCDDE0 /* SpeechRecognizerButtonTests.swift */; };
814DED4420442E4100DCDDE0 /* SFWaveformViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 814DED4320442E4100DCDDE0 /* SFWaveformViewTests.swift */; };
814DED5120442E5700DCDDE0 /* Speech.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 814DED5020442E5700DCDDE0 /* Speech.framework */; };
814DED5320442FBA00DCDDE0 /* SFButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 814DED5220442FBA00DCDDE0 /* SFButton.swift */; };
814DED5B2044305F00DCDDE0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 814DED5A2044305F00DCDDE0 /* AppDelegate.swift */; };
Expand All @@ -22,6 +22,7 @@
814DED7D2044332A00DCDDE0 /* SpeechRecognizerButton.h in Headers */ = {isa = PBXBuildFile; fileRef = 814DED3820442E4000DCDDE0 /* SpeechRecognizerButton.h */; settings = {ATTRIBUTES = (Public, ); }; };
D557879B204968A200B88855 /* SFWaveformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D557879A204968A200B88855 /* SFWaveformView.swift */; };
D557879D204D695C00B88855 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D557879C204D695C00B88855 /* UIKit.framework */; };
D557879F204EBAEB00B88855 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D557879E204EBAEB00B88855 /* AudioToolbox.framework */; };
D5F5C13C204571D400F11B18 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D5F5C13B204571D400F11B18 /* AVFoundation.framework */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -68,7 +69,7 @@
814DED3820442E4000DCDDE0 /* SpeechRecognizerButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SpeechRecognizerButton.h; sourceTree = "<group>"; };
814DED3920442E4000DCDDE0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
814DED3E20442E4100DCDDE0 /* SpeechRecognizerButtonTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SpeechRecognizerButtonTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
814DED4320442E4100DCDDE0 /* SpeechRecognizerButtonTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeechRecognizerButtonTests.swift; sourceTree = "<group>"; };
814DED4320442E4100DCDDE0 /* SFWaveformViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFWaveformViewTests.swift; sourceTree = "<group>"; };
814DED4520442E4100DCDDE0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
814DED5020442E5700DCDDE0 /* Speech.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Speech.framework; path = System/Library/Frameworks/Speech.framework; sourceTree = SDKROOT; };
814DED5220442FBA00DCDDE0 /* SFButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFButton.swift; sourceTree = "<group>"; };
Expand All @@ -84,6 +85,7 @@
814DED712044306000DCDDE0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D557879A204968A200B88855 /* SFWaveformView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFWaveformView.swift; sourceTree = "<group>"; };
D557879C204D695C00B88855 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
D557879E204EBAEB00B88855 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; };
D5F5C13B204571D400F11B18 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
D5F5C13F2045953A00F11B18 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Main.strings; sourceTree = "<group>"; };
D5F5C1402045953B00F11B18 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/LaunchScreen.strings; sourceTree = "<group>"; };
Expand All @@ -94,6 +96,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
D557879F204EBAEB00B88855 /* AudioToolbox.framework in Frameworks */,
D557879D204D695C00B88855 /* UIKit.framework in Frameworks */,
D5F5C13C204571D400F11B18 /* AVFoundation.framework in Frameworks */,
814DED5120442E5700DCDDE0 /* Speech.framework in Frameworks */,
Expand Down Expand Up @@ -163,7 +166,7 @@
814DED4220442E4100DCDDE0 /* SpeechRecognizerButtonTests */ = {
isa = PBXGroup;
children = (
814DED4320442E4100DCDDE0 /* SpeechRecognizerButtonTests.swift */,
814DED4320442E4100DCDDE0 /* SFWaveformViewTests.swift */,
814DED4520442E4100DCDDE0 /* Info.plist */,
);
path = SpeechRecognizerButtonTests;
Expand All @@ -172,6 +175,7 @@
814DED4F20442E5700DCDDE0 /* Frameworks */ = {
isa = PBXGroup;
children = (
D557879E204EBAEB00B88855 /* AudioToolbox.framework */,
D557879C204D695C00B88855 /* UIKit.framework */,
D5F5C13B204571D400F11B18 /* AVFoundation.framework */,
814DED5020442E5700DCDDE0 /* Speech.framework */,
Expand Down Expand Up @@ -383,7 +387,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
814DED4420442E4100DCDDE0 /* SpeechRecognizerButtonTests.swift in Sources */,
814DED4420442E4100DCDDE0 /* SFWaveformViewTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down Expand Up @@ -481,7 +485,7 @@
CURRENT_PROJECT_VERSION = "$(DYLIB_CURRENT_VERSION)";
DEBUG_INFORMATION_FORMAT = dwarf;
DYLIB_COMPATIBILITY_VERSION = 0.1.0;
DYLIB_CURRENT_VERSION = 0.1.0;
DYLIB_CURRENT_VERSION = 0.1.1;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
Expand Down Expand Up @@ -545,7 +549,7 @@
CURRENT_PROJECT_VERSION = "$(DYLIB_CURRENT_VERSION)";
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DYLIB_COMPATIBILITY_VERSION = 0.1.0;
DYLIB_CURRENT_VERSION = 0.1.0;
DYLIB_CURRENT_VERSION = 0.1.1;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
Expand Down
81 changes: 55 additions & 26 deletions SpeechRecognizerButton/SFButton.swift
Expand Up @@ -9,8 +9,10 @@
import UIKit
import AVFoundation
import Speech
import AudioToolbox

@IBDesignable public class SFButton: UIButton {
@IBDesignable
public class SFButton: UIButton {

public enum SFButtonError: Error {
public enum AuthorizationReason {
Expand Down Expand Up @@ -40,13 +42,15 @@ import Speech
AVSampleRateKey: 12000,
AVNumberOfChannelsKey: 1,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue]
public var maxDuration = TimeInterval(60)
@IBInspectable public var maxDuration: Double = 60
public var locale = Locale.autoupdatingCurrent
public var taskHint = SFSpeechRecognitionTaskHint.unspecified
public var queue = OperationQueue.main
public var contextualStrings = [String]()
public var interactionIdentifier: String?
public var animationDuration = TimeInterval(0.5)
@IBInspectable public var animationDuration: Double = 0.5
@IBInspectable public var shouldVibrate: Bool = true
@IBInspectable public var shouldSound: Bool = true
@IBOutlet public weak var waveformView: SFWaveformView?

private var audioPlayer: AVAudioPlayer?
Expand Down Expand Up @@ -89,35 +93,64 @@ import Speech
self.handleAuthorizationError(error, self.authorizationErrorHandling)
}
} else {
do {
if self.audioRecorder == nil {
if self.audioRecorder == nil {
do {
self.audioRecorder = try AVAudioRecorder(url: self.recordURL, settings: self.audioFormatSettings)
self.audioRecorder?.delegate = self
self.audioRecorder?.isMeteringEnabled = true
self.audioRecorder?.prepareToRecord()
}
try self.audioSession.setCategory(AVAudioSessionCategoryPlayAndRecord)
try self.audioSession.setActive(true)
} catch {
self.queue.addOperation {
self.errorHandler?(.unknown(error: error))
} catch {
self.queue.addOperation {
self.errorHandler?(.unknown(error: error))
}
}
self.audioRecorder?.delegate = self
self.audioRecorder?.isMeteringEnabled = true
self.audioRecorder?.prepareToRecord()
}
OperationQueue.main.addOperation {
if self.audioRecorder?.isRecording == false, self.isHighlighted {
self.audioRecorder?.record(forDuration: self.maxDuration)
if self.displayLink == nil {
self.displayLink = CADisplayLink(target: self, selector: #selector(self.updateMeters(_:)))
self.displayLink?.add(to: .current, forMode: .commonModes)
if self.shouldVibrate {
AudioServicesPlaySystemSound(1519)
}
if self.shouldSound {
AudioServicesPlaySystemSoundWithCompletion(1113, {
OperationQueue.main.addOperation {
self.beginRecord()
}
})
} else {
self.beginRecord()
}
self.displayLink?.isPaused = false
self.waveformView(show: true, animationDuration: self.animationDuration)
}
}
}
}
}

private func beginRecord() {
try? audioSession.setCategory(AVAudioSessionCategoryPlayAndRecord)
try? audioSession.setActive(true)
audioRecorder?.record(forDuration: maxDuration)
if displayLink == nil {
displayLink = CADisplayLink(target: self, selector: #selector(self.updateMeters(_:)))
displayLink?.add(to: .current, forMode: .commonModes)
}
displayLink?.isPaused = false
waveformView(show: true, animationDuration: self.animationDuration)
}

private func endRecord() {
displayLink?.isPaused = true
audioRecorder?.stop()
waveformView(show: false, animationDuration: animationDuration)
try? audioSession.setCategory(AVAudioSessionCategoryPlayback)
try? audioSession.setActive(true)
if self.shouldVibrate {
AudioServicesPlaySystemSound(1519)
}
if self.shouldSound {
AudioServicesPlaySystemSound(1114)
}
}

open func waveformView(show: Bool, animationDuration: TimeInterval) {
if animationDuration > 0 {
UIView.animate(withDuration: animationDuration, animations: {
Expand All @@ -138,16 +171,12 @@ import Speech
}

@objc private func touchUpInside(_ sender: Any? = nil) {
displayLink?.isPaused = true
audioRecorder?.stop()
waveformView(show: false, animationDuration: animationDuration)
endRecord()
}

@objc private func touchUpOutside(_ sender: Any? = nil) {
displayLink?.isPaused = true
audioRecorder?.stop()
endRecord()
audioRecorder?.deleteRecording()
waveformView(show: false, animationDuration: animationDuration)
}

private func handleAuthorizationError(_ error: SFButtonError, _ handling: AuthorizationErrorHandling) {
Expand Down
18 changes: 9 additions & 9 deletions SpeechRecognizerButton/SFWaveformView.swift
Expand Up @@ -13,8 +13,8 @@ let pi = Double.pi

@IBDesignable
public class SFWaveformView: UIView {
fileprivate var _phase: CGFloat = 0.0
fileprivate var _amplitude: CGFloat = 0.3
fileprivate(set) var _phase: CGFloat = 0.0
fileprivate(set) var _amplitude: CGFloat = 0.3

@IBInspectable public var waveColor: UIColor = .black
@IBInspectable public var numberOfWaves = 5
Expand All @@ -38,16 +38,16 @@ public class SFWaveformView: UIView {
}

override public func draw(_ rect: CGRect) {
let context = UIGraphicsGetCurrentContext()!
context.clear(bounds)
let context = UIGraphicsGetCurrentContext()
context?.clear(bounds)

backgroundColor?.set()
context.fill(rect)
context?.fill(rect)

// Draw multiple sinus waves, with equal phases but altered
// amplitudes, multiplied by a parable function.
for waveNumber in 0...numberOfWaves {
context.setLineWidth((waveNumber == 0 ? primaryWaveLineWidth : secondaryWaveLineWidth))
context?.setLineWidth((waveNumber == 0 ? primaryWaveLineWidth : secondaryWaveLineWidth))

let halfHeight = bounds.height / 2.0
let width = bounds.width
Expand All @@ -71,15 +71,15 @@ public class SFWaveformView: UIView {
let y = scaling * maxAmplitude * normedAmplitude * CGFloat(sinf(Float(tempCasting))) + halfHeight

if x == 0 {
context.move(to: CGPoint(x: x, y: y))
context?.move(to: CGPoint(x: x, y: y))
} else {
context.addLine(to: CGPoint(x: x, y: y))
context?.addLine(to: CGPoint(x: x, y: y))
}

x += density
}

context.strokePath()
context?.strokePath()
}
}
}

0 comments on commit 1846cc8

Please sign in to comment.