Check for Reachability in viewDidAppear or AppDelegate? - swift

I am using AlamoreFire and checking for network reachability in two of the app's view controllers in the viewDidAppear method of both. But sometimes when the network is found the view controller collection view loads twice.
I am guessing that maybe the Reachability should be put in one place only in the entire app.
What is the best and cleanest way to implement Reachability when you have multiple view controllers to check?
I would like to use the NetworkReachabilityManager from AlamoFire.

Normally with Reachability you'd have some sort of error view on top of the screen, don't worry if the background is trying to load or not.
Create a container view and in viewDidLoad()
if Reachability.isConnectedToNetwork() == true {
self.errorView.isHidden = true
} else {
self.errorView.isHidden = false
}
That solves your problem and helps with UX.

I have my method below. To be more direct to your question though, I would not check for connection in either.
Example: I get all the way to the apps first network call and then turn my wifi on.
Problem: My app fails even though nothing that needed wifi in the App was used.
My Method: I can understand checking for a network connection on the login page, but besides that, I would put it into your network. If a network call fails, check the connection and then relate to the user whether the call failed because of the server or because of the network.
This is what I use for Reachability:
import Foundation
/// This class is designed to check if the network is connected.
open class Reachability {
/// This function will hit two urls that should never be totally down to see if the device is connected
/// If either url connects, this returns true
///
/// - Parameter resultHandler: returns the result of the connection existing as a Bool in a resultHandler
class func isConnectedToNetwork(_ resultHandler: #escaping (_ isTrue: Bool) -> Void) {
let urlA = URL(string: "https://google.com/")
let urlB = URL(string: "https://baidu.com/")
Reachability.fetchURL(urlA!) { isTrue in
if isTrue {
print("NETWORK STATUS: \(isTrue)")
resultHandler(isTrue)
} else {
Reachability.fetchURL(urlB!, resultHandler: resultHandler)
}
}
}
/// Hits a URL in order to see if the device can connect to it
///
/// - Parameters:
/// - url: the url to request
/// - resultHandler: returns whether the url was successfully retrieved or not
class func fetchURL(_ url:URL, resultHandler: #escaping (_ isTrue: Bool) -> Void) {
var request = URLRequest(url: url)
request.httpMethod = "HEAD"
request.cachePolicy = URLRequest.CachePolicy.reloadIgnoringLocalAndRemoteCacheData
request.timeoutInterval = 10.0
let session = URLSession(configuration: .default)
let dataTask = session.dataTask(with: request) { data, response, error in
if let httpResponse = response as? HTTPURLResponse {
if httpResponse.statusCode == 200 {
resultHandler(true)
} else {
resultHandler(false)
}
} else {
resultHandler(false)
}
}
dataTask.resume()
}
}
Then anywhere in code you can call it like so:
Reachability.isConnectedToNetwork() { isConnected in
if isConnected {
//Do something when connected
} else {
//Do something when not connected
}
}

Related

Set URLSession Delegate To Another Swift Class

