When I try to call sessionManager.initialize() neither func sessionManager(manager: SPTSessionManager, didFailWith error: Error) nor func sessionManager(manager: SPTSessionManager, didInitiate session: SPTSession) are called.
I have a nodeJS server running on AWS for token access and refresh and I have also tried running a local Ruby server to get the token. No matter what, calling initialize() does nothing. It does fail or succeed and nothing is output to console. I have tried running the XCode debugger and it seems as if the program just skips past initialize. Here is my complete ViewController.swift file with the unrelated/private parts deleted:
import UIKit
import Firebase
class LobbyAdminViewController: UIViewController, SPTSessionManagerDelegate, SPTAppRemoteDelegate, SPTAppRemotePlayerStateDelegate {
fileprivate let SpotifyClientID = "client_id"
fileprivate let SpotifyRedirectURI = URL(string: "redirect_url")!
fileprivate var lastPlayerState: SPTAppRemotePlayerState?
var refreshAPI = "token_server/refresh_token"
var tokenAPI = "token_server/token"
lazy var configuration: SPTConfiguration = {
let configuration = SPTConfiguration(clientID: SpotifyClientID, redirectURL: SpotifyRedirectURI)
configuration.playURI = ""
configuration.tokenSwapURL = URL(string: tokenAPI)
configuration.tokenRefreshURL = URL(string: refreshAPI)
return configuration
}()
lazy var sessionManager: SPTSessionManager = {
let manager = SPTSessionManager(configuration: configuration, delegate: self)
return manager
}()
lazy var appRemote: SPTAppRemote = {
let appRemote = SPTAppRemote(configuration: configuration, logLevel: .debug)
appRemote.delegate = self
return appRemote
}()
override func viewDidLoad() {
super.viewDidLoad()
let random = Int(arc4random_uniform(900000) + 100000)
lobbyCode = String(random)
lobbyCodeLabel.text = lobbyCode
var ref: DatabaseReference!
ref = Database.database().reference()
ref.child(lobbyCode).child("null").setValue("null")
let scope: SPTScope = [.appRemoteControl]
if #available(iOS 11, *) {
print("ios 11+")
sessionManager.initiateSession(with: scope, options: .clientOnly)
} else {
print("ios 11-")
sessionManager.initiateSession(with: scope, options: .clientOnly, presenting: self)
}
}
func update(playerState: SPTAppRemotePlayerState) {
print("Updating")
lastPlayerState = playerState
currentSongLabel.text = playerState.track.name
currentArtistLabel.text = playerState.track.artist.name
if playerState.isPaused {
pausePlayButton.setBackgroundImage(UIImage(named: "play"), for: .normal)
} else {
pausePlayButton.setBackgroundImage(UIImage(named: "pause"), for: .normal)
}
}
func fetchPlayerState() {
print("Getting player state")
appRemote.playerAPI?.getPlayerState({ [weak self] (playerState, error) in
if let error = error {
print("Error getting player state:" + error.localizedDescription)
} else if let playerState = playerState as? SPTAppRemotePlayerState {
self?.update(playerState: playerState)
}
})
}
#IBAction func onTap_pausePlayButton(_ sender: UIButton) {
print("tapped")
if let lastPlayerState = lastPlayerState, lastPlayerState.isPaused {
appRemote.playerAPI?.resume(nil)
print("Resuming")
} else {
appRemote.playerAPI?.pause(nil)
print("Pausing")
}
}
func sessionManager(manager: SPTSessionManager, didFailWith error: Error) {
print("Bad init")
print(error.localizedDescription)
}
func sessionManager(manager: SPTSessionManager, didRenew session: SPTSession) {
print("Renewed")
}
func sessionManager(manager: SPTSessionManager, didInitiate session: SPTSession) {
print("Trying to connect")
appRemote.connectionParameters.accessToken = session.accessToken
print(session.accessToken)
appRemote.connect()
}
// MARK: - SPTAppRemoteDelegate
func appRemoteDidEstablishConnection(_ appRemote: SPTAppRemote) {
print("App Remote Connected")
appRemote.playerAPI?.delegate = self
appRemote.playerAPI?.subscribe(toPlayerState: { (success, error) in
if let error = error {
print("Error subscribing to player state:" + error.localizedDescription)
}
})
fetchPlayerState()
}
func appRemote(_ appRemote: SPTAppRemote, didDisconnectWithError error: Error?) {
lastPlayerState = nil
print("Error connecting to app remote")
}
func appRemote(_ appRemote: SPTAppRemote, didFailConnectionAttemptWithError error: Error?) {
lastPlayerState = nil
print("Another error connectiong to app remote")
}
// MARK: - SPTAppRemotePlayerAPIDelegate
func playerStateDidChange(_ playerState: SPTAppRemotePlayerState) {
print("Player state changed")
update(playerState: playerState)
}
// MARK: - Private Helpers
fileprivate func presentAlertController(title: String, message: String, buttonTitle: String) {
let controller = UIAlertController(title: title, message: message, preferredStyle: .alert)
let action = UIAlertAction(title: buttonTitle, style: .default, handler: nil)
controller.addAction(action)
present(controller, animated: true)
}
}
The only print() statement that fires is "ios 11" in viewDidLoad()
I have scoured the internet for anyone with the same issue and have come up empty.
The only thing I can think of that could be causing this issue is a known runtime issue with iOS 13. This error:
Can't end BackgroundTask: no background task exists with identifier 8 (0x8), or it may have already been ended. Break in UIApplicationEndBackgroundTaskError() to debug.
fires every time the app is sent to the background (ie when the app redirects to spotify to authenticate). However, this issue exists with even a blank app in XCode and does not halt execution.
I just figured that out now. In the scene delegate class you have to implement the
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
}
method and you have to access the sessionManager that you have in your LobbyAdminViewController and create an instance of it and add these lines of code into the method
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
print("Opened url")
guard let url = URLContexts.first?.url else {
return
}
lobbyAdminVC.sessionManager.application(UIApplication.shared, open: url, options: [:])
}
After doing this, the app remote connected and all the print statements printed and said app remote was connected.
I was having the same issue, kept getting the exact same Can't end BackgroundTask error and was stumped for quite a while. Until I figured out the issue (in my case). It has to do with your AppDelegate.swift file. This error code doesn't actually have to do with the issue I don't think, this is just the last thing logged to the console before the session initialization abruptly stops.
With the introduction of scene delegates, the default App Delegate file has changed in recent months. What you need to do is make sure that you are not using this newer App Delegate that works with the scene delegate, but rather, you need to convert your App Delegate to the way it looked in the past.
For me, removing scene delegate completely from my app involved two steps:
1. Revert your AppDelegate.swift file
Mine looks something like this:
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var application: UIApplication!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
return true
}
func applicationWillResignActive(_ application: UIApplication) {
//SpotifyManager.shared.appRemote is of type SPTAppRemote
if SpotifyManager.shared.appRemote.isConnected {
SpotifyManager.shared.appRemote.disconnect()
}
}
func applicationDidBecomeActive(_ application: UIApplication) {
//SpotifyManager.shared.appRemote is of type SPTAppRemote
if let _ = SpotifyManager.shared.appRemote.connectionParameters.accessToken {
SpotifyManager.shared.appRemote.connect()
}
}
}
2. Remove Application Scene Manifest from your info.plist
In your info.plist file, there is a property that tells your app that you are using the scene delegate. We need to delete this from the plist. It should look something like this:
<key>UIApplicationSceneManifest</key>
<dict>
<key>New item</key>
<string></string>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
If you update your app delegate and remove this property from the plist, it should work for you (or, at least, it worked for me).
P.S. if you want to use scene delegate and use the Spotify SDK, I believe you have to do it in the way outlined in this resource. Notably, look for the part of the authorization guide that mentions "If you are using UIScene then you need to use appropriate method in your scene delegate."
Related
I've been trying to add Shazam matching to my app using the new ShazamKit. I've used Apple's sample code found here and adapted it slightly.
import ShazamKit
import AVFAudio
import Combine
#available(iOS 15.0, *)
class ShazamMatcher: NSObject, ObservableObject, SHSessionDelegate {
// MARK: - Properties
#Published var result: SHMatch?
#Published var isRecording = false
private var isInitialSetupDone = false
private var session: SHSession?
private let audioEngine = AVAudioEngine()
// MARK: - Actions
func match() throws {
result = nil
session = SHSession()
session?.delegate = self
try doInitialSetupIfNeeded()
AVAudioSession.sharedInstance().requestRecordPermission { [weak self] success in
guard success, let self = self else {
return
}
try? self.audioEngine.start()
self.isRecording = true
}
}
func stopMatching() {
audioEngine.stop()
isRecording = false
}
// MARK: - Setup
private func doInitialSetupIfNeeded() throws {
guard !isInitialSetupDone else {
return
}
let audioFormat = AVAudioFormat(
standardFormatWithSampleRate: audioEngine.inputNode.outputFormat(forBus: 0).sampleRate,
channels: 1
)
audioEngine.inputNode.installTap(onBus: 0, bufferSize: 2048, format: audioFormat) { [weak session] buffer, audioTime in
session?.matchStreamingBuffer(buffer, at: audioTime)
}
try AVAudioSession.sharedInstance().setCategory(.record)
isInitialSetupDone = true
}
// MARK: - SHSessionDelegate
func session(_ session: SHSession, didFind match: SHMatch) {
// Handle match here
}
func session(_ session: SHSession, didNotFindMatchFor signature: SHSignature, error: Error?) {
// Handle error here
}
}
However, when calling match(), the delegate eventually reports an error The operation couldn’t be completed. (com.apple.ShazamKit error 202.)
I've added a new key using my bundle identifier for the ShazamKit services and downloaded the .p8 file. Do I need this file and if so, how?
Has anybody been able to resolve this error?
I've found a solution. First, apparently the inter-app audio entitlement has to be enabled.
Second, it seems like you need a SHSignatureGenerator as well (I though it would be enough to call matchStreamingBuffer
Here's code that works:
https://github.com/heysaik/ShazamKit-Demo/blob/main/Shazam/ViewController.swift
I need to keep my server updated with user's location even when the app is in the background or terminated.
The location updating is working just fine and seems to wake the application as wanted.
My problem is regarding the forwarding of the user's location via a PUT request to the server.
I was able to go through the code with breakpoints and it goes well except that when I check with Charles if requests are going though, nothing appears.
Here is what I have so far:
API Client
final class BackgroundNetwork: NSObject, BackgroundNetworkInterface, URLSessionDelegate {
private let keychainStorage: Storage
private var backgroundURLSession: URLSession?
init(keychainStorage: Storage) {
self.keychainStorage = keychainStorage
super.init()
defer {
let sessionConfiguration = URLSessionConfiguration.background(withIdentifier: "backgroundURLSession")
sessionConfiguration.sessionSendsLaunchEvents = true
sessionConfiguration.allowsCellularAccess = true
backgroundURLSession = URLSession(configuration: sessionConfiguration,
delegate: self,
delegateQueue: nil)
}
}
func put<T: Encodable>(url: URL, headers: Headers, body: T) {
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "PUT"
let authenticationToken: String? = try? keychainStorage.get(forKey: StorageKeys.authenticationToken)
if let authenticationToken = authenticationToken {
urlRequest.setValue(String(format: "Bearer %#", authenticationToken), forHTTPHeaderField: "Authorization")
}
headers.forEach { (key, value) in
if let value = value as? String {
urlRequest.setValue(value, forHTTPHeaderField: key)
}
}
do {
let jsonData = try JSONEncoder().encode(body)
urlRequest.httpBody = jsonData
} catch {
#if DEBUG
print("\(error.localizedDescription)")
#endif
}
backgroundURLSession?.dataTask(with: urlRequest)
}
}
AppDelegate
// ...
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if launchOptions?[UIApplication.LaunchOptionsKey.location] != nil {
environment.locationInteractor.backgroundDelegate = self
_ = environment.locationInteractor.start()
}
return true
}
// ...
extension AppDelegate: LocationInteractorBackgroundDelegate {
func locationDidUpdate(location: CLLocation) {
taskId = UIApplication.shared.beginBackgroundTask {
UIApplication.shared.endBackgroundTask(self.taskId)
self.taskId = .invalid
}
environment.tourInteractor.updateLocationFromBackground(latitude: Float(location.coordinate.latitude),
longitude: Float(location.coordinate.longitude))
UIApplication.shared.endBackgroundTask(taskId)
taskId = .invalid
}
}
SceneDelegate (yes, the application is using SwiftUI and Combine and I target iOS 13 or later)
func sceneWillEnterForeground(_ scene: UIScene) {
if let environment = (UIApplication.shared.delegate as? AppDelegate)?.environment {
environment.locationInteractor.backgroundDelegate = nil
}
}
func sceneDidEnterBackground(_ scene: UIScene) {
if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) {
appDelegate.environment.locationInteractor.backgroundDelegate = appDelegate
_ = appDelegate.environment.locationInteractor.start()
}
}
So basically, whenever my app goes in background, I set my delegate, restart the location updates and whenever an update comes, my interactor is called and a request is triggered.
According to breakpoints, eveything just works fine up to backgroundURLSession?.dataTask(with: urlRequest). But for some reason the request never gets fired.
I obviously checked Background Modes capabilities Location updates and Background fetch.
Any idea why ?
That’s correct, the line
backgroundURLSession?.dataTask(with: urlRequest)
does nothing. The way to do networking with a session task is to say resume, and you never say that. Your task is created and just thrown away. (I’m surprised the compiler doesn’t warn about this.)
I want to check if both the volume buttons are working fine. So I set the observer AVSystemController_SystemVolumeDidChangeNotification to check that.
NotificationCenter.default.addObserver(self, selector: #selector(volumeCallback(notification:)), name: NSNotification.Name("AVSystemController_SystemVolumeDidChangeNotification"), object: nil)
Given is volumeCallback method:
#objc private func volumeCallback(notification: NSNotification) {
// check if app is in forground
guard UIApplication.shared.applicationState == .active else {
return
}
//get volume level
if let userInfo = notification.userInfo {
if let volumeChangeType = userInfo["AVSystemController_AudioVolumeChangeReasonNotificationParameter"] as? String {
if volumeChangeType == "ExplicitVolumeChange" {
print("value changed")
let level = userInfo["AVSystemController_AudioVolumeNotificationParameter"] as? Float
guard let volLevel = level else {
return
}
// my work here
}
}
}
}
Now the problem is, I am not getting callback in volumeCallback for the first installation of the app. The weird thing is, this method is being called when the app is in background, but not being called in foreground.
I am using iPhone 5s (iOS 10.3.3).
I don't understand what is the problem in this code. Any help will be appreciated.
This can be easily done with key-value observer as AVAudioSession provides outputVolume property. Check here.
You can just add observer on this property and get callbacks.
Here's a simple way of doing this in Swift 5:
// Audio session object
private let session = AVAudioSession.sharedInstance()
// Observer
private var progressObserver: NSKeyValueObservation!
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
do {
try session.setActive(true, options: .notifyOthersOnDeactivation)
} catch {
print("cannot activate session")
}
progressObserver = session.observe(\.outputVolume) { [weak self] (session, value) in
print(session.outputVolume)
}
return true
}
The Problem
I wanted to create a quick action (3D Touch) that allows a user to go directly to the statistics section of a game. I based my solution on the code from this tutorial and got it working with this code. I added some extra code because I want to use the quick action to do something different than in the tutorial. This is the function that is responsible for performing a segue from the initial view to the statistics view. It's in AppDelegate.swift:
From AppDelegate.swift
func handleQuickAction(shortcutItem: UIApplicationShortcutItem) -> Bool {
var quickActionHandled = false
let type = shortcutItem.type.components(separatedBy: ".").last
if let shortcutType = Shortcut.init(rawValue: type!) {
switch shortcutType {
case .statistics:
quickActionHandled = true
// I use dispatchQueue to ensure that the segue occurs immediately
DispatchQueue.main.async {
self.window?.rootViewController?.performSegue(withIdentifier: "ARView_to_stats", sender: self.window?.rootViewController)
}
}
}
return quickActionHandled
}
This function is called from the following delegate function:
From AppDelegate.swift
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: #escaping (Bool) -> Void) {
completionHandler(handleQuickAction(shortcutItem: shortcutItem))
}
This setup works perfectly if the app has been closed (from the app switcher / multitasking view) and I tap on the quick action. However, if I go home without closing the application completely and try to do the same, it does not go to the statistics view but rather to the game view.
I've tried this many times over and it crashes once in a while with the following message:
com.apple.CoreMotion.MotionThread (11): EXC_BAD_ACCESS (code=1, address=0x48307beb8)
How could I change this to make it work as desired.
What I tried
Removing DispatchQueue.main.async
Try something like this
var isQuickLaunched = false
var quickLaunchOption: Shortcut!
func handleQuickAction(shortcutItem: UIApplicationShortcutItem) -> Bool {
var quickActionHandled = false
let type = shortcutItem.type.components(separatedBy: ".").last
if let shortcutType = Shortcut.init(rawValue: type!) {
isQuickLaunched = true
quickLaunchOption = shortcutType
quickActionHandled = true
}
return quickActionHandled
}
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: #escaping (Bool) -> Void) {
completionHandler(handleQuickAction(shortcutItem: shortcutItem))
}
func applicationDidBecomeActive(application: UIApplication) {
if (isQuickLaunched == true) {
isQuickLaunched = false
switch quickLaunchOption {
case .statistics:
DispatchQueue.main.async {
self.window?.rootViewController?.performSegue(withIdentifier: "ARView_to_stats", sender: self.window?.rootViewController)
}
}
}
}
I am trying to write a complication for watchOS 2 GM that displays a value it gets from my iPhone (iOS 9 GM) using WCSession.
Unfortunately I get the following error when sending a message:
Error Domain=WCErrorDomain Code=7014 "Payload could not be delivered." UserInfo={NSLocalizedDescription=Payload could not be delivered.}
This is what my code looks like in ComplicationController.swift:
import ClockKit
import WatchConnectivity
class ComplicationController: NSObject, CLKComplicationDataSource,WCSessionDelegate {
// MARK: - Timeline Configuration
var session : WCSession.defaultSession()
var myValue : Int?
...
func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {
getInfo()
if self.myValue != nil {
if complication.family == .CircularSmall {
let template = CLKComplicationTemplateCircularSmallRingText()
template.textProvider = CLKSimpleTextProvider(text: "\(self.myValue)")
template.fillFraction = Float(self.myValue!) / 100
template.ringStyle = CLKComplicationRingStyle.Closed
let timelineEntry = CLKComplicationTimelineEntry(date: NSDate(), complicationTemplate: template)
handler(timelineEntry)
} else {
handler(nil)
}
}
}
func requestedUpdateDidBegin(){
getInfo()
}
// MARK: - Update Scheduling
func getNextRequestedUpdateDateWithHandler(handler: (NSDate?) -> Void) {
// Call the handler with the date when you would next like to be given the opportunity to update your complication content
handler(NSDate(timeIntervalSinceNow: 5)); // only that low for debugging
}
func getInfo(){
if (WCSession.defaultSession().reachable) {
let messageToSend = ["Value":"Info"]
session.sendMessage(messageToSend, replyHandler: { replyMessage in
//handle and present the message on screen
let value:[String:AnyObject] = replyMessage
if value.indexForKey("myValue") != nil{
self.myValue = value["myValue"]! as? Int
print("Value: \(self.myValue)")
}
}, errorHandler: {error in
// catch any errors here
print(error)
})
}
}
This is my ExtensionDelegate.swift:
import WatchKit
import WatchConnectivity
class ExtensionDelegate: NSObject, WKExtensionDelegate,WCSessionDelegate {
var session:WCSession!
func applicationDidFinishLaunching() {
// Perform any final initialization of your application.
if (WCSession.isSupported()) {
session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
...
And finally my iOS AppDelegate:
import UIKit
import WatchConnectivity
class AppDelegate: UIResponder, UIApplicationDelegate, WCSessionDelegate {
var window: UIWindow?
var myDevice: UIDevice?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
if (WCSession.isSupported()) {
let session = WCSession.defaultSession()
session.delegate = self // conforms to WCSessionDelegate
session.activateSession()
}
application.statusBarStyle = UIStatusBarStyle.LightContent
return true
}
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
var reply = [String:AnyObject]()
// some logic
let value = //some Int value
reply.updateValue(value, forKey: "myValue")
replyHandler(reply)
}
Any ideas?
Thanks in advance!
A few things that will help you set things up so you can update your complications.
Generally, you'd want to have your timeline data already available for those points when the CLKComplicationDataSource methods are called. (Not always easy to do).
It looks like both your ComplicationController and ExtensionDelegate are being used as WCSessionDelegates. Use it in one place (probably ExtensionDelegate) and not the other, on the watch.
You have set up your AppDelegate to respond to a message, but any message handled by that didReceiveMessage method will only be coming from your Watch.
Determine where your message is originally coming from (maybe an external notification?), and send that info to the watch as a dictionary via WCSession 'send' methods.
Have your ExtensionDelegate (or whomever is responding to WCSessionDelegate methods) respond to the corresponding 'receive' methods to capture that sent info.
THEN: Kick off a refresh of your timeline by having the CLKComplicationServer reload your timeline.