Swift completion handler for Alamofire seemingly not executing - ios

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.

Related

How can I implement protocols in method parameters in swift?

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. :)

Swift 4 accessing completionhandler returned value from class variable

Problem: I’m trying to access the value which is returned from completionHandler, to assign it to a variable which is outside the scope of completionHandler returned method. I can access the variable in the scope of the method, but I can’t from outside. I’ve tried to assign it to the class variable when I access, but didn’t work. Any ideas?
var marketler = [MarketModel]()
var marketAdiArray = [String]()
override func viewDidLoad() {
getMarkets { (marketdizi) in
self.objAryToTableView(markets: marketdizi)
print(self.marketAdiArray) // -> this returns the right array
}
print(self.marketAdiArray) // -> this returns an empty array
}
func getMarkets(completionHandler : #escaping ([MarketModel])->()) {
let uid = "userID(02)"
print("uid : \(uid)")
MobileUserViewModel().getUser(userId: uid, completionHandler: { (user) in
// here returns an user object
self.loginUser = user
MarketViewModel().getMarketFromDb(mobilUser: user, completionHandler: { (marketler) in
print("marketler : \(marketler)")
completionHandler(marketler)
})
})
}
func objAryToTableView(markets : [MarketModel]) {
var ary = [String]()
for m in markets {
ary.append(m.marketName as String!)
}
self.marketAdiArray = ary
}
The code inside the block (completion handler) is getting executed after getMarketFromDb call succeeds. The code which is outside the block is executed just after the previous line where you don't have any data on the array.
If You need to trigger the UI update which updated data from data Db then you need to invoke the UI update from inside the completion block.
getMarkets { (marketdizi) in
self.objAryToTableView(markets: marketdizi)
print(self.marketAdiArray) // -> this returns the right array
self.tableView.reloadData()
}
print(self.marketAdiArray) // -> this returns an empty array

Are there any ways to access a variable modified in an IBAction?

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
}
}

Swift - How to properly remove blocks from an array when a caller is deallocated?

