I'm learning Test Driven Development in Swift. I hit a wall when I realized the delegate pattern I regularly use for asynchronous requests is difficult to test. I've learned that if something's difficult to test, the design pattern behind the implementation could probably be better. This is confusing me because I think the delegate pattern I'm using is common and I'm wondering how others have dealt with this issue.
The pattern:
I wrote a service, which executes an asynchronous request in a static function which takes a delegate instance. The delegate instance conforms to a protocol which requires implementation of a success and failure method. I've contrived an example which hits Google.com. Please ignore the Type safety issues in this example. The actual code I'm running to hit an endpoint and parse JSON is safer. I just wanted to come up with a very small snippet of code to depict the issue that's causing difficulty while testing:
protocol GoogleServiceDelegate {
func gotGoogle(str: String);
func gotError(str: String);
}
struct GoogleService {
static func getGoogle(delegate: GoogleServiceDelegate) {
let url: NSURL! = NSURL(string: "http://google.com")
NSURLSession.sharedSession().dataTaskWithURL(url) { data, response, error in
if let data = data {
let str: NSString! = NSString(data: data, encoding: NSUTF8StringEncoding)
delegate.gotGoogle(str as String)
} else {
delegate.gotError("\(error)")
}
}
}
}
Here's the test which illustrates the problem:
class AsyncTestingTests: XCTestCase {
func testExample() {
let responseExpectation = expectationWithDescription("Got google response!")
struct GoogleDelegate: GoogleServiceDelegate {
func gotGoogle(str: String) {
// expectations about response
responseExpectation.fulfill()
}
func gotError(str: String) {
// expectations about error
responseExpectation.fulfill()
}
}
let myGoogleServiceDelegate = GoogleDelegate()
GoogleService.getGoogle(myGoogleServiceDelegate)
waitForExpectationsWithTimeout(5) { _ in
print("Never got a response from Google :(")
}
}
}
The problem arises at the two .fulfill() lines. I get the following error from Xcode:
Struct declaration cannot close over value 'responseExpectation' defined in outer scope
I understand the error, but am unsure what to adjust... Is there a workaround for this which I can use in the test, or is there a better (easily testable) pattern for asynchronous callbacks than what I am attempting? If you know of a better testable solution, would you mind taking the time to write down an example?
Yes, you can not close over variables defined outside of struct, to workaround, we need to use closures/functions and pass it to the struct. Methods in struct can invoke it when they receive the response.
func testExample() {
let responseExpectation = expectationWithDescription("Got google response!")
//Let a function capture the fulfilling of the expectation
func fullFillExpectation(){
responseExpectation.fullFill()
}
struct GoogleDelegate: GoogleServiceDelegate {
var fullFiller : (()->Void)!
func gotGoogle(str: String) {
// expectations about response via invoke the closure
fullFiller()
}
func gotError(str: String) {
// expectations about error - invoke the closure
fullFiller()
}
}
//Create the delegate with full filler function.
let myGoogleServiceDelegate = GoogleDelegate(fullFiller: fullFillExpectation)
GoogleService.getGoogle(myGoogleServiceDelegate)
waitForExpectationsWithTimeout(5) { _ in
print("Never got a response from Google :(")
}
}
}
PS: I could not test this, please test and let me know.
I'm trying to design an API helper function for an app. The idea is that I'll be able to call the function from a viewController, using code such as:
let api = APIController();
api.request("get_product_list")
api.delegate = self
Here's the class so far:
import Foundation
protocol APIControllerProtocol {
func didReceiveAPIResults(originalRequest: String, status: Bool, data: String, message: String)
}
class APIController {
var delegate: APIControllerProtocol?
let url = "https://example.co.uk/api.php"
let session = NSURLSession.sharedSession()
let appID = "EXAMPLEAPPID";
let deviceID = "EXAMPLEDEVICE"
func request(req:String)-> Void {
let urlString = "\(url)?request=\(req)"
let combinedUrl = NSURL(string: urlString)
let request = NSMutableURLRequest(URL: combinedUrl!)
request.HTTPMethod = "POST"
let stringPost="app_id=\(appID)&device_id=\(deviceID)"
let data = stringPost.dataUsingEncoding(NSUTF8StringEncoding)
request.timeoutInterval = 60
request.HTTPBody=data
request.HTTPShouldHandleCookies=false
let task = session.dataTaskWithRequest(request) {
(data, response, error) -> Void in
do {
let jsonData = try NSJSONSerialization.JSONObjectWithData(data!, options:NSJSONReadingOptions.MutableContainers ) as! NSDictionary
let statusInt = jsonData["status"]! as! Int
let status = (statusInt == 1)
let data = String(jsonData["data"])
let message = String(jsonData["message"])
self.delegate?.didReceiveAPIResults(req,status: status,data: data,message: message)
} catch _ {
print("ERROR")
}
}
task.resume()
}
}
The difficulty I'm having is that the 'data' parameter might one of the following:
A string / number, such as the number of purchases a customer has made
An array of items, such as a list of products
A dictionary, such as the customer's details
I've set the data parameter to String as that allowed me to do some testing, but then converting it back into something usable for a tableView got very messy.
Are there any experts on here that can advise me the best way to do this? Perhaps showing me how I'd use the results in a cellForRowAtIndexPath method? Here's an example response from the API, in case it's useful:
{
"status":1,
"message":"",
"cached":0,
"generated":1447789113,
"data":[
{"product":"Pear","price":0.6},
{"product":"Apple","price":0.7},
{"product":"Raspberry","price":1.1}
]
}
One function doing too many things at once makes a really messy code. Also you don't want too many if statements or enums - your View controller will grow really fast.
Id suggest splitting request and parse logic. Your API class would be then responsible only for requests. It would return data to another class, that would be responsible for parsing. Then in the Parser class you could add methods like toDictionary() or toArray(), toArrayOfClasses() and so on. That would be the basic API structure.
If you want to expand it a little bit, you could add another class layer that would handle all that logic so your View Controller doesn't know if it uses API or another data source - this way you could easy implement new things in the future, like Core Data or migrate from your API class to some framework, maybe Parse.com - this layer gives you flexibility.
Example structure:
API - requests
Parser - parsing API response
DataManager - Send request to API and return parsed response.
or if you don't want this third point, you can just request & parse in the view controller.
How can i wait until function get all data from alamofire get request?
GetData.swift file:
import Foundation
import Alamofire
import SwiftyJSON
import ObjectMapper
func getStartData() -> Void {
let sharedBranch = BranchSingleton.sharedInstance
let sharedArticle = ArticleSingleton.sharedInstance
Alamofire.request(.GET, Config().apiBranch)
.responseJSON { request, response, result in
let jsonObj = SwiftyJSON.JSON(result.value!)
for obj in jsonObj {
let branch = Mapper<Branch>().map(obj.1.rawString()!)
sharedBranch.addBranch(branch!)
}
}
Alamofire.request(.GET, Config().apiArticle)
.responseJSON { request, response, result in
let jsonObj = SwiftyJSON.JSON(result.value!)
for obj in jsonObj {
let article = Mapper<Article>().map(obj.1.rawString()!)
sharedArticle.addArticle(article!)
}
}
}
ViewController.swift file:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
getStartData() // need to wait until all requests are finished then do print
print(sharedArticle.articleList)
}
}
SingletonObj.swift file:
import Foundation
class BranchSingleton {
var branchList: [Branch] = []
class var sharedInstance: BranchSingleton {
struct Static {
static let instance: BranchSingleton = BranchSingleton()
}
return Static.instance
}
func addBranch(branch: Branch) {
branchList.append(branch)
}
}
class ArticleSingleton {
var articleList: [Article] = []
class var sharedInstance: ArticleSingleton {
struct Static {
static let instance: ArticleSingleton = ArticleSingleton()
}
return Static.instance
}
func addArticle(article: Article) {
articleList.append(article)
}
}
i need to wait until getStartData() finish, then pring singleton array..
How can i do that?
This getStartData contains more than 2 requests, but i just gave example with 2..
You're asking a non-question. There is no reason to "wait". Nor can you. You just do what you do, asynchronously. Meanwhile the interface must stay active; the user must be able to continue to work. Thus there is nothing to "wait" for.
Now, if the question is, how can you send a signal in some elegant way to the rest of your app when all of the requests are done, one good answer is to use NSProgress. All the different requests can contribute to a common NSProgress object. The nice thing is that its fractionCompleted is observable with KVO, so when it comes greater-than-or-equal-to 1.0, you're done.
But you don't actually need the NSProgress; you could just increment or decrement an instance variable that's KVO-observable (being careful about threading, of course). If you know there are n processes, then you could just start a variable at n and have each process decrement it when it completes; a didSet observer on the variable can then take action when we hit zero.
The point is: you don't "wait": you just have all the different activities contribute to some common central value that "knows" when this means we've "finished" and can then take action.
As #Matt says, you can't, and shouldn't, try to wait until Alamofire is done with your request. That's like hiring somebody to run an errand for so you can work and then stopping everything and sitting by the door until they get back. You might as well have run the errand yourself.
Dropping the analogy, you might as well have performed the task synchronously. However, synchronous networking is a very bad idea. It freezes the UI until the network request is complete, which can be a very long wait if something goes wrong.
An async method like Alamofire's request method takes a completion block, a block of code that should be run when the work is finished.
The request method returns immediately, before the request has even been sent to the server, much less completed.
Instead of waiting around for the request to complete, you should refactor your getStartData method to take a completion handler, and use that to respond once the work is done:
func getStartData(completion: () -> void) -> Void {
let sharedBranch = BranchSingleton.sharedInstance
let sharedArticle = ArticleSingleton.sharedInstance
Alamofire.request(.GET, Config().apiBranch)
.responseJSON { request, response, result in
let jsonObj = SwiftyJSON.JSON(result.value!)
for obj in jsonObj {
let branch = Mapper<Branch>().map(obj.1.rawString()!)
sharedBranch.addBranch(branch!)
}
}
Alamofire.request(.GET, Config().apiArticle)
.responseJSON { request, response, result in
let jsonObj = SwiftyJSON.JSON(result.value!)
for obj in jsonObj {
let article = Mapper<Article>().map(obj.1.rawString()!)
sharedArticle.addArticle(article!)
}
//At this point the Alamofire .GET request for Config().apiArticle
//is complete. Call our completion block (passed in as a parameter)
completion()
}
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
getStartData()
{
//This is a "trailing closure", a block of code passed to getStartData
print("At this point, we've finished getting our data from Alamofire.")
print(sharedArticle.articleList)
}
}
}
Note that your getStartData method makes 2 Alamofire.request() commands in a row. If the second request requires that the first request be finished then you will need to restructure that code so that the second Alamofire request is inside the completion block for the first call. (That's more editing than I'm in the mood to do at the moment.)
The code works perfectly. The problem is that, after trying for a while, I cannot figure out how to make my program process a second link of different JSON data.
Here is my viewDidLoad where everything goes on:
override func viewDidLoad() {
super.viewDidLoad()
var err: NSError?
let urlPath: String = "https://na.api.pvp.net/api/lol/na/v1.4/summoner/by-name/" + searchFieldDataPassed + "?api_key=(removed my private api key for obvious reasons"
var url: NSURL = NSURL(string: urlPath)!
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url) { data, response, error in
// cast response as NSHTTPURLResponse and switch on statusCode if you like
if let httpResponse = response as? NSHTTPURLResponse { switch httpResponse.statusCode { case 200..<300: println("OK") default: println("Not OK") } }
// parse JSON using NSJSONSerialization if you've got data
if let jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &err) as? NSDictionary,
let include = jsonResult.objectForKey(self.searchFieldDataPassed) as? NSDictionary {
if let summLevel = include[ "summonerLevel" ] as? NSNumber {
dispatch_async(dispatch_get_main_queue()) {
self.summonerLevel.text = "\(summLevel.integerValue)"
println("summoner level: \(summLevel.integerValue)")
}
}
if let profIconId = include[ "profileIconId" ] as? NSNumber {
dispatch_async(dispatch_get_main_queue()) {
self.profileIconId.text = "\(profIconId.integerValue)"
println("profile icon id: \(profIconId.integerValue)")
}
}
if let idNum = include [ "id" ] as? NSNumber {
dispatch_async(dispatch_get_main_queue()) {
self.idNumber = idNum
println("id number: \(self.idNumber)")
}
}
}
// spawn off another network call here if you like
}
task.resume()
}
That is from my secondViewController where all the processing goes on for JSON and then is displayed.
Here is the JSON data that I'm processing (for the first JSON parsing):
{"soon2challenger":{"id":43993167,"name":"soon2challenger","profileIconId":844,"summonerLevel":30,"revisionDate":1435549418000}}
All of that works fine, now, I want to process this JSON data which actually takes the id from the first parsed JSON data and uses it in the link to process more data, which I would like to output, part of it, to the screen.
Second JSON data:
{"summonerId":43993167,"playerStatSummaries":[{"playerStatSummaryType":"AramUnranked5x5","wins":25,"modifyDate":1423007927000,"aggregatedStats":{"totalChampionKills":676,"totalTurretsKilled":20,"totalAssists":991}},{"playerStatSummaryType":"CAP5x5","wins":15,"modifyDate":1429065922000,"aggregatedStats":{"totalChampionKills":312,"totalMinionKills":4885,"totalTurretsKilled":31,"totalNeutralMinionsKilled":511,"totalAssists":216}},{"playerStatSummaryType":"CoopVsAI","wins":28,"modifyDate":1421882181000,"aggregatedStats":{"totalChampionKills":266,"totalMinionKills":2802,"totalTurretsKilled":50,"totalNeutralMinionsKilled":385,"totalAssists":164,"maxChampionsKilled":0,"averageNodeCapture":0,"averageNodeNeutralize":0,"averageTeamObjective":0,"averageTotalPlayerScore":49,"averageCombatPlayerScore":0,"averageObjectivePlayerScore":49,"averageNodeCaptureAssist":0,"averageNodeNeutralizeAssist":0,"maxNodeCapture":0,"maxNodeNeutralize":0,"maxTeamObjective":0,"maxTotalPlayerScore":49,"maxCombatPlayerScore":0,"maxObjectivePlayerScore":49,"maxNodeCaptureAssist":0,"maxNodeNeutralizeAssist":0,"totalNodeNeutralize":0,"totalNodeCapture":0,"averageChampionsKilled":0,"averageNumDeaths":0,"averageAssists":0,"maxAssists":0}},{"playerStatSummaryType":"CoopVsAI3x3","wins":15,"modifyDate":1421882181000,"aggregatedStats":{"totalChampionKills":140,"totalMinionKills":1114,"totalTurretsKilled":9,"totalNeutralMinionsKilled":449,"totalAssists":91}},{"playerStatSummaryType":"OdinUnranked","wins":1,"modifyDate":1421882181000,"aggregatedStats":{"totalChampionKills":31,"totalAssists":45,"maxChampionsKilled":10,"averageNodeCapture":4,"averageNodeNeutralize":4,"averageTeamObjective":0,"averageTotalPlayerScore":843,"averageCombatPlayerScore":268,"averageObjectivePlayerScore":575,"averageNodeCaptureAssist":3,"averageNodeNeutralizeAssist":1,"maxNodeCapture":6,"maxNodeNeutralize":7,"maxTeamObjective":2,"maxTotalPlayerScore":1468,"maxCombatPlayerScore":529,"maxObjectivePlayerScore":939,"maxNodeCaptureAssist":5,"maxNodeNeutralizeAssist":2,"totalNodeNeutralize":22,"totalNodeCapture":25,"averageChampionsKilled":5,"averageNumDeaths":5,"averageAssists":8,"maxAssists":19}},{"playerStatSummaryType":"RankedSolo5x5","wins":116,"losses":120,"modifyDate":1433630047000,"aggregatedStats":{"totalChampionKills":1699,"totalMinionKills":33431,"totalTurretsKilled":219,"totalNeutralMinionsKilled":6501,"totalAssists":1969}},{"playerStatSummaryType":"RankedTeam3x3","wins":0,"losses":0,"modifyDate":1377726216000,"aggregatedStats":{}},{"playerStatSummaryType":"RankedTeam5x5","wins":3,"losses":0,"modifyDate":1383784473000,"aggregatedStats":{"totalChampionKills":28,"totalMinionKills":636,"totalTurretsKilled":6,"totalNeutralMinionsKilled":101,"totalAssists":41}},{"playerStatSummaryType":"Unranked3x3","wins":9,"modifyDate":1421882181000,"aggregatedStats":{"totalChampionKills":90,"totalMinionKills":1427,"totalTurretsKilled":11,"totalNeutralMinionsKilled":428,"totalAssists":105}},{"playerStatSummaryType":"URF","wins":4,"modifyDate":1435024847000,"aggregatedStats":{"totalChampionKills":68,"totalMinionKills":642,"totalTurretsKilled":14,"totalNeutralMinionsKilled":182,"totalAssists":55}},{"playerStatSummaryType":"Unranked","wins":566,"modifyDate":1435549418000,"aggregatedStats":{"totalChampionKills":8419,"totalMinionKills":128213,"totalTurretsKilled":960,"totalNeutralMinionsKilled":26117,"totalAssists":7812}}]}
Heres the link of the second JSON data I want to parse (just adding it, could be useful, but not sure):
https://na.api.pvp.net/api/lol/na/v1.3/stats/by-summoner/43993167/summary?season=SEASON2015&api_key=(took-out-my-private-api-key-for-obvious-reasons)
The link doesn't work because I have to keep my api key private to myself, but the JSON data that it displays is right above the link, which is the what it would result if you were to use the link with the api key.
Just to restate, I would like to process the second part (above of this) of JSON data, but I do not understand how to process multiple links of JSON. I have the first JSON data parsed, but am unable to parse the second JSON data.
I believe Apple is deprecating NSURLConnection. Take a look at NSURLSession. Using it, you can pass in a completion block that takes three arguments: NSData?, NSURLResponse?, and NSError?. The data object contains the JSON you can pass into the JSON serializer. After that, if you need to make another network call, just call it from inside the completion block with another NSURLSession data task. Alamofire is a great framework, but sometimes you don't need everything it provides, and it adds complexity into your app that if something goes wrong or doesn't behave the way you intend/understand, you may not fully understand why. If you want to keep it simple and under your control, use NSURLSession.
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url) { data, response, error in
// cast response as NSHTTPURLResponse and switch on statusCode if you like
// parse JSON using NSJSONSerialization if you've got data
// spawn off another network call here if you like
}
task.resume() // or in Swift 2, task?.resume()
First, i would totally prefer using some common frameworks for http requests - expecially if youre new in swift. For example here with alamofire.
https://github.com/Alamofire/Alamofire
There is also a version with integrated SwiftyJSON, so you are able to parse JSON Responses very easily.
https://github.com/SwiftyJSON/Alamofire-SwiftyJSON
So if you want to make a request, use this:
Alamofire.request(.GET, "http://httpbin.org/get")
.responseJSON { (_, _, json, _) in
var json = JSON(json)
// get the id out (depends on your structure of JSON):
let id = json["id"].int
}
Now you are able to perform a second Request (with the same Code) - Read the Documentation, how to make different Requests (like with POST) and add Parameters.
If you want to use Segues, so you want to load more data from the ID in another ViewController, you can use Segues to push the data to a second ViewController, and Load the new Content from JSON when the new ViewController is initialised.
Check out this how to send data through segues:
Sending data with Segue with Swift
Hey stackoverflow members,
I work really hard on getting better on Swift, now I have a trivial problem. I'm a former C# developer so Error Handling until now was try {}... catch {}... Message... Done!
Now I'm developing an App which uses some JSON APIs. It's all working, downloading JSON Data, pack them into my Objects but there is one problem. The proper Error Handling..
I have the following code to download & parse JSON:
//Download & Parse JSON
func getJSON(urlToRequest: String) -> NSDictionary {
var url: NSURL = NSURL(string: urlToRequest)
var jsonRequest: NSURLRequest = NSURLRequest(URL: url)
var jsonResponse: AutoreleasingUnsafePointer<NSURLResponse?> = nil
var error: NSError?
var dataValue: NSData = NSURLConnection.sendSynchronousRequest(jsonRequest, returningResponse: jsonResponse, error:&error)
if error.description.isEmpty {
var jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(dataValue, options: NSJSONReadingOptions.MutableContainers, error: &error) as NSDictionary
if error.description.isEmpty {
return jsonResult
}
else {
return NSDictionary(object: "Error: Something with parsing went wrong :(", forKey: "error")
}
}
else {
return NSDictionary(object: "Error: There was an error with your connection :(", forKey: "error")
}
}
The error part is just temporary, the problem is my ViewController just calls one function to get the whole data for the week (days, matchups and so on)
I call it this way:
var rWrapper = RiotWrapper()
let lcsWeek: Week = rWrapper.getWeek("2014-07-07")
My getWeek method calls 3-4 functions which all parse JSON data in relation to the previous responses.
Ok, long story short question: I want to abort all Tasks if JSON or HTTP fails and fill my TableView just with an error message, how can I achieve this?
Something like: if error occurred -> Stop whatever you are doing -> return for example a null erm.. nil for week -> print error
Can someone help me? If someone have some lecture according this topic it will be fine also :D
Thanks in advance!
Btw: sorry for my "bad" english
All of your JSON parsing methods including getWeek should return a tuple with an optional return value and an error. If at any point you get an error in one of your methods from the JSON parsing, just immediately return the error with nil for the return value. Each method up the chain should check for an error from the previous methods and immediately return the error if it finds one:
func getWeek(string : String) -> (Week?, NSError) {
let (result, error) = self.otherMethod()
if error {
return (nil, error)
}
// continue happily ...
}
I would suggest having your function return an optional. It is easy to test and functions can be chained and shortcut.
func getWeek(string : String) -> Week? {
...
if error {
return nil
}
}
This can be quickly evaluated like this:
if let week = getWeek("2014-07-07") {
// handle success case
} else {
// handle nil
}
and check here for how to string together a number of functions that return optional:
https://developer.apple.com/library/prerelease/mac/documentation/Swift/Conceptual/Swift_Programming_Language/OptionalChaining.html