I have done some reading and there was recommendation in a similar post (Swift closure with Alamofire) and tried to do the same to my code but I can't find the way to call the function now?
I get an error of: Cannot convert the expression's type '(response: #lvalue String)' to type '((response: String) -> ()) -> ()'
import UIKit
class myClass101: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
var api_error: String = ""
activityInd.startAnimating()
call_api_function(response: api_error)
activityInd.stopAnimating()
if (api_error != "") {
let alertController = UIAlertController(title: "Server Alert", message: "Could not connect to API!", preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.Default,handler: nil))
self.presentViewController(alertController, animated: true, completion: nil)
}
}
}
the function is as follows:
func call_api_function(completion: (response: String) -> ()) {
let api_url_path: String = "http://\(str_api_server_ip):\(str_api_server_port)"
let api_url = NSURL(string: api_url_path + "/devices.xml")!
Alamofire.request(.GET, api_url)
.authenticate(user: str_api_username, password: str_api_password)
.validate(statusCode: 200..<300)
.response { (request, response, data, error) in
var senderror: String = error!.localizedDescription
completion(response: senderror )
if (error != nil) {
println(senderror)
}
// do other tasks here
}
}
Thanks!
Kostas
Given your definition of call_api_function, you would call it like so:
call_api_function() { response in
activityInd.stopAnimating()
// now use `response` here
}
I'd suggest you do a little research on trailing closures in the The Swift Programming Language: Closures.
But, having said that, your call_api_function has problems of its own.
You're doing a forced unwrapping of the error optional. What if there was no error? Then, the forced unwrapping of the nil optional would fail and the code would crash.
If the request succeeded, you're not doing anything with the data that is returned. Presumably you did the request because you wanted to do something with the returned data.
Unfortunately, you don't provide information about the nature of the XML response you're expecting, but presumably you would instantiate a NSXMLParser instance to parse it and then implement the NSXMLParserDelegate methods and call the parse method.
Following up on the prior points, rather than a closure with a single non-optional parameter, I'd expect to see a closure with two optional parameters, an optional with the parsed data (which would be set if the request and parsing was successful) and an optional with a NSError (which would be set only if there was an error).
A very minor point, but you might want to adopt a Cocoa naming conventions (e.g. camelCase convention of callApiFunction).
Related
Im making a very basic app which has a search field to get data that is passed to a tableview.
What I want to do is run an Async task to get the data and if the data is succesfully fetched go to the next view, during the loading the screen must not freeze thats why the async part is needed.
When the user pressed the searchbutton I run the following code to get data in my
override func shouldPerformSegueWithIdentifier(identifier: String, sender: AnyObject?) -> Bool {
method.
var valid = true
let searchValue = searchField.text
let session = NSURLSession.sharedSession()
let url = NSURL(string: "https://someapi.com/search?query=" + searchValue!)
let task = session.dataTaskWithURL(url!, completionHandler: {(data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in
if let theData = data {
dispatch_async(dispatch_get_main_queue(), {
//for the example a print is enough, later this will be replaced with a json parser
print(NSString(data: theData, encoding: NSUTF8StringEncoding) as! String)
})
}
else
{
valid = false;
print("something went wrong");
}
})
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
task.resume()
return valid;
I removed some code that checks for connection/changes texts to show the app is loading data to make the code more readable.
This is the part where I have the problem, it comes after all the checks and im sure at this part I have connection etc.
What happens is it returns true (I can see this because the view is loaded), but also logs "Something went wrong" from the else statement.
I Understand this is because the return valid (at the last line) returns valid before valid is set to false.
How can I only return true (which changes the view) if the data is succesfully fetched and dont show the next view if something went wrong?
Because you want the data fetching to be async you cannot return a value, because returning a value is sync (the current thread has to wait until the function returns and then use the value). What you want instead is to use a callback. So when the data is fetched you can do an action. For this you could use closures so your method would be:
func shouldPerformSegue(identifier: String, sender: AnyObject?, completion:(success:Bool) -> ());
And just call completion(true) or completion(false) in your session.dataTaskWithURL block depending on if it was successful or not, and when you call your function you give a block for completion in which you can perform the segue or not based on the success parameter. This means you cannot override that method to do what you need, you must implement your own mechanism.
In XCTest with swift you can define mock objects in the test function you need it in. Like so
func testFunction(){
class mockClass: AnyObject{
func aFunction(){
}
}
}
I'm trying to use these mock objects to test that another function sends out the correct notification given a certain condition (in my case that the success notification is broadcast with a 204 status code to an object.
The problem I am having is that i get an "Unrecognised selector" runtime error even though the deletedSuccess() function is clearly there/
Heres some code dump
func testDelete(){
let expectation = expectationWithDescription("Post was deleted")
class MockReciever : AnyObject {
func deleteSuccess(){
println("delete successfull")
expectation.fulfill()
}
}
let mockReciever = MockReciever()
NSNotificationCenter.defaultCenter().addObserver(mockReciever, selector: "deleteSuccess", name: PostDeletedNotification, object: post)
let response = NSHTTPURLResponse(URL: NSURL(), statusCode: 204, HTTPVersion: nil, headerFields: nil)
let request = NSURLRequest()
post.deleteCompletion(request, response: response, data: nil, error: nil)
waitForExpectationsWithTimeout(30, handler: { (error) -> Void in
if error != nil{
XCTFail("Did not recieve success notification")
} else {
XCTAssertTrue(true, "post deleted successfully")
}
NSNotificationCenter.defaultCenter().removeObserver(mockReciever)
})
}
Is there some sort of problem with using mock objects and selectors like this that I don't know about?
You don't have to implement mock objects to test notifications. There is a -[XCTestCase expectationForNotification:object:handler:] method.
And here's an answer on how to receive notifications from NSNotificationCenter in Swift classes that do not inherit from NSObject. Technically it's a duplicate question.
I am needing to implement a progress bar that takes into account a couple of factors.
I have three different classes, my ViewController, a Networking class to handle the network calls and a dataManager class to handle all the db operations.
Now my progressView lives in my viewcontroller and I am looking at a way of updating it as each of the different operations are performed in the other classes.
I am using Alamofire so I know I can use .progress{} to catch the value of the JSON progress but that would also mean exposing the ViewController to the Networking class, which I assume is bad practice?
I think this should be achieved using completion handlers but as I have already setup another thread for handling the JSON / DB operation I'm not wanting to over complicate it anymore than I need to
Networking:
func makeGetRequest(url : String, params : [String : String]?, completionHandler: (responseObject: JSON?, error: NSError?) -> ()) -> Request? {
return Alamofire.request(.GET, url, parameters: params, encoding: .URL)
.progress { _, _, _ in
//bad practice?
progressView.setProgress(request.progress.fractionCompleted, animated: true)
}
.responseJSON { request, response, data, error in completionHandler(
responseObject:
{
let json = JSON(data!)
if let anError = error
{
println(error)
}
else if let data: AnyObject = data
{
let json = JSON(data)
}
return json
}(),
error: error
)
}
}
ViewController:
dataManager.loadData({(finished: Bool, error:NSError?) -> Void in
if let errorMessage = error{
self.syncProgress.setProgress(0, animated: true)
let alertController = UIAlertController(title: "Network Error", message:
errorMessage.localizedDescription, preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.Default,handler: nil))
self.presentViewController(alertController, animated: true, completion: nil)
}
if finished{
for i in 0..<100 {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), {
sleep(1)
dispatch_async(dispatch_get_main_queue(), {
self.counter++
return
})
})
}
}
})
As you can see I am waiting on the finished boolean in the datamanger class to be set before updating the progress bar. The thing is, dataManager makes a call to networking and performs a bunch of other stuff before it finishes, it would be handy to update the progress bar along the way but I'm not sure of the best approach?
DataManager:
func loadData(completion: (finished: Bool, error: NSError?) -> Void) {
var jsonError: NSError?
networking.makeGetRequest(jobsUrl, params: nil) { json, networkError in
//....
}
I'm not too familiar with swift so I can't give you a code example but the way I would do this is create a protocol on your Networking class NetworkingDelegate and implement that protocol in your ViewController. The protocol method would be something like (in objective-c) NetworkingRequestDidUpdatProgress:(float progress)
This is assuming your ViewController calls Networking.makeGetRequest. If it's another class, you would implement the delegate in that class, or you could bubble up the delegate calls to your ViewController through the DataManager class.
I have been trying to integrate a Pinboard bookmarks view (by parsing an RSS Feed and displaying it in a UITableView) in my browser app, and I'm encountering a few issues. You can see my previous questions here and here. I'm trying to use the variables I collected from my user in the Alert, to get the user's secret RSS feed token. However when I run the app, it causes a crash with a error of "EXC_BAD_INSTRUCTION" with the following console output:
fatal error: unexpectedly found nil while unwrapping an Optional value
I checked everything but I can't find a nil.
This is the code I'm using:
class SettingsTableViewController: UITableViewController, MFMailComposeViewControllerDelegate, NSXMLParserDelegate {
var _pinboardUsername: String?
var _pinboardAPIToken: String?
var parser: NSXMLParser = NSXMLParser()
var bookmarks: [Bookmark] = []
var pinboardSecret: String = String()
var eName: String = String()
...
#IBAction func pinboardUserDetailsRequestAlert(sender: AnyObject) {
//Create the AlertController
var pinboardUsernameField :UITextField?
var pinboardAPITokenField :UITextField?
let pinboardUserDetailsSheetController: UIAlertController = UIAlertController(title: "Pinboard Details", message: "Please enter your Pinboard Username and API Token to access your bookmarks", preferredStyle: .Alert)
//Add a text field
pinboardUserDetailsSheetController.addTextFieldWithConfigurationHandler({(usernameField: UITextField!) in
usernameField.placeholder = "Username"
usernameField.text = self._pinboardUsername
var parent = self.presentingViewController as! ViewController
pinboardUsernameField = usernameField
})
pinboardUserDetailsSheetController.addTextFieldWithConfigurationHandler({(apiTokenField: UITextField!) in
apiTokenField.placeholder = "API Token"
apiTokenField.text = self._pinboardAPIToken
var parent = self.presentingViewController as! ViewController
pinboardAPITokenField = apiTokenField
})
pinboardUserDetailsSheetController.addAction(UIAlertAction(title: "Cancel", style: .Cancel, handler: nil))
pinboardUserDetailsSheetController.addAction(UIAlertAction(title: "Done", style: .Default, handler: { (action) -> Void in
// Now do whatever you want with inputTextField (remember to unwrap the optional)
var valueUser = pinboardUsernameField?.text
NSUserDefaults.standardUserDefaults().setValue(valueUser, forKey: AppDefaultKeys.PinboardUsername.rawValue)
var valueAPI = pinboardAPITokenField?.text
NSUserDefaults.standardUserDefaults().setValue(valueAPI, forKey: AppDefaultKeys.PinboardAPIToken.rawValue)
let url:NSURL = NSURL(string: "https://api.pinboard.in/v1/user/secret/?auth_token=\(valueAPI)")!
self.parser = NSXMLParser(contentsOfURL: url)!
self.parser.delegate = self
self.parser.parse()
func parser(parser: NSXMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [NSObject : AnyObject]) {
self.eName = elementName
if elementName == "result" {
self.pinboardSecret = String()
NSUserDefaults.standardUserDefaults().setValue(self.pinboardSecret, forKey: AppDefaultKeys.PinboardSecret.rawValue)
}
}
}))
self.presentViewController(pinboardUserDetailsSheetController, animated: true, completion: nil)
}
The error is in the let url: NSURL = ... line of the parser function.
What am I doing wrong?
The issue is that valueAPI is an optional. If, for example, valueAPI was foo, your URL string would look like:
https://api.pinboard.in/v1/user/secret/?auth_token=Optional("foo")
I'd suggest building your URL string first, looking at it, and you'll see what I mean.
Bottom line, that Optional("...") reference in the URL is not valid and thus instantiating the URL with that string will fail. And using the ! to unwrap that optional NSURL will crash as you outlined.
You have to unwrap the valueAPI optional before using it in the URL string.
let url:NSURL = NSURL(string: "https://api.pinboard.in/v1/user/secret/?auth_token=\(valueAPI!)")!
Or you could make valueAPI an implicitly unwrapped optional.
Frankly, if there's any chance that valueAPI might be nil, I'd rather use optional binding (i.e. if let clauses), so you could gracefully detect these issues in the future.
Regarding the completely separate XML parsing problems, there are a couple of issues:
I'd suggest retrieving the response asynchronously via NSURLSession. Using NSXMLParser with contentsOfURL performs the request synchronously which results in a poor UX and the iOS watchdog process may kill your app.
Instead, use NSURLSession method dataTaskWithURL and then use the resulting NSData with the NSXMLParser object instead.
If you do that, you can also convert the NSData response object to a string and examine it. Often if there are API problems, the response will include some description of the nature of the error (sometimes it's text, sometimes it's HTML, sometimes it's XML; depends upon how that web service was designed). If you don't get the XML response you expected, you really will want to look at the raw response to identify what's wrong.
Your NSXMLParserDelegate method didStartElement is defined inside the addAction block. It must be a method of the class, itself, not buried inside this closure.
Your didStartElement is saving the secret. But you don't have the secret yet, so there's no point in trying to save it yet.
I don't see the other NSXMLParserDelegate methods (foundCharacters, didEndElement, and parseErrorOccurred, at the very least). Do you have those defined elsewhere? See https://stackoverflow.com/a/27104883/1271826
let url:NSURL = NSURL(string: "https://api.pinboard.in/v1/user/secret/?auth_token=\(valueAPI)")!
NSURL() is a failable initializer, meaning it may fail on initialization (malformed URL, server not responding, ...).
By forcing the unwrap with ! you declare that you are absolutely sure to get a valid object, but you don't. This leads to the fatal error.
I'm using Alamofire in my application and wish to display an alert if the request has an error (e.g. wrong URL), etc.
I have this function in a separate class as it is shared among the pages of the application.
Alamofire.request(.GET, api_url)
.authenticate(user: str_api_username, password: str_api_password)
.validate(statusCode: 200..<300)
.response { (request, response, data, error) in
if (error != nil) {
let alertController = UIAlertController(title: "Server Alert", message: "Could not connect to API!", preferredStyle: UIAlertControllerStyle.Alert)
alertController.addAction(UIAlertAction(title: "Dismiss", style: UIAlertActionStyle.Default,handler: nil))
self.presentViewController(alertController, animated: true, completion: nil)
}
}
As Alamofire works asynchronously I need to do the error check then & there (unless you suggest otherwise) because then I want to manipulate the results and if the URL was wrong then it can get messy.
No surprise, the
self.presentViewController(alertController, animated: true, completion: nil)
does not work so how can I display this alert?
I'd say the conventional approach for this is to have whoever calls this network request be responsible for displaying the alert. If when the request is complete, you call back to the original calling object, they are responsible for displaying the alert. One of the reasons for this is that errors can mean different things in different contexts. You may not always want to display an alert - this provides you with more flexibility as you're building out your app. The same way that AlamoFire calls your response closure when it is done, I think it's best to pass that back to whoever initiated this call in your Downloader object.
Update:
You want to structure it the same way AlamoFire structures it. You pass the closure to AF which gets called when the AF request finishes.
You'll have to add a closure param to your download function (See downloadMyStuff). Then, once the AF request finishes, you can call the closure you previously defined ( completion). Here's a quick example
class Downloader {
func downloadMyStuff(completion: (AnyObject?, NSError?) -> Void) {
Alamofire.request(.GET, "http://myapi.com")
.authenticate(user: "johndoe", password: "password")
.validate(statusCode: 200..<300)
.response { (request, response, data, error) in
completion(data, error)
}
}
}
class ViewController: UIViewController {
let downloader = Downloader()
override func viewDidLoad() {
super.viewDidLoad()
self.downloader.downloadMyStuff { (maybeResult, maybeError) -> Void in
if let error = maybeError {
println("Show your alert here from error \(error)")
}
if let result: AnyObject = maybeResult {
println("Parse your result and do something cool")
}
}
}
}