How to pass URLSessionTask as a parameter - ios

I have a cell and I'm using URLSessionTask inside of it for some images. When the cell scrolls off of the screen, in prepareForReuse I cancel the task and everything works fine. Because I'm doing several thing with the image once I get it from the task, I want to create a function for everything. The problem is I can't pass task: URLSessionDataTask? as a parameter because it is a let constant. I understand the error Cannot assign to value: 'task' is a 'let' constant, but I can't figure out how to get around it because I have to cancel the task once prepareForReuse runs?
func setImageUsingURLSessionTask(photoUrlStr: String, imageView: UIImageView, task: URLSessionDataTask?)
if let cachedImage = imageCache.object(forKey: photoUrlStr as AnyObject) as? UIImage {
let resizedImage = funcToResizeImage(cachedImage)
imageView.image = resizedImage
return
}
guard let url = URL(string: photoUrlStr) else { return }
task = URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
// eventually resize image, set it to the imageView, and save it to cache
})
task?.resume()
}
Here's the cell if it's not clear. Once cellForItem runs and myObject gets initialized, I pass the photoUrlStr, imageView, and task, to the function. If the cell is scrolled off screen in prepareForReuse the task is cancelled so the incorrect image never appears. This works 100% fine if I set the URLSessionTask inside the cell itself instead of inside a function.
class MyCell: UICollectionViewCell {
lazy var photoImageView: UIImageView = {
// ...
}()
var task: URLSessionTask?
var myObject: MyObject? {
didSet {
guard let photoUrlStr = myObject?.photoUrlStr else { return }
setImageUsingURLSessionTask(photoUrlStr: photoUrlStr, imageView: photoImageView, task: task)
}
}
override func prepareForReuse() {
super.prepareForReuse()
task?.cancel()
task = nil
photoImageView.image = nil
}
}

You can pass your task as an inout parameter. A simplified version of your function would be:
func changeDataTask(inout task: URLSessionDataTask) {
task = // You can change it
}
The variable what you pass to the function has to be var. You would call it like this:
var task = // Declare your initial datatask
changeDataTask(task: &task)

Related

Couldn't figure out where my data was passed in networking Swift