I am attempting to call an API to login to a website. I currently have all my API calls in a swift class called APICalls. My view controller I'm using to login with is called CreateAccountViewController.
In my API call to login I create a URL session and set the delegate like this:
let task = URLSession.init(configuration: URLSessionConfiguration.default, delegate: CreateAccountViewController.init(), delegateQueue: nil)
task.dataTask(with: request).resume()
Then in my VC class I have this function
func urlSession(_: URLSession, task: URLSessionTask, didCompleteWithError: Error?) {
// Check the data returned from API call, ensure user is logged in
}
This function is being called when the API is done, but I feel like I'm causing a memory leak or something by using .init in the delegate declaration when creating my URL session. Is there a better way to do this?
Also, how do I access the data from the API call? In completion handlers there's a data response I can get at, but not in this delegate call.
Yes, you technically can have a separate object be the delegate for the session. But it doesn’t make much sense to instantiate a view controller for this, for a few reasons:
Your code is creating a view controller instance as the delegate object, but you’re handing this off to the URLSession without keeping a reference to it. Thus, there’s no way to add this to the view controller hierarchy (e.g. to present it, to push to it, perform a segue to it, whatever).
Sure, you might be presenting another instance of this view controller elsewhere, but that will be a completely separate instance, with no connection to the one you just created here. You’d end up with two separate CreateAccountViewController objects.
From an architectural perspective, many would argue that network delegate code doesn’t really belong in view controllers, anyway. View controllers are for populating views and responding to user events, not for network code.
So, in short, while you technically can have your API manager class use a separate object for the delegate calls, that’s a bit unusual. And if you did do that, you certainly wouldn’t create a UIViewController subclass for that.
A more common pattern (if you use the delegate pattern at all) might be to make the API manager, itself, the delegate for its URLSession. (Adding a separate dedicate delegate object in the mix probably only complicates the situation.) But by keeping all of this network-specific code out of the view controllers, you abstract your view controllers away from the gory details of parsing network responses, handling all of the various delegate methods, etc.
All of this begs the question: Do you really need to use the delegate-based API? It’s critical in those rare cases where you need the rich delegate API (handling custom challenge responses, etc.), but in most cases, the simple completion handler rendition of dataTask is much easier.
Give your API method a completion handler closure, so that the caller can specify what should happen if the network request succeeds. You can do this with delegate based sessions, but it’s a lot more complicated and we’d generally only go down that rabbit hole if absolutely necessary, which is not the case here.
So a common pattern would be to give your API manager (which I’ll assume is a singleton) a login method, like so:
/// Perform login request
///
/// - Parameters:
/// - userid: Userid string.
/// - password: Password string
/// - completion: Calls with `.success(true)` if successful. Calls `.failure(Error)` on error.
///
/// - Returns: The `URLSessionTask` of the network request (in case caller wants to cancel request).
#discardableResult
func login(userid: String, password: String, completion: #escaping (Result<Bool, Error>) -> Void) -> URLSessionTask {
let request = ... // Build your `URLRequest` here
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard
error == nil,
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
DispatchQueue.main.async { completion(.failure(error ?? APIManagerError.invalidResponse(data, response))) }
return
}
// parse `responseData` here
let success = true
DispatchQueue.main.async {
if success {
completion(.success(true))
} else {
completion(.failure(error))
}
}
}
task.resume()
return task
}
Where you might have a custom error class like so:
enum APIManagerError: Error {
case invalidResponse(Data?, URLResponse?)
case loginFailed(String)
}
And you’d call it like so:
APIManager.shared.login(userid: userid, password: password) { result in
switch result {
case .failure(let error):
// update UI to reflect error
print(error)
case .success:
// do whatever you want if the login was successful
}
}
Below is a more complete example, where I’ve broken up the network code down a bit (one to perform network requests, one generic method for parsing JSON, one specific method to parse the JSON associated with login), but the idea is still the same. When you perform an asynchronous method, give the method an #escaping completion handler closure which is called when the asynchronous task is done.
final class APIManager {
static let shared = APIManager()
private var session: URLSession
private init() {
session = .shared
}
let baseURLString = "https://example.com"
enum APIManagerError: Error {
case invalidResponse(Data?, URLResponse?)
case loginFailed(String)
}
/// Perform network request with `Data` response.
///
/// - Parameters:
/// - request: The `URLRequest` to perform.
/// - completion: Calls with `.success(Data)` if successful. Calls `.failure(Error)` on error.
///
/// - Returns: The `URLSessionTask` of the network request (in case caller wants to cancel request).
#discardableResult
func perform(_ request: URLRequest, completion: #escaping (Result<Data, Error>) -> Void) -> URLSessionTask {
let task = session.dataTask(with: request) { data, response, error in
guard
error == nil,
let responseData = data,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
completion(.failure(error ?? APIManagerError.invalidResponse(data, response)))
return
}
completion(.success(responseData))
}
task.resume()
return task
}
/// Perform network request with JSON response.
///
/// - Parameters:
/// - request: The `URLRequest` to perform.
/// - completion: Calls with `.success(Data)` if successful. Calls `.failure(Error)` on error.
///
/// - Returns: The `URLSessionTask` of the network request (in case caller wants to cancel request).
#discardableResult
func performJSON<T: Decodable>(_ request: URLRequest, of type: T.Type, completion: #escaping (Result<T, Error>) -> Void) -> URLSessionTask {
return perform(request) { result in
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let data):
do {
let responseObject = try JSONDecoder().decode(T.self, from: data)
completion(.success(responseObject))
} catch let parseError {
completion(.failure(parseError))
}
}
}
}
/// Perform login request
///
/// - Parameters:
/// - userid: Userid string.
/// - password: Password string
/// - completion: Calls with `.success()` if successful. Calls `.failure(Error)` on error.
///
/// - Returns: The `URLSessionTask` of the network request (in case caller wants to cancel request).
#discardableResult
func login(userid: String, password: String, completion: #escaping (Result<Bool, Error>) -> Void) -> URLSessionTask {
struct ResponseObject: Decodable {
let success: Bool
let message: String?
}
let request = prepareLoginRequest(userid: userid, password: password)
return performJSON(request, of: ResponseObject.self) { result in
switch result {
case .failure(let error):
completion(.failure(error))
case .success(let responseObject):
if responseObject.success {
completion(.success(true))
} else {
completion(.failure(APIManagerError.loginFailed(responseObject.message ?? "Unknown error")))
}
print(responseObject)
}
}
}
private func prepareLoginRequest(userid: String, password: String) -> URLRequest {
var components = URLComponents(string: baseURLString)!
components.query = "login"
components.queryItems = [
URLQueryItem(name: "userid", value: userid),
URLQueryItem(name: "password", value: password)
]
var request = URLRequest(url: components.url!)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
request.setValue("application/json", forHTTPHeaderField: "Accept")
return request
}
}
Better is to don't do this by delegating other class in URL Session case ... return data in completion handler and access that in your class
And for data you can use other method
func urlSession(_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive data: Data)
Here you will get received data

