iOS: "An instance of SPAudioStreamingController is already in use." - iphone

i'm developping app using spotify-iOS-SDK, i have succesfully connect my app to Spotify and the audio is playing, but the problem is: When i close my PlaySongViewController, my app will be crash
"An instance of SPAudioStreamingController is already in use."
unless i stop my spotifyPlayer with this code after i logout
var spotifyPlayer: SPTAudioStreamingController?
#IBAction func closeView(_ sender: UIButton) {
print("close view")
self.dismiss(animated: true, completion: nil)
self.spotifyPlayer?.logout()
invalidateTimers()
}
func audioStreamingDidLogout(_ audioStreaming: SPTAudioStreamingController!) {
print("after logout")
try! self.spotifyPlayer?.stop()
}
The problem is continue if i close my ViewController directly before this code is working properly
self.spotifyPlayer = SPTAudioStreamingController.sharedInstance()
self.spotifyPlayer!.playbackDelegate = self
self.spotifyPlayer!.delegate = self
try! spotifyPlayer?.start(withClientId: auth.clientID)
self.spotifyPlayer!.login(withAccessToken: authSession.accessToken)
When i pick another song to open my PlaySongViewController again, it will be crashed with
"An instance of SPAudioStreamingController is already in use."
Another problem is when i try to log in with non-premium account, when i open PlaySongViewController, it will show "Spotify Premium Required" and when i close my PlaySongViewController and open another PlaySongViewController to play another song, it will be crashed again with the 'already in use' error
Can i bypass this code if i have start my spotifyPlayer?
try! spotifyPlayer?.start(withClientId: auth.clientID)
Or Are there any solutions?

Well there are two things I am seeing here:
try! spotifyPlayer?.start(withClientId: auth.clientID)
That should be a do and catch, if you look at the full signature for the start signature it states that it will throw errors. Instead make sure to catch errors when you can.
do {
try self.spotifyPlayer?.start(withClientId: auth.clientID)
} catch {
print("Failed to start with clientId")
}
It is important not force a try but instead handle errors.
You use a do-catch statement to handle errors by running a block of
code. If an error is thrown by the code in the do clause, it is
matched against the catch clauses to determine which one of them can
handle the error.
Additionally on the SPTAudioStreamingController.sharedInstance() is a property that is worth checking before the do and catch which is player?.loggedIn you can use this method to check before you attempt to call login, I've put an example of this in the singleton's login method below.
/** Returns `YES` if the receiver is logged into the Spotify service, otherwise `NO`. */
open var loggedIn: Bool { get }
Secondly you might be better off creating a singleton to handle all the logic of playing music that the View controller interfaces with so you don't end up with multiple view controllers trying to use the same spotifyPlayer and call start when it isn't necessary.
class MusicPlayer: {
static let shared = MusicPlayer()
fileprivate let player = SPTAudioStreamingController.sharedInstance()
override init() {
player?.playbackDelegate = self
player?.delegate = self
}
func login() {
if player?.loggedIn { return }
do {
try self.spotifyPlayer?.start(withClientId: auth.clientID)
} catch {
print("Failed to start with clientId")
}
}
}
and then in the view controller link to MusicPlayer.shared.

Related

Extra trailing closure passed in call - Task showing error

I'm pretty new to Swift, and attempting to convert a program in c# to swift, and I'm receiving the following error whenever I try to use a Task.
I tried to make the button's action function async, and ran into more errors, and read that one should use a wrapper function. Thus the following code is my attempt..
#IBAction func noteBrowser_button(_ sender: Any){
Task{
await load_noteBrowserScreen()
}
}
func load_noteBrowserScreen()async throws {
do{
if ( try await Server.serverVersion() >= Server.retrieveCustomerLimit )
{
//screen to change to
let screen = storyboard!.instantiateViewController(identifier: "Note_Browser") as UIViewController?
//show screen
show(screen!, sender: self)
}
else
{
Popup.showMessage(parent: Popup.parent,
title: "Server Update Required",
msg: "Your server needs updated to enable this feature.")
}
}
catch {}
}
So it turns out the answer to the problem above was conflicting class names. There is the built-in Task with swift, and there was a Task.swift class, which was throwing a conflict, and by renaming that class removed the above errors.
Thanks everyone for your time offering suggestions.

