From 1846cc82b38fa96d8c7edbbc0a0cc970e12226ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Rupe=CC=81rez?= Date: Tue, 6 Mar 2018 16:37:36 +0100 Subject: [PATCH] Release 0.1.1 --- .travis.yml | 6 ++ CHANGELOG.md | 5 ++ Example/ViewController.swift | 2 +- ExampleUITests/ExampleUITests.swift | 15 +++- README.md | 18 ++++- SpeechRecognizerButton.podspec | 2 +- .../project.pbxproj | 16 ++-- SpeechRecognizerButton/SFButton.swift | 81 +++++++++++++------ SpeechRecognizerButton/SFWaveformView.swift | 18 ++--- .../SFWaveformViewTests.swift | 58 +++++++++++++ .../SpeechRecognizerButtonTests.swift | 18 ----- 11 files changed, 171 insertions(+), 68 deletions(-) create mode 100644 .travis.yml create mode 100644 SpeechRecognizerButtonTests/SFWaveformViewTests.swift delete mode 100644 SpeechRecognizerButtonTests/SpeechRecognizerButtonTests.swift diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..8853e71 --- /dev/null +++ b/.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) \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ad2e8e..1380caa 100644 --- a/CHANGELOG.md +++ b/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. diff --git a/Example/ViewController.swift b/Example/ViewController.swift index b845158..452a6b9 100644 --- a/Example/ViewController.swift +++ b/Example/ViewController.swift @@ -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() diff --git a/ExampleUITests/ExampleUITests.swift b/ExampleUITests/ExampleUITests.swift index ff1cdad..01f6b70 100644 --- a/ExampleUITests/ExampleUITests.swift +++ b/ExampleUITests/ExampleUITests.swift @@ -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)) } } diff --git a/README.md b/README.md index b90f3d9..63e14a1 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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: diff --git a/SpeechRecognizerButton.podspec b/SpeechRecognizerButton.podspec index ec84fd9..851d151 100644 --- a/SpeechRecognizerButton.podspec +++ b/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' diff --git a/SpeechRecognizerButton.xcodeproj/project.pbxproj b/SpeechRecognizerButton.xcodeproj/project.pbxproj index f9325c2..37f6c97 100644 --- a/SpeechRecognizerButton.xcodeproj/project.pbxproj +++ b/SpeechRecognizerButton.xcodeproj/project.pbxproj @@ -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 */; }; @@ -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 */ @@ -68,7 +69,7 @@ 814DED3820442E4000DCDDE0 /* SpeechRecognizerButton.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SpeechRecognizerButton.h; sourceTree = ""; }; 814DED3920442E4000DCDDE0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 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 = ""; }; + 814DED4320442E4100DCDDE0 /* SFWaveformViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFWaveformViewTests.swift; sourceTree = ""; }; 814DED4520442E4100DCDDE0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 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 = ""; }; @@ -84,6 +85,7 @@ 814DED712044306000DCDDE0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D557879A204968A200B88855 /* SFWaveformView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFWaveformView.swift; sourceTree = ""; }; 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 = ""; }; D5F5C1402045953B00F11B18 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/LaunchScreen.strings; sourceTree = ""; }; @@ -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 */, @@ -163,7 +166,7 @@ 814DED4220442E4100DCDDE0 /* SpeechRecognizerButtonTests */ = { isa = PBXGroup; children = ( - 814DED4320442E4100DCDDE0 /* SpeechRecognizerButtonTests.swift */, + 814DED4320442E4100DCDDE0 /* SFWaveformViewTests.swift */, 814DED4520442E4100DCDDE0 /* Info.plist */, ); path = SpeechRecognizerButtonTests; @@ -172,6 +175,7 @@ 814DED4F20442E5700DCDDE0 /* Frameworks */ = { isa = PBXGroup; children = ( + D557879E204EBAEB00B88855 /* AudioToolbox.framework */, D557879C204D695C00B88855 /* UIKit.framework */, D5F5C13B204571D400F11B18 /* AVFoundation.framework */, 814DED5020442E5700DCDDE0 /* Speech.framework */, @@ -383,7 +387,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 814DED4420442E4100DCDDE0 /* SpeechRecognizerButtonTests.swift in Sources */, + 814DED4420442E4100DCDDE0 /* SFWaveformViewTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -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; @@ -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; diff --git a/SpeechRecognizerButton/SFButton.swift b/SpeechRecognizerButton/SFButton.swift index daea804..8a43713 100644 --- a/SpeechRecognizerButton/SFButton.swift +++ b/SpeechRecognizerButton/SFButton.swift @@ -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 { @@ -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? @@ -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: { @@ -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) { diff --git a/SpeechRecognizerButton/SFWaveformView.swift b/SpeechRecognizerButton/SFWaveformView.swift index b0817e0..3435b0b 100644 --- a/SpeechRecognizerButton/SFWaveformView.swift +++ b/SpeechRecognizerButton/SFWaveformView.swift @@ -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 @@ -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 @@ -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() } } } diff --git a/SpeechRecognizerButtonTests/SFWaveformViewTests.swift b/SpeechRecognizerButtonTests/SFWaveformViewTests.swift new file mode 100644 index 0000000..6857fd7 --- /dev/null +++ b/SpeechRecognizerButtonTests/SFWaveformViewTests.swift @@ -0,0 +1,58 @@ +// +// SFWaveformViewTests.swift +// SpeechRecognizerButtonTests +// +// Created by Alejandro Ruperez Hernando on 26/2/18. +// Copyright © 2018 alexruperez. All rights reserved. +// + +import XCTest +@testable import SpeechRecognizerButton + +class SFWaveformViewTests: XCTestCase { + + var button: SFButton! + var waveformView: SFWaveformView! + + override func setUp() { + super.setUp() + waveformView = SFWaveformView() + button = SFButton() + button.waveformView = waveformView + } + + func testWaveformView() { + waveformView.draw(.zero) + } + + func testWaveformViewAlpha() { + button.draw(.zero) + XCTAssertEqual(waveformView.alpha, 0) + } + + func testWaveformViewWeakReference() { + button.waveformView = SFWaveformView() + XCTAssertNil(button.waveformView) + } + + func testWaveformViewUpdateWithLevel() { + let level: CGFloat = waveformView.idleAmplitude * 10 + waveformView.updateWithLevel(level) + XCTAssertEqual(waveformView._phase, waveformView.phaseShift) + XCTAssertEqual(waveformView._amplitude, level) + } + + func testWaveformViewUpdateWithLevelUnderIdle() { + let level: CGFloat = waveformView.idleAmplitude / 10 + waveformView.updateWithLevel(level) + XCTAssertEqual(waveformView._phase, waveformView.phaseShift) + XCTAssertEqual(waveformView._amplitude, waveformView.idleAmplitude) + } + + override func tearDown() { + button = nil + waveformView = nil + super.tearDown() + } + +} diff --git a/SpeechRecognizerButtonTests/SpeechRecognizerButtonTests.swift b/SpeechRecognizerButtonTests/SpeechRecognizerButtonTests.swift deleted file mode 100644 index 6037f18..0000000 --- a/SpeechRecognizerButtonTests/SpeechRecognizerButtonTests.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// SpeechRecognizerButtonTests.swift -// SpeechRecognizerButtonTests -// -// Created by Alejandro Ruperez Hernando on 26/2/18. -// Copyright © 2018 alexruperez. All rights reserved. -// - -import XCTest -@testable import SpeechRecognizerButton - -class SpeechRecognizerButtonTests: XCTestCase { - - func testSFButton() { - XCTFail() - } - -}