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.
Related
I want to know how to pass data using closure. I know that there are three types of data pass approaches:
delegate
Notification Center
closure
I want proper clarification of closure with an example.
Well passing data with blocks / closures is a good and reasonable approach and much better than notifications.
Below is the same code for it.
First ViewController (where you make object of Second ViewController)
#IBAction func push(sender: UIButton) {
let v2Obj = storyboard?.instantiateViewControllerWithIdentifier("v2ViewController") as! v2ViewController
v2Obj.completionBlock = {[weak self] dataReturned in
//Data is returned **Do anything with it **
print(dataReturned)
}
navigationController?.pushViewController(v2Obj, animated: true)
}
Second ViewController (where data is passed back to First VC)
import UIKit
typealias v2CB = (infoToReturn :String) ->()
class v2ViewController: UIViewController {
var completionBlock:v2CB?
override func viewDidLoad() {
super.viewDidLoad()
}
func returnFirstValue(sender: UIButton) {
guard let cb = completionBlock else {return}
cb(infoToReturn: "any value")
}
}
This example explains use of service call with Alamofire and send the response back to calling View Controller with closure.
Code in Service Wrapper Class:
Closure declaration
typealias CompletionHandler = (_ response: NSDictionary?, _ statusCode: Int?, _ error: NSError?) -> Void
Closure implementation in method
func doRequestFor(_ url : String, method: HTTPMethod, dicsParams : [String: Any]?, dicsHeaders : [String: String]?, completionHandler:#escaping CompletionHandler) {
if !NetworkReachablity().isNetwork() {
return
}
if (dicsParams != nil) {print(">>>>>>>>>>>>>Request info url: \(url) --: \(dicsParams!)")}
else {print(">>>>>>>>>>>>>Request info url: \(url)")}
Alamofire.request(url, method: method, parameters: dicsParams, encoding:
URLEncoding.default, headers: dicsHeaders)
.responseJSON { response in
self.handleResponse(response: response, completionHandler: completionHandler)
}
}
Code at calling view controller:
ServiceWrapper().doRequestFor(url, method: .post, dicsParams: param, dicsHeaders: nil) { (dictResponse, statusCode, error) in
}
Soooo.... I'm a JS dev and I'm building an app in Swift 3.0 for iOS 10+ and I want to interact with data much in the same way I do in Angular 1.6+...
Here is my situation:
I have a Singleton sharedInstance that house session data for a webRTC session. One of the properties of the sharedInstance holds an array of UInt and I need to update a UI element (IBOutlet) to show the user who the available opponents are they can call at any given moment.
In angular I would just update the model and the view/UI would change automatically... boom bang... done...
I am looking to create the same behavior in Swift 3.0 so here goes some code:
class Singleton {
static let sharedInstance = Singleton()
var session = (
peers: [UInt]()
)
private init() { }
}
Here is the controller:
class ViewController: UIViewController {
#IBOutlet weak var UIPeerList: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
UIPeerList.text = String(describing: Singleton.sharedInstance.session.peers)
self.updatePeerList(room: roomName, completion: {(oUser: AnyObject) -> Void in
QBRequest.dialogs(
for: QBResponsePage(limit: 100, skip: 0),
extendedRequest: [roomName : "name"],
successBlock: {(
response: QBResponse,
dialogs: [QBChatDialog]?,
dialogsUsersIDs: Set<NSNumber>?,
page: QBResponsePage?
) -> Void in
print("<------------------------------<<< Successfully found chat dialog - " + roomName)
}, errorBlock: { (response: QBResponse) -> Void in
print("<------------------------------<<< Handle Error finding chat dialog - " + roomName)
let chatDialog: QBChatDialog = QBChatDialog(dialogID: nil, type: QBChatDialogType.group)
//chatDialog.occupantIDs = []
//chatDialog.occupantIDs?.append(NSNumber(value: DataModel.sharedInstance.qbLoginParams.id))
//chatDialog.occupantIDs?.append(12186)
chatDialog.setValue(roomName, forKey: "Name")
QBRequest.createDialog(chatDialog, successBlock: {(response: QBResponse?, createdDialog: QBChatDialog?) in
print("<------------------------------<<< Success creating chat dialog")
print(response ?? "No Response")
print(createdDialog ?? "No Created Dialog")
}, errorBlock: {(response: QBResponse!) in
print("<------------------------------<<< Error creating chat dialog")
print(response ?? "No Response")
})
}
)
})
So above in the controller I set the UI element to the datasource right after the viewDidLoad is triggered (I know this is not exactly what's going on currently.) Now whenever I change the Singleton.sharedInstance.session.peers data model like for instance here in this ASYNC API request I want the UI to update:
func updatePeerList( room: String, completion: #escaping (_ response: AnyObject) -> ()) {
QBRequest.users(
withTags: [room],
page: QBGeneralResponsePage(currentPage: 1, perPage: 10),
successBlock: {( response: QBResponse, page: QBGeneralResponsePage?, users: [QBUUser]? ) -> Void in
guard users != nil else { return }
print("<------------------------------<<< Success getting users with room tag - "+room)
DataModel.sharedInstance.sessionInfo.peers.removeAll()
for object in users! {
DataModel.sharedInstance.sessionInfo.peers.append(object.id)
}
DispatchQueue.main.async { completion(response) }
}, errorBlock: {(response: QBResponse!) in
print("<------------------------------<<< Error getting users with room tag - "+room)
print(response)
DispatchQueue.main.async { completion(response) }
}
)
}
How do I achieve such a feat?
NOTE: I believe this is possible as I looked into the UISwitch method however that didn't exactly work as I wanted to..
One solution that came to my mind is using delegation. There may be more elegant solutions though.
To use delegation, you could go through the following steps:
1- Create a protocol that have one method update(), and let's name that protocol UpdateUIDelegate.
2- Make your ViewController implement that protocol, and implement update() method in the way you want.
3- Set your ViewController object as a property in the Singleton class.
4- Make all changes that affect Singleton.sharedInstance.session.peers happen through a method inside Singleton, and make that method calls your delegate's update method in its end.
I'm trying to write failing tests for my network calls but can't find anyway to access the connection settings from within my tests.
This code works fine for testing a success case:
func testRetrieveProducts() {
let expectation = expectationWithDescription("asynchronous request")
Requests().retrieveProducts({ (products) -> () in
// check that we have two arrays returned.
XCTAssert(products.count == 2)
expectation.fulfill()
}) { (error) -> () in
XCTFail("Request failed")
}
waitForExpectationsWithTimeout(5.0, handler: nil)
}
But I've been looking for a way to test network timeouts, and incorrect data being returned.
I can probably test incorrect data by calling functions individually inside the retrieveProducts function and stubbing stuff out, but doing something as simple as turning off the internet is proving to be quite difficult.
I know we have access to the Network Link Conditioner, but turning this on and off for each test isn't an option.
I'd love to have access to something as simple as:
func testFailRetrieveProducts() {
let expectation = expectationWithDescription("asynchronous request")
SomeNetworkClass.disableInternet()
Requests().retrieveProducts({ (products) -> () in
}) { (error) -> () in
XCTAssert(error == SomeError.TimeoutError)
}
waitForExpectationsWithTimeout(5.0, handler: nil)
}
Any solutions out there that can handle what I'm after, or am I going about this all wrong?
Take a look at this NSHipster article about Apple's Network Link Conditioner. There's a lot of presets and you can create your own custom network profile. Unfortunately there doesn't seem to be a way to throttle the network in code.
A somewhat viable alternative however is to use ReactiveCocoa and model all your network events as SignalProducers. Then you can use the throttle or wait function, depending on your intentions.
I ended up just mocking the network calls, which to be honest is a lot better than performing tests over an actual connection, as these can be very unreliable anyway.
Here's my mock NSURLSession
class MockSession: NSURLSession {
var completionHandler:((NSData!, NSURLResponse!, NSError!) -> Void)?
static var mockResponse: (data: NSData?, urlResponse: NSURLResponse?, error: NSError?)
override class func sharedSession() -> NSURLSession {
return MockSession()
}
override func dataTaskWithRequest(request: NSURLRequest, completionHandler: (NSData?, NSURLResponse?, NSError?) -> Void) -> NSURLSessionDataTask {
self.completionHandler = completionHandler
return MockTask(response: MockSession.mockResponse, completionHandler: completionHandler)
}
class MockTask: NSURLSessionDataTask {
typealias Response = (data: NSData?, urlResponse: NSURLResponse?, error: NSError?)
var mockResponse: Response
let completionHandler: ((NSData!, NSURLResponse!, NSError!) -> Void)?
init(response: Response, completionHandler:((NSData!, NSURLResponse!, NSError!) -> Void)?) {
self.mockResponse = response
self.completionHandler = completionHandler
}
override func resume() {
completionHandler!(mockResponse.data, mockResponse.urlResponse, mockResponse.error)
}
}
}
Here's how I use it in a test:
Note that you still have to use an expectation even though there's no network delay.
func testRetrieveProductsValidResponse() {
let testBundle = NSBundle(forClass: self.dynamicType)
let filepath = testBundle.pathForResource("products", ofType: "txt")
let data = NSData(contentsOfFile: filepath!)
let urlResponse = NSHTTPURLResponse(URL: NSURL(string: "https://anyURL.com")!, statusCode: 200, HTTPVersion: nil, headerFields: nil)
MockSession.mockResponse = (data, urlResponse: urlResponse, error: nil)
let requestsClass = RequestManager()
requestsClass.Session = MockSession.self
let expectation = expectationWithDescription("ready")
requestsClass.retrieveProducts("N/R FOR TEST", branchID: "N/R FOR TEST", products: { (products) -> () in
XCTAssertTrue(products.count == 7)
expectation.fulfill()
}) { (error) -> () in
XCTAssertFalse(error == Errors.NetworkError, "Its a network error")
XCTAssertFalse(error == Errors.ParseError, "Its a parse error")
XCTFail("Error not covered by previous asserts. Shouln't get to here anyway.")
expectation.fulfill()
}
waitForExpectationsWithTimeout(3.0, handler: nil)
}
Finally, I have an accessible property on my RequestManager class that I can swap out with the MockSession when doing my tests.
var Session = NSURLSession.self
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")
}
}
}
}
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).