Swift launch view only when data received

I'm getting info from an API using the following function where I pass in a string of a word. Sometimes the word doesn't available in the API if it doesn't available I generate a new word and try that one.
The problem is because this is an asynchronous function when I launch the page where the value from the API appears it is sometimes empty because the function is still running in the background trying to generate a word that exists in the API.
How can I make sure the page launches only when the data been received from the api ?
static func wordDefin (word : String, completion: #escaping (_ def: String )->(String)) {
let wordEncoded = word.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
let uri = URL(string:"https://dictapi.lexicala.com/search?source=global&language=he&morph=false&text=" + wordEncoded! )
if let unwrappedURL = uri {
var request = URLRequest(url: unwrappedURL);request.addValue("Basic bmV0YXlhbWluOk5ldGF5YW1pbjg5Kg==", forHTTPHeaderField: "Authorization")
let dataTask = URLSession.shared.dataTask(with: request) { (data, response, error) in
do {
if let data = data {
let decoder = JSONDecoder()
let empty = try decoder.decode(Empty.self, from: data)
if (empty.results?.isEmpty)!{
print("oops looks like the word :" + word)
game.wordsList.removeAll(where: { ($0) == game.word })
game.floffWords.removeAll(where: { ($0) == game.word })
helper.newGame()
} else {
let definition = empty.results?[0].senses?[0].definition
_ = completion(definition ?? "test")
return
}
}
}
catch {
print("connection")
print(error)
}
}
dataTask.resume()
}
}
You can't stop a view controller from "launching" itself (except not to push/present/show it at all). Once you push/present/show it, its lifecycle cannot—and should not—be stopped. Therefore, it's your responsibility to load the appropriate UI for the "loading state", which may be a blank view controller with a loading spinner. You can do this however you want, including loading the full UI with .isHidden = true set for all view objects. The idea is to do as much pre-loading of the UI as possible while the database is working in the background so that when the data is ready, you can display the full UI with as little work as possible.
What I'd suggest is after you've loaded the UI in its "loading" configuration, download the data as the final step in your flow and use a completion handler to finish the task:
override func viewDidLoad() {
super.viewDidLoad()
loadData { (result) in
// load full UI
}
}
Your data method may look something like this:
private func loadData(completion: #escaping (_ result: Result) -> Void) {
...
}
EDIT
Consider creating a data manager that operates along the following lines. Because the data manager is a class (a reference type), when you pass it forward to other view controllers, they all point to the same instance of the manager. Therefore, changes that any of the view controllers make to it are seen by the other view controllers. That means when you push a new view controller and it's time to update a label, access it from the data property. And if it's not ready, wait for the data manager to notify the view controller when it is ready.
class GameDataManager {
// stores game properties
// updates game properties
// does all thing game data
var score = 0
var word: String?
}
class MainViewController: UIViewController {
let data = GameDataManager()
override func viewDidLoad() {
super.viewDidLoad()
// when you push to another view controller, point it to the data manager
let someVC = SomeOtherViewController()
someVC.data = data
}
}
class SomeOtherViewController: UIViewController {
var data: GameDataManager?
override func viewDidLoad() {
super.viewDidLoad()
if let word = data?.word {
print(word)
}
}
}
class AnyViewController: UIViewController {
var data: GameDataManager?
}

