How to make SFSpeechRecognizer available on macOS? - swift

I am trying to use Apple's Speech framework to do speech recognition on macOS 10.15.1. Before macOS 10.15, speech recognition was only available on iOS, but according to the documentation and this talk, should now be available on macOS as well.
However, all my my attempts to use it have resulted in the SFSpeechRecognizer's isAvailable property being set to false. Per that talk and the documentation I've enabled Siri and made sure that my app has the "Privacy - Speech Recognition Usage Description" key set to a string value in Info.plist.
I've also tried enabling code signing (which this question suggests might be necessary), enabling Dictation under Keyboard > Dictation in the System preferences.
Here's some example code, although the specifics probably aren't important; I've tried it using a Storyboard instead of SwiftUI, putting the instantiation of the SFSpeechRecognizer inside and outside the requestAuthorization callback, leaving the locale unspecified, etc. Nothing seems to have any effect:
import SwiftUI
import Speech
struct ContentView: View {
func tryAuth() {
SFSpeechRecognizer.requestAuthorization { authStatus in
switch authStatus {
case .authorized:
print("authorized")
case .denied:
print("denied")
case .restricted:
print("restricted")
case .notDetermined:
print("notDetermined")
#unknown default:
print("unanticipated auth status encountered")
}
}
}
func speechTest() {
guard let recognizer = SFSpeechRecognizer(locale: Locale(identifier: "en-US")) else {
// Not supported for device's locale
print("couldnt get recognizer")
return
}
if !recognizer.isAvailable {
print("not available")
return
}
print("Success")
}
var body: some View {
VStack {
Button("Try auth") {
self.tryAuth()
}
Button("Test") {
self.speechTest()
}
}
}
}
What's especially odd is that if I run the app and then click the "Try auth" button, the authStatus returned from the callback is always .authorized. However, I've never been presented with a dialog asking me to authorize the app, and the app doesn't show up in the list of authorized apps under System Preferences > Security and Privacy > Privacy > Speech Recogniztion.
Nonetheless, clicking the "Test" button afterwards results in printing not available.
It seems like there's some hole in my understanding of the macOS privacy/permissions system, but I'm not sure how to debug further. I also think it should be possible to get this working, because I've seen other questions on StackOverflow suggesting that people have done so, for example here, here.
EDIT: At the suggestion of a comment, I tried simply ignoring the fact that isAvailable is false by replacing my check for it with code to actually try to transcribe a file, e.g.:
let request = SFSpeechURLRecognitionRequest(url: URL(fileURLWithPath: "/Users/james/Downloads/test.wav"))
recognizer.recognitionTask(with: request) { (result, error) in
guard let result = result else {
print("There was an error transcribing that file")
print("print \(error!.localizedDescription)")
return
}
if result.isFinal {
print(result.bestTranscription.formattedString)
}
}
Then it fails, printing: The operation couldn’t be completed. (kAFAssistantErrorDomain error 1700.). So it seems like it really is necessary to check for isAvailable, and my question remains: how to get it to be true?

Had similar problems with SFSpeechRecognizer... Perhaps you can set the delegate of SFSpeechRecognizer before requesting for authorization, as shown here.
For example:
class ViewController: NSViewController {
var speechRecognizer: SFSpeechRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "en-US"))
speechRecognizer.delegate = self
}
override func viewWillAppear() {
SFSpeechRecognizer.requestAuthorization { authStatus in
...
}
if !speechRecognizer.isAvailable {
print("Not available!")
}
let url = Bundle.main.url(forResource: "sample", withExtension: "mp3")!
let request = SFSpeechURLRecognitionRequest(url: url)
// will now ask for authorisation
speechRecognizer.recognitionTask(with: request) { (result, error) in
...
}
}
}
extension ViewController: SFSpeechRecognizerDelegate {
}
Then the authorisation dialog will be properly shown.
In addition, it seems that only when there is a call to recognitionTask, that the user will be asked to give permission. Instead, calling requestAuthorization alone will not have any effect.

Related

Is there a way to set user desktop background wallpaper programmatically in Swift?

I haven't been able to find much documentation on this except this (Setting the Desktop background on OSX using Swift 2)
Is there a way to programmatically set users' desktop wallpaper using Swift on a macOS app? I understand I'll have to disable sandbox for it, but what can I use to programmatically set the desktop wallpaper?
your quoted link has the answer. Here is a demo:
struct ContentView: View {
#State private var showFileImporter = false
var body: some View {
Button("Pick Background Image") {
showFileImporter = true
}
.fileImporter(isPresented: $showFileImporter, allowedContentTypes: [.jpeg, .tiff, .png]) { result in
switch result {
case .failure(let error):
print("Error selecting file \(error.localizedDescription)")
case .success(let url):
print("selected url = \(url)")
setDesktopImage(url: url)
}
}
}
func setDesktopImage(url: URL) {
do {
if let screen = NSScreen.main {
try NSWorkspace.shared.setDesktopImageURL(url, for: screen, options: [:])
}
} catch {
print(error)
}
}
}

