Completion Handlers in swift 4 - ios

I have a problem I'm trying to wrap my head around relating to the use of completion handlers. I have 3 layers in my iOS program, the ViewController->Service->Networking. I need to load some data through API call from the view controller.
I have defined functions(completionHandlers) in the ViewController that should execute once the data request is complete and am comfortable when in implementing completion handlers when only two layers exists, but confused when in the following scenario:
DashboardViewController.swift
import UIKit
#IBDesignable class DashboardViewController: UIViewController {
#IBOutlet weak var stepCountController: ExpandedCardView!
var articles:[Article]?
let requestHandler = RequestHandler()
let dashboardService = DashboardService()
override func viewDidLoad() {
super.viewDidLoad()
dashboardService.getDashboardData(completionHandler: getDashboardDataCompletionHandler)
}
func getDashboardDataCompletionHandler(withData: DashboardDataRequest) {
print(withData)
}
}
DashboardService.swift
import Foundation
class DashboardService: GeneralService {
var requestHandler: DashboardRequestHandler
override init() {
requestHandler = DashboardRequestHandler()
super.init()
}
//this function should execute requestHandler.requestDashboardData(), and then execute convertDashboardData() with the result of previous get request
func getDashboardData(completionHandler: #escaping (DashboardDataRequest) -> Void) {
//on network call return
guard let url = URL(string: apiResourceList?.value(forKey: "GetDashboard") as! String) else { return }
requestHandler.requestDashboardData(url: url, completionHandler: convertDashboardData(completionHandler: completionHandler))
}
func convertDashboardData(completionHandler: (DashboardDataRequest) -> Void) {
//convert object to format acceptable by view
}
}
DashboardRequestHandler.swift
import Foundation
class DashboardRequestHandler: RequestHandler {
var dataTask: URLSessionDataTask?
func requestDashboardData(url: URL, completionHandler: #escaping (DashboardDataRequest) -> Void) {
dataTask?.cancel()
defaultSession.dataTask(with: url, completionHandler: {(data, response, error) in
if error != nil {
print(error!.localizedDescription)
}
guard let data = data else {
return
}
do {
let decodedJson = try JSONDecoder().decode(DashboardDataRequest.self, from: data)
completionHandler(decodedJson)
} catch let jsonError {
print(jsonError)
}
}).resume()
}
}
If you look at the comment in DashboardService.swift, my problem is quite obvious. I pass a completion handler from ViewController to Service and Service has its own completion handler that it passes to RequestHandler where the view controller completion handler (getDashboardDataCompletionHandler) should be executed after the Service completion handler (convertDashboardData())
Please help me in clarifying how to implement this. Am I making a design mistake by trying to chain completionHandlers like this or am I missing something obvious.
Thank you
--EDIT--
My Request Handler implementation is as follows:
import Foundation
class RequestHandler {
// let defaultSession = URLSession(configuration: .default)
var defaultSession: URLSession!
init() {
guard let path = Bundle.main.path(forResource: "Api", ofType: "plist") else {
print("Api.plist not found")
return
}
let apiResourceList = NSDictionary(contentsOfFile: path)
let config = URLSessionConfiguration.default
if let authToken = apiResourceList?.value(forKey: "AuthToken") {
config.httpAdditionalHeaders = ["Authorization": authToken]
}
defaultSession = URLSession(configuration: config)
}
}

In this case is more clear to use delegation, for example like this:
protocol DashboardServiceDelegate {
func didLoaded(_ viewModel: DashboardViewModel)
}
class DashboardViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
dashboardService.delegate = self
dashboardService.getDashboardData()
}
}
extension DashboardViewController: DashboardServiceDelegate {
func didLoaded(_ viewModel: DashboardViewModel) {
///
}
}
protocol DashboardRequestHandlerDelegate() {
func didLoaded(request: DashboardDataRequest)
}
class DashboardService: GeneralService {
private lazy var requestHandler: DashboardRequestHandler = { reqHandler in
reqHandler.delegate = self
return reqHandler
}(DashboardRequestHandler())
func getDashboardData() {
guard let url = ...
requestHandler.requestDashboardData(url: url)
}
}
extension DashboardService: DashboardRequestHandlerDelegate {
func didLoaded(request: DashboardDataRequest) {
let viewModel = convert(request)
delegate.didLoaded(viewModel)
}
}

Related

How to perform UI actions from Socket.io On Function