I'm new to Swift and generally lack of experience in programming. Currently I'm working on a project trying to display a list of star war characters on view controller, but I'm having some issues in passing data through networking Manager. When I ran the program, I couldn't get the name label displayed on the screen.
I have checked the tableView cell and the label is connected with viewController. I feel that the problem is somewhere related with networking manager but couldn't figure out by myself.
var charactersArray: [Characters] = []
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
getCharacters(){
DispatchQueue.main.async {
self.starWarTableViewController.reloadData()
}
}
self.title = "Star War Characters"
// print(charactersArray), returns an empty array
}
private func getURL() -> String {
return "https://swapi.dev/api/people/"
}
func getCharacters(completion: #escaping () -> Void) {
self.starWarTableViewController.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
self.starWarTableViewController.dataSource = self
self.starWarTableViewController.delegate = self
DispatchQueue.global(qos: .background).async {
for i in 1...20 {
NetworkingManager.shared.getDecodedObject(from: self.getURL() + "\(i)"){
(characters: Characters?, error) in
guard let characters = characters else{ return }
self.charactersArray.append(characters)
print(self.charactersArray) //this will return an array list of characters names
}
}
print(self.charactersArray) // here the characterArray is empty
}
completion()
}
For what I found, the issue seems in my networking manager or the table view cell
enum NetworkError: Error {
case invalidURLString
}
final class NetworkingManager{
static let shared = NetworkingManager()
private init(){
}
func getDecodedObject <T: Decodable> (from urlString: String, completion: #escaping (T?, Error?) -> Void){
guard let url = URL(string: urlString) else {
completion(nil, NetworkError.invalidURLString)
return
}
URLSession.shared.dataTask(with: url){
(data, response, error) in
guard let data = data else{
completion(nil, error)
return }
guard let characters = try? JSONDecoder().decode(T.self, from: data) else{ return }
completion(characters, nil)
}.resume()
}
}
class TableViewCell: UITableViewCell {
#IBOutlet weak var nameLabel: UILabel!
func configure (with characters: Characters) {
self.nameLabel.text = characters.name
}
}
Here is Characters
struct Characters: Decodable {
let name: String
enum CodingKeys: String, CodingKey {
case name
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
// print(name), this will return a list of character names
}
}
This is the tableview extension
extension ViewController: UITableViewDataSource, UITableViewDelegate{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.charactersArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as! TableViewCell
cell.configure(with: self.charactersArray[indexPath.row])
// cell.nameLabel.text = "Hi"
return cell
}
}
Right now, you are initiating a series of asynchronous network requests, but calling the completion handler after the requests are initiated, rather than waiting for when them to finish. Thus, when you call completion, the results have not been received.
When you start a bunch of asynchronous tasks and you want to know when they are done, a common pattern is to use DispatchGroup, calling enter before the asynchronous call, calling leave in the completion handler, and then specifying what you want to do when they are all done in a closure supplied to notify. If you put your call to your own completion handler in notify, then it will not get called until the requests are done and you have data to present in the UI.
A few other observations:
getDecodedObject is already asynchronous method, so there's no need to dispatch it to a background queue.
You will want to update both the model and the UI from the main queue. (URLSession calls its completion handlers on a serial background queue.) To accomplish this can basically tell the aforementioned notify to run its closure on the .main queue. I would also not update the model object until you are ready to update the UI.
When performing a series of asynchronous network requests in parallel, you have no assurances of the order in which they may complete. So, you would generally want to use some structure that is independent of the order that the tasks finish, such as a dictionary. Then, when they are done, if you want to build a sorted array of results, then build the array of results from that.
Thus:
func getCharacters(completion: #escaping ([Characters]) -> Void) {
// to keep track of when the 20 requests finish
let group = DispatchGroup()
// temporary structure to keep track of the results
var charactersDictionary: [Int: Characters] = [:]
// perform the requests
for i in 1...20 {
group.enter()
NetworkingManager.shared.getDecodedObject(from: getURL() + "\(i)") { (characters: Characters?, error) in
defer { group.leave() }
guard let characters = characters else { return }
charactersDictionary[i] = characters
}
}
// when the group tells us that all 20 are done, let's build array of the
// results and pass it back to the main queue via the completion handler parameter.
group.notify(queue: .main) {
let results = (1...20).compactMap { charactersDictionary[$0] }
print(results)
completion(results)
}
}
Note, by the way, I have pulled the table configuration code out of this routine (as you really don't want to intermingle network code with UI code):
func configureTableView() {
starWarTableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
starWarTableView.dataSource = self
starWarTableView.delegate = self
}
I've also renamed that outlet to be starWarTableView, because it is not a view controller, but a table view:
Anyway, you would then call it like so:
override func viewDidLoad() {
super.viewDidLoad()
configureTableView()
getCharacters { characters in
self.charactersArray = characters
self.starWarTableView.reloadData()
}
}

UICollectionView displays wrong images in cells

I am building a UITableView that is going to have cells with different layouts in them. The cell I am having issues with has a UICollectionView embedded in it that is generated from an API.
The category name and id populate in the cell correctly, but the images in the UICollectionView do not. The images load, but they are not the right ones for that category. Screen capture of how the collection is loading currently
Some of the things I've tried:
Hard-coding the ids for each one of the categories instead of dynamically generating them. When I do this, the correct images load (sometimes but not always) ... and if they do load correctly, when I scroll the images change to wrong ones
The prepareForReuse() function ... I'm not exactly sure where I would put it and what I would reset in it (I have code I believe already kind of nils the image out [code included below])
I have spent a few hours trying to figure this out, but I am stuck ... any suggestions would be appreciated.
My View Controller:
class EcardsViewController: BaseViewController {
#IBOutlet weak var categoryTable: UITableView!
var categories = [CategoryItem]()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.categoryTable.dataSource! = self
self.categoryTable.delegate! = self
DispatchQueue.main.async {
let jsonUrlString = "https://*********/******/category"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
if err == nil {
do {
let decoder = JSONDecoder()
let ecardcategory = try decoder.decode(Category.self, from: data)
self.categories = ecardcategory.category
self.categories.sort(by: {$0.title < $1.title})
self.categories = self.categories.filter{$0.isFeatured}
} catch let err {
print("Err", err)
}
DispatchQueue.main.async {
self.categoryTable.reloadData()
}
}
}.resume()
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
extension EcardsViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return categories.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "EcardsCategoriesTableViewCell", for: indexPath) as! EcardsCategoriesTableViewCell
cell.categoryName.text = ("\(categories[indexPath.row].title)**\(categories[indexPath.row].id)")
cell.ecardCatId = String(categories[indexPath.row].id)
return cell
}
}
My Table Cell:
class EcardsCategoriesTableViewCell: UITableViewCell {
#IBOutlet weak var categoryName: UILabel!
#IBOutlet weak var thisEcardCollection: UICollectionView!
var ecardCatId = ""
var theseEcards = [Content]()
let imageCache = NSCache<NSString,AnyObject>()
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
self.thisEcardCollection.dataSource! = self
self.thisEcardCollection.delegate! = self
DispatchQueue.main.async {
let jsonUrlString = "https://**********/*******/content?category=\(self.ecardCatId)"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
if err == nil {
do {
let decoder = JSONDecoder()
let ecards = try decoder.decode(Ecards.self, from: data)
self.theseEcards = ecards.content
self.theseEcards = self.theseEcards.filter{$0.isActive}
} catch let err {
print("Err", err)
}
DispatchQueue.main.async {
self.thisEcardCollection.reloadData()
}
}
}.resume()
}
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
}
extension EcardsCategoriesTableViewCell: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return theseEcards.count > 7 ? 7 : theseEcards.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "EcardCategoriesCollectionViewCell", for: indexPath) as! EcardCategoriesCollectionViewCell
cell.ecardImage.contentMode = .scaleAspectFill
let ecardImageLink = theseEcards[indexPath.row].thumbSSL
cell.ecardImage.downloadedFrom(link: ecardImageLink)
return cell
}
}
Collection View Cell:
class EcardCategoriesCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var ecardImage: UIImageView!
}
Extension to "download" image:
extension UIImageView {
func downloadedFromReset(url: URL, contentMode mode: UIViewContentMode = .scaleAspectFit, thisurl: String) {
contentMode = mode
self.image = nil
// check cache
if let cachedImage = ImageCache.shared.image(forKey: thisurl) {
self.image = cachedImage
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
guard
let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
let data = data, error == nil,
let image = UIImage(data: data)
else { return }
ImageCache.shared.save(image: image, forKey: thisurl)
DispatchQueue.main.async() {
self.image = image
}
}.resume()
}
func downloadedFrom(link: String, contentMode mode: UIViewContentMode = .scaleAspectFit) {
guard let url = URL(string: link) else { return }
downloadedFromReset(url: url, contentMode: mode, thisurl: link)
}
}
Both UICollectionViewCell and UITableViewCell are reused. As one scrolls off the top of the screen, it is reinserted below the visible cells as the next cell that will appear on screen. The cells retain any data that they have during this dequeuing/requeuing process. prepareForReuse exists to give you a point to reset the view to default values and to clear any data from the last time it was displayed. This is especially important when working with asynchronous processes, such as network calls, as they can outlive the amount of time that a cell is displayed. Additionally, you're doing a lot of non-setup work in awakeFromNib. This method is not called every time a cell is displayed, it is only called the FIRST time a cell is displayed. If that cell goes off screen and is reused, awakeFromNib is not called. This is likely a big reason that your collection views have the wrong data, they're never making their network request when they appear on screen.
EcardsCategoriesTableViewCell:
prepareForReuse should be implemented. A few things need to occur in this method:
theseEcards should be nilled. When a table view scrolls off screen, you want to get rid of the collection view data or else the next time that cell is displayed, it will show the collection view data potentially for the wrong cell.
You should keep a reference to the dataTask that runs in awakeFromNib and then call cancel on this dataTask in prepareForReuse. Without doing this, the cell can display, disappear, then get reused before the dataTask completes. If that is the case, it may replace the intended values with the values from the previous dataTask (the one that was supposed to run on the cell that was scrolled off screen).
Additionally, the network call needs to be moved out of awakeFromNib:
You are only ever making the network call in awakeFromNib. This method only gets called the first time a cell is created. When you reuse a cell, it is not called. This method should be used to do any additional setup of views from the nib, but is not your main entry point in adding data to a cell. I would add a method on your cell that lets you set the category id. This will make the network request. It will look something like this:
func setCategoryId(_ categoryId: String) {
DispatchQueue.main.async {
let jsonUrlString = "https://**********/*******/content?category=\(categoryId)"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
if err == nil {
do {
let decoder = JSONDecoder()
let ecards = try decoder.decode(Ecards.self, from: data)
self.theseEcards = ecards.content
self.theseEcards = self.theseEcards.filter{$0.isActive}
} catch let err {
print("Err", err)
}
DispatchQueue.main.async {
self.thisEcardCollection.reloadData()
}
}
}.resume()
}
}
This will be called in the cellForRowAt dataSource method in EcardsViewController.
EcardCategoriesCollectionViewCell:
This cell has similar issues. You are setting images asynchronously, but are not clearing the images and cancelling the network requests when the cell is going to be reused. prepareForReuse should be implemented and the following should occur within it:
The image on the image view should be cleared or set to a default image.
The image request should be cancelled. This is going to take some refactoring to accomplish. You need to hold a reference to the dataTask in the collection view cell so that you can cancel it when appropriate.
After implementing these changes in the cells, you'll likely notice that the tableview and collection view feel slow. Data isn't instantly available. You'll want to cache the data or preload it some way. That is a bigger discussion than is right for this thread, but it will be your next step.