How to remember devices using AWS Amplify SDK?

I have implemented a sign up/ sign in feature using AWS Amplify and swift using my own View controllers instead of the Drop-in auth. The problem starts once I quit the app and restart it. After I do that the user is no longer signed in. I have set remember devices to always in the User Pool Settings. Has anyone ever encountered this problem?
Here is my function where the user gets confirmed and everything works properly except for remembering the user
#objc func confirm(){
print("confirm pressed")
guard let verificationCode = verificationTextField.text else{
return
}
AWSMobileClient.default().confirmSignUp(username: username, confirmationCode: verificationCode) { (signUpResult, error) in
if let signUpResult = signUpResult{
switch(signUpResult.signUpConfirmationState){
case .confirmed:
AWSMobileClient.default().deviceOperations.updateStatus(remembered: true) { (result, error) in //This is where I try to save the users device
print("User is signed up and confirmed")
DispatchQueue.main.async {
let signedInTabBar = SignedInTabBarController()
self.view.window!.rootViewController = signedInTabBar
}
}
case .unconfirmed:
print("User is not confirmed and needs verification via \(signUpResult.codeDeliveryDetails!.deliveryMedium) sent at \(signUpResult.codeDeliveryDetails!.destination!)")
case.unknown:
print("Unexpected case")
}
}else if let error = error {
print("\(error.localizedDescription)")
}
}
}
As I right understand you need to check if a user signed in or not. To do this you need to add this code on the start of the app or wherever you check a user status:
AWSMobileClient.default().initialize { userState, error in
OperationQueue.main.addOperation {
if let error = error {
AWSMobileClient.default().signOut()
assertionFailure("Logic after init error: \(error.localizedDescription)")
}
guard let userState = userState else {
AWSMobileClient.default().signOut()
return
}
guard userState == .signedIn else {
return
}
}
}

Why i get error 7014 when trying Sign in with Apple only on tvOS target

i have an application with 2 different target: one for iOS and one for tvOS. We need to add Sign in with Apple functionality so i wrote an object to manage that and use it on both target. When i try to login on iOS target it works perfectly but when i try to use it on tvOS target, i always get the error Code=-7014.
Here some code.
/// Execute apple login, asking for user first and last name and user e-mail
private func appleLogin() {
if #available(iOS 13, tvOS 13, *) {
let request = ASAuthorizationAppleIDProvider().createRequest()
request.requestedScopes = [.fullName, .email]
request.requestedOperation = .operationLogin
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = self
controller.presentationContextProvider = _presentingViewController as? ASAuthorizationControllerPresentationContextProviding
controller.performRequests()
}
}
And delegate method to manage Apple response
//MARK: - Sing in with Apple
#available(iOS 13.0, tvOS 13.0, *)
extension VVAuthenticationManager: ASAuthorizationControllerDelegate {
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
LOGI("Apple complete authorization")
switch authorization.credential {
case let appleIDCredential as ASAuthorizationAppleIDCredential:
LOGI("I have user credential")
if let mail = appleIDCredential.email, let authCode = appleIDCredential.authorizationCode, let authStringCode = String(data: authCode, encoding: .utf8) {
LOGI("TOKEN: \(authStringCode)")
// API Registration here
}
default:
// Custom manage error here
break
}
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
LOGE("Error: \(error.localizedDescription) code: \(error.asAFError?.responseCode ?? 0)")
if let e = error as? ASAuthorizationError {
LOGP("code \(e.code.rawValue)")
LOGP("code \(e.localizedDescription)")
e.userInfo.keys.forEach { (body) in
LOGI("BODY \(body)")
}
e.errorUserInfo.keys.forEach { (body) in
LOGI("BODY ERR \(body)")
}
switch e.code {
case .failed:
LOGP("Failed")
case .canceled:
LOGP("Canceled")
case .invalidResponse:
LOGP("Invalid Response")
case .notHandled:
LOGP("Not Handled")
case .unknown:
LOGP("Unknow error code")
default:
LOGD("NOT RECOGNIZED")
}
}
}
}
Everytime i try to login on tvOS delegate method
authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error)
is called and i get an unknown error (code 1000). Any suggestion?
Thanks in advance
i had a similar problem in past and the issue was on simulator. Sign in with Apple required a 2 factor authentication account and, when try it on Apple TV the second phase of authentication will continue on an iPhone or an iPad.
You should try to run your app on a phisic Apple Tv and sign in with an account active on an iPhone or iPad.

Executing code after nested completion handlers

