I'm building a basic iOS app with Xcode that mainly just contains a webview with my web app inside.
I was wondering if there was a decent way to save the users username to the devices storage when logging in so that it can be automatically entered when opening the app next time. Since the app is a webview, I don't believe there is a way to keep the user logged in (like other major apps do, such as Facebook), so I think that auto filling the username will be beneficial for them.
I found this question and answer that could possibly solve my problem, although it's in good ol' Objective C.
My current attempt, that does absolutely nothing:
let savedUsername = "testusername"
let loadUsernameJS = "document.getElementById(\"mainLoginUsername\").value = " + savedUsername + ";"
self.Webview.stringByEvaluatingJavaScriptFromString(loadUsernameJS)
Is this a possibility with Swift?
for storing the password you should use the keychain, specifically web credentials. if done right, this will allow your app to use any existing keychain entries entered via Safari and will also allow Safari to access the password if saved via your app.
Code for setting and retrieving provided below:
private let domain = "www.youdomain.com"
func saveWebCredentials(username: String, password: String, completion: Bool -> Void) {
SecAddSharedWebCredential(domain, username, password) { error in
guard error == nil else { print("error saving credentials: \(error)"); return completion(false) }
completion(true)
}
}
func getExistingWebCredentials(completion: ((String, String)?, error: String?) -> Void) {
SecRequestSharedWebCredential(domain, nil) { credentials, error in
// make sure we got the credentials array back
guard let credentials = credentials else { return completion(nil, error: String(CFErrorCopyDescription(error))) }
// make sure there is at least one credential
let count = CFArrayGetCount(credentials)
guard count > 0 else { return completion(nil, error: "no credentials stored") }
// extract the username and password from the credentials dict
let credentialDict = unsafeBitCast(CFArrayGetValueAtIndex(credentials, 0), CFDictionaryRef.self)
let username = CFDictionaryGetValue(credentialDict, unsafeBitCast(kSecAttrAccount, UnsafePointer.self))
let password = CFDictionaryGetValue(credentialDict, unsafeBitCast(kSecSharedPassword, UnsafePointer.self))
// return via completion block
completion((String(unsafeBitCast(username, CFStringRef.self)), String(unsafeBitCast(password, CFStringRef.self))), error: nil)
}
}
which is used like this:
// save the credentials
saveWebCredentials("hello", password: "world", completion: { success in
// retrieve the credentials
getExistingWebCredentials { credentials, error in
guard let credentials = credentials else { print("Error: \(error)"); return }
print("got username: \(credentials.0) password: \(credentials.1)")
}
})
UPDATE
Recommend switching to using a WKWebView so you can easily pull out the response headers. Here is boilerplate code:
import UIKit
import WebKit
class ViewController: UIViewController, WKNavigationDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let webView = WKWebView(frame: self.view.bounds)
webView.navigationDelegate = self
self.view.addSubview(webView)
webView.loadRequest(NSURLRequest(URL: NSURL(string: "https://www.google.com")!))
}
func webView(webView: WKWebView, decidePolicyForNavigationResponse navigationResponse: WKNavigationResponse, decisionHandler: (WKNavigationResponsePolicy) -> Void) {
// make sure the response is a NSHTTPURLResponse
guard let response = navigationResponse.response as? NSHTTPURLResponse else { return decisionHandler(.Allow) }
// get the response headers
let headers = response.allHeaderFields
print("got headers: \(headers)")
// allow the request to continue
decisionHandler(.Allow);
}
}
You code is not working because you did not wrap savedUsername with quotes.
You should have this instead:
let loadUsernameJS = "document.getElementById(\"mainLoginUsername\").value = \"\(savedUsername)\";"
Also, this library might help you.
You are not passing a string to JavaScript, you should encapsulate the variable in additional quotes
let loadUsernameJS = "document.getElementById(\"mainLoginUsername\").value = \"" + savedUsername + "\";"
or
let loadUsernameJS = "document.getElementById('mainLoginUsername').value = '" + savedUsername + "';"
Related
I have this very basic piece of code in one of my Views:
.onOpenURL(perform: { url in
avm.handleRedirect(viewContext,url: url) })
where avm is defined as
#ObservedObject var avm = AccountsViewModel.makeAccountsViewModel()
avm has this basic property:
#Published var isLoading = true
That when set, my view listens to and shows a loading spinner. This works in all other situations, except that outlined below.
The handleRedirect function looks as follows:
func handleRedirect(_ context: NSManagedObjectContext, url: URL) {
debugPrint("Handling redirect")
let url = URLComponents(string: url.absoluteString)!
let code = url.queryItems?.first(where: { $0.name == "code" })?.value
trueLayerClient.getAccessToken(code: code!) { res in
if res == nil {
debugPrint("accessTokenResponse was nil")
}
self.api.storeTokens(
str: API.StoreTokensRequest(
userID: "***",
authToken: res!.accessToken,
refreshToken: res!.refreshToken,
apiKey: "***"
), finished: { success in
if success{
debugPrint("was successful")
DispatchQueue.main.async{
self.isLoading = false
}
} else {
debugPrint("store token failed")
}
})
}
}
I'm triggering this onOpenUrl using a universal Link I have setup.
When I hit the link, my app opens from the background and I see the following logs:
Handling Redirect
Was successful
However, the app never goes into a loading state. Furthermore, the UI becomes "blocked" and I have to hard kill the app to be able to press anything.
Once I do reopen the app, the new state (which I fetch via api and store in CoreData) is reflected in the view.
At first I thought my API was responding too quickly, but I put a 5 second sleep in it and I still see the UI get blocked but with the same logs (just further apart).
I would appreciate any help on this.
If helpful, my storeTokens API call looks like this:
func storeTokens(str: StoreTokensRequest , finished: #escaping (Bool)->Void) {
let u = URL(string:"\(self.baseURL)/\(storeTokenPath)")!
var req = URLRequest(url: u)
req.addValue("application/json", forHTTPHeaderField: "Content-Type")
req.httpMethod = "POST"
let reqBody = str
debugPrint("req bodyed")
guard let encoded = try? JSONEncoder().encode(reqBody) else {
debugPrint("failed to encode req")
finished(false)
return
}
req.httpBody = encoded
debugPrint("encoded")
let task = URLSession.shared.dataTask(with: req) { data, response, error in
guard let httpResponse = response as? HTTPURLResponse else {
debugPrint("wasnt http rep")
finished(false)
return
}
debugPrint("status code is: \(httpResponse.statusCode)")
if httpResponse.statusCode < 299 {
debugPrint("About to call true")
finished(true)
return
}
}
task.resume()
}
}
I see all the logs in this function in my output too, I just left them out for succinctness.
I have tried an implementation similar to yours. My "onOpenUrl()" method is waking up, if necessary, my app, and passing the url to my app, and the app is updating its state correctly.
Basic check.
Forget for a moment "onOpenUrl". Implement inside your view this method:
.onAppear {
viewModel.onAppear()
}
where inside your view model play with the "isLoading" property. For example,
func onAppear(){
self.isLoading = true
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
print("- loading complete")
self.isLoading = false
}
}
Does the view update (the activity loader) accordingly?
I have followed some tutorials and used their methods to implement auto login for my app, but once I relaunch the app after entering the credentials, the app does not log in.
var userDefaults = UserDefaults.standard
here I initiate the user defaults feature
let session = URLSession.shared
session.dataTask(with: request) { (data, response, error) in
if let safeData = data {
if let dataString = String(data: safeData, encoding: String.Encoding.utf8) {
print(dataString)
if dataString == "Hello" {
self.userDefaults.setValue(true, forKey: "UserIsLoggedIn")
DispatchQueue.main.async {
self.performSegue(withIdentifier: "loginSegue", sender: self)
}
} else {
DispatchQueue.main.async {
self.validationLabel.isHidden = false
self.validationLabel.text = " Username or password is incorrect. "
self.loginSuccessful = false
}
}
}
} else {
print(error ?? "Error with data API URLSession")
}
}.resume()
here, inside the API call. if the response from the API is "hello" which means the login was successful, i set the value to true with an identifier.
if userDefaults.value(forKey: "UserIsLoggedIn") as? Bool == true {
performSegue(withIdentifier: "loginSegue", sender: self)
} else {}
here in the view did load I use the userDefaults to perform the segue to the next screen for future launches.. but it is not working.
initiation
viewdidload
API call
var userDefaults = UserDefaults() seems wrong. Use let userDefaults = UserDefaults.standard instead. Also there's no need to make this a property of your class, simply use this within your methods whereever it's needed.
Swift 5. Auto login myApplication According woking & help you best my try
//First Time key
// According API Response success then add
if !UserDefaults.standard.bool(forKey:"isLogin") { // Success
UserDefaults.standard.set(true, forKey: "isLogin") // To do....
UserDefaults.standard.synchronize()
} else { // response fail
// To do ....
}
Have you checked if the value is being saved in your User Defaults file? If you are running your app on the simulator, try printing the file path to your User Defaults file and try locating it. It might not fix your immediate problem but it will hopefully give you an idea as to where the problem is coming from.
Try printing the location of your User Defaults file using the following line in your AppDelegate.swift
print(NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).last! as String)
I am trying to download a video from my firebase storage. The way I am doing that is by using the .downloadURLWithCompletion function. When ever the function executes, I receive this error
Error Domain=FIRStorageErrorDomain Code=-13010 "Object videos/video1.m4v
does not exist." UserInfo={object=videos/video1.m4v
, bucket=**********.appspot.com, ResponseBody={
"error": {
"code": 404,
"message": "Not Found"
}
}, data=<7b0a2020 22657272 6f72223a 207b0a20 20202022 636f6465 223a2034 30342c0a 20202020 226d6573 73616765 223a2022 4e6f7420 466f756e 64220a20 207d0a7d>, NSLocalizedDescription=Object videos/video1.m4v
does not exist., ResponseErrorDomain=com.google.HTTPStatus, ResponseErrorCode=404}
I have changed my storage settings on firebase to allow unauthenticated access:
I have also checked to make sure that the storage link is correct:
Here is the code that is accessing the Firebase storage:
import UIKit
import AVKit
import AVFoundation
import FirebaseStorage
class VideoViewController: UIViewController
{
var videoUrl:NSURL!
var storageRef:FIRStorageReference!
override func viewDidLoad()
{
let storage = FIRStorage.storage()
storageRef = storage.referenceForURL("gs://**********.appspot.com")
let videosRef = storageRef.child("videos")
let videoName = NSUserDefaults.standardUserDefaults().objectForKey("videoName") as! String
videosRef.child(videoName).downloadURLWithCompletion { (URL, error) -> Void in
if (error != nil)
{
print(error!)
}
else
{
self.videoUrl = URL
do
{
try self.playVideo()
}
catch
{
print("Error")
}
}
}
super.viewDidLoad()
// Do any additional setup after loading the view.
}
So, I tried using a direct link and it worked!
override func viewDidLoad()
{
let storage = FIRStorage.storage()
storageRef = "gs://*************.appspot.com"
let videosRef = "videos"
let videoName = NSUserDefaults.standardUserDefaults().objectForKey("videoName") as! String
storage.referenceForURL("\(storageRef)/\(videosRef)/\(videoName)").downloadURLWithCompletion { (URL, error) in
if (error != nil)
{
print(error!)
}
else
{
self.videoUrl = URL
do
{
try self.playVideo()
}
catch
{
print("Error")
}
}
}
Of course, using a direct link for something like this isn't exactly the best way to get data. So next I compared the two links generated by printing them out. Here is how I printed the first link:
var videoUrl:NSURL!
var storageRef:FIRStorageReference!
override func viewDidLoad()
{
let storage = FIRStorage.storage()
storageRef = storage.referenceForURL("gs://*********.appspot.com")
let videosRef = storageRef.child("videos")
let videoName = NSUserDefaults.standardUserDefaults().objectForKey("videoName") as! String
print(videosRef.child(videoName))
and it printed
gs://***********.appspot.com/videos/video1.m4v
And the second link:
var videoUrl:NSURL!
var storageRef:String!
override func viewDidLoad()
{
let storage = FIRStorage.storage()
storageRef = "gs://***********.appspot.com"
let videosRef = "videos"
let videoName = NSUserDefaults.standardUserDefaults().objectForKey("videoName") as! String
print("\(storageRef)/\(videosRef)/\(videoName)")
What it printed
gs://***********.appspot.com/videos/video1.m4v
Now, I also tried printing the value of videoName to make sure that it was correct and every time that I printed it out it was video1.m4v
I banked out the link to my firebase storage, but I can assure you that the link is correct all around.
Can someone explain to me why I am getting this error? To me everything looks to be in place.
Thanks!
Try this -- if there is an issue with the underlying representation of a ref this may help:
instead of:
videosRef.child(videoName).downloadURLWithCompletion { (URL, error) -> Void in
do:
storage.referenceForURL(String(videosRef.child(videoName))).downloadURLWithCompletion { (URL, error) -> Void in
that is, does referenceForURL of the stringValue do something different than a direct call. It shouldn't -- if it does, it might have something to do with your videoName. Maybe it ends with a slash? Can you post the value of your videoName?
So, if I understand correctly, you want to download the image without passing the full URL path?
If so, I think downloadURLWithCompletion requires the full URL path.
I can't test this, since I don't have my data set up this way (I just store the full URLs to media files in firebase storage to my firebase database), but try this:
videosRef.child(videoName).dataWithMaxSize(INT64_MAX, completion: { (data, error) in
if let error = error {
print("Error downloading: \(error)")
return
}
cell.imageView?.image = UIImage.init(data: data!)
})
In your firebase storage, you haven't placed your video file inside a folder called videos.
And despite this you try to access to .../videos/filename which doesn't exist. Either try to remove the /videos from: gs://***********.appspot.com /videos /video1.m4v
or
Either create a folder called videos inside your firebase storage and then add the same video inside it with the same name (since you cant drag and drop files into other folders), or remove the:
let videosRef = "videos"
from your path.
Hope it helps.
We're trying to save the content (HTML) of WKWebView in a persistent storage (NSUserDefaults, CoreData or disk file). The user can see the same content when he re-enters the application with no internet connection. WKWebView doesn't use NSURLProtocol like UIWebView (see post here).
Although I have seen posts that "The offline application cache is not enabled in WKWebView." (Apple dev forums), I know that a solution exists.
I've learned of two possibilities, but I couldn't make them work:
1) If I open a website in Safari for Mac and select File >> Save As, it will appear the following option in the image below. For Mac apps exists [[[webView mainFrame] dataSource] webArchive], but on UIWebView or WKWebView there is no such API. But if I load a .webarchive file in Xcode on WKWebView (like the one I obtained from Mac Safari), then the content is displayed correctly (html, external images, video previews) if there is no internet connection. The .webarchive file is actually a plist (property list). I tried to use a mac framework that creates a .webarchive file, but it was incomplete.
2) I obtanined the HTML in webView:didFinishNavigation but it doesn't save external images, css, javascript
func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
webView.evaluateJavaScript("document.documentElement.outerHTML.toString()",
completionHandler: { (html: AnyObject?, error: NSError?) in
print(html)
})
}
We're struggling over a week and it is a main feature for us.
Any idea is really appreciated.
Thank you!
I know I'm late, but I have recently been looking for a way to store web pages for offline reading, and still could't find any reliable solution that wouldn't depend on the page itself and wouldn't use the deprecated UIWebView. A lot of people write that one should use the existing HTTP caching, but WebKit seems to do a lot of stuff out-of-process, making it virtually impossible to enforce complete caching (see here or here). However, this question guided me into the right direction. Tinkering with the web archive approach, I found that it's actually quite easy to write your own web archive exporter.
As written in the question, web archives are just plist files, so all it takes is a crawler that extracts the required resources from the HTML page, downloads them all and stores them in a big plist file. This archive file can then later be loaded into the WKWebView via loadFileURL(URL:allowingReadAccessTo:).
I created a demo app that allows archiving from and restoring to a WKWebView using this approach: https://github.com/ernesto-elsaesser/OfflineWebView
EDIT: The archive generation code is now available as standalone Swift package: https://github.com/ernesto-elsaesser/WebArchiver
The implementation only depends on Fuzi for HTML parsing.
I would recommend investigating the feasibility of using App Cache, which is now supported in WKWebView as of iOS 10: https://stackoverflow.com/a/44333359/233602
I'm not sure if you just want to cache the pages that have already been visited or if you have specific requests that you'd like to cache. I'm currently working on the latter. So I'll speak to that. My urls are dynamically generated from an api request. From this response I set requestPaths with the non-image urls and then make a request for each of the urls and cache the response. For the image urls, I used the Kingfisher library to cache the images. I've already set up my shared cache urlCache = URLCache.shared in my AppDelegate. And allotted the memory I need: urlCache = URLCache(memoryCapacity: <setForYourNeeds>, diskCapacity: <setForYourNeeds>, diskPath: "urlCache") Then just call startRequest(:_) for each of the urls in requestPaths. (Can be done in the background if it's not needed right away)
class URLCacheManager {
static let timeout: TimeInterval = 120
static var requestPaths = [String]()
class func startRequest(for url: URL, completionWithErrorCallback: #escaping (_ error: Error?) -> Void) {
let urlRequest = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: timeout)
WebService.sendCachingRequest(for: urlRequest) { (response) in
if let error = response.error {
DDLogError("Error: \(error.localizedDescription) from cache response url: \(String(describing: response.request?.url))")
}
else if let _ = response.data,
let _ = response.response,
let request = response.request,
response.error == nil {
guard let cacheResponse = urlCache.cachedResponse(for: request) else { return }
urlCache.storeCachedResponse(cacheResponse, for: request)
}
}
}
class func startCachingImageURLs(_ urls: [URL]) {
let imageURLs = urls.filter { $0.pathExtension.contains("png") }
let prefetcher = ImagePrefetcher.init(urls: imageURLs, options: nil, progressBlock: nil, completionHandler: { (skipped, failed, completed) in
DDLogError("Skipped resources: \(skipped.count)\nFailed: \(failed.count)\nCompleted: \(completed.count)")
})
prefetcher.start()
}
class func startCachingPageURLs(_ urls: [URL]) {
let pageURLs = urls.filter { !$0.pathExtension.contains("png") }
for url in pageURLs {
DispatchQueue.main.async {
startRequest(for: url, completionWithErrorCallback: { (error) in
if let error = error {
DDLogError("There was an error while caching request: \(url) - \(error.localizedDescription)")
}
})
}
}
}
}
I'm using Alamofire for the network request with a cachingSessionManager configured with the appropriate headers. So in my WebService class I have:
typealias URLResponseHandler = ((DataResponse<Data>) -> Void)
static let cachingSessionManager: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = cachingHeader
configuration.urlCache = urlCache
let cachingSessionManager = SessionManager(configuration: configuration)
return cachingSessionManager
}()
private static let cachingHeader: HTTPHeaders = {
var headers = SessionManager.defaultHTTPHeaders
headers["Accept"] = "text/html"
headers["Authorization"] = <token>
return headers
}()
#discardableResult
static func sendCachingRequest(for request: URLRequest, completion: #escaping URLResponseHandler) -> DataRequest {
let completionHandler: (DataResponse<Data>) -> Void = { response in
completion(response)
}
let dataRequest = cachingSessionManager.request(request).responseData(completionHandler: completionHandler)
return dataRequest
}
Then in the webview delegate method I load the cachedResponse. I use a variable handlingCacheRequest to avoid an infinite loop.
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let reach = reach {
if !reach.isReachable(), !handlingCacheRequest {
var request = navigationAction.request
guard let url = request.url else {
decisionHandler(.cancel)
return
}
request.cachePolicy = .returnCacheDataDontLoad
guard let cachedResponse = urlCache.cachedResponse(for: request),
let htmlString = String(data: cachedResponse.data, encoding: .utf8),
cacheComplete else {
showNetworkUnavailableAlert()
decisionHandler(.allow)
handlingCacheRequest = false
return
}
modify(htmlString, completedModification: { modifiedHTML in
self.handlingCacheRequest = true
webView.loadHTMLString(modifiedHTML, baseURL: url)
})
decisionHandler(.cancel)
return
}
handlingCacheRequest = false
DDLogInfo("Currently requesting url: \(String(describing: navigationAction.request.url))")
decisionHandler(.allow)
}
Of course you'll want to handle it if there is a loading error as well.
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
DDLogError("Request failed with error \(error.localizedDescription)")
if let reach = reach, !reach.isReachable() {
showNetworkUnavailableAlert()
handlingCacheRequest = true
}
webView.stopLoading()
loadingIndicator.stopAnimating()
}
I hope this helps. The only thing I'm still trying to figure out is the image assets aren't being loaded offline. I'm thinking I'll need to make a separate request for those images and keep a reference to them locally. Just a thought but I'll update this when I have that worked out.
UPDATED with images loading offline with below code
I used the Kanna library to parse my html string from my cached response, find the url embedded in the style= background-image: attribute of the div, used regex to get the url (which is also the key for Kingfisher cached image), fetched the cached image and then modified the css to use the image data (based on this article: https://css-tricks.com/data-uris/), and then loaded the webview with the modified html. (Phew!) It was quite the process and maybe there is an easier way.. but I had not found it. My code is updated to reflect all these changes. Good luck!
func modify(_ html: String, completedModification: #escaping (String) -> Void) {
guard let doc = HTML(html: html, encoding: .utf8) else {
DDLogInfo("Couldn't parse HTML with Kannan")
completedModification(html)
return
}
var imageDiv = doc.at_css("div[class='<your_div_class_name>']")
guard let currentStyle = imageDiv?["style"],
let currentURL = urlMatch(in: currentStyle)?.first else {
DDLogDebug("Failed to find URL in div")
completedModification(html)
return
}
DispatchQueue.main.async {
self.replaceURLWithCachedImageData(inHTML: html, withURL: currentURL, completedCallback: { modifiedHTML in
completedModification(modifiedHTML)
})
}
}
func urlMatch(in text: String) -> [String]? {
do {
let urlPattern = "\\((.*?)\\)"
let regex = try NSRegularExpression(pattern: urlPattern, options: .caseInsensitive)
let nsString = NSString(string: text)
let results = regex.matches(in: text, options: [], range: NSRange(location: 0, length: nsString.length))
return results.map { nsString.substring(with: $0.range) }
}
catch {
DDLogError("Couldn't match urls: \(error.localizedDescription)")
return nil
}
}
func replaceURLWithCachedImageData(inHTML html: String, withURL key: String, completedCallback: #escaping (String) -> Void) {
// Remove parenthesis
let start = key.index(key.startIndex, offsetBy: 1)
let end = key.index(key.endIndex, offsetBy: -1)
let url = key.substring(with: start..<end)
ImageCache.default.retrieveImage(forKey: url, options: nil) { (cachedImage, _) in
guard let cachedImage = cachedImage,
let data = UIImagePNGRepresentation(cachedImage) else {
DDLogInfo("No cached image found")
completedCallback(html)
return
}
let base64String = "data:image/png;base64,\(data.base64EncodedString(options: .endLineWithCarriageReturn))"
let modifiedHTML = html.replacingOccurrences(of: url, with: base64String)
completedCallback(modifiedHTML)
}
}
Easiest way to use cache webpage is as following in Swift 4.0: -
/* Where isCacheLoad = true (Offline load data) &
isCacheLoad = false (Normal load data) */
internal func loadWebPage(fromCache isCacheLoad: Bool = false) {
guard let url = url else { return }
let request = URLRequest(url: url, cachePolicy: (isCacheLoad ? .returnCacheDataElseLoad: .reloadRevalidatingCacheData), timeoutInterval: 50)
//URLRequest(url: url)
DispatchQueue.main.async { [weak self] in
self?.webView.load(request)
}
}
I have an application that allow users to stream songs from spotify. So to achieve that I need to renew session from time to time whenever users want to stream song from spotify. I'm using latest spotify sdk (beta-9), and I'm currently following tutorial from https://www.youtube.com/watch?v=GeO00YdJ3cE. In that tutorial we need to refresh token swap but when I looked from https://developer.spotify.com/technologies/spotify-ios-sdk/tutorial/ there is no need to refresh token swap.
and I end up not using the token swap, when I refresh my session then play song with renewed session, I got below error:
Error Domain=com.spotify.ios-sdk.playback Code=8 "Login to Spotify failed because of invalid credentials." UserInfo=0x7f840bf807b0 {NSLocalizedDescription=Login to Spotify failed because of invalid credentials.}
And I'm using this code below, for renewing my session:
let userDefaults = NSUserDefaults.standardUserDefaults()
if let sessionObj : AnyObject = NSUserDefaults.standardUserDefaults().objectForKey("spotifySession") {
let sessionDataObj : NSData = sessionObj as! NSData
let session = NSKeyedUnarchiver.unarchiveObjectWithData(sessionDataObj) as! SPTSession
self.playUsingSession(session)
if !session.isValid() {
SPTAuth.defaultInstance().renewSession(session, callback: { (error : NSError!, newsession : SPTSession!) -> Void in
if error == nil {
let sessionData = NSKeyedArchiver.archivedDataWithRootObject(session)
userDefaults.setObject(sessionData, forKey: "spotifySession")
userDefaults.synchronize()
self.session = newsession
self.playUsingSession(newsession)
}else{
println("renew session having problerm >>>>> \(error)")
}
})
}else{
println("session is still valid")
self.playUsingSession(session)
}
}else{
spotifyLoginButton.hidden = false
}
and below code to stream spotify songs:
func playUsingSession(sessionObj:SPTSession!){
if spotifyPlayer == nil {
spotifyPlayer = SPTAudioStreamingController(clientId: kSpotifyClientID)
}
spotifyPlayer?.loginWithSession(sessionObj, callback: { (error : NSError!) -> Void in
if error != nil {
println("enabling playback got error : \(error)")
return
}
var spotifyTrackUri : NSURL = NSURL(string: "spotify:track:3FREWTEY2uFxOorJZMmZPX")!
self.spotifyPlayer!.playURIs([spotifyTrackUri], fromIndex: 0, callback: { (error : NSError!) -> Void in
if error != nil {
println("\(error)")
}
})
})
}
Do I still need to refresh token swap for latest sdk? Or is there something missing with my code?
By default, users need to login once per hour for apps using the Spotify SDK unless you use the Authorization Code flow. To use this flow you'll need to setup a server to handle token swap and refresh.
Setup a free server with this one-click-deploy to Heroku https://github.com/adamontherun/SpotifyTokenRefresh
Using the URL of the server created above add the following when configuring your SPTAuth.defaultInstance():
SPTAuth.defaultInstance().tokenSwapURL = URL(string: "https://YOURSERVERNAME.herokuapp.com/swap")
SPTAuth.defaultInstance().tokenRefreshURL = URL(string: "https://YOURSERVERNAME.herokuapp.com/refresh")
Before using your session check if it is valid:
if SPTAuth.defaultInstance().session.isValid()
and if it isn't call
SPTAuth.defaultInstance().renewSession(SPTAuth.defaultInstance().session, callback: { (error, session) in
if let session = session {
SPTAuth.defaultInstance().session = session
}
})
I recommend follow this tutorial: https://medium.com/#brianhans/getting-started-with-the-spotify-ios-sdk-435607216ecc and this one too: https://medium.com/#brianhans/spotify-ios-sdk-authentication-b2c35cd4affb
After you finished, you'll see that you create a file called "Constants.swift" just like this:
import Foundation
struct Constants {
static let clientID = "XXXXXXXXXXXXXXXXXXXX"
static let redirectURI = URL(string: "yourappname://")!
static let sessionKey = "spotifySessionKey"
}
Then, you can follow the steps in Heroku (Do not enter in panic is very simple):
https://github.com/adamontherun/SpotifyTokenRefresh
almost ready, when your server is "working", come back to your Xcode Project and add two static constants in your "Constants.swift" file, just like this:
import Foundation
struct Constants {
static let clientID = "XXXXXXXXXXXXXXXXXXXX"
static let redirectURI = URL(string: "yourappname://")!
static let sessionKey = "spotifySessionKey"
static let tokenSawp = URL(string: "https://yourappname.herokuapp.com/swap")
static let tokenRefresh = URL(string:"https://yourappname.herokuapp.com/refresh")
}
To finish, go to AppDelegate.swift and search "func setupSpotify()".. Add the new two constants, your function should look like this:
func setupSpotify() {
SPTAuth.defaultInstance().clientID = Constants.clientID
SPTAuth.defaultInstance().redirectURL = Constants.redirectURI
SPTAuth.defaultInstance().sessionUserDefaultsKey = Constants.sessionKey
SPTAuth.defaultInstance().tokenSwapURL = Constants.tokenSawp //new constant added
SPTAuth.defaultInstance().tokenRefreshURL = Constants.tokenRefresh //new constant added
SPTAuth.defaultInstance().requestedScopes = [SPTAuthStreamingScope]
do {
try SPTAudioStreamingController.sharedInstance().start(withClientId: Constants.clientID)
} catch {
fatalError("Couldn't start Spotify SDK")
}
}
As a last step, just add the SPTAuth.defaultInstance().renewSession in you signInSpotify function, should look like this:
#IBAction func SignInSpotify(_ sender: Any) {
if SPTAuth.defaultInstance().session == nil {
let appURL = SPTAuth.defaultInstance().spotifyAppAuthenticationURL()
let webURL = SPTAuth.defaultInstance().spotifyWebAuthenticationURL()!
// Before presenting the view controllers we are going to start watching for the notification
NotificationCenter.default.addObserver(self,
selector: #selector(receievedUrlFromSpotify(_:)),
name: NSNotification.Name.Spotify.authURLOpened,
object: nil)
if SPTAuth.supportsApplicationAuthentication() {
UIApplication.shared.open(appURL!, options: [:], completionHandler: nil)
} else {
let webVC = SFSafariViewController(url: webURL)
present(webVC, animated: true, completion: nil)
}
} else if SPTAuth.defaultInstance().session.isValid() == true {
print("YOUR SESSION IS VALID")
self.successfulLogin()
} else {
print("YOUR SESSION IS NOT VALID / NEED RENEW")
//Every 60 minutes the token need a renew https://github.com/spotify/ios-sdk
SPTAuth.defaultInstance().renewSession(SPTAuth.defaultInstance().session, callback: { (error, session) in
if let session = session {
SPTAuth.defaultInstance().session = session
self.successfulLogin()
print("RENEW OK")
}
if let error = error {
print("RENEW NOT OK \(error)")
}
})
}
}
Good Luck!