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.
Related
I'd like to implement protocol methods in the other method parameters.
First, I defined a protocol containing a method,
protocol MyProtocol {
func myProtocolFunc();
}
and I made a method taking the protocol as a parameter.
func myFunc(myProtocol : MyProtocol) {
. . .
}
and when I using this method, I want to override protocolFunc().
myFunc( . . . )
Where should I override protocolFunc() in my myFunc() method?
p.s. In Kotlin, I made that by doing like this.
interface MyProtocol {
fun myProtocolFunc();
}
fun myFunc(myProtocol : MyProtocol) {
. . .
}
myFunc(object : MyProtocol {
override fun myProtocolFunc() {
. . .
}
})
I want to do same thing in swift code.
========================================
Edit:
Actually, I'm planning to make Http Request class.
After getting some data from web server, I'd like to do some work in ViewController class.
Because Http Request runs on thread, while fetching some data from web server, next code regarding the data should wait.
Here is my Http Request class,
class HttpConnector {
static let basicURL = "http://******"
static func getData(url : String, parameters : String, listener : UIModifyAvailableListener) {
if let fullURL = URL(string : "\(basicURL)\(url)") {
var request = URLRequest(url : fullURL)
request.httpMethod = "POST"
request.httpBody = parameters.data(using: .utf8)
let task = URLSession.shared.dataTask(with: request, completionHandler: { data, response, error in
guard let data = data, error == nil else {
print("error = \(error!)")
return
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(response!)")
}
if let result = String(data: data, encoding: .utf8) {
listener.taskCompleted(result: result)
}
})
task.resume()
}
}
}
protocol UIModifyAvailableListener {
func taskCompleted(result : String)
}
and this class might be called in ViewController like this
HttpConnector.getData("my_url", "my_parameter", [[some codes regarding UIModifyAvailableListener protocol]])
If it can't be done in swift, I want to get some alternatives.
In your protocol, change the function to a variable of a function.
protocol UIModifyAvailableListener {
var taskCompleted : ((result : String) -> Void)? {get set}
}
Then in your HttpConnector class that implements UIModifyAvailableListener, add this:
var taskCompleted : ((result : String) -> Void)?
Within HttpConnector class's method(s), you can call taskCompleted like so:
self.taskCompleted?(result)
Then in the calling code that wants to be called back with taskCompleted(), simply set the var:
myHttpConnector.taskCompleted =
{
print("Done!") // note if you want to reference self here, you'll probably want to use weakSelf/strongSelf to avoid a memory leak.
}
BTW, this is an important pattern if you're doing MVVM, so that your ViewModel can call back into the ViewController. Since the ViewModel would never have a reference to its ViewController, all it can do is have callback properties that the ViewController can set with the closure blocks of code it wants called. And by using a protocol the ViewModel can be mocked when doing unit tests against the ViewController. :)
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'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
I created a class as shown in the code below, and as u can see I am parsing a JSON file in the class outside the viewController.
When I create the AllCards object in the view controller obviously return 0 at the beginning but after a while it returns the correct number of cards.
here my questions:
1) How can I wait the object creation before the viewDidLoad so at the view did load the AllCard object will return the correct number of cards?
2) If I add a button in the viewController updating the number of cards it freezes until all the cards have been created. I think because in my code everything is in the main queue. How can I resolve that?
3) Is it a good practice parsing JSON in a separate class like I did?
AllCards class:
import Foundation
import Alamofire
import SwiftyJSON
class AllCards {
var allCard = [Card]()
let dispatchGroup = DispatchGroup()
//gzt the JSON with Alamofire request
let allCardsHTTP: String = "https://omgvamp-hearthstone-v1.p.mashape.com/cards?mashape"
init() {
dispatchGroup.enter()
Alamofire.request(allCardsHTTP, method: .get).responseJSON { (response) in
if response.result.isSuccess {
let jsonCards : JSON = JSON(response.value!)
print("success")
//create the cards
if jsonCards["messagge"].stringValue != "" {
print(jsonCards["message"].stringValue)
}
else {
for (set, value) in jsonCards {
if jsonCards[set].count != 0 {
for i in 0...jsonCards[set].count - 1 {
let card = Card(id: jsonCards[set][i]["cardId"].stringValue, name: jsonCards[set][i]["name"].stringValue, cardSet: set, type: jsonCards[set][i]["type"].stringValue, faction: jsonCards[set][i]["faction"].stringValue, rarity: jsonCards[set][i]["rarity"].stringValue, cost: jsonCards[set][i]["cost"].intValue, attack: jsonCards[set][i]["attack"].intValue, durability: jsonCards[set][i]["durability"].intValue, text: jsonCards[set][i]["text"].stringValue, flavor: jsonCards[set][i]["flavor"].stringValue, artist: jsonCards[set][i]["artist"].stringValue, health: jsonCards[set][i]["health"].intValue, collectible: jsonCards[set][i]["collectible"].boolValue, playerClass: jsonCards[set][i]["playerClass"].stringValue, howToGet: jsonCards[set][i]["howToGet"].stringValue, howToGetGold: jsonCards[set][i]["howToGetGold"].stringValue, mechanics: [""], img: jsonCards[set][i]["img"].stringValue, imgGold: jsonCards[set][i]["imgGold"].stringValue, race: jsonCards[set][i]["race"].stringValue, elite: jsonCards[set][i]["elite"].boolValue, locale: jsonCards[set][i]["locale"].stringValue)
if jsonCards[set][i]["mechanics"].count > 0 {
for n in 0...jsonCards[set][i]["mechanics"].count - 1 {
card.mechanics.append(jsonCards[set][i]["mechanics"][n]["name"].stringValue)
}
}
else {
card.mechanics.append("")
}
self.allCard.append(card)
}
}
else {
print("The set \(set) has no cards")
}
}
print(self.allCard.count)
}
}
else {
print("No network")
}
self.dispatchGroup.leave()
}
}
}
View Controller:
import UIKit
class ViewController: UIViewController {
let allcards = AllCards()
let mygroup = DispatchGroup()
#IBAction func updateBtn(_ sender: Any) {
print(allcards.allCard.count) //Button is frozen until all the cards have been created then it shows the correct number of cards
}
override func viewDidLoad() {
super.viewDidLoad()
print(allcards.allCard.count) / This returns 0
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Here is an example of completion handler.
First you have to write a function in a single class ex: APICall
func getDataFromJson(allCardsHTTP: String, completion: #escaping (_ success: Any) -> Void) {
Alamofire.request(allCardsHTTP, method: .get).responseJSON { response in
if response.result.isSuccess {
completion(response)
}
}
}
and call this method from any class.
let callApi = APICall()
callApi.getDataFromJson(allCardsHTTP: "https://omgvamp-hearthstone-v1.p.mashape.com/cards?mashape",completion: { response in
print(response)
})
1) if you pass an object via a UIStoryboard segue, it's set before viewDidLoad() is called. However if you want to wait for UI elements to be ready, I generally go for a didSet on the UI element, you could add a guardstatement checking your object in there if you want.
2) first of all you'll probably want a closure so maybe read 3) first. you're using dispatchGroup.enter() here, DispatchQueue.global.async { } is the usual way to accomplish what you're doing. Add in a DispatchQueue.main.async { } when done if you want, or dip into the main thread in the view controller, up to you really. Look into the differences between [unowned self]
and [weak self] when you have the time.
3) Give your Card object an init(from: JSON) initialiser where it parses its properties from the JSON object you're passing it.
Let the function responsible for the download (Alamofire in your case) live in a different class (like for example APIClient) and give it a download function with a closure in the argument list like completion: ((JSON?) -> ())? to return the JSON object. Let that class download a JSON object and then initialise your Cardobject with the init(from: JSON) initializer you wrote earlier. Note that this isn't an approach fit for use with Core Data NSManagedObjects so if you'll need local storage maybe keep that in mind.
In the end you should be able to build an array of Cards something like this:
APIClient.shared.fetchCards(completion: { cardJSONs in
let cards = [Card]()
for cardJSON: JSON in cardJSONs {
let card = Card(from; JSON)
cards.append(card)
}
}