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.
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.
I would like to only execute a segue if I get a certain response from the server. In swift, how can I wait until I get a response to continue?
Bottom line, you don't "wait" for the response, but rather simply specify what you want to happen when the response comes in. For example, if you want to perform a segue when some network request is done, you should employ the completion handler pattern.
The issue here is that you're probably accustomed to just hooking your UI control to a segue in Interface Builder. In our case, we don't want to do that, but rather we want to perform the network request, and then have its completion handler invoke the segue programmatically. So, we have to create a segue that can be performed programmatically and then hook your button up to an #IBAction that performs the network request and, if appropriate, performs the segue programmatically. But, note, there should be no segue hooked up to the button directly. We'll do that programmatically.
For example:
Define the segue to be between the two view controllers by control-dragging from the view controller icon in the bar above the first scene to the second scene:
Give that segue a storyboard identifier by selecting the segue and going to the "Attributes Inspector" tab:
Hook up the button (or whatever is going to trigger this segue) to an #IBAction.
Write an #IBAction that performs network request and, upon completion, programmatically invokes that segue:
#IBAction func didTapButton(_ sender: Any) {
let request = URLRequest(...). // prepare request however your app requires
let waitingView = showWaitingView() // present something so that the user knows some network request is in progress
// perform network request
let task = URLSession.shared.dataTask(with: request) { data, response, error in
// regardless of how we exit this, now that request is done, let's
// make sure to remove visual indication that network request was underway
defer {
DispatchQueue.main.async {
waitingView.removeFromSuperview()
}
}
// make sure there wasn't an error; you'll undoubtedly have additional
// criteria to apply here, but this is a start
guard let data = data, error == nil else {
print(error ?? "Unknown error")
return
}
// parse and process the response however is appropriate in your case, e.g., if JSON:
//
// guard let responseObject = try? JSONSerialization.jsonObject(with data) else {
// // handle parsing error here
// return
// }
//
// // do whatever you want with the parsed JSON here
// do something with response
DispatchQueue.main.async {
performSegue(withIdentifier: "SegueToSceneTwo", sender: self)
}
}
task.resume()
}
/// Show some view so user knows network request is underway
///
/// You can do whatever you want here, but I'll blur the view and add `UIActivityIndicatorView`.
private func showWaitingView() -> UIView {
let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .Dark))
effectView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(effectView)
NSLayoutConstraint.activateConstraints([
effectView.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor),
effectView.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor),
effectView.topAnchor.constraintEqualToAnchor(view.topAnchor),
effectView.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor)
])
let spinner = UIActivityIndicatorView(activityIndicatorStyle: .WhiteLarge)
effectView.addSubview(spinner)
spinner.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activateConstraints([
spinner.centerXAnchor.constraintEqualToAnchor(view.centerXAnchor),
spinner.centerYAnchor.constraintEqualToAnchor(view.centerYAnchor)
])
spinner.startAnimating()
return effectView
}
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.
I'm using Alamofire for network request. When I load a new viewController I make a new request in ViewDidAppear to get example url to images ect. When I make the request in ViewDidAppear there is a delay before the data appear, I also tried in ViewDidLoad the request was a little bit faster, but you can stil see the data appear after a small delay. It is okay that when a user access the viewController first time the user will see the data is loading, but is there a way to keep the data so that when a user navigate away from the controller, example when a user go back from a push in a navigationController and then navigate forward again without making the request to get the data again?
Here is one of my request in ViewDidAppear.
Hope you guys can help - Thank you
override func viewDidAppear(animated: Bool) {
navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .Plain, target: nil, action: nil)
var parameters = [String: AnyObject]()
if(self.MySelf) {
parameters = ["userId": LoginViewController.CurrentUser.UserID as AnyObject]
}
else {
parameters = ["userId": self.UserID as AnyObject]
}
//GET posts
Alamofire.request(.POST, Config.ApiURL + "getUserPosts?token=" + LoginViewController.CurrentUser.Token, parameters: parameters as! [String : AnyObject]).responseJSON{ response in
print(response)
switch response.result {
case .Success(let data):
let json = JSON(data)
if let posts = json["post"]["data"].array {
self.postArray = posts
self.postArray = self.postArray.reverse()
self.navigationItem.title = json["user"]["firstname"].string! + " " + json["user"]["lastname"].string!
self.User = json["user"]
self.UserPic = self.User["photourl"].string!
}
else {
print("Array is empty")
}
case .Failure(let error):
print("Request failed with error: \(error)")
}
self.ProfilePostColView?.reloadData()
}
}
Try making the network request in an earlier view controller - say a loading screen and then pass the response to this view controller.
Alternatively you could store the response in a cache service of sorts - when the user navigates back to this view controller you could check it already in the cache if so load it up to the view if not call the request.
Also making the network request in viewDidLoad will be faster as it called before viewDidAppear - but keep in mind viewDidLoad is only called once for a specific instance of a viewController where as viewDidAppear is called every time that instance is displayed again (eg. if it as the bottom of the navigation stack and the user presses back to it).
Keep the user in mind - you do not want to be chewing up their data so if you know the request will have the same response you only want to make the http request once.
I am new to swift and I am trying to do post request using dataTaskWithRequest. I have two view controllers, LoginViewController and SecondViewController. In the LoginViewController I have submit button to submit form login and then go to the second view. However in the button action function I called dataTaskWithRequest to get authentication.
How can dataTaskWithRequest complete his task before SecondViewController load?
The key here is that the login view controller's button should not be a segue to the second view controller. Instead, it should simply be an #IBAction which performs the authentication with the dataTaskWithRequest, and only if you determined it was successful in the completionHandler closure, would it programmatically transition to the next view controller.
So, let's pick that apart:
Hook up button in login scene to an #IBAction, which creates a request, initiates it, and in the completion block determines if it was successful and if so, tells the :
#IBAction func didTapLoginButton(sender: UIButton) {
let request = NSMutableURLRequest(URL: NSURL(string: "loginurl")!)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.HTTPBody = ...
let task = NSURLSession.sharedSession().dataTaskWithRequest(request) { (data, response, error) -> Void in
if data == nil {
// handle error here
println("\(error)")
} else {
// parse response here and determine if successful
var loginSuccessful: Bool = ...
if loginSuccessful {
dispatch_async(dispatch_get_main_queue()) { () -> Void in
self.performSegueWithIdentifier("SegueToSecond", sender: sender)
}
}
}
}
task.resume()
}
Clearly, there's a lot more you might want to do there (e.g. tell the user if the authorization failed, etc.), but this illustrates the moving parts:
create request;
in completionHandler, see if authorization succeeded; and
anything you do with the UI must be dispatched back to the main queue.
Note, the above assumes you have a segue from the login scene to the second scene. You can do this by control drag from the view controller icon above the login scene to the second scene:
When that's done, select the segue and give it a unique storyboard id (the same one you will use in the #IBAction code, above):