ApplicationWillTerminate - how do i run JS callback?

What I am dealing with: web page + backend (php), swift app (wkwebview + some native components).
What I am trying to achieve: I need to let the web page know my app is changing it's state, by executing some java scripts (one script for "enter background mode", another one for "enter foreground", and so on).
What is my issue: I can't handle the case of app termination. Whenever the user is killing the app by home-double-click/swipe up, ApplicationWillTerminate method from AppDelegate is being called, but it fails to execute webView.evaluateJavaScript from here, as far as I was able to understand - due to the closure / completion handler is has, for it is async by design.
All other cases are covered, works fine, and with the last one for termination I am stuck.
I am playing around with the following code:
func applicationWillTerminate(_ application: UIApplication) {
if let callback = unloadListenerCallback {
if callback == "" {
debugPrint("got null callback - stop listening")
unloadListenerCallback = nil
}
else {
jsCallbackInjection(s: callback) //executing JS callback injection into WebView
}
}
}
unloadListenerCallback is the string? variable, works as a charm in other cases (just to mention it for clarity). jsCallbackInjection is the function, also works perfectly elsewhere:
func jsCallbackInjection(s: String) {
let jscallbackname = s
HomeController.instance.webView?.evaluateJavaScript(jscallbackname) { (result, error) in
if error == nil {
print(result as Any)
print("Executed js callback: \(jscallbackname)")
} else {
print("Error is \(String(describing: error)), js callback is: \(jscallbackname)")
}
}
}
How do I made this code working, if possible? Any ideas, suggestions, tips or tricks? If not possible, any ideas on "how do I let my web counterpart know my app was terminated"? What do I do from here?
This is a cross-post from Apple Dev forum, where it is sitting for >1 week unattended.
Answering my own question:
I was not able to find a way of running JS from ApplicationWillTerminate.
However, I found a way of solving my issue, which is - instead of running JS, I am posting to my web service like that:
func applicationWillTerminate(_ application: UIApplication) {
let semaphore = DispatchSemaphore(value: 0)
//setup your request here - Alamofire in my case
DispatchQueue.global(qos: .background).async {
//make your request here
onComplete: { (response: Update) in
//handle response if needed
semaphore.signal()
},
onFail: {
//handle failure if needed
semaphore.signal()
})
}
semaphore.wait(timeout: .distantFuture)
}
This way, I am able to consistently report my app termination to the web page. I was lucky enough to have ajax already set up on the other end of a pipe, which I am just POSTing into with the simple AF request, so I don't need to struggle with JS anymore.
Basing on what I was able to find, there is NO suitable way of managing JS execution, due to
with semaphores, as soon as webview and it's methods are to be handled in main thread, you'll deadlock by using semaphore.wait()
no way I was able to find to run evaluateJavaScript synchronously
no way I was able to find to run the JS itself from JavaScriptCore, or some other way, within the same session and context
However, if someone still can contribute and provide solution, I'll be happy to accept it!

how to detach a listener in a local scope?