I have an array of 'updateBlocks' (closures) that I use in a singleton class to notify any observers (UIViewControllers, etc) when data updates.
I am wondering what the best way to remove the observer would be so that it is not executed when the observer is deallocated (or no longer wants updates).
Here is my current setup:
MySingleton Class
var updateBlock: (() -> ())? {
didSet {
self.updateBlocks.append(updateBlock!)
self.updateBlock!() // Call immediately to give initial data
}
}
var updateBlocks = [() -> ()]()
func executeUpdateBlocks() {
for block in updateBlocks {
block()
}
}
MyObserver Class
MySingleton.shared.updateBlock = {
...handle updated data...
}
MySingleton.shared.updateBlock = nil // How to properly remove???
Your singleton design has some problems.
Having updateBlock be a variable who's didSet method appends a block to your updateBlocks array is bad design.
I would suggest getting rid of the updateBlock var, and instead defining an addUpdateBlock method and a removeAllUpdateBlocks method:
func addUpdateBlock(_ block () -> ()) {
updateBlocks.append(block)
}
func removeAllUpdateBlocks() {
updateBlocks.removeAll()
}
func executeUpdateBlocks() {
for block in updateBlocks {
block()
}
}
If you want to remove single blocks then you'll need some way to keep track of them. As rmaddy says, you would need some sort of ID for each block. You could refactor your container for your blocks to be a dictionary and use sequential integer keys. When you add a new block, your addBlock function could return the key:
var updateBlocks = [Int: () -> ()]()
var nextBlockID: Int = 0
func addUpdateBlock(_ block () -> ()) -> Int {
updateBlocks[nextBlockID] = block
let result = nextBlockID
nextBlockID += 1
//Return the block ID of the newly added block
return result
}
func removeAllUpdateBlocks() {
updateBlocks.removeAll()
}
func removeBlock(id: Int) -> Bool {
if updateBlocks[id] == nil {
return false
} else {
updateBlocks[id] = nil
return true
}
func executeUpdateBlocks() {
for (_, block) in updateBlocks {
block()
}
If you save your blocks in a dictionary then they won't be executed in any defined order.
That's a very confusing API. From the client's point of view you are setting the value of a single block. But the implementation actually adds that block to an array and then immediately calls that block. And why would you force-unwrap the optional block?
Since you want to support several observers and provide the ability to remove observers, you really show have addBlock and removeBlock methods in your singleton. Then the API and its functionality are clear.
The trick is how to provide an API that lets an observer tell the singleton to remove a specific block. I would model the API after how it is done in the NotificationCenter class where the addBlock method returns some generated token. That token is then passed to the removeBlock method.
The implementation would likely be a dictionary keyed on the token and the value is the block. The token can be a UUID or some other generated, unique opaque value. That makes the addBlock and removeBlock methods simple. Then the executeBlocks method would iterate the values of the dictionary and call those blocks.
Here's one possible implementation:
class UpdateBlocks {
static let shared = UpdateBlocks()
var blocks = [UUID: () -> ()]()
private init() {
}
func addBlock(_ block: #escaping () -> ()) -> Any {
let token = UUID()
blocks[token] = block
return token
}
func removeBlock(_ token: Any) {
if let token = token as? UUID {
blocks[token] = nil
}
}
func executeBlocks() {
for (_, value) in blocks {
value()
}
}
}
let token = UpdateBlocks.shared.addBlock {
print("hello")
}
UpdateBlocks.shared.executeBlocks() // Outputs "hello"
UpdateBlocks.shared.removeBlock(token)
UpdateBlocks.shared.executeBlocks() // No output

Populate array of custom classes from two different network requests

In my Swift iOS project, I am trying to populate an array of custom class objects using JSON data retrieved with Alamofire and parsed with SwiftyJSON. My problem, though, is combining the results of two different network request and then populating a UITableView with the resulting array.
My custom class is implemented:
class teamItem: Printable {
var name: String?
var number: String?
init(sqljson: JSON, nallenjson: JSON, numinjson: Int) {
if let n = sqljson[numinjson, "team_num"].string! as String! {
self.number = n
}
if let name = nallenjson["result",0,"team_name"].string! as String! {
self.name = name
}
}
var description: String {
return "Number: \(number) Name: \(name)"
}
}
Here is my viewDidLoad():
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
refresh() {
() -> Void in
self.tableView(self.tableView, numberOfRowsInSection: self.teamsArr.count)
self.tableView.reloadData()
for item in self.teamsArr {
println(item)
}
return
}
self.tableView.reloadData()
}
which goes to the refresh() method:
func refresh(completionHandler: (() -> Void)) {
populateArray(completionHandler)
}
and finally, populateArray():
func populateArray(completionHandler: (() -> Void)) {
SqlHelper.getData("http://cnidarian1.net16.net/select_team.php", params: ["team_num":"ALL"]) {
(result: NSData) in
let jsonObject : AnyObject! = NSJSONSerialization.JSONObjectWithData(result, options: NSJSONReadingOptions.MutableContainers, error: nil)
let json = JSON(jsonObject)
self.json1 = json
println(json.count)
for var i = 0; i < json.count; ++i {
var teamnum = json[i,"team_num"].string!
NSLog(teamnum)
Alamofire.request(.GET, "http://api.vex.us.nallen.me/get_teams", parameters: ["team": teamnum])
.responseJSON { (req, res, json, err) in
let json = JSON(json!)
self.json2 = json
self.teamsArr.append(teamItem(sqljson: self.json1, nallenjson: self.json2, numinjson: i))
}
}
completionHandler()
}
}
the first problem I had was that i in the for loop reached 3 and caused errors when I thought it really shouldn't because that JSON array only contains 3 entries. My other main problem was that the table view would be empty until I manually triggered reloadData() with a reload button in my UI, and even then there were problems with the data in the tables.
really appreciate any assistance, as I am very new to iOS and Swift and dealing with Alamofire's asynchronous calls really confused me. The code I have been writing has grown so large and generated so many little errors, I thought there would probably be a better way of achieving my goal. Sorry for the long-winded question, and thanks in advance for any responses!
The Alamofire request returns immediately and in parallel executes the closure, which will take some time to complete. Your completion handler is called right after the Alamofire returns, but the data aren't yet available. You need to call it from within the Alamofire closure - this ensures that it is called after the data became available.

Resources