I'm using http request with JSON serialization in collectionViews, but the API interval is limited.
"We do enforce a small amount of rate limiting. Our current limits are 40 requests every 10 seconds and are limited by IP address, not API key. You can think of this is being burstable to 40 in a single second, or as an average of 4 requests/second. The timer will reset 10 seconds from your first request within the current 10 second "bucket". This means that if you trigger the limit you will have to wait up to 9 seconds before the timer resets but depending where you are within the 10 second window, it could be the very next second.
You can use the X-RateLimit headers that get returned with every request to keep track of your current limits. If you exceed the limit, you will receive a 429 HTTP status with a Retry-After header. As soon your cool down period expires, you are free to continue making requests."
Source: https://developers.themoviedb.org/3/getting-started/request-rate-limiting
I will like to know if it is possible to set a global variable on application(_:didFinishLaunchingWithOptions:) that will automatically increase time between all http requests that are made, I need to avoid to trigger the 10 seconds waiting timmer.
private func requestJSON(for dataItem: Movie) -> UIImage? {
let query = dataItem.title!.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)
let requestURL = URL(string: "https://api.themoviedb.org/3/search/movie?api_key=25149a6f75e14fb0672911327a13939a&language=en-US&query=\(query!)&page=1&include_adult=false&year=\(dataItem.primary_release_year!)")!
if let data = try? Data(contentsOf: requestURL) {
if let JSON = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] {
if let x = JSON?["results"] as? [Dictionary<String, Any>], let y = x.first {
if let pp = y["poster_path"] as? String {
dataItem.poster = try? Data(contentsOf: URL(string: defaultBaseURL + pp)!)
print("Downloading... \(dataItem.title!)'s poster.")
}
}
}
}
return dataItem.poster?.first != nil ? UIImage(data: dataItem.poster!) : UIImage(named: "default-movie")
}
I will like to avoid to use lots of tasks as much as I can where possible, anyway, any other solution will also be very appreciated.
No, there is no global variable, NSURLSessionConfiguration property or anything built into iOS that implements this - you'll have to do this yourself.
Related
The code this and it crashes on "try!", but I don't know how to catch the error and it has it be explicit otherwise it won't work.
func downloadPicture2(finished: () -> Void) {
let imageUrlString = self.payments[indexPath.row].picture
let imageUrl = URL(string: imageUrlString!)!
let imageData = try! Data(contentsOf: imageUrl)
cell.profilePicture.image = UIImage(data: imageData)
cell.profilePicture.layer.cornerRadius = cell.profilePicture.frame.size.width / 2
cell.profilePicture.clipsToBounds = true
}
The short answer is don't use try! - Use do/try/catch and recover from the problem in the catch clause.
For example -
func downloadPicture2(finished: () -> Void) {
cell.profilePicture.image = nil
if let imageUrlString = self.payments[indexPath.row].picture,
let imageUrl = URL(string: imageUrlString) {
do {
let imageData = try Data(contentsOf: imageUrl)
cell.profilePicture.image = UIImage(data: imageData)
}
catch {
print("Error fetching image - \(error)")
}
}
cell.profilePicture.layer.cornerRadius = cell.profilePicture.frame.size.width / 2
cell.profilePicture.clipsToBounds = true
}
Now you have code that won't crash if the url is invalid or there is no network, but there are still some serious issues with this code.
Data(contentsOf:) blocks the current thread while it fetches the data. Since you are executing on the main thread this will freeze the user interface and give a poor user experience.
Apple specifically warns not to do this
Important
Don't use this synchronous initializer to request network-based URLs. For network-based URLs, this method can block the current thread for tens of seconds on a slow network, resulting in a poor user experience, and in iOS, may cause your app to be terminated.
Rather, you should use an asynchronous network operations, such as a dataTask.
This code operates on cell - an external property. Once you move to asynchronous code you will probably be fetching images for multiple cells simultaneously. You should pass the relevant cell to this function to avoid clashes.
The use of the network isn't particularly efficient either; assuming this is part of a table or collection view, cells are reused as the view scrolls. You will repeatedly fetch the same image as this happens. Some sort of local caching would be more efficient.
If it is possible to use external frameworks in your project (i.e. your employer doesn't specifically disallow it) then I strongly suggest you look at a framework like SDWebImage or KingFisher. They will make this task much easier and much more efficient.
I have to play HLS stream and it is secured/protected. In order to download manifest file and further ts file it is expected that request have valid Cookies information sent. I do receive Cookies information from server but it is not part of response header whereas it comes as part response String. It means Cookies automatically does not become part of NSHTTPStorage and application have to consume response string and fetch Cookies information from there; further need to be set while initialising AVURLAsset. As per AVURLAsst documentation in order to send Cookie information we should use options "AVURLAssetHTTPCookiesKey". I have mentioned in below code how to initialise AVURLAsset using this key
let cookieOptions = [AVURLAssetHTTPCookiesKey: Any]
let assets = AVURLAsset(url: url as URL, options: cookieArrayOptions)
My main problem how to set value against AVURLAssetHTTPCookiesKey. It take value of 'Any' type.Since Apple just mention it expect Any value and does not tell how internally it interpret and convert that value in correct format.
I tried two approach to set value: Say I have to set two Cookies key viz MyCookies1, MyCookies2
Approach 1 using NSHTTPStorage
let propertiesKey1 = [HTTPCookiePropertyKey.domain : "Domain=abc.com",HTTPCookiePropertyKey.path:"/",HTTPCookiePropertyKey.secure:true,HTTPCookiePropertyKey.init("HttpOnly"):true,HTTPCookiePropertyKey.value:"abc1",HTTPCookiePropertyKey.name:"MyCookies1"] as [HTTPCookiePropertyKey : Any]
let propertiesKey2 = [HTTPCookiePropertyKey.domain : "Domain=abc.com",HTTPCookiePropertyKey.path:"/",HTTPCookiePropertyKey.secure:true,HTTPCookiePropertyKey.init("HttpOnly"):true,HTTPCookiePropertyKey.value:"abc2",HTTPCookiePropertyKey.name:"MyCookies2"] as [HTTPCookiePropertyKey : Any]
let cookieKey1 = HTTPCookie(properties: propertiesKey1)
let cookieKey2 = HTTPCookie(properties: propertiesKey2)
HTTPCookieStorage.shared.setCookie(cookieKey1!)
HTTPCookieStorage.shared.setCookie(cookieKey2!)
let cookiesArray = HTTPCookieStorage.shared.cookies!
let cookieArrayOptions = [AVURLAssetHTTPCookiesKey: cookiesArray]
guard let url = URL(string:"abc.com") else { return }
let assets = AVURLAsset(url: url as URL, options: cookieArrayOptions)
But this is not working and AVPlayer just initializing and stopped working
Approach 2
let values = ["Cookie": "MyCookies1=abc1; MyCookies2=abc2"]
let cookieArrayOptions = [AVURLAssetHTTPCookiesKey: values]
guard let url = URL(string:"abc.com") else { return }
let assets = AVURLAsset(url: url as URL, options: cookieArrayOptions)
But this options always crashes.May be I am not setting values in correct format.
I have tried to provide as much information as possible but let me know if you need any further information.
After couple of tries I am able to make it working with Approach 1 usign NSHTTPStorage. I made a mistake of not using HTTPS streaming URL. I had already allowed to play non-HTTP url by passsing ATS and allow arbitrary loads. But AVURLAssetHTTPCookiesKey expect to provide only HTTPS url to be set for cookies. I used HTTPS streams and ultimately Cookies started to be sent internally in request header while downloading manifest/segment file in playing HLS stream
I have an API that generates signed download links that expire after a short amount of time. I'd like to add the ability to resume downloads, but the URLSession APIs don't provide the native ability to resume downloads if the URL for the asset changes.
My attempt at solving this was to track the bytes downloaded at the time of pausing, store the data blob that was downloaded, fetch a new signed download url, resume downloading using Range headers, and then concatenate all the data blobs together when the download is completed.
Here's the code used to start the download:
let session = URLSession(configuration: URLSessionConfiguration.default, delegate: self, delegateQueue: nil)
let task = session.downloadTask(with: signedURL)
self.sessionDownloadRequest = task
The problem that I am facing is that the resume data var doesn't appear to actually contain the data that was downloaded.
self.sessionDownloadRequest.cancel(byProducingResumeData: { (data) in
print(data.count) //This surprisingly always returns the same count
}
It appears that the size of that data blob is always the same regardless of how long I let the download continue for before pausing. Where/How can I access the chunk of data that was downloaded?
Thanks!
The resume data that is returned by:
- (void)cancelByProducingResumeData:(void (^)(NSData *resumeData))completionHandler;
is actually a plist that includes:
NSURLSessionDownloadURL
NSURLSessionResumeBytesReceived
NSURLSessionResumeCurrentRequest
NSURLSessionResumeEntityTag
NSURLSessionResumeInfoTempFileName
NSURLSessionResumeInfoVersion
NSURLSessionResumeOriginalRequest
NSURLSessionResumeServerDownloadDate
You can access the plist with the following code:
if let resumeDictionary = try? PropertyListSerialization.propertyList(from: self, options: PropertyListSerialization.MutabilityOptions.mutableContainersAndLeaves, format: nil), let plist = resumeDictionary as? [String: Any] {
print(plist)
}
You don't actually need to store and concatenate the data blobs as you initially suggested. You can replace the current request stored in the plist (NSURLSessionResumeCurrentRequest) with a new one with your updated signed URL. After this, create a new resumeData instance to use instead of the original.
guard let bytesReceived = plist["NSURLSessionResumeBytesReceived"] as? Int
else {
return nil
}
let headers = ["Range":"bytes=\(bytesReceived)"]
let newReq = try! URLRequest(url: signedURL, method: .get, headers: headers)
let archivedData = NSKeyedArchiver.archivedData(withRootObject: newReq)
if let updatedResumeData = try? PropertyListSerialization.data(fromPropertyList: plist, format: PropertyListSerialization.PropertyListFormat.binary, options: 0) {
return updatedResumeData
}
From there you can manipulate the plist and actually create a new one to pass it thru to the instance method:
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData;
NOTE: If you are working with iOS 10 and macOS10.12.*, there is a bug that prevents the resume ability to work as the plist is corrupted. Check this article out for a fix. You may need to fix the plist before accessing certain properties on it.
Resume NSUrlSession on iOS10
I'm trying to make a simple messaging app using an API. Right now, I have a thread that checks a request each second and see if the number of messages have changed, but this causes so much trouble, the RAM is constantly going up and the API becomes unresponsive because of the large number of requests. At the moment my code looks like this :
var request = URLRequest(url: URL(string: "URL")!)
let session = URLSession.shared
public func thread()
{
DispatchQueue.global(qos: .background).async {
while(true)
{
self.request.httpMethod = "GET"
self.session.dataTask(with: self.request) {data, response, err in
let json = try! JSONSerialization.jsonObject(with: data!, options: []) as? [String: Any]
let data = json?["data"] as? [[String: Any]]
if((data?.count)! > self.nbMessages)
{
self.messages.removeAll()
for message in data! {
let text = message["message_body"] as? String
let creator = message["creator_id"] as? Int
self.messages.append([text!, String(creator!)])
}
DispatchQueue.main.async {
self.nbMessages = (data?.count)!
self.TableView.reloadData()
let scrollPoint = CGPoint(x: 0, y: self.TableView.contentSize.height - self.TableView.frame.size.height)
self.TableView.setContentOffset(scrollPoint, animated: false)
}
}
}.resume()
usleep(2000)
}
}
}
This works fine, I can send messages and see messages sent to me (with a decent delay), but my logic with the request at every 2 second is way off and I acknowledge it. I'm still learning Swift so I'd really appreciate some advises on this matter, thanks!
In comments you provide elaboration, saying that you are implementing a messenger. For that purpose simple HTTP requests are not appropriate approach. Instead, you want to introduce so-called socket connection. I dare quote myself from another relevant thread:
It's called socket-connections, at a glance it looks like a channel, that hosts on a server side, and any clients (devices) can join this channel (two or more, whatever you want). If device send a message to the server, it should broadcast the message toward all other participants (it can broadcast the message even to the sender itself, but with some meta-information so we can distiguish our own messages and ignore them).
Thereby first of all you need server with socket connection established, then you can implement any of already existing solutions (e.g. https://github.com/daltoniam/Starscream for iOS). Also you may want to take a look at AWS https://aws.amazon.com, as it has the socket connection service out of the box for the server side and required SDK for both Android and iOS platforms.
Let me give you some insight on my application itself.
To put it in short, I am creating a social-networking app. Each post consists of an image, profile picture, and caption. Each post exists in my MySQL database. I am using my own framework to retrieve each post. However, once I retrieve each post I still have to retrieve the profile picture and image using the URLs which I retrieved from the database. I would like to retrieve all images at once rather than running in sequential order.
As of now, there are about 5 posts in the database. Loading the necessary images for one post takes about 4 seconds. So right now I am loading the images for one post then retrieving the next in sequential order. So this whole process takes around 20 seconds. So say have 50 posts then it will take an extremely long time to load all the posts. I have some knowledge of GCD (grand-dispatch-queues) however I don't know how to implement it in my app.
Here is my code for retrieving my posts and images:
ConnectionManager.sharedInstance.retrievePosts(UserInformationInstance.SCHOOL) {
(result: AnyObject) in
if let posts = result as? [[String: AnyObject]] {
print("Retrieved \(posts.count) posts.")
for post in posts {
let postIDCurrent = post["id"] as? Int
var UPVOTES = 0;
var UPVOTED: Bool!
var query = ""
if let profilePictureCurrent = post["profile_picture"] {
// Loading profile picture image
let url = NSURL(string: profilePictureCurrent as! String)
let data = NSData(contentsOfURL: url!)
let image = UIImage(data: data!)
UserInformationInstance.postsProfilePictures.append(image!)
print("added profile pic")
} else {
print("error")
}
if let postPictureCurrent = post["image"] {
if (postPictureCurrent as! String != "") {
// Loading image associated with post
let url = NSURL(string: postPictureCurrent as! String)
let data = NSData(contentsOfURL: url!)
let image = UIImage(data: data!)
let imageArray: [AnyObject] = [postIDCurrent!, image!]
UserInformationInstance.postsImages.append(imageArray)
print("added image pic")
}
} else {
print("error")
}
UserInformationInstance.POSTS.append(post)
}
} else {
self.loadSearchUsers()
}
}
So my question is, how can I retrieve all the images at the same time instead of retrieving one after the other?
It would be great if someone could give an explanation as well as some code :)
I would recommend to revise your approach. If your server is fine - it's not busy and well reachable, so that resources downloading is limited by device network adapter bandwidth (X mbps), then it does not matter how you downloading images - concurrently or sequently.
Let me show this. Downloading time of 10 files with size Y mb simultaneously is equal to downloading time of one file, but in this case the downloading speed will be 10 times slower per file:
X/10 - downloading speed per one file
Time = Amount / Speed
T = Y / (X/10) = 10 * Y / X
Now if your are downloading sequently:
T = 10 * (Y / X) = 10 * Y / X
I would recommend to show posts immediately once you retrived them from the storage, then you need to start image downloading asynchronously and set image once that's downloaded. That's the best practice in the industry, consider Facebook, Twitter, Instagram apps.