I want to detach a snapshotListener in a viewController when a button is pressed. I was reading other stack over flow questions and the documentation and they were calling the remove method for the listener in the same function. I tried to do that in my situation but my snapshotListener just didn't end up working at all.
Here's my function and block of code that I want to tweak.
#objc func doneTapped() {
let updateListener = db.collection("school_users/\(user?.uid)/events").whereField("event_name", isEqualTo: navigationItem.title).addSnapshotListener(includeMetadataChanges: true) { (querySnapshot, error) in
if let error = error {
print("There was an error fetching the documents: \(error)")
} else {
self.eventName = querySnapshot!.documents.map { document in
return EventName(eventName: (document.get("event_name") as! String))
}
self.db.document("school_users/\(self.user?.uid)/events/\(self.docIDUneditableTextF.text!)").updateData(["event_date": self.dateEditableTextF.text, "event_cost": self.costEditableTextF.text, "for_grades": self.gradesEditableTextF.text]) { (error) in
if let error = error {
print("There was an error updating the document: \(error)")
} else {
print("The document was successfully updated."
}
}
}
}
dateEditableTextF.resignFirstResponder()
dateEditableTextF.isEnabled = false
costEditableTextF.resignFirstResponder()
costEditableTextF.isEnabled = false
gradesEditableTextF.resignFirstResponder()
gradesEditableTextF.isEnabled = false
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(editTapped))
}
I tried calling updateListener.remove() but it made my snapshotListener not work at all, also when the document updates, the print statement is never ending, is that also because the listener is still active or is that a different issue?
addSnapshotListener will give you updates whenever the data changes. I'm unclear why you would want to immediately remove the listener, if you really did want to receive updates -- and, as you pointed out, immediately removing it will basically cause to it not function at all. Perhaps post a link to some of those posts/documentation where you saw the code you're referencing and someone can give insight into what's happening.
My suspicion is that you don't actually need the updates to the data. In that case, you can just use .getDocuments() instead. See the Firestore documentation here about different ways to get data: https://firebase.google.com/docs/firestore/query-data/get-data
The second problem (the infinite print) is related to the first. Because you have a listener, which will return updates when the data changes, when you do your second database call (your updateData), that updates your data, triggering the listener again. This will keep looping because they will keep calling each other. This is another sign that perhaps you don't actually want a listener, but a single call to get the data. If you do in fact want updates, you'll have to find a way to decouple your second request so that you don't get in the loop.
Update based on comments: (Example of removing the listener in a different function)
On your view, view controller, etc, declare a property for the listener:
class MyViewController : UIViewController {
private var documentListener: ListenerRegistration? //assuming that ListenerRegistration is the correct type here, but you can check the current type of your updateListener to check
}
Then, in your function, set your listener to that:
documentListener = db.collection("school_users/\(user?.uid)/events").whereField("event_name", isEqualTo: navigationItem.title).addSnapshotListener()...
Then, later (like in viewDidDisappear), you can remove it:
documentListener?.remove()

Customizing sandboxed NSSavePanel alert

I am validating urls from NSSavePanel using the delegate's panel(_:validate) method, throwing error in case of invalid url. In such case the NSSavePanel presents an alert, which I want to customize (meaning present some human readable description) depending on the error thrown, keeping the save panel window open and then letting you choose another path.
LocalizedError works just fine when not using App Sandbox but in a sandboxed app the getter for error description is never called and the message in the alert is generic "Operation couldn't be completed. (#yourErrorType)", which I guess is somehow caused by the different inheritance chain for sandboxed NSSavePanels.
I am struggling figuring a way around this - is it possible to customize the alert somehow while still keeping the app sandboxed?
Addendum: Permissions for User Selected File => r/w. Running the following example produces different alerts with/without sandbox.
func runSavePanel()
{
let panel = NSSavePanel()
let delegate = SavePanelDelegate()
panel.delegate = delegate
_ = panel.runModal()
}
class SavePanelDelegate: NSObject, NSOpenSavePanelDelegate {
func panel(_ sender: Any, validate url: URL) throws {
throw CustomError.whatever
}
}
enum CustomError: LocalizedError {
case whatever
var errorDescription: String? {
get {
return "my description"
}
}
}
So, after a bit of further digging I can tell the solution of the riddle finally although I can only guess the reasons why it was made tricky by Apple. Apparently NSError exclusively needs to be used. The customization has to be done in userInfo, say
let userInfo = [NSLocalizedDescriptionKey: "yourLocalizedDescription", NSLocalizedRecoverySuggestionErrorKey: "yourSuggestion"]
throw NSError(domain: "whatever", code: 0, userInfo: userInfo)
etc. By the way subclassing NSError doesn't work, the Sandbox will just happily ignore you :)