I am new to using socket.io. I have made a singleton class for socket works and I am using an Instance of it in my app.
My question is: How to perform any action after receiving the data in the On function when I am on a different view controller?
import Foundation
import SocketIO
import UIKit
protocol testUI {
func fareUpdate(amount:NSNumber)
}
var delegate:testUI?
class SocketHelper {
static let shared = SocketHelper()
var socket: SocketIOClient!
let manager = SocketManager(socketURL: URL(string: "http://13.59.81.136:3355/")!, config: [.log(true), .compress, .connectParams(["token":user.shared.token])] )
private init() {
socket = manager.defaultSocket
}
func connectSocket(completion: #escaping(Bool) -> () ) {
disconnectSocket()
// socket.on(clientEvent: .connect) {[weak self] (data, ack) in
// print("socket connected")
// self?.socket.removeAllHandlers()
// completion(true)
// }
socket.connect()
}
func disconnectSocket() {
socket.removeAllHandlers()
socket.disconnect()
print("socket Disconnected")
}
func testStatus(){
print(socket.status)
}
func checkConnection() -> Bool {
if socket.manager?.status == .connected {
return true
}
return false
}
enum Events {
case search
func getFareEstimate(params:NSDictionary){
SocketHelper.shared.socket.emit("fareEstimate" , params)
}
func listen(completion: #escaping (Any) -> Void) {
SocketHelper.shared.socket!.on("rideFares") { (response, emitter) in
print(response)
let dic = response as! NSArray
let data = dic[0] as! NSDictionary
let fare = data["fare"] as! NSNumber
DispatchQueue.main.async {
delegate?.fareUpdate(amount: fare)
}
}
// func off() {
// SocketHelper.shared.socket.off(listnerName)
// }
}
}
}
I have tried using a delegate but it doesn't seem to work. I could be on multiple view controllers when the socket receives the data in the On function.

How to get an array from URLSession