I am writing a Safari app extension and want to fetch the URL for the active page in my view controller.
This means nested completion handlers to fetch the window, to fetch the tab, to fetch the page, to access its properties. Annoying but simple enough. It looks like this:
func doStuffWithURL() {
var url: URL?
SFSafariApplication.getActiveWindow { (window) in
window?.getActiveTab { (tab) in
tab?.getActivePage { (page) in
page?.getPropertiesWithCompletionHandler { (properties) in
url = properties?.url
}
}
}
}
// NOW DO STUFF WITH THE URL
NSLog("The URL is \(String(describing: url))")
}
The obvious problem is it does not work. Being completion handlers they will not be executed until the end of the function. The variable url will be nil, and the stuff will be done before any attempt is made to get the URL.
One way around this is to use a DispatchQueue. It works, but the code is truly ugly:
func doStuffWithURL() {
var url: URL?
let group = DispatchGroup()
group.enter()
SFSafariApplication.getActiveWindow { (window) in
if let window = window {
group.enter()
window.getActiveTab { (tab) in
if let tab = tab {
group.enter()
tab.getActivePage { (page) in
if let page = page {
group.enter()
page.getPropertiesWithCompletionHandler { (properties) in
url = properties?.url
group.leave()
}
}
group.leave()
}
}
group.leave()
}
}
group.leave()
}
// NOW DO STUFF WITH THE URL
group.notify(queue: .main) {
NSLog("The URL is \(String(describing: url))")
}
}
The if blocks are needed to know we are not dealing with a nil value. We need to be certain a completion handler will return, and therefore a .leave() call before we can call a .enter() to end up back at zero.
I cannot even bury all that ugliness away in some kind of getURLForPage() function or extension (adding some kind of SFSafariApplication.getPageProperties would be my preference) as obviously you cannot return from a function from within a .notify block.
Although I tried creating a function using queue.wait and a different DispatchQueue as described in the following answer to be able to use return…
https://stackoverflow.com/a/42484670/2081620
…not unsurprisingly to me it causes deadlock, as the .wait is still executing on the main queue.
Is there a better way of achieving this? The "stuff to do," incidentally, is to update the UI at a user request so needs to be on the main queue.
Edit: For the avoidance of doubt, this is not an iOS question. Whilst similar principles apply, Safari app extensions are a feature of Safari for macOS only.
Thanks to Larme's suggestions in the comments, I have come up with a solution that hides the ugliness, is reusable, and keep the code clean and standard.
The nested completion handlers can be replaced by an extension to the SFSafariApplication class so that only one is required in the main body of the code.
extension SFSafariApplication {
static func getActivePageProperties(_ completionHandler: #escaping (SFSafariPageProperties?) -> Void) {
self.getActiveWindow { (window) in
guard let window = window else { return completionHandler(nil) }
window.getActiveTab { (tab) in
guard let tab = tab else { return completionHandler(nil) }
tab.getActivePage { (page) in
guard let page = page else { return completionHandler(nil) }
page.getPropertiesWithCompletionHandler { (properties) in
return completionHandler(properties)
}
}
}
}
}
}
Then in the code it can be used as:
func doStuffWithURL() {
SFSafariApplication.getActivePageProperties { (properties) in
if let url = properties?.url {
// NOW DO STUFF WITH THE URL
NSLog("URL is \(url))")
} else {
// NOW DO STUFF WHERE THERE IS NO URL
NSLog("URL ERROR")
}
}
}

xcode swift 3 lock screen remote control not working?

i am trying to catch play/stop/next/prev user action from lock screen when player is active and playing , for some how its not working .
inside class MusicPlayerViewController: BaseViewController
override func viewDidLoad() {
super.viewDidLoad()
do {
UIApplication.shared.beginReceivingRemoteControlEvents()
print("bb> Receiving remote control events\n")
} catch {
print("bb> Audio Session error.\n")
}
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.nextTrackCommand.isEnabled = true
commandCenter.nextTrackCommand.addTarget(self, action: #selector(MusicPlayerViewController.nextTrackCommandSelector))
}
func nextTrackCommandSelector()
{
print("omg")
}
in the log i can see only
bb> Receiving remote control events
also inside AppDelegate.swift has
override func remoteControlReceived(with event: UIEvent?) {
print("remote::")
guard let event = event else {
print("no event\n")
return
}
guard event.type == UIEventType.remoteControl else {
print("received other event type\n")
return
}
switch event.subtype {
case UIEventSubtype.remoteControlPlay:
print("received remote play\n")
case UIEventSubtype.remoteControlPause:
print("received remote pause\n")
case UIEventSubtype.remoteControlTogglePlayPause:
print("received toggle\n")
case UIEventSubtype.remoteControlNextTrack:
print("clicked next \n")
case UIEventSubtype.remoteControlPreviousTrack:
print("clicked Prev \n")
default:
print("received \(event.subtype) which we did not process\n")
}
}
and capabilities
what did i miss ?
A few things:
You're using both the delegate style and MPRemoteCommandCenter style of remote event handling. Pick one, rather than both to see if they're conflicting. Apple recommends the MPRemoteCommandCenter style, but if you're supporting older iOS versions, you may need to stick with the delegate style.
If you do elect to use the delegate style, my recollection is that you must also become the first responder in order to begin receiving remote control events.
Regardless of which style of event handling you choose, you must play audio in your app to let the system know to route events to you. The lock screen (or audio control center) should have your app listed in the "Now Playing" area.
i found the solution
for swift 3 i have to add this
try! AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback, with: [])
try! AVAudioSession.sharedInstance().setActive(true)