All asynchronous calls succeed or none, how to handle

I'm trying to create an online mobile application and can't figure out the best way to handle functions with multiple asynchronous calls. Say I have a function for example that updates a user in some way, but involved multiple asynchronous calls in the single function call. So for example:
// Function caller
update(myUser) { (updatedUser, error) in
if let error = error {
// Present some error UI to the user
}
if let updatedUser = updatedUser {
// Do something with the user
}
}
// Function implementation
public func updateUser(user: User, completion: #escaping (User?, Error?) -> () {
// asynchronous call A
updateUserTable(user: User) { error in
if let error = error {
completion(nil, error)
} else {
// create some new user object
completion(user, nil)
}
}
// asynchronous call B
uploadMediaForUser(user: User) { error in
if let error = error {
completion(nil, error)
}
}
// asynchronous call C
removeOldReferenceForUser(user: User) { error in
if let error = error {
completion(nil, error)
}
}
// Possibly any additional amount of asynchronous calls...
}
In a case like this, where one function call like updating a user involved multiple asynchronous calls, is this an all or nothing situation? Say for example the updateUserTable() call completes, but the user disconnects from the internet as uploadMediaForUser() was running, and that throws an error. Since updateUserTable() completed fine, my function caller thinks this method succeeded when in fact not everything involved in updating the user completed. Now I'm stuck with a user that might have mismatched references or wrong information in my database because the user's connection dropped mid call.
How do I handle this all or nothing case? If EVERY asynchronous call completed without an error, I know updating the user was a success. If only a partial amount of asynchronous calls succeeded and some failed, this is BAD and I need to either undo the changes that succeeded or attempt the failed methods again.
What do I do in this scenario? And also, and how do I use my completion closure to help identify the actions needed depending on the success or failure of the method. Did all them succeed? Good, tell the user. Do some succeed and some failed? Bad, revert changes or try again (i dont know)??
Edit:
Just calling my completion with the error doesn't seem like enough. Sure the user sees that something failed, but that doesn't help with the application knowing the steps needed to fix the damage where partial changes were made.
I would suggest adding helper enums for your tasks and returned result, things like (User?, Error?) have a small ambiguity of the case when for example both are nil? or you have the User and the Error set, is it a success or not?
Regarding the all succeeded or some failed - I would suggest using the DispatchGroup to notify when all tasks finished (and check how they finished in the end).
Also from you current code, when some request fails it's not clear for which user - as you pass nil, so it might bring difficulties in rolling it back after failure.
So in my point of view something like below (not tested the code, but think you should catch the idea from it) could give you control about the issues you described:
public enum UpdateTask {
case userTable
case mediaUpload
// ... any more tasks you need
}
public enum UpdateResult {
case success
case error([UpdateTask: Error])
}
// Function implementation
public func updateUser(user: User, completion: #escaping (User, UpdateResult) -> ()) {
let updateGroup = DispatchGroup()
var tasksErrors = [UpdateTask: Error]()
// asynchronous call A
updateGroup.enter()
updateUserTable(user: User) { error in
if let error = error {
tasksErrors[.userTable] = error
}
updateGroup.leave()
}
// ... any other similar tasks here
updateGroup.notify(queue: DispatchQueue.global()) { // Choose the Queue that suits your needs here by yourself
if tasksErrors.isEmpty {
completion(user, .success)
} else {
completion(user, .error(tasksErrors))
}
}
}
Keep a “previous” version of everything changed, then if something failed revert back to the “previous” versions. Only change UI once all returned without failure, and if one failed, revert to “previous” version.
EX:
var temporary = “userName”
getChanges(fromUser) {
If error {
userName = temporary //This reverts back due to failure.
}
}