How do I reinitialized a property in a singleton class?

My problem that I'm facing right now is that whenever user loads up the app. The singleton object will run
Singleton design
import SocketIO
class SocketIOManager: NSObject {
static let sharedInstance = SocketIOManager()
var socket: SocketIOClient!
override init() {
socket = SocketIOClient(socketURL: URL(string: mainURL)!, .connectParams(["token": getToken()])])
super.init()
}
func establishConnection() {
socket.connect()
}
func closeConnection() {
socket.disconnect()
}
func getToken() -> String {
if let token = keychain["token"] {
return token
}
return ""
}
}
Take a look at init() and the .connectParams, in order for the user to connect to the server, token must be present thus the getToken() being passed.
If the token is not there it will initialize the socket object without the token. I run the establishConnection at the applicationDidBecomeActive
func applicationDidBecomeActive(_ application: UIApplication) {
SocketIOManager.sharedInstance.establishConnection()
}
The token will only be there after the user logs in.
The main question is, is there any way to reinitialized the socket object? or do i use didSet or willSet method?
Maybe something like this?
var socket: SocketIOClient! {
didSet {
oldValue.closeConnection()
}
}
It looks like you could probably get rid of the ! too if you want, since you're setting it in your init, assuming SocketIOClient.init returns a non-optional instance.
It is simple, You just need to declare a method in your class:
func resetConnection() {
socket.disconnect()
socket = SocketIOClient(socketURL: URL(string: mainURL)!, .connectParams(["token": getToken()])])
socket.connect()
}
and use in the following
SocketIOManager.sharedInstance.resetConnection()
let socket =
SocketIOManager.sharedInstance.socket // this will be the newer
One way to to do that is to create a public method inside SocketIOManager, and use that method to initialize the socket:
func initializeSocket() {
socket = SocketIOClient(socketURL: URL(string: mainURL)!, .connectParams(["token": getToken()])])
}
And call this method after the user has logged in.
But the way, your initializer must be private in order to implement the Singleton design pattern properly.
Another note is that the initialization of static variables in Swift happens lazily, which means that they only get initialized the first time they are used. Check this answer and the Swift documentation on this topic for more information
First, you are calling this flow from AppDelegate, trouble with this is you depend on this token being present. So what jumps out at me here is that you're missing a method that checks if this token is actually present before initiating the connection, the method should just forgo connecting the socket entirely if you can't produce the token (that is, if your connection is actually token dependent, if it is not then previous answers should help you out).
Since you're right to initialize the socket within the init override of your manager class, it's going against what I think you want, which is to reset a connection once a token does become present if it was not there initially. For this, you should hold back on creating the socket as I mention above.
What I usually do for singletons: I give them a blank "Configure" method, to commit it to memory, usually on AppDelegate's didFinishLaunchin withOptions. If this method contains anything, it's those methods which check for any values the singleton is dependent on, and to assign a custom internal state to the singleton based on those values (like some enum cases). I would then call up establishConnection like you do here, but establishConnection should be a generic method which can run at every appDidEnterForeground method, but without having to worry about altering things, and it should re-establish things that were dropped while your app was backgrounded.
So i'd recommend altering your class to something along the lines of:
import SocketIO
enum SocketIOManagerState {
case invalidURL
case launched
case tokenNotPresent
case manuallyDisconnected
case backgroundedByOS
}
class SocketIOManager: NSObject {
private var state : SocketIOManagerState = SocketIOManagerState.launched
private var staticSocketURL : URL?
static let sharedInstance = SocketIOManager()
var socket: SocketIOClient?
override init() {
super.init()
}
func configure() {
//fetch the url string from wherever and apply it to staticSocketURL
guard let url = URL(string: "The URL from wherever") else {
state = SocketIOManagerState.invalidURL
return
}
if getToken() == nil {
state = .tokenNotPresent
} else {
//only here can we be sure the socket doesn't have any restrictions to connection
staticSocketURL = url
state = SocketIOManagerState.launched
}
}
func evaluateConnection() {
guard let token = getToken() else {
//maybe something went wrong, so make sure the state is updated
if socket != nil {
return evaluateSocketAsNotNil()
}
return closeConnection(true, .tokenNotPresent)
}
switch state {
case .tokenNotPresent, .invalidURL:
closeConnection(true)
break
case .launched:
//means token was present, so attempt a connection
guard socket == nil else {
evaluateSocketAsNotNil()
return
}
guard let url = staticSocketURL else {
//maybe something went wrong with the url? so make sure the state is updated.
if socket != nil {
return closeConnection(true, .invalidURL)
}
return setState(.invalidURL)
}
if socket == nil {
socket = SocketIOClient(socketURL: url, .connectParams(["token": token]))
}
socket?.connect()
default:
//unless you care about the other cases, i find they all fall back on the same logic : we already checked if the token is there, if we get here, it means it is, so should we reconnect?
guard weCanReconnect /*some param or method which you create to determine if you should*/ else {
//you determine you should not, so do nothing
return
}
//you determine you do, so:
}
}
private func evaluateSocketAsNotNil() {
guard let sock = socket else { return }
switch sock.state {
case .notConnected:
//evaluate if it should be connected
establishConnection()
case .disconnected:
evaluateSocketAsNotNil()
case .connecting:
//do nothing perhaps?
case connected:
guard getToken() != nil else {
//token is not present, but the socket is initialized, this can't happen so disconnect and reset the instance
closeConnection(true, .tokenNotPresent)
return
}
break //nothing to do here
}
}
private func establishConnection() {
guard let sock = socket else { return }
sock.connect()
}
func setState(_ to: SocketIOManagerState) {
self.state = to
}
func closeConnection(_ clearMemory: Bool) {
guard let sock = socket else { return }
sock.disconnect()
setState(.launched)
if clearMemory {
socket = nil
}
}
private func closeConnection(_ clearMemory: Bool,_ to: SocketIOManagerState) {
socket?.disconnect()
setState(to)
if clearMemory {
socket = nil
}
}
func getToken() -> String? {
guard let token = keychain["token"] else {
state = .tokenNotPresent
return nil }
return token
}
}
And your AppDelegate would then look like this:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
SocketIOManager.sharedInstance.configure()
return true
}
func applicationDidEnterBackground(_ application: UIApplication) {
SocketIOManager.sharedInstance.closeConnection(false, .backgroundedByOS)
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
SocketIOManager.sharedInstance.evaluateConnection()
}
From here, you can always call evaluateConnection() and closeConnection(_:, _:) anywhere else in the app, and add more state cases, and more ways to handle those cases logically. Either way, it's up to you to determine how you should connect and reconnect based on the token.
With this structure, if your user logs in, and you set your token properly in your app, you should then be able to connect the socket properly when calling evaluateConnection during the login process.
There's also alot of comments, and some things might seem generic (apologies), but it's up to you to fill in the blanks for your use-case.
Hope it helps!

