In order to fully render my View from my View Controller, I need to have a response from a network request.
I have been trying to do this is many different ways, but have been unsuccessful each time.
Originally, I had gotten it to work by making a "synchronous" network request prior to calling any methods to render the View. However, the compiler is warning me that the synchronous network requests are deprecated as of ios 8.
What is the best way to accomplish this in the most performant way?
I have tried:
override func loadView() {
dispatch_sync(dispatch_get_main_queue()){
// GET the Markup
let url = NSURL(string: self.PageURL)
let request = NSURLRequest(URL: url!)
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
let RequiredViewData = session.dataTaskWithRequest(request) {(data, response, error) in
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments)
self.RequiredViewJSON = json
self.view = UIView(frame: UIScreen.mainScreen().bounds)
print(data)
} catch {
print("error serializing JSON: \(error)")
}
}
RequiredViewData.resume()
}
}
But that just makes my app render as a blank screen.
Essentially what I need to accomplish is this:
Make a network request and receive the response before any view rendering can occur.
Thanks in Advance!
I never really tried to override loadView nor know if you should, but I think what you need to do is call super in this case to get your view to render again.
Edit
Also per your comment I put the main thread call "after" you get the call back from the NSURLSession. I might have a } in the wrong spot but should get you close enough.
override func loadView() {
// GET the Markup
let url = NSURL(string: self.PageURL)
let request = NSURLRequest(URL: url!)
let session = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
let RequiredViewData = session.dataTaskWithRequest(request) {(data, response, error) in
do {
let json = try NSJSONSerialization.JSONObjectWithData(data!, options: .AllowFragments)
self.RequiredViewJSON = json
dispatch_sync(dispatch_get_main_queue()){
//You may also want to try commenting this out unless you are intentionally creating a blank view.
self.view = UIView(frame: UIScreen.mainScreen().bounds)
print(data)
//call super after you get what you need
super.loadView()
}
} catch {
print("error serializing JSON: \(error)")
}
}
RequiredViewData.resume()
}
}
Hopefully that helps.
The view controller should handle all of this networking logic in viewDidLoad or viewWillAppear, not in loadView. I'd suggest setting a loading state an initial empty state on the subview, then once you have what you need, update the view with that data. You may need to call setNeedsLayout on the view to update for the new data.
Related
I'm trying to download images from my firebase database and load them into collectionviewcells. The images download, however I am having trouble having them all download and load asynchronously.
Currently when I run my code the last image downloaded loads. However, if I update my database the collection view updates and the new last user profile image also loads in but the remainder are missing.
I'd prefer to not use a 3rd party library so any resources or suggestions would be greatly appreciated.
Here's the code that handles the downloading:
func loadImageUsingCacheWithUrlString(_ urlString: String) {
self.image = nil
// checks cache
if let cachedImage = imageCache.object(forKey: urlString as NSString) as? UIImage {
self.image = cachedImage
return
}
//download
let url = URL(string: urlString)
URLSession.shared.dataTask(with: url!, completionHandler: { (data, response, error) in
//error handling
if let error = error {
print(error)
return
}
DispatchQueue.main.async(execute: {
if let downloadedImage = UIImage(data: data!) {
imageCache.setObject(downloadedImage, forKey: urlString as NSString)
self.image = downloadedImage
}
})
}).resume()
}
I believe the solution lies somewhere in reloading the collectionview I just don't know where exactly to do it.
Any suggestions?
EDIT:
Here is where the function is being called; my cellForItem at indexpath
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: userResultCellId, for: indexPath) as! FriendCell
let user = users[indexPath.row]
cell.nameLabel.text = user.name
if let profileImageUrl = user.profileImageUrl {
cell.profileImage.loadImageUsingCacheWithUrlString(profileImageUrl)
}
return cell
}
The only other thing that I believe could possibly affect the images loading is this function I use to download the user data, which is called in viewDidLoad, however all the other data downloads correctly.
func fetchUser(){
Database.database().reference().child("users").observe(.childAdded, with: {(snapshot) in
if let dictionary = snapshot.value as? [String: AnyObject] {
let user = User()
user.setValuesForKeys(dictionary)
self.users.append(user)
print(self.users.count)
DispatchQueue.main.async(execute: {
self.collectionView?.reloadData()
})
}
}, withCancel: nil)
}
Current Behavior:
As for the current behavior the last cell is the only cell that displays the downloaded profile image; if there are 5 cells, the 5th is the only one that displays a profile image. Also when I update the database, ie register a new user into it, the collectionview updates and displays the newly registered user correctly with their profile image in addition to the old last cell that downloaded it's image properly. The rest however, remain without profile images.
I know you found your problem and it was unrelated to the above code, yet I still have an observation. Specifically, your asynchronous requests will carry on, even if the cell (and therefore the image view) have been subsequently reused for another index path. This results in two problems:
If you quickly scroll to the 100th row, you are going to have to wait for the images for the first 99 rows to be retrieved before you see the images for the visible cells. This can result in really long delays before images start popping in.
If that cell for the 100th row was reused several times (e.g. for row 0, for row 9, for row 18, etc.), you may see the image appear to flicker from one image to the next until you get to the image retrieval for the 100th row.
Now, you might not immediately notice either of these are problems because they will only manifest themselves when the image retrieval has a hard time keeping up with the user's scrolling (the combination of slow network and fast scrolling). As an aside, you should always test your app using the network link conditioner, which can simulate poor connections, which makes it easier to manifest these bugs.
Anyway, the solution is to keep track of (a) the current URLSessionTask associated with the last request; and (b) the current URL being requested. You can then (a) when starting a new request, make sure to cancel any prior request; and (b) when updating the image view, make sure the URL associated with the image matches what the current URL is.
The trick, though, is when writing an extension, you cannot just add new stored properties. So you have to use the associated object API to associate these two new stored values with the UIImageView object. I personally wrap this associated value API with a computed property, so that the code for retrieving the images does not get too buried with this sort of stuff. Anyway, that yields:
extension UIImageView {
private static var taskKey = 0
private static var urlKey = 0
private var currentTask: URLSessionTask? {
get { objc_getAssociatedObject(self, &UIImageView.taskKey) as? URLSessionTask }
set { objc_setAssociatedObject(self, &UIImageView.taskKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
private var currentURL: URL? {
get { objc_getAssociatedObject(self, &UIImageView.urlKey) as? URL }
set { objc_setAssociatedObject(self, &UIImageView.urlKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) }
}
func loadImageAsync(with urlString: String?, placeholder: UIImage? = nil) {
// cancel prior task, if any
weak var oldTask = currentTask
currentTask = nil
oldTask?.cancel()
// reset image view’s image
self.image = placeholder
// allow supplying of `nil` to remove old image and then return immediately
guard let urlString = urlString else { return }
// check cache
if let cachedImage = ImageCache.shared.image(forKey: urlString) {
self.image = cachedImage
return
}
// download
let url = URL(string: urlString)!
currentURL = url
let task = URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
self?.currentTask = nil
// error handling
if let error = error {
// don't bother reporting cancelation errors
if (error as? URLError)?.code == .cancelled {
return
}
print(error)
return
}
guard let data = data, let downloadedImage = UIImage(data: data) else {
print("unable to extract image")
return
}
ImageCache.shared.save(image: downloadedImage, forKey: urlString)
if url == self?.currentURL {
DispatchQueue.main.async {
self?.image = downloadedImage
}
}
}
// save and start new task
currentTask = task
task.resume()
}
}
Also, note that you were referencing some imageCache variable (a global?). I would suggest an image cache singleton, which, in addition to offering the basic caching mechanism, also observes memory warnings and purges itself in memory pressure situations:
class ImageCache {
private let cache = NSCache<NSString, UIImage>()
private var observer: NSObjectProtocol?
static let shared = ImageCache()
private init() {
// make sure to purge cache on memory pressure
observer = NotificationCenter.default.addObserver(
forName: UIApplication.didReceiveMemoryWarningNotification,
object: nil,
queue: nil
) { [weak self] notification in
self?.cache.removeAllObjects()
}
}
deinit {
NotificationCenter.default.removeObserver(observer!)
}
func image(forKey key: String) -> UIImage? {
return cache.object(forKey: key as NSString)
}
func save(image: UIImage, forKey key: String) {
cache.setObject(image, forKey: key as NSString)
}
}
A bigger, more architectural, observation: One really should decouple the image retrieval from the image view. Imagine you have a table where you have a dozen cells using the same image. Do you really want to retrieve the same image a dozen times just because the second image view scrolled into view before the first one finished its retrieval? No.
Also, what if you wanted to retrieve the image outside of the context of an image view? Perhaps a button? Or perhaps for some other reason, such as to download images to store in the user’s photos library. There are tons of possible image interactions above and beyond image views.
Bottom line, fetching images is not a method of an image view, but rather a generalized mechanism of which an image view would like to avail itself. An asynchronous image retrieval/caching mechanism should generally be incorporated in a separate “image manager” object. It can then detect redundant requests and be used from contexts other than an image view.
As you can see, the asynchronous retrieval and caching is starting to get a little more complicated, and this is why we generally advise considering established asynchronous image retrieval mechanisms like AlamofireImage or Kingfisher or SDWebImage. These guys have spent a lot of time tackling the above issues, and others, and are reasonably robust. But if you are going to “roll your own,” I would suggest something like the above at a bare minimum.
just wanted some clarification on the best practices to make network api calls in Swift 2.
Here is how my typical network requests looks like to download JSON data:
let session = NSURLSession(configuration: .defaultSessionConfiguration())
let url = NSURL(string: my_url_string)
let request = NSURLRequest(URL: url)
let dataTask = session.dataTaskWithRequest(request) { data, response, error in
do {
self.tableData = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as! [NSDictionary]
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
} catch let error {
print(error)
}
}
dataTask.resume()
My question is: should I wrap all of this code block into a background queue? Should I do as follows:
let download_queue = dispatch_queue_create("download", nil)
dispatch_async(download_queue) { () -> Void in
previous code here
}
Or should I use one of the given high priority queues such as:
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0)
Also, when making additional network requests on subsequent view controllers, should I use the same queue I use here or should I create a new one?
By default NSURLSession API is highly asynchronous. Usefull information from the Apple docs.
There is no visible issues that indicate to wrap you're code block with GCD and also completion block runs on background thread so there is right usage of the GCD to update UITableview
I am using NSURLSession for API call. Once API gets hit, before getting response I am moving to another view controller. How to stop all NSURLSession running in the current view controller? Kindly guide me.
My Sample code is not working. If I move to another UIViewController, that new session creates and working fine but previous UIViewControllers's session resuming. How to stop that Session? Kindly guide me.
Sample Get API Cal:
var testSession = NSURLSession.sharedSession()
var testTask = NSURLSessionDataTask()
override func viewDidAppear(animated: Bool) {
testTask = testSession.dataTaskWithURL(NSURL(string: "http://httpbin.org/get")!, completionHandler: { (data, response, error) -> Void in
do{
let str = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.AllowFragments) as! [String:AnyObject]
print(str)
}
catch {
print("json error: \(error)")
}
})
testTask.resume()
}
override func viewDidDisappear(animated: Bool) {
testTask.cancel()
testSession.finishTasksAndInvalidate()
}
Don't use the shared session. Use a real session that you create explicitly. Then call invalidateAndCancel on the session when you no longer need any of its pending requests.
First of all, I am just a beginner who is currently developing an app with the Swift language, so please don't mind my question too much because I really need to know and I am having trouble with maintaining the code that I constructed.
It's about the async delegate pattern.
Here is my API class. Assume that there are many API classes like that which makes async calls.
protocol InitiateAPIProtocol{
func didSuccessInitiate(results:JSON)
func didFailInitiate(err:NSError)
}
class InitiateAPI{
var delegate : InitiateAPIProtocol
init(delegate: InitiateAPIProtocol){
self.delegate=delegate
}
func post(wsdlURL:String,action:String,soapMessage : String){
let request = NSMutableURLRequest(URL: NSURL(string: wsdlURL)!)
let msgLength = String(soapMessage.characters.count)
let data = soapMessage.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
request.HTTPMethod = "POST"
request.addValue("text/xml; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.addValue(msgLength, forHTTPHeaderField: "Content-Length")
request.addValue(action, forHTTPHeaderField: "SOAPAction")
request.HTTPBody = data
let task = session.dataTaskWithRequest(request) {
data, response, error in
if error != nil {
self.delegate.didFailInitiate(error!)
return
}
let jsonData = JSON(data: data)
self.delegate.didSuccessInitiate(jsonData)
}
task.resume()
}
func doInitiate(token : String){
let soapMessage = “”
// WSDL_URL is the main wsdl url i will request.
action = “”
post(WSDL_URL, action: action, soapMessage: soapMessage)
}
}
Here is my ViewController:
class ViewController : UIViewController,InitiateAPIProtocol{
var initiateAPI : InitiateAPI!
var token : String = “sWAFF1”
override func viewWillAppear(animated: Bool) {
// Async call start
initiateAPI = InitiateAPI(delegate:self)
initiateAPI.doInitiate(token)
}
// Here comes call back
func didSuccessInitiate(results: JSON) {
//handle results
}
func didFailInitiate(err: NSError) {
//handle errors
}
}
My problem is I said that there are many API classes like that, so if one view controller handles 4 API classes, I have to handle many protocol delegates methods as I extend the view controller. There will be many delegates method below of view controller. If other view controllers call the same API and have to handle the same delegates, I have a problem maintaining the code because every time I change some delegate parameters, I have to fix the code at all view controllers which use those API classes.
Is there any other good way to handle async call?
If my question seems a little complex, please leave a comment, I will reply and explain it clearly.
Delegates (OOP) and "completion handlers" (function like programming) just don't fit well together.
In order to increase comprehension and to make the code more concise, an alternative approach is required. One of this approach has been already proposed by #PEEJWEEJ using solely completion handlers.
Another approach is using "Futures or Promises". These greatly extend the idea of completion handlers and make your asynchronous code look more like synchronous.
Futures work basically as follows. Suppose, you have an API function that fetches users from a remote web service. This call is asynchronous.
// Given a user ID, fetch a user:
func fetchUser(id: Int) -> Future<User> {
let promise = Promise<User>()
// a) invoke the asynchronous operation.
// b) when it finished, complete the promise accordingly:
doFetchAsync(id, completion: {(user, error) in
if error == nil {
promise.fulfill(user!)
} else {
promise.reject(error!)
}
})
return.promise.future
}
First, the important fact here is, that there is no completion handler. Instead, the asynchronous function returns you a future. A future represents the eventual result of the underlying operation. When the function fetchUser returns, the result is not yet computed, and the future is in a "pending" state. That is, you cannot obtain the result immediately from the future. So, we have to wait?? - well not really, this will be accomplished similar to an async function with a completion handler, i.e. registering a "continuation":
In order to obtain the result, you register a completion handler:
fetchUser(userId).map { user in
print("User: \(user)")
}.onFailure { error in
print("Error: \(error)")
}
It also handles errors, if they occur.
The function map is the one that registered the continuation. It is also a "combinator", that is it returns another future which you can combine with other functions and compose more complex operations.
When the future gets finally completed, the code continues with the closure registered with the future.
If you have two dependent operations, say OP1 generates a result which should be used in OP2 as input, and the combined result should be returned (as a future), you can accomplish this in a comprehensive and concise manner:
let imageFuture = fetchUser(userId).flatMap { user in
return user.fetchProfileImage()
}
imageFuture.onSuccess { image in
// Update table view on main thread:
...
}
This was just a very short intro into futures. They can do much more for you.
If you want to see futures in action, you may start the Xcode playgrounds "A Motivating Example" in the third party library FutureLib (I'm the author). You should also examine other Future/Promise libraries, for example BrightFutures. Both libraries implement Scala-like futures in Swift.
Have you looked into NSNotificationCenter?
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSNotificationCenter_Class/
You'll be able to post events from your api class, then each view controller would subscribe to the events and be notified accordingly
Does that make sense? There are lots of good examples of this pattern:
https://stackoverflow.com/a/24049111/2678994
https://stackoverflow.com/a/28269217/2678994
I've updated your code below:
class InitiateAPI{
//
// var delegate : InitiateAPIProtocol
// init(delegate: InitiateAPIProtocol){
// self.delegate=delegate
// }
func post(wsdlURL:String,action:String,soapMessage : String){
let request = NSMutableURLRequest(URL: NSURL(string: wsdlURL)!)
let msgLength = String(soapMessage.characters.count)
let data = soapMessage.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
request.HTTPMethod = "POST"
request.addValue("text/xml; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.addValue(msgLength, forHTTPHeaderField: "Content-Length")
request.addValue(action, forHTTPHeaderField: "SOAPAction")
request.HTTPBody = data
let task = session.dataTaskWithRequest(request) {
data, response, error in
if error != nil {
// self.delegate.didFailInitiate(error!)
/* Post notification with error */
NSNotificationCenter.defaultCenter().postNotificationName("onHttpError", object: error)
return
}
let jsonData = JSON(data: data)
// self.delegate.didSuccessInitiate(jsonData)
/* Post notification with json body */
NSNotificationCenter.defaultCenter().postNotificationName("onHttpSuccess", object: jsonData)
}
task.resume()
}
func doInitiate(token : String){
let soapMessage = “”
// WSDL_URL is the main wsdl url i will request.
action = “”
post(WSDL_URL, action: action, soapMessage: soapMessage)
}
}
Your view controller class:
class ViewController : UIViewController { //,InitiateAPIProtocol{
var initiateAPI : InitiateAPI!
var token : String = “sWAFF1”
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.didSuccessInitiate(_:)), name: "onHttpSuccess", object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.didFailInitiate(_:)), name: "onHttpError", object: nil)
}
override func viewWillAppear(animated: Bool) {
// Async call start
initiateAPI = InitiateAPI(delegate:self)
initiateAPI.doInitiate(token)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
/* Remove listeners when view controller disappears */
NSNotificationCenter.defaultCenter().removeObserver(self, name: "onHttpSuccess", object: nil)
NSNotificationCenter.defaultCenter().removeObserver(self, name: "onHttpError", object: nil)
}
// Here comes call back
func didSuccessInitiate(notification : NSNotification) { //results: JSON) {
if let payload = notification.object as? JSON {
//handle results
}
}
func didFailInitiate(notification : NSNotification) { //err: NSError) {
if let payload = notification.object as? NSError {
//handle errors
}
}
}
Instead of using a delegate, you could (should?) use closers/functions:
func post(/*any other variables*/ successCompletion: (JSON) -> (), errorCompletion: (NSError) ->()){
/* do whatever you need to*/
/*if succeeds*/
successCompletion("")
/*if fails*/
errorCompletion(error)
}
// example using closures
post({ (data) in
/* handle Success*/
}) { (error) in
/* handle error */
}
// example using functions
post(handleData, errorCompletion: handleError)
func handleData(data: JSON) {
}
func handleError(error: NSError) {
}
This would also give you the option to handle all the errors with one function.
Also, it's ideal to parse your JSON into their desired objects before returning them. This keeps your ViewControllers clean and makes it clear where the parsing will occur.
What it's supposed to be
I have a username field. When the username is entered and the sendButton is clicked, the userdata is fetched with a asynchronousRequest as a JSON file.
After the sendButton is clicked, I want to display an ActivityIndicator.
The UI shall still be responsive, while the request is made.
How it is now
I click the sendButton and the UI freezes. Even the ActivityIndicator does NOT get displayed.
The code
LoginVC:
func buttonTouchedUpInside(sender: UIButton) {
toggleActivityIndicatorVisibilityOn(true)
LoginManager.sharedInstance.checkUserForCredentials(username: textFieldLogin.text, password: "")
toggleActivityIndicatorVisibilityOn(false)
}
func loginManagerDidFinishAuthenticationForUser(userData: [String:String]?){
// Delegate Method, which works with the fetched userData.
}
LoginManager
func checkUserForCredentials(#username: String ,password: String) -> Void {
let url = NSURL(string: "\(Config.checkCredentialsUrl)username=\(username)")
let request = NSURLRequest(URL: url!)
NSURLConnection.sendAsynchronousRequest(request, queue: .mainQueue()) { (response, data, error) -> Void in
if error != nil {
//Display error-message
}
var error : NSError?
let json = NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers, error: &error) as? [String:String]
self.delegate?.loginManagerDidFinishAuthenticationForUser(json)
}
}
In short: I want the request to be made in background, that the Activityindicator is shown and the UI stays responsive. After the asynchronous request successfully fetched the json, the delegate method shall be called
The second line of code in the buttonTouchedUpInside method, which reads LoginManager.sharedInstance.checkUserForCredentials(username: textFieldLogin.text, password: "") is calling an asynchronous function within it, which means that it is not blocking the next line of code... which is the one that (I am guessing) triggers your loading screen to become invisible again.
Basically, your loading screen is showing up, but it is immediately being hidden again. To fix at least the part with your loading screen, put the third line of code in the buttonTouchedUpInside function in the callback method loginManagerDidFinishAuthenticationForUser instead.