Unknown error for variable getter in extension

I'm trying to add a variable to UIImageView with an extension like this
extension UIImageView {
var urlSession: URLSessionDataTask? {
get {
if self.urlSession != nil {
return self.urlSession
}
return nil
}
set {
urlSession?.cancel()
}
}
}
but I'm getting an unknown error (in console it's just printing (lldb)) for the getter if self.urlSession != nil {. What am I doing wrong?
Because you want to get urlSession property, and you call get, inside get you repeat this action again. You just get infinity loop.
You should use stored property, but extensions may not contain stored properties, so the solution is Subclassing.
Try this code:
import UIKit
class CustomImageView: UIImageView {
var urlSession: URLSessionDataTask? {
willSet {
urlSession?.cancel()
}
}
}
let image = CustomImageView()
image.urlSession = URLSessionDataTask()
As extension does not provide functionality for store property and you have to use SubClass of imageView
However your get and set blocks also have some problem
You are accessing self (urlSession) in get block of self (urlSession), it will create infinite loop,
Please check sample code for same
class MyImageView:UIImageView {
private var dataTask:URLSessionDataTask? = nil
var urlSession: URLSessionDataTask? {
get {
if dataTask != nil {
return dataTask
}
return nil
}
set {
dataTask?.cancel()
}
}
}
Here you need to manage dataTask variable as per get & set are changed urlSession

Static tableView updates only when selecting a row

I have a UITableViewController that contains a static UITableView from which you can request quotes and buy or sell a stock. I handle all communications to the backend in a separate ServerCommunicator class. When a request completes, the ServerCommunicator calls the delegate (the tableViewController) which updates the fields in the tableView.
In the main queue, I call tableView.reloadData and display the fields.
The problem is that the fields display immediately but show stale values. When I select a row by clicking on it, however, value is updated.
What am I doing wrong?
class AddStockTableViewController: UITableViewController, ServerCommunicatorDelegate {
………..
override func viewDidLoad() {
super.viewDidLoad()
nameLabel.hidden = true
symbolLabel.hidden = true
quoteLabel.hidden = true
changeLabel.hidden = true
….….
}
#IBAction func getQuoteAction(sender: UIButton) {
if let symbol = getQuoteTextField.text {
serverCommunicator.updateQuote(symbol, delegate: self)
}
}
func didCompleteRequest(data : NSData)
{
quote = parseQuote(data)
self.symbolLabel.text = "(\(self.quote.symbol))"
self.nameLabel.text = self.quote.name
self.quoteLabel.text = "\(self.quote.currentPrice)"
self.changeLabel.text = “\(self.quote.change)"
…….
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
self.nameLabel.hidden = false
self.symbolLabel.hidden = false
self.quoteLabel.hidden = false
………
}
}
class ServerCommunicator
{
func sendRequest(requestURL : String, requestString : String, delegate : ServerCommunicatorDelegate)
{
if let requestData = requestString.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false){
let request = NSMutableURLRequest(URL: NSURL(string: requestURL)!)
request.HTTPMethod = "POST"
let urlSession = NSURLSession.sharedSession()
let task = urlSession.uploadTaskWithRequest(request, fromData: requestData, completionHandler: {(data, response, error) -> Void in
if error != nil {
println(error.localizedDescription)
}
else {
delegate.didCompleteRequest(data)
}
})
task.resume()
} else {
print("Could not encode requestString to data")
}
}
…….
}
There is no such thing as Static TableView Update. All updates are supposed to occur through a cell change, and such a change shall be triggered by some kind of table view or table view cell refresh.
An update not showing up in a UITableViewCell indicate that the cell is cached and not reloading fresh data.
reloadData in the UITableView triggers reloading all table view cells. Make certain that cellForRowAtIndexPath returns fresh data, and that dequeueReusableCellWithIdentifier uses proper id.
Finally, reloading all cells if you change a single one is not best practice. Is it possible to refresh a single UITableViewCell in a UITableView? Yes it is.