NSNotificationCenter Notification Not Being Received When Posted in a Closure

What I am trying to accomplish is posting a notification through NSNotificationCenter's default center. This is being done within a closure block after making a network call using Alamofire. The problem I am having is that a class that should be responding to a posted notification isn't receiving such notification.
My ViewController simply creates a First object that get's things moving:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let first = First()
}
}
My First class creates and instance of a Second class and adds itself as an observer to my NSNotificationCenter. This is the class that can't seem to get the notification when the notification is posted.
class First : NSObject {
let second = Second()
override init(){
super.init()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(First.gotDownloadNotification(_:)), name: "test", object: nil)
second.sendRequest()
}
// NOT REACHING THIS CODE
func gotDownloadNotification(notification: NSNotification){
print("Successfully received download notification from Second")
}
}
My Second class is what makes the network call through my NetworkService class and posts a notification in a closure once the request is successful and complete.
class Second : NSObject {
func sendRequest(){
let networkService = NetworkService()
networkService.downloadFile() { statusCode in
if let statusCode = statusCode {
print("Successfully got a status code")
// Post notification
NSNotificationCenter.defaultCenter().postNotificationName("test", object: nil)
}
}
}
}
Finally, my NetworkService class is what makes a network call using Alamofire and returns the status code from the response through a closure.
class NetworkService : NSObject {
func downloadFile(completionHandler: (Int?) -> ()){
Alamofire.download(.GET, "https://www.google.com") { temporaryURL, response in
let fileManager = NSFileManager.defaultManager()
let directoryURL = fileManager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0]
let pathComponent = response.suggestedFilename
return directoryURL.URLByAppendingPathComponent(pathComponent!)
}
.response { (request, response, _, error) in
if let error = error {
print("File download failed with error: \(error.localizedDescription)")
completionHandler(nil)
} else if let response = response{
print("File downloaded successfully")
// Pass status code through completionHandler to Second
completionHandler(response.statusCode)
}
}
}
}
The output after execution is:
File downloaded successfully
Successfully got a status code
From this output I know the download was successful and Second got the status code from the closure and posted a notification right after.
I believe that I have tried resolving most other suggestions on Stack Overflow related to not receiving notifications such as objects not being instantiated before notification is posted or syntax of either adding an observer or posting a notification.
Does anyone have any idea why the posted notification is not being received in the First class?
Since there is a direct relationship between First and Second the protocol/delegate pattern is the better way to notify. Even better with this pattern and you don't have to take care of unregistering the observer. NSNotificationCenter is supposed to be used only if there is no relationship between sender and receiver.
And basically the thread doesn't matter either.
protocol SecondDelegate {
func gotDownloadNotification()
}
class Second : NSObject {
var delegate : SecondDelegate?
init(delegate : SecondDelegate?) {
self.delegate = delegate
}
func sendRequest(){
let networkService = NetworkService()
networkService.downloadFile() { statusCode in
if let statusCode = statusCode {
print("Successfully got a status code")
// Post notification
self.delegate?.gotDownloadNotification()
}
}
}
}
class First : NSObject, SecondDelegate {
let second : Second
override init(){
super.init()
second = Second(delegate:self)
second.sendRequest()
}
func gotDownloadNotification(){
print("Successfully received download notification from Second")
}
}

Code maintaining at delegates and callback pattern

First of all, I am just a beginner who is currently developing an app with the Swift language, so please don't mind my question too much because I really need to know and I am having trouble with maintaining the code that I constructed.
It's about the async delegate pattern.
Here is my API class. Assume that there are many API classes like that which makes async calls.
protocol InitiateAPIProtocol{
func didSuccessInitiate(results:JSON)
func didFailInitiate(err:NSError)
}
class InitiateAPI{
var delegate : InitiateAPIProtocol
init(delegate: InitiateAPIProtocol){
self.delegate=delegate
}
func post(wsdlURL:String,action:String,soapMessage : String){
let request = NSMutableURLRequest(URL: NSURL(string: wsdlURL)!)
let msgLength = String(soapMessage.characters.count)
let data = soapMessage.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
request.HTTPMethod = "POST"
request.addValue("text/xml; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.addValue(msgLength, forHTTPHeaderField: "Content-Length")
request.addValue(action, forHTTPHeaderField: "SOAPAction")
request.HTTPBody = data
let task = session.dataTaskWithRequest(request) {
data, response, error in
if error != nil {
self.delegate.didFailInitiate(error!)
return
}
let jsonData = JSON(data: data)
self.delegate.didSuccessInitiate(jsonData)
}
task.resume()
}
func doInitiate(token : String){
let soapMessage = “”
// WSDL_URL is the main wsdl url i will request.
action = “”
post(WSDL_URL, action: action, soapMessage: soapMessage)
}
}
Here is my ViewController:
class ViewController : UIViewController,InitiateAPIProtocol{
var initiateAPI : InitiateAPI!
var token : String = “sWAFF1”
override func viewWillAppear(animated: Bool) {
// Async call start
initiateAPI = InitiateAPI(delegate:self)
initiateAPI.doInitiate(token)
}
// Here comes call back
func didSuccessInitiate(results: JSON) {
//handle results
}
func didFailInitiate(err: NSError) {
//handle errors
}
}
My problem is I said that there are many API classes like that, so if one view controller handles 4 API classes, I have to handle many protocol delegates methods as I extend the view controller. There will be many delegates method below of view controller. If other view controllers call the same API and have to handle the same delegates, I have a problem maintaining the code because every time I change some delegate parameters, I have to fix the code at all view controllers which use those API classes.
Is there any other good way to handle async call?
If my question seems a little complex, please leave a comment, I will reply and explain it clearly.
Delegates (OOP) and "completion handlers" (function like programming) just don't fit well together.
In order to increase comprehension and to make the code more concise, an alternative approach is required. One of this approach has been already proposed by #PEEJWEEJ using solely completion handlers.
Another approach is using "Futures or Promises". These greatly extend the idea of completion handlers and make your asynchronous code look more like synchronous.
Futures work basically as follows. Suppose, you have an API function that fetches users from a remote web service. This call is asynchronous.
// Given a user ID, fetch a user:
func fetchUser(id: Int) -> Future<User> {
let promise = Promise<User>()
// a) invoke the asynchronous operation.
// b) when it finished, complete the promise accordingly:
doFetchAsync(id, completion: {(user, error) in
if error == nil {
promise.fulfill(user!)
} else {
promise.reject(error!)
}
})
return.promise.future
}
First, the important fact here is, that there is no completion handler. Instead, the asynchronous function returns you a future. A future represents the eventual result of the underlying operation. When the function fetchUser returns, the result is not yet computed, and the future is in a "pending" state. That is, you cannot obtain the result immediately from the future. So, we have to wait?? - well not really, this will be accomplished similar to an async function with a completion handler, i.e. registering a "continuation":
In order to obtain the result, you register a completion handler:
fetchUser(userId).map { user in
print("User: \(user)")
}.onFailure { error in
print("Error: \(error)")
}
It also handles errors, if they occur.
The function map is the one that registered the continuation. It is also a "combinator", that is it returns another future which you can combine with other functions and compose more complex operations.
When the future gets finally completed, the code continues with the closure registered with the future.
If you have two dependent operations, say OP1 generates a result which should be used in OP2 as input, and the combined result should be returned (as a future), you can accomplish this in a comprehensive and concise manner:
let imageFuture = fetchUser(userId).flatMap { user in
return user.fetchProfileImage()
}
imageFuture.onSuccess { image in
// Update table view on main thread:
...
}
This was just a very short intro into futures. They can do much more for you.
If you want to see futures in action, you may start the Xcode playgrounds "A Motivating Example" in the third party library FutureLib (I'm the author). You should also examine other Future/Promise libraries, for example BrightFutures. Both libraries implement Scala-like futures in Swift.
Have you looked into NSNotificationCenter?
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSNotificationCenter_Class/
You'll be able to post events from your api class, then each view controller would subscribe to the events and be notified accordingly
Does that make sense? There are lots of good examples of this pattern:
https://stackoverflow.com/a/24049111/2678994
https://stackoverflow.com/a/28269217/2678994
I've updated your code below:
class InitiateAPI{
//
// var delegate : InitiateAPIProtocol
// init(delegate: InitiateAPIProtocol){
// self.delegate=delegate
// }
func post(wsdlURL:String,action:String,soapMessage : String){
let request = NSMutableURLRequest(URL: NSURL(string: wsdlURL)!)
let msgLength = String(soapMessage.characters.count)
let data = soapMessage.dataUsingEncoding(NSUTF8StringEncoding, allowLossyConversion: false)
request.HTTPMethod = "POST"
request.addValue("text/xml; charset=utf-8", forHTTPHeaderField: "Content-Type")
request.addValue(msgLength, forHTTPHeaderField: "Content-Length")
request.addValue(action, forHTTPHeaderField: "SOAPAction")
request.HTTPBody = data
let task = session.dataTaskWithRequest(request) {
data, response, error in
if error != nil {
// self.delegate.didFailInitiate(error!)
/* Post notification with error */
NSNotificationCenter.defaultCenter().postNotificationName("onHttpError", object: error)
return
}
let jsonData = JSON(data: data)
// self.delegate.didSuccessInitiate(jsonData)
/* Post notification with json body */
NSNotificationCenter.defaultCenter().postNotificationName("onHttpSuccess", object: jsonData)
}
task.resume()
}
func doInitiate(token : String){
let soapMessage = “”
// WSDL_URL is the main wsdl url i will request.
action = “”
post(WSDL_URL, action: action, soapMessage: soapMessage)
}
}
Your view controller class:
class ViewController : UIViewController { //,InitiateAPIProtocol{
var initiateAPI : InitiateAPI!
var token : String = “sWAFF1”
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.didSuccessInitiate(_:)), name: "onHttpSuccess", object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.didFailInitiate(_:)), name: "onHttpError", object: nil)
}
override func viewWillAppear(animated: Bool) {
// Async call start
initiateAPI = InitiateAPI(delegate:self)
initiateAPI.doInitiate(token)
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
/* Remove listeners when view controller disappears */
NSNotificationCenter.defaultCenter().removeObserver(self, name: "onHttpSuccess", object: nil)
NSNotificationCenter.defaultCenter().removeObserver(self, name: "onHttpError", object: nil)
}
// Here comes call back
func didSuccessInitiate(notification : NSNotification) { //results: JSON) {
if let payload = notification.object as? JSON {
//handle results
}
}
func didFailInitiate(notification : NSNotification) { //err: NSError) {
if let payload = notification.object as? NSError {
//handle errors
}
}
}
Instead of using a delegate, you could (should?) use closers/functions:
func post(/*any other variables*/ successCompletion: (JSON) -> (), errorCompletion: (NSError) ->()){
/* do whatever you need to*/
/*if succeeds*/
successCompletion("")
/*if fails*/
errorCompletion(error)
}
// example using closures
post({ (data) in
/* handle Success*/
}) { (error) in
/* handle error */
}
// example using functions
post(handleData, errorCompletion: handleError)
func handleData(data: JSON) {
}
func handleError(error: NSError) {
}
This would also give you the option to handle all the errors with one function.
Also, it's ideal to parse your JSON into their desired objects before returning them. This keeps your ViewControllers clean and makes it clear where the parsing will occur.