Change a variable within a URLSession callback - swift

I want to check if a file exists and then change the context of a variable.
If I'm working with playgrounds, it will work but coding in a viewport, nothing happens.
I check if the file named "nameoffile.xml" exist, using the statuscode, and then the var color need to change into "blue".
Below you will find my (not working) code.
let url = URL(string: "http://myurl/nameoffile.xml")!
var color = ""
func checkFile()
{
let req = NSMutableURLRequest(url: url)
req.httpMethod = "HEAD"
req.timeoutInterval = 1.0
var response: URLResponse?
var task = URLSession.shared.dataTask(with: url) {(data, response, error) in
if let httpStatus = response as? HTTPURLResponse
{ if httpStatus.statusCode == 200 {
self.kleur = "blue"
}
}
}

From your example it isn't totally clear if the color and url are attributes of the class you are calling checkFile on. I'll assume they are, because it makes the most sense.
So if the colour you want to change is an attribute of UIView.backgroundColor and you want to see the change immediately, you need to change it on a main thread like this:
// Your view controller you are using for the task
class MyViewController: UIViewController {
// The ui element you want to change the colour of
// If you are using a storyboard you would have a #IBOutlet here
let button = UIButton()
func checkFile() {
// I've noticed you aren't using the NSMutableURLRequest anywhere
// Also I've cleaned up your code a bit, hope you don't mind
URLSession.shared.dataTask(with: url) { data, response, error in
guard let httpStatus = response as? HTTPURLResponse else {
return
}
if httpStatus.statusCode == 200 {
DispatchQueue.main.async {
// This line of code will change the color
self.button.backgroundColor = .blue
}
}
}
}
}
Hope this helps. If I haven't guessed your problem, feel free to change your question or leave a comment.

Related

How to download and show a list of images in a collection view so to avoid flickering and images in wrong spots?

I'm struggling to solve the very common problem of loading images inside a collection view given a list of urls. I have implemented a UIImageView extension. In there I defined a cache variable:
static var imageCache = NSCache<NSString, UIImage>()
In addition I have created a loadImage method that takes as input the image cache key, the url itself, a placeholder image and an error image and works as follows:
public func loadImage(cacheKey: String?, urlString: String?, placeholderImage: UIImage?, errorImage: UIImage?) {
image = nil
if let cacheKey = cacheKey, let cachedImage = UIImageView.imageCache.object(forKey: cacheKey as NSString) {
image = cachedImage
return
}
if let placeholder = placeholderImage {
image = placeholder
}
if let urlString = urlString, let url = URL(string: urlString) {
self.downloadImage(url: url, errorImage: errorImage)
} else {
image = errorImage
}
}
The download image function proceeds to create a data task, download the image and assign to the image view:
private func downloadImage(url: URL, errorImage: UIImage?) {
let dataTask = URLSession.shared.dataTask(with: url) { [weak self] (data, response, error ) in
if error != nil {
DispatchQueue.main.async {
self?.image = errorImage
}
return
}
if let statusCode = (response as? HTTPURLResponse)?.statusCode {
if !(200...299).contains(statusCode) {
DispatchQueue.main.async {
self?.image = errorImage
}
return
}
}
if let data = data, let image = UIImage(data: data) {
UIImageView.imageCache.setObject(image, forKey: url.absoluteString as NSString)
DispatchQueue.main.async {
self?.image = image
}
}
}
dataTask.resume()
}
I call the loadImage method inside cellForItemAt, so that I don't have to download all the data at once, but only the images that are effectively displayed on screen. The way I call the function is the following:
cell.albumImage.loadImage(
cacheKey: bestQualityImage,
urlString: bestQualityImage,
placeholderImage: UIImage(named: "Undefined"),
errorImage: UIImage(named: "Undefined")
)
The main problem I face with the current implementation is that sometimes the images are not displayed in the correct spot. In other words, if I have three elements on screen I would sometimes see all three elements with the same image instead of their respective image.
I believe that the problem is that by the time the download is complete for a specific cell, the cellForItemAt for that cell has already ended and the cell gets the wrong image instead.
Is there a way I can modify my function to fix this bug or should I completely change my approach?
EDIT.
At the beginning I thought the problem was that I was using an extension and I tried to use a function with a closure returning the downloaded image but that didn't solve my problem.

Why my DateTask code block does not work?

I create a request to the server, and in the end I expect to receive data, which I then transform into a model using a function, for this I created a session
func fetchNewsData(forCoutry country: String, category: String, complition: #escaping (NewsDataModel) -> ()) {
let urlString = "some url string"
guard let url = URL(string: urlString) else { return }
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { data, response, error in
print ("ERROR: \(error)")
guard let data = data else { return }
guard let newsData = self.parseJSON(withData: data) else { return }
complition(newsData)
}
task.resume()
}
but the following code just doesn't work
print ("ERROR: \(error)")
guard let data = data else { return }
guard let newsData = self.parseJSON(withData: data) else { return }
complition(newsData)
I used breakpoints to find out until what point everything is going well, and I realized that this particular block of code is not working.
when I set a breakpoint between the let session and the let task, the code stopped there, but when I set my code to an print(error), this breakpoint did not work
I used the function fetchNewsData in viewDidLoad and I want to work to fill the array with elements that I expect to receive from the data that will come on this request, but my array does not receive any elements, and it remains empty, because of this my application does not work
why part of the code doesn't work, and how can I get the data I need from it?
The problem turned out to be a poor understanding of closures
I was not calling my method correctly to get the data. Having figured it out, I realized that the problem is precisely in a different approach when calling this method

Refer to variable from do block in catch

I need to access a variable inside a do statement. Will it behave like the if-else statement in the sense that you con't use variables outside of the if statement?
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
override func loadView() {
do {
//let url = URL?("https://www.hackingwithswift.com")
let TECIONEXContent = try String(contentsOf: URL("https://www.hackingwithswift.com"))
} catch { print("error")}
//I need to access TECIONEXContent variable outside the do statement
// Error: Use of unresolved identifier 'TECIONEXContent'
var TECGrid = TECIONEXContent.components(separatedBy: "\n")
}
}
The error is on the last line, 'unresolved identifier'.
Will it behave like the if-else statement in the sense that you con't use variables outside of the if statement?
Yes. But just like an if-else statement, you can define the variable before the do-catch:
E.g. in an if-else statement:
let foo: String
if bar > 1 {
foo = "bigger than one"
} else {
foo = "one or smaller"
}
Or, in your case:
let url = URL(string: "https://www.hackingwithswift.com")!
let contents: String
do {
contents = try String(contentsOf: url)
} catch {
print(error)
return
}
let grid = contents.components(separatedBy: "\n")
Or, you aren’t really doing anything with the error message, you can eliminate the do-catch altogether:
guard let contents = try? String(contentsOf: url) else {
print("error")
return
}
let grid = contents.components(separatedBy: "\n")
Frankly, all of that having been said, using String(contentsOf:) is probably not the best pattern, anyway, because that performs a synchronous network request, which risks having the OS “watchdog” process kill your app unceremoniously if the main thread is blocked; and even if that doesn’t happen, it’s not a good user experience to freeze the app while the network request is in progress. Usually we’d use URLSession:
let url = URL(string: "https://www.hackingwithswift.com")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard
let data = data,
let httpResponse = response as? HTTPURLResponse,
let string = String(data: data, encoding: .utf8) else {
print(error ?? "Unknown error")
return
}
guard 200 ..< 300 ~= httpResponse.statusCode else {
print("Expected 2xx response, but got \(httpResponse.statusCode)")
return
}
let grid = string.components(separatedBy: "\n")
DispatchQueue.main.async {
// use `grid` here
}
}.resume()
Unrelated, but:
The convention is to start variable names with lowercase letters.
You implemented loadView. It’s rare that we do that, and instead we implement viewDidLoad, making sure to call super.viewDidLoad(), too.
If you’re doing this in a playground, you’d obviously also set needsIndefiniteExecution, if you haven’t already.
Thus:
import UIKit
import PlaygroundSupport
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
performRequest()
}
func performRequest() {
let url = URL(string: "https://www.hackingwithswift.com")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard
let data = data,
let httpResponse = response as? HTTPURLResponse,
let string = String(data: data, encoding: .utf8) else {
print(error ?? "Unknown error")
return
}
guard 200 ..< 300 ~= httpResponse.statusCode else {
print("Expected 2xx response, but got \(httpResponse.statusCode)")
return
}
let grid = string.components(separatedBy: "\n")
DispatchQueue.main.async {
print(grid)
// use `grid` here
}
}.resume()
}
}
PlaygroundPage.current.liveView = ViewController()
PlaygroundPage.current.needsIndefiniteExecution = true
In your code are four(!) very bad practices.
Never load data synchronously with API like String(contentsOf from a remote URL. Use asynchronous API like URLSession.
Never print a meaningless literal string in a catch block. Print the error instance.
According to the naming convention variable names should be lowerCamelCased.
In a do - catch block put always all good code in the do scope. This solves your issue.
do {
let tecionexContent = try String(contentsOf: URL("https://www.hackingwithswift.com")!)
let tecGrid = tecionexContent.components(separatedBy: "\n")
} catch { print(error) }
recommended
URLSession.shared.dataTask(with: URL("https://www.hackingwithswift.com")!) { data, _ , error in
if let error = error { print(error); return }
let tecionexContent = String(data: data!, encoding: .utf8)!
let tecGrid = tecionexContent.components(separatedBy: "\n")
}.resume()
The Issue
The problem with your code is that you are defining the variable inside a block/closure, which is a local scope. Your variable should be at a scope where both the blocks are able to see it. This means that in the catch block, the variable does not exist. Specifically you had attempted to reference the string variable you called TECIONEXContent from outside of the closure in which it was instantiated.
On some general style points: please stick with the swift naming convention for variables (i.e. camel case) whereas your classes etc should be capitalized. (For the purposes of running the below code in the playground I've used an arbitrary function name, but you could employ it from your lifecycle methods).
Basic Playground Demo Code
import UIKit
import PlaygroundSupport
class MyViewController : UIViewController {
private var tecGrid: [String]? {
didSet {
// Update some UI Here (insuring your on the main thread)
print(self.tecGrid)
}
}
func test() {
do {
var texionicContent = try String(contentsOf:URL(string: "https://www.hackingwithswift.com")!)
tecGrid = texionicContent.components(separatedBy: "\n")
}
catch let error {
print("Catch the error")
}
}
}
let play = MyViewController(nibName: nil, bundle: nil)
play.test()

How to update uilabel text in response swift 4

I try to update label.text with server response. But label is updated only after second call of method. What is wrong?
dispatch_async(dispatch_get_main_queue()) is deprecated. I changed it to DispatchQueue.main.async, but it does not work.
Example for swift 2:
Swift changing text of a label after downloading content of a web page
Not working code:
#IBAction func buttonAction(_ sender: Any) {
let url = URL(string: "https://api.nasa.gov/planetary/apod?date=2005-2-22&api_key=DEMO_KEY")!
let task = URLSession.shared.dataTask(with: url) { [weak self] (data, response, error) in
if let data = data,
let stringData = String(data: data, encoding: .utf8) {
print("stringData: \(stringData)") // prints every time
DispatchQueue.main.async {
// change label text after second calling.
// ??? How to change label text at first calling ???
self?.mainLabel.text = stringData
}
}
}
task.resume()
}
Try this
var labelString : String = "" {
didSet {
self.mainLabel.text = labelString
}
}
Write below code inside buttonAction
DispatchQueue.main.async {
// change label text after second calling.
// ??? How to change label text at first calling ???
self?.labelString = stringData
}

My UILabel isn't updating after I change the text value

This is in my viewDidLoad method
http://i.imgur.com/zTD7wHr.png
This is my first post so stackoverflow won't let me post images I guess I'll just have to drop this link.
Basically, I'm trying to assign text from a json located here : http://catfacts-api.appspot.com/api/facts using the code above. Ive gone through the values in the Xcode with a breakpoint at the self.catLabel.text = catFactArray[0] as? String line, and the catLabel.text string has the value I want it to, but the label does not update. Ive already gone over this with a few people and I'm really not sure where the problem is so any help would be appreciated.
If you are doing any network operation in background and want to update the UI, those UI update should be done on main thread.
Try it.
dispatch_async(dispatch_get_main_queue(), {
self.catLabel.text = catFactArray[0] as? String
self.view.setNeedsDisplay()
self.catLabel.setNeedsDisplay()
})
Your UI layout updates (label text changes, frame modifications..) must be to the main thread, in your code you make a network call that it's probably launched in a background thread:
let url = NSURL(string: "http://catfacts-api.appspot.com/api/facts")
let request = NSURLRequest(URL: url!)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request, completionHandler : { data, response, error in
if let feed = (try? NSJSONSerialization.JSONObjectWithData(data!, options: .MutableContainers)) as! NSDictionary! {
if let catFactArray = feed.valueForKeyPath("facts") as! NSArray! {
if !NSThread.isMainThread {
print("Yes , I'm not in the main thread..")
}
dispatch_async(dispatch_get_main_queue(), {
print("Now I'm in the main thread..")
self.catLabel.text = catFactArray[0] as? String
self.view.setNeedsDisplay()
self.catLabel.setNeedsDisplay()
self.catLabel.layoutIfNeeded()
}
}
}
})
task.resume()