Swift: UIImages appearing out of order in table view

I'm querying images from my Parse backend, and displaying them in order in a UITableView. Although I'm downloading and displaying them one at a time, they're appearing totally out of order in my table view. Each image (album cover) corresponds to a song, so I'm getting incorrect album covers for each song. Would someone be so kind as to point out why they're appearing out of order?
class ProfileCell: UITableViewCell {
#IBOutlet weak var historyAlbum: UIImageView!
}
class ProfileViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var historyAlbums = [PFFile]()
var albumCovers = [UIImage]()
// An observer that reloads the tableView
var imageSet:Bool = false {
didSet {
if imageSet {
// Reload tableView on main thread
dispatch_async(dispatch_get_global_queue(Int(QOS_CLASS_USER_INITIATED.value), 0)) { // 1
dispatch_async(dispatch_get_main_queue()) { // 2
self.tableView.reloadData() // 3
}
}
}
}
}
// An observer for when each image has been downloaded and appended to the albumCovers array. This then calls the imageSet observer to reload tableView.
var dataLoaded:Bool = false {
didSet {
if dataLoaded {
let albumArt = historyAlbums.last!
albumArt.getDataInBackgroundWithBlock({ (imageData, error) -> Void in
if error == nil {
if let imageData = imageData {
let image = UIImage(data: imageData)
self.albumCovers.append(image!)
}
} else {
println(error)
}
self.imageSet = true
})
}
self.imageSet = false
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Queries Parse for each image
var query = PFQuery(className: "Songs")
query.whereKey("user", equalTo: PFUser.currentUser()!.email!)
query.orderByDescending("listenTime")
query.limit = 20
query.findObjectsInBackgroundWithBlock({ (objects, error) -> Void in
if error == nil {
if let objects = objects as? [PFObject] {
for object in objects {
if let albumCover = object["albumCover"] as? PFFile {
// Appending each image to albumCover array to convert from PFFile to UIImage
self.historyAlbums.append(albumCover)
}
self.dataLoaded = true
}
}
} else {
println(error)
}
self.dataLoaded = false
})
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var profileCell = tableView.dequeueReusableCellWithIdentifier("ProfileCell", forIndexPath: indexPath) as! ProfileCell
profileCell.historyAlbum.image = albumCovers[indexPath.row]
return profileCell
}
}
}
The reason you are getting them out of order is that you are firing off background tasks for each one individually.
You get the list of objects all at once in a background thread. That is perfectly fine. Then once you have that you call a method (via didset) to iterate through that list and individually get each in their own background thread. Once each individual thread is finished it adds it's result to the table array. You have no control on when those background threads finish.
I believe parse has a synchronous get method. I'm not sure of the syntax currently. Another option is to see if you can "include" the image file bytes with the initial request, which would make the whole call a single background call.
Another option (probably the best one) is to have another piece of data (a dictionary or the like) that marks a position to each of your image file requests. Then when the individual background gets are finished you know the position that that image is supposed to go to in the final array. Place the downloaded image in the array at the location that the dictionary you created tells you to.
That should solve your asynchronous problems.

Resources