Trying to make a program for a news site. I take information from the site through the api, everything works fine.
The only question is, how do I get this array out of the loop?
Here is my code:
import UIKit
class ViewController: UIViewController {
var news:[News] = []
override func viewDidLoad() {
super.viewDidLoad()
getUsers()
print(news)
}
func getUsers() {
guard let url = URL(string: "http://prostir.news/swift/api2.php") else {return}
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
do {
news = try JSONDecoder().decode([News].self, from: data)
// print(self.news)
} catch let error {
print(error)
}
}
}.resume()
}
}
struct News:Codable, CustomStringConvertible{
let href:String?
let site:String?
let title:String?
let time:String?
var description: String {
return "(href:- \(href), site:- \(site), title:- \(title), time:- \(time))"
}
}
Declare news array in your class and assign the response to this array in getUsers method
var news:[News] = []
func getUsers(){
guard let url = URL(string: "https") else {return}
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
do {
self.news = try JSONDecoder().decode([News].self, from: data)
print(self.news)
} catch let error {
print(error)
}
}
}.resume()
}
The fundamental problem is you are retrieving data asynchronously (e.g. getUsers will initiate a relatively slow request from the network using URLSession, but returns immediately). Thus this won’t work:
override func viewDidLoad() {
super.viewDidLoad()
getUsers()
print(news)
}
You are returning from getUsers before the news has been retrieved. So news will still be [].
The solution is to give getUsers a “completion handler”, a parameter where you can specify what code should be performed when the asynchronous request is done:
enum NewsError: Error {
case invalidURL
case invalidResponse(URLResponse?)
}
func getUsers(completion: #escaping (Result<[News], Error>) -> Void) {
let queue = DispatchQueue.main
guard let url = URL(string: "http://prostir.news/swift/api2.php") else {
queue.async { completion(.failure(NewsError.invalidURL)) }
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
queue.async { completion(.failure(error)) }
return
}
guard
let data = data,
let httpResponse = response as? HTTPURLResponse,
200 ..< 300 ~= httpResponse.statusCode
else {
queue.async { completion(.failure(NewsError.invalidResponse(response))) }
return
}
do {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let news = try decoder.decode([News].self, from: data)
queue.async { completion(.success(news)) }
} catch let parseError {
queue.async { completion(.failure(parseError)) }
}
}.resume()
}
Then your view controller can fetch the news, passing a “closure”, i.e. code that says what to do when the asynchronous call is complete. In this case, it will set self.news and trigger the necessary UI update (e.g. maybe refresh tableview):
class ViewController: UIViewController {
var news: [News] = []
override func viewDidLoad() {
super.viewDidLoad()
fetchNews()
}
func fetchNews() {
getUsers() { result in
switch result {
case .failure(let error):
print(error)
case .success(let news):
self.news = news
print(news)
}
// trigger whatever UI update you want here, e.g., if using a table view:
//
// self.tableView.reloadData()
}
// but don't try to print the news here, as it hasn't been retrieved yet
// print(news)
}

UITextField to change API URL in Swift 5

I am new iOS Developer
I want to change the websiteLogo API with a textfield to change the URL.
how can I change the line with the ***
with a var and a textfield in my viewcontroller?
With screenshoot it's will be easier to understand what I want? Thank you !!! Guys. OneDriveLink. 1drv.ms/u/s!AsBvdkER6lq7klAqQMW9jOWQkzfl?e=fyqOeN
private init() {}
**private static var pictureUrl = URL(string: "https://logo.clearbit.com/:http://www.rds.ca")!**
private var task: URLSessionDataTask?
func getQuote(callback: #escaping (Bool, imageLogo?) -> Void) {
let session = URLSession(configuration: .default)
task?.cancel()
task = session.dataTask(with: QuoteService.pictureUrl) { (data, response, error) in
DispatchQueue.main.async {
guard let data = data, error == nil else {
callback(false, nil)
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
callback(false, nil)
return
}
let quote = imageLogo(image: data)
callback(true, quote)
print(data)
}
}
task?.resume()
}
First, please don't use screenshots do show your code. If you want help, others typically copy/paste your code to check whats wrong with it.
There are some minor issues with your code. Some hints from me:
Start your types with a big letter, like ImageLogo not imageLogo:
Avoid statics
Avoid singletons (they are almost statics)
Hand in the pictureUrl into getQuote
struct ImageLogo {
var image:Data
}
class QuoteService {
private var task: URLSessionDataTask?
func getQuote(from pictureUrl:URL, callback: #escaping (Bool, ImageLogo?) -> Void) {
let session = URLSession(configuration: .default)
task?.cancel()
task = session.dataTask(with: pictureUrl) {
(data, response, error) in
DispatchQueue.main.async {
guard let data = data, error == nil else {
callback(false, nil)
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
callback(false, nil)
return
}
let quote = ImageLogo(image: data)
callback(true, quote)
print(data)
}
}
task?.resume()
}
}
Store an instance of QuoteService in your view controller
Call getQuote on that instance, handing in the pictureUrl
class ViewController : UIViewController {
var quoteService:QuoteService!
override func viewDidLoad() {
self.quoteService = QuoteService()
}
func toggleActivityIndicator(shown:Bool) { /* ... */ }
func update(quote:ImageLogo) { /* ... */ }
func presentAlert() { /* ... */ }
func updateconcept() {
guard let url = URL(string:textField.text!) else {
print ("invalid url")
return
}
toggleActivityIndicator(shown:true)
quoteService.getQuote(from:url) {
(success, quote) in
self.toggleActivityIndicator(shown:false)
if success, let quote = quote {
self.update(quote:quote)
} else {
self.presentAlert()
}
}
}
/* ... */
}
Hope it helps.
I think you want to pass textfield Text(URL Enter By user) in Web Services
Add a parameter url_str in getQuote function definition first and pass textfield value on that parameters
fun getQuote(url_str : String, callback : #escaping(Bool, ImgaeLogo/)->void){
}

Write unit test for function calling Async request and return nothing

Here is my ViewModel Call with function fetch Products, I need to test function which is internally calling Async request and setting some data
class ViewModel : NSObject {
public var array : [Product]?
func fetchProduct() {
ProductRouter.fetchByCategory.send(modelType: ProductSearchResponse.self, success: { (success) in
self.array = (success as! ProductSearchResponse).skus
}, fail: { (error : NSError) in
print(error.localizedDescription)
}, showHUD: true)
}
}
class MyNetworkRequestTests: XCTestCase {
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
let url = Bundle(for: type(of: self)).url(forResource: "Listing", withExtension: "json")!
let data = try! Data(contentsOf: url)
stub(uri(ProductRouter.fetchByCategory.path), jsonData(data))
let vm = ViewModel()
vm.fetchProduct()
XCTAssertNotNil(vm.sku)
}
}
// Json File have some Listing.json have correct json format.
So what you want to do is set up an expectation and wait for it
class MyNetworkRequestTests: XCTestCase {
func testExample() {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
let expectation = expectation(description: "fetch expectation")
let url = Bundle(for: type(of: self)).url(forResource: "Listing", withExtension: "json")!
let data = try! Data(contentsOf: url)
stub(uri(ProductRouter.fetchByCategory.path), jsonData(data))
let vm = ViewModel()
vm.fetchDone = {
expectation.fullfill()
}
vm.fetchProduct()
waitForExpectations(timeout: 10) { (error) in
XCTAssertNotNil(vm.sku)
}
}
}
The "fetchDone" is this case will be whatever tells your viewController that the data has arrived. Basically looking like this:
class vc: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = vm()
viewModel.fetchDone = {
self.populate(array: viewModel.array)
}
}
func populate(array: [String]) {
}
}
class vm: NSObject {
var fetchDone: (() ->())?
var array: [String] = [] {
didSet {
fetchDone?()
}
}
func fetchProduct() {
// Do something
self.array = ["some data"]
}
}

swift 2 how to use variable outside the function?

I have a class ApiManager() that make request to JSON data in which I learned from this tutorial, which used protocol delegate approach to pass the data to ViewController class
The data is acquired fine but I am not sure how to use it around?! in this case I am trying to use it inside TableView
class ViewController: UITableViewController, ApiManagerDelegate{
var names:[String] = [] // the variable which will hold the JSON data
override func viewDidLoad() {
super.viewDidLoad()
//instantiate the ApiManager Class
//Set the ViewController as its delegate
//perform request to Restaurants info
let manager = ApiManager()
manager.delegate = self
manager.getRestaurantsData()
func didReceiveResponse (info: [String : AnyObject]){
//Read name property from data dictionary (JSON)
if let restaurantsData = info["restaurants"] as? [[String: AnyObject]]{
for restaurant in restaurantsData{
let name = restaurant["name"] as? String
self.names.append(name!)
}
}
print("Data1: \(names)") // prints the data perfectly
}
func didFailToReceiveResponse() {
print("There was an error in recieving API data")
}
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print (names) //no data here
return names.count //not working
}
I am a bit confused how to work around this, I tried to make return value to didReieveResponse(), but the issue is when I call the function it needs the argument (which is passed to it in the Delegator class "dictionary").. I am completely confused.
Here is the delegator class and protocol for reference:
import UIKit
//Custom Protocol Declaration
#objc protocol ApiManagerDelegate {
optional func didReceiveResponse(info: [ String : AnyObject ])
optional func didFailToReceiveResponse()
}
class ApiManager: NSObject, NSURLSessionDelegate {
//open restaurant web API
private let requestURL = NSURL(string:"http://some-url-here.com")
var delegate: ApiManagerDelegate?
override init() {
super.init()
}
func getRestaurantsData() {
let defaultConfigObject = NSURLSessionConfiguration.defaultSessionConfiguration()
let defaultSession = NSURLSession (configuration: defaultConfigObject, delegate: self, delegateQueue: NSOperationQueue.mainQueue ())
let request = NSMutableURLRequest(URL: requestURL!, cachePolicy: NSURLRequestCachePolicy.ReloadIgnoringCacheData, timeoutInterval: 60 )
request.HTTPMethod = "POST"
request.setValue( "application/x-www-form-urlencoded" , forHTTPHeaderField: "Content-Type" )
let dataTask = defaultSession.dataTaskWithRequest(request, completionHandler: { (data: NSData?, response: NSURLResponse?, error: NSError?) in
if let responseError = error {
self.delegate?.didFailToReceiveResponse?()
print("Reponse Error: \( responseError )" )
} else {
do {
let dictionary = try NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers) as! [String: AnyObject]
self.delegate?.didReceiveResponse?(dictionary)
//print( "Response: \( dictionary )" )
print("Response: Success")
} catch let jsonError as NSError {
// Handle parsing error
self.delegate?.didFailToReceiveResponse?()
print( "JSONError: \(jsonError.localizedDescription)")
}
}
})
dataTask.resume()
}
}
thanks,
Update:
For future Developers who might suffer like me, the solution is to use TableViewName.reloadData() as mentioned below..
But please notice, it did only worked with me when I placed DidRecieveResponse() function outside ViewDidLoad, not sure why Hopefully one of the experts can explain it later.
Enjoy!
do like
class ViewController: UITableViewController, ApiManagerDelegate{
var names:[String] = []
let manager = ApiManager()
override func viewDidLoad() {
super.viewDidLoad()
manager.delegate = self
manager.getRestaurantsData()
}
func didReceiveResponse (info: [String : AnyObject]){
//Read name property from data dictionary (JSON)
if let restaurantsData = info["restaurants"] as? [[String: AnyObject]]{
for restaurant in restaurantsData{
let name = restaurant["name"] as? String
self.names.append(name!)
}
print("Data1: \(names)") // prints the data perfectly
if (self.names.count>0)
{
yourtableview.reloadData()
}
}
}
func didFailToReceiveResponse() {
print("There was an error in recieving API data")
}

Resources