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
Related
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)
When I create a recursion in a didSet actually I turned out i could just put a return and program exits from didSet . But i did not find anywhere (i was searching for a long time) that i can put a return word to exit from didSet. So, is didSet works like a computed property where we can return value? Please if someone know anything i would very appreciated. Thank you.
class AppDetailController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
var app : App? {
didSet {
if app?.screenshots != nil {
return
}
if let id = app?.id {
let urlString = "https://api.letsbuildthatapp.com/appstore/appdetail?id=\(id)"
URLSession.shared.dataTask(with: URL(string: urlString)!, completionHandler: { (data, response, error) in
if error != nil {
print(error)
}
do {
let json = try(JSONSerialization.jsonObject(with: data!, options: .mutableContainers ))
let appDetail = App()
appDetail.setValuesForKeys(json as! [String: AnyObject])
self.app = appDetail
} catch let error as NSError {
print(error.localizedDescription)
}
}).resume()
}
}
}
No, didSet does not have any relation to computed property. Actually didSet {} and willSet are for stored properties, and they are play the role of didChangeValue and willChangeValue. So you can change other data in your class or struct, or run some method in according to changing the value of the property that you want to define didSet and willSet for it.
They're working like a function and have a property. newValue for willSet that gives you new value will set to your property and oldValue for didSet that gives you the old value for your property before changing its value. You can change the name of these properties like this:
willSet(myNewValue) { }
didSet(myOldValue) { }
didSet is a property observer, and it is called every time a new value is stored in a stored property.
Consider didSet like a method returning void.
You check for a condition in your code, if met, you may return so that the rest of the code may not be executed.
The thing in curly braces after the term willSet or didSet is a function body. The way you exit from a function body is by saying return.
Here is what I am trying to do:
var usernameCheckerResponse : String = ""
//This IBAction is a UITextfield that sends post request when editing is finshed.
#IBAction func usernameChecker(_ sender: Any) {
// perform post request with URLSession
// post request returns url response from URLSession
// the value of this response is either 'usernameExists' or 'usernameAvailable'
// usernameCheckerResponse = String(describing : response)
}
//use modified usernameCheckerResponse variable outside the IBAction function.
//For example like this:
func UsernameExists () -> Bool {
if(usernameCheckerResponse == "usernameExists"){
return true
} else { return false }
}
I am aware that an IBAction will only return a void, so is there anyway around this problem?
Any help and/or advice will be greatly appreciated.
Yes absolutely. Here is an example,
var usernameCheckerResponse : String = ""
//This IBAction is a UITextfield that sends post request when editing is finshed.
#IBAction func usernameChecker(_ sender: Any) {
//post request
// post request returns url response
// usernameCheckerResponse = String(describing : response)
}
//use modified usernameCheckerResponse variable outside the IBAction function.
func accessVariable() {
print("\(usernameCheckerResponse")
}
Keep in mind that the trick here is to access the variable when it has changed. To do that you need to pick some sort of way to keep track of that. Delegation is probably the most standard way to do that. See this. You would have to be more specific as to why you want the variable changed, because I would need to know what is using it (delegation required that you have are very specific on who is participating).
I would like to also be more specific with how delegation works. You would specify when the 'accessVariable()' function is called, in the place where you want the modified variable (this would always be between two different classes or structures). Keep in mind that you do not need to use delegation if you are just trying to share the variable in the same class. Calling the function 'accessVariable()' will suffice. However if this is the case where you want something to happen in the same class, but you really want to control in what order the functions finish then you need to use callbacks.
BTW Leo, doing it that way will make the app crash...
In general, you should think of IBAction functions as
connection points for controls like buttons etc.
You would never call it yourself.
If you need to do that, make another function
and have the IBAction function call that.
Since you are using URLSession to fetch the data from an external
source, you will need to be aware that this does not happen synchronously.
Send the call to your API and have the completion handler get called
when data is returned.
All of this code goes into your ViewController
// Set up a reusable session with appropriate timeouts
internal static var session: URLSession {
let sessionConfig = URLSessionConfiguration.default
sessionConfig.timeoutIntervalForRequest = 6.0
sessionConfig.timeoutIntervalForResource = 18.0
return URLSession( configuration: sessionConfig )
}
// Create an httpPost function with a completion handler
// Completion handler takes :
// success: Bool true/false if things worked or did not work
// value: String string value returned or "" for failures
// error: Error? the error object if there was one else nil
func httpPost(_ apiPath: String, params: [String: String], completion:#escaping (Bool, String, Error?) -> Void) {
// Create POST request
if let requestURL = URL( string: apiPath ) {
print("requestUrl \(apiPath)")
// Create POST request
var request = URLRequest( url: requestURL )
request.httpMethod = "POST"
var postVars : [String : String ] = params
var postString = postVars.toHttpArgString()
request.httpBody = postString.data( using: String.Encoding.utf8, allowLossyConversion: true )
let sendTask = ViewController.session.dataTask( with: request) {
(data, response, error) in
if let nserror = error as NSError? {
// There was an error
// Log it or whatever
completion(false, "", error)
return
}
// Here you handle getting data into a suitable format
let resultString = "whatever you got from api call"
// Send it back to the completion block
completion(true, resultString, nil)
}
sendTask.resume()
}
}
// I assume you have a text field with the user name you want to try
#IBOutlet weak var usernameToCheck : UITextField!
#IBAction func usernameChecker(_ sender: Any) {
guard let username = usernameToCheck.text else {
// This is unlikely to happen but just in case.
return
}
httpPost("https://someapicall", params: ["username" : username] ) {
(success, value, error) in
// This code gets called when the http request returns data.
// This does not happen on the main thread.
if success {
if value == "usernameExists" {
// User name already exists. Choose a different one.
DispatchQueue.main.async {
// put code here if you need to do anything to the UI, like alerts, screen transitions etc.
}
}
else if value == "usernameAvailable" {
// You can use this user name
DispatchQueue.main.async {
// put code here if you need to do anything to the UI, like alerts, screen transitions etc.
}
}
else {
// Unexpected response from server
}
}
else {
// Something did not work
// alert "Unable to connect to server"
}
}
}
To make this code work you will need this:
// Syntatic sugar to convert [String:String] to http arg string
protocol ArgType {}
extension String: ArgType {}
extension Dictionary where Key: ArgType, Value: ArgType {
// Implement using a loop
func toHttpArgString() -> String {
var r = String()
for (n, v) in self {
if !r.isEmpty { r += "&" }
r += "\(n)=\(v)"
}
return r
}
}
I want to use my custom delegate methods in Alamofire's response callback, like below:
func startDownloadSegment() {
let destination: DownloadRequest.DownloadFileDestination = { _, _ in
let filePath = self.generateFilePath()
return (filePath, [.createIntermediateDirectories])
}
// 1
print(self.delegate)
Alamofire.download(downloadURL, to: destination).response { response in
// 2
print(self.delegate)
if response.error == nil {
self.delegate?.segmentDownloadSucceeded(with: self)
} else {
self.delegate?.segmentDownloadFailed(with: self)
}
}
}
As you can see, No.1 print(self.delegate) returns the delegator I set. But No.2 always returns nil so delegate method like downloadSucceeded(with:) cannot be called.
Thank you.
I find the problem. The problem is I set the delegate as
weak var delegate
But as in Alamofire response callback, I should omit 'weak' keyword to get it done.
I have the following function in a class in my program:
func getXMLForTrips(atStop: String, forRoute: Int, completionHandler: #escaping (String) -> Void) {
let params = [api key, forRoute, atStop]
Alamofire.request(apiURL, parameters: params).responseString { response in
if let xmlData = response.result.value {
completionHandler(xmlData)
} else {
completionHandler("Error")
}
}
}
In the init() for the class, I attempt to call it like this:
getXMLForTrips(atStop: stop, forRoute: route) { xmlData in
self.XMLString = xmlData
}
This compiles without errors, but after init() is executed, my class's self.XMLString is still nil (shown both by the Xcode debugger and by my program crashing due to the nil value later on). I see no reason why this shouldn't work. Can anyone see what I am missing?
You shouldn't be making internet calls in the initializer of a class. You will reach the return of the init method before you go into the completion of your internet call, which means it is possible that the class will be initialized with a nil value for the variable you are trying to set.
Preferably, you would have another class such as an API Client or Data Source or View Controller with those methods in it. I am not sure what your class with the init() method is called, but lets say it is called Trips.
class Trips: NSObject {
var xmlString: String
init(withString xml: String) {
xmlString = xml
}
}
Then one option is to put the other code in whatever class you are referencing this object in.
I'm gonna use a view controller as an example because I don't really know what you are working with since you only showed two methods.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//setting some fake variables as an example
let stop = "Stop"
let route = 3
//just going to put that method call here for now
getXMLForTrips(atStop: stop, forRoute: route) { xmlData in
//initialize Trip object with our response string
let trip = Trip(withString: xmlData)
}
}
func getXMLForTrips(atStop: String, forRoute: Int, completionHandler: #escaping (String) -> Void) {
let params = [api key, forRoute, atStop]
Alamofire.request(apiURL, parameters: params).responseString { response in
if let xmlData = response.result.value {
completionHandler(xmlData)
} else {
completionHandler("Error")
}
}
}
}
If you want to be able to initialize the class without requiring setting the xmlString variable, you can still do the same thing.
Change the Trips class init() method to whatever you need it to be and set var xmlString = "" or make it optional: var xmlString: String?.
Initialize the class wherever you need it initialized, then in the completion of getXMLForTrips, do trip.xmlString = xmlData.