Wednesday, 9 May 2018

Web Service Calling using Alamofire With Image uploading and downloading to AWS Server in Swift 4

Install following pod


# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'NewSwiftDemo' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for NewSwiftDemo
pod 'Alamofire', '~> 4.6’
pod 'AlamofireNetworkActivityIndicator', '~> 2.2'
pod 'SVProgressHUD'
pod 'AWSS3'

pod 'AWSCore'



 Network Manager Swift Class

//
//  NetworkManager.swift
//  NewSwiftDemo
//

//  Copyright © 2018 vp. All rights reserved.
//

import UIKit
import Alamofire
import CocoaLumberjack
import SVProgressHUD
class NetworkManager: NSObject {
    
    typealias CompletionHandler = (_ success:Any) -> Void
    typealias ErrorHandler = (_ error: NSError) -> Void
    
    //Mark: - Post Request
    func postReuestData(baseUrl: String, params: [String: String], result:@escaping CompletionHandler, failerror:@escaping ErrorHandler) -> Void {
        
        SVProgressHUD.show(withStatus: "Please Wait")
        SVProgressHUD.setDefaultMaskType(SVProgressHUDMaskType.black)

         defaultDebugLevel = DDLogLevel.info
       let headers: HTTPHeaders = getHeadersValue(urlMethod: "")
        Alamofire.request(baseUrl, method:.post, parameters:params,encoding: JSONEncoding.default, headers: headers).responseJSON { response in
            SVProgressHUD.dismiss()
            switch response.result {
            case .success:
                DDLogDebug("first response:- \(response)")
                if let status = response.response?.statusCode {
                    switch(status){
                    case 200:
                        DDLogDebug("success")
                    default:
                        DDLogDebug("error with response status: \(status)")
                    }
                }
                if let resultVal = response.result.value {
                    let JSON = resultVal as! NSDictionary
                    result(JSON)
                }
                
            case .failure(let error):
                SVProgressHUD.dismiss()
                failerror(error as NSError)
            }
        }
}
    
    // Mark :- Get Request with optional parameters
    func getReuestData(baseUrl: String, params: [String: String]?, result:@escaping CompletionHandler, failerror:@escaping ErrorHandler) -> Void {
        
        SVProgressHUD.show(withStatus: "Please Wait")
        SVProgressHUD.setDefaultMaskType(SVProgressHUDMaskType.black)
         defaultDebugLevel = DDLogLevel.info
        let headers: HTTPHeaders = getHeadersValue(urlMethod: "")
        Alamofire.request(baseUrl, method:.get, parameters:params,encoding: JSONEncoding.default, headers: headers).responseJSON { response in
              SVProgressHUD.dismiss()
            switch response.result {
            case .success:
                DDLogInfo("first response\(response)")
                if let status = response.response?.statusCode {
                    switch(status){
                    case 200:
                        DDLogDebug("success")
                    default:
                        DDLogDebug("error with response status: \(status)")
                    }
                }
                if let resultVal = response.result.value {
                    let JSON = resultVal as! NSDictionary
                    result(JSON)
                }
            case .failure(let error):
                  SVProgressHUD.dismiss()
                failerror(error as NSError)
            }
        }
        
    }
    
    
    //Mark :- Get Request Without Header values
    func getData(baseUrl: String, params: [String: String]?, result:@escaping CompletionHandler, failerror:@escaping ErrorHandler) -> Void {
        
        SVProgressHUD.show(withStatus: "Please Wait")
        SVProgressHUD.setDefaultMaskType(SVProgressHUDMaskType.black)
        
         defaultDebugLevel = DDLogLevel.info
        Alamofire.request(baseUrl, method:.get, parameters:params,encoding: JSONEncoding.default).responseJSON { response in
            SVProgressHUD.dismiss()
            switch response.result {
            case .success:
                DDLogDebug("first response\(response)")
                if let status = response.response?.statusCode {
                    switch(status){
                    case 200:
                        DDLogDebug("success")
                    default:
                        DDLogDebug("error with response status: \(status)")
                    }
                }
                if let resultVal = response.result.value {
                    let JSON = resultVal as! NSDictionary
                    result(JSON)
                }
            case .failure(let error):
                SVProgressHUD.dismiss()
                failerror(error as NSError)
            }
        }
    }
    
  func getHeadersValue(urlMethod: String) -> HTTPHeaders
  {
        // Header Values
        let headers: HTTPHeaders = [
            "Client-Service":"frontend-client",
            "Auth-Key":"beautyfaceapi",
            "Authorization":"Basic c3lzdGVtOm1hbmFnZXI=",
            "Content-Type":"application/json",
            "Token":"46QjfYKYuFYGxhjr20WyXzcvGsL2qsFMKCd7h1UuKfwzoznEhOUCJMZjvZXF8q620aNBR2so2O5GUr0J",
            "UserId":"9"
        ]
        return headers
    }

}


Singleton Class 


//
//  Singleton.swift
//  NewSwiftDemo
//

//  Copyright © 2018 vp. All rights reserved.
//

import UIKit
import Foundation

class Singleton {

    static let sharedInstance = Singleton()
    
    
    //Mark:- Check Email is valid or not
    func validateEmail(email: String) -> Bool {
        let emailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}"
        let emailTest = NSPredicate(format: "SELF MATCHES %@", emailRegex)
        return emailTest.evaluate(with: email)
    }
    
    func isPasswordValid(password : String) -> Bool{
        let passwordTest = NSPredicate(format: "SELF MATCHES %@", "^(?=.*[a-z])(?=.*[$@$#!%*?&])[A-Za-z\\d$@$#!%*?&]{8,}")
        return passwordTest.evaluate(with: password)
    }
    
    //  Mark:- check name or surname is valid or not
   func isValidNameOrSurname(Input:String) -> Bool {
        let RegEx = "\\A\\w{7,18}\\z"
        let Test = NSPredicate(format:"SELF MATCHES %@", RegEx)
        return Test.evaluate(with: Input)
    }
    
    //Mark:- check current password is same with the confirm password or not
  public func isPasswordSame(password: String , confirmPassword : String) -> Bool {
        if password == confirmPassword{
            return true
        }else{
            return false
        }
    }
    
    //Mark:- Check current password length is same with confirm password length or not
    func isPwdLenth(password: String , confirmPassword : String) -> Bool {
        if password.count <= 7 && confirmPassword.count <= 7{
            return true
        }else{
            return false
        }
    }
    
    //Mark:- check pin code is valid or not
    func isValidPincode(value: String) -> Bool {
        if value.count == 6{
            return true
        }
        else{
            return false
        }
    }
    
    //Mark:- check mobile number is valid or not
    public func validMobileNumber(inputString: String?) -> Bool {
        if (inputString?.count ?? 0) < 10 || (inputString?.count ?? 0) > 13 {
            return false
        }
        let ACCEPTABLE_CHARECTERS = "0123456789()-+"
        let cs = CharacterSet(charactersIn: ACCEPTABLE_CHARECTERS).inverted
        let filtered = inputString?.components(separatedBy: cs).joined(separator: "")
        return inputString == filtered
    }
    
    //Mark:- Alert Message with title
    public func ShowAlert(title: String, message: String, in vc: UIViewController) {
        let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertControllerStyle.alert)
        alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.default, handler: nil))
        vc.present(alert, animated: true, completion: nil)
    }
    
    //Mark:- To get Device ID
    public func getDeviceID() -> String{
        
        let deviceUUID: String = (UIDevice.current.identifierForVendor?.uuidString)!
        return deviceUUID
    }
    
    //Mark:- Retrieve value from NSUserDefaultValue
    func getNSUserDefaultValue(strKey: String) -> String {
        let strValue = UserDefaults.standard.value(forKey: strKey) as? String
        return strValue!
    }

    //Mark:- save value into NSUserDefaultValue
    func saveNSUserDefaultValue(strKey: String, value: String) {
        UserDefaults.standard.setValue(value, forKey: strKey)
        UserDefaults.standard.synchronize()
    }
    
    
}

// http://iosrevisited.blogspot.in/2017/10/convert-date-string-swift4.html

extension Date
{
    //Mark:- convert date to string with required format
    func toString( dateFormat format  : String ) -> String
    {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = format
        return dateFormatter.string(from: self)
    }
}

extension String
{
    //Mark:- convert string to date with required format
    func toDate( dateFormat format  : String) -> Date
    {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = format
        dateFormatter.timeZone = NSTimeZone(name: "UTC") as TimeZone!
        return dateFormatter.date(from: self)!
    }
    
}


Constant Class


import Foundation

struct API {
   static let contactAPI : String = "https://api.androidhive.info/contacts/"
}

struct Alert {
    
}



Model Class (Codable)



import UIKit

class ContactsModel: Codable {
    
    var address : String
    var email : String
    var gender : String
    var id : String
    var name : String
    var phone : Phone
}

class Phone: Codable {
    var mobile:String
    var home:String
    var office:String
}




AppDelegate Class


import UIKit
import CoreData
import CocoaLumberjack
import AlamofireNetworkActivityIndicator

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        NetworkActivityIndicatorManager.shared.isEnabled = true
        setupLogger()
        return true
    }

    public let fileLogger: DDFileLogger = DDFileLogger()
    
    private func setupLogger() {
        DDLog.add(DDTTYLogger.sharedInstance)    // TTY = Xcode console
        DDLog.add(DDASLLogger.sharedInstance)    // ASL = Apple System Logs
        
        
        // File logger
        fileLogger.rollingFrequency = TimeInterval(60*60*24)
        fileLogger.logFileManager.maximumNumberOfLogFiles = 7
        DDLog.add(fileLogger, with: .info)
    }
    
    func applicationWillResignActive(_ application: UIApplication) {
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    }

    func applicationWillEnterForeground(_ application: UIApplication) {
        // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
    }

    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.
    }

    func applicationWillTerminate(_ application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
        // Saves changes in the application's managed object context before the application terminates.
        self.saveContext()
    }


    // MARK: - Core Data stack

    lazy var persistentContainer: NSPersistentContainer = {
        /*
         The persistent container for the application. This implementation
         creates and returns a container, having loaded the store for the
         application to it. This property is optional since there are legitimate
         error conditions that could cause the creation of the store to fail.
        */
        let container = NSPersistentContainer(name: "NewSwiftDemo")
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                 
                /*
                 Typical reasons for an error here include:
                 * The parent directory does not exist, cannot be created, or disallows writing.
                 * The persistent store is not accessible, due to permissions or data protection when the device is locked.
                 * The device is out of space.
                 * The store could not be migrated to the current model version.
                 Check the error message to determine what the actual problem was.
                 */
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()

    // MARK: - Core Data Saving support
    func saveContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }
}



ViewController class



import UIKit
import CocoaLumberjack

class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource,UIImagePickerControllerDelegate,UINavigationControllerDelegate {

    @IBOutlet weak var txtUserName: UITextField!
    @IBOutlet weak var txtPassword: UITextField!
    @IBOutlet weak var btnLogInClicked: UIButton!
    
    @IBOutlet weak var tblContactList: UITableView!
    
    @IBOutlet weak var profileImg: UIImageView!
    
    let imagePicker = UIImagePickerController()
    
    
    //--User Defined Varible
    
    var arrContacts = [ContactsModel]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.tblContactList.delegate=self
        self.tblContactList.dataSource=self
        defaultDebugLevel = DDLogLevel.info
        imagePicker.delegate = self
        // Do any additional setup after loading the view, typically from a nib.
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func PickImage(_ sender: Any) {
        
        imagePicker.allowsEditing = false
        imagePicker.sourceType = .photoLibrary
        imagePicker.mediaTypes = UIImagePickerController.availableMediaTypes(for: .photoLibrary)!
        present(imagePicker, animated: true, completion: nil)
        
    }
    
    @IBAction func uploadImage(_ sender: Any) {
        //Mark:- upload Image
        /*
        let fileName  = "1006.jpeg"
        var strFilePath = "user/"
        var filePath = ""
        var urlFilePath : URL!
        try?   FileManager.default.createDirectory(atPath: NSTemporaryDirectory().appending(strFilePath), withIntermediateDirectories: true, attributes: nil)
        
        strFilePath = strFilePath.appending(fileName)
        filePath = NSTemporaryDirectory().appending(strFilePath)
        urlFilePath = NSURL(fileURLWithPath: filePath) as URL?
        let data = UIImageJPEGRepresentation(self.profileImg.image!, 0.5)
        try? data?.write(to: urlFilePath)
        
        let aws1 = AWSCommonController()
        aws1.aWSControllerDelegate = self as? AWSControllerDelegate
        
        print("my file path url is: \(urlFilePath)")
        aws1.uploadImage(filePath: urlFilePath, fileName: fileName, strFolder: "/user")
 */
        
        //Mark:- Download Image
        
        let fileName = "1000.jpeg"
        let strFilePath = "user/1000.jpeg"
       // var urlFilePath : URL!
        try?   FileManager.default.createDirectory(atPath: NSTemporaryDirectory().appending("user"), withIntermediateDirectories: true, attributes: nil)
        
       let filePath = NSTemporaryDirectory().appending(strFilePath)
        
        let aws2 = AWSCommonController()
        aws2.aWSControllerDelegate = self as? AWSControllerDelegate
        aws2.downloadImage(filePath: filePath, fileName: fileName, strFolder: "/user")
        
    }
    
    @IBAction func logInClicked(_ sender: Any) {
        let myResult = NetworkManager()
        
        
        myResult.getData(baseUrl:API.contactAPI, params: nil, result: {(result) in
           DDLogDebug("ohh my result is \(result)")
            
            for aContact in ((result as AnyObject).object(forKey: "contacts") as! NSArray) {
            do {
                let jsonData = try JSONSerialization.data(withJSONObject: aContact, options: .prettyPrinted)
                let reqJSONStr = String(data: jsonData, encoding: .utf8)
                let data = reqJSONStr?.data(using: .utf8)
                let jsonDecoder = JSONDecoder()
                let aContact = try jsonDecoder.decode(ContactsModel.self, from: data!)
                self.arrContacts.append(aContact)
            }
            catch {
                
            }
            }
        
            self.tblContactList.reloadData()
            
        }, failerror: {(error) in
            Singleton.sharedInstance.ShowAlert(title: "Alert", message: error.localizedDescription, in: self)
        })

        }
    
    //Mark: Table View Delegate and Datasource  Methods
    public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
        return self.arrContacts.count
    }
    
    //the method returning each cell of the list
    public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell{
        
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! ContactTableViewCell
        
        let contact: ContactsModel
        contact = arrContacts[indexPath.row]
        
        cell.lblName!.text = contact.name
        cell.lblEmail!.text = contact.phone.mobile
    
        //displaying image
       /* Alamofire.request(hero.imageUrl!).responseImage { response in
            debugPrint(response)
            
            if let image = response.result.value {
                cell.heroImage.image = image
            }
        }*/
        
        return cell
    }
    public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 110.0
    }
    
    internal func imagePickerController(_ picker: UIImagePickerController,
                               didFinishPickingMediaWithInfo info: [String : Any])
    {
        let chosenImage = info[UIImagePickerControllerOriginalImage] as! UIImage //2
        profileImg.contentMode = .scaleAspectFit //3
        profileImg.image = chosenImage //4
        dismiss(animated:true, completion: nil) //5
    }
    
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        dismiss(animated: true, completion: nil)
    }
}



Tableview Cell Class 

//
//  ContactTableViewCell.swift
//  NewSwiftDemo
//
//  Created by BhagyashreeM on 18/04/18.
//  Copyright © 2018 vp. All rights reserved.
//

import UIKit

class ContactTableViewCell: UITableViewCell {

    @IBOutlet weak var lblName: UILabel?
    @IBOutlet weak var lblEmail: UILabel?
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }

}



Bridging File for SVProgress Hud

//
//  NewSwiftDemo-Bridging-Header.m
//  NewSwiftDemo
//

//  Copyright © 2018 vp. All rights reserved.
//

#import <Foundation/Foundation.h>

// Mark :- SVProgress Hud Library Bridging
#import <SVProgressHUD/SVProgressHUD.h>
#import <SVProgressHUD/SVProgressAnimatedView.h>
#import <SVProgressHUD/SVRadialGradientLayer.h>
#import <SVProgressHUD/SVProgressHUD-umbrella.h>




AWSCommonController 



//
//  AWSCommonController.swift
//  NewSwiftDemo
//

//  Copyright © 2018 vp. All rights reserved.
//

import UIKit
import AWSCore
import AWSS3

@objc protocol AWSControllerDelegate : class {
    
    //Mark:- Optional Methods to call after upload and Download Request 
    @objc optional func didFinishUploadingFile(strMessge: String?)
    @objc optional func didFailedUploadingFile (strMessage:String?)
    @objc optional func didFinishDownloadingFile(strMessge: String?, filePath: URL?)
    @objc optional func didFailedDownloadingFile (strMessage : String)

}

class AWSCommonController: NSObject {

    //Mark:- AWSControllerDelegate for object comunication.
     weak var aWSControllerDelegate : AWSControllerDelegate?
    
    //Mark:- AWS Setup Configuration
    override init() {
        let credentialsProvider = AWSCognitoCredentialsProvider(regionType: AWSRegionType.EUWest1, identityPoolId: "POOL_ID")
        let configuration = AWSServiceConfiguration(region: AWSRegionType.EUWest1, credentialsProvider: credentialsProvider)
        AWSServiceManager.default().defaultServiceConfiguration = configuration
    }
   
    // Mark:- Function for upload image request
    func uploadImage(filePath: URL!, fileName: String?,strFolder: String!) {
        let bucketName : String =  "BucketName"
        let uploadRequest = AWSS3TransferManagerUploadRequest()!
        uploadRequest.body = filePath
        uploadRequest.contentType = "image/png"
        uploadRequest.key = fileName
        uploadRequest.bucket = bucketName.appending(strFolder)
        self.upload(uploadRequest: uploadRequest)
    }
    
    //Mark:- Function for download image Request
    func downloadImage(filePath: String?, fileName: String?,strFolder: String!) {
        let bucketName : String =  "BucketName"
        let downloadRequest = AWSS3TransferManagerDownloadRequest()!
        downloadRequest.key = fileName
        downloadRequest.downloadingFileURL = URL.init(fileURLWithPath: filePath!)
        downloadRequest.bucket = bucketName.appending(strFolder)
        self.download(downloadRequest: downloadRequest)
}
    //Mark:- upload Image
    func upload(uploadRequest:AWSS3TransferManagerUploadRequest?)  {
        
        let transferManager = AWSS3TransferManager.default()
        transferManager.upload(uploadRequest!).continueWith { (task:AWSTask) -> Any? in
            if task.error != nil{
                print("Error: \(String(describing: task.error?.localizedDescription))")
            }else{
                print("upload Successfull")
            }
            return nil
        }
    }

    //Mark:- Download Image
    func download(downloadRequest : AWSS3TransferManagerDownloadRequest) {
        
        let transferManager = AWSS3TransferManager.default()
        transferManager.download(downloadRequest).continueWith { (task: AWSTask) -> Any? in
            if task.error != nil{
               print("Error: \(String(describing: task.error?.localizedDescription))")
            }else{
                print("download Successfull")
                print("download request url is: \(downloadRequest.downloadingFileURL)")
            }
            return nil
        }
    }
    
    //Mark:- Copy Image from one place to other
    func copyFile()  {
        let s3 = AWSS3.default()
        let replicateRequest = AWSS3ReplicateObjectRequest()
        let destinationString = ""
        replicateRequest?.bucket = destinationString
        let sourceString = ""
        replicateRequest?.replicateSource = sourceString
        replicateRequest?.acl = AWSS3ObjectCannedACL.publicReadWrite
        s3.replicateObject(replicateRequest!, completionHandler: {(response: AWSS3ReplicateObjectOutput?,error: Error?) -> Void in
            if error != nil {
                print("error occur : \((error?.localizedDescription)!)")
            }
        })
    }
}



AWSImageView Class


//
//  AWSImageView.swift
//  NewSwiftDemo
//

//  Copyright © 2018 vp. All rights reserved.
//

import UIKit

@objc protocol AWSImageViewDelegate : class {
    
    //Mark:- Optional Function
    @objc optional func didFinishDownloadingFile()
    @objc optional func didFailedDownloadingFile(view : UIView?)

}
class AWSImageView: UIImageView {

    //Mark:- AWSImageView Delegate to communicate with the object
   var awsImageDelegate = AWSImageViewDelegate.self
    
    //Mark: - Download Image
    func downloadImage(fileName: String!,strFilePath: String!,strFolder: String?, strPlaceholder: String?) {
        var strPath = ""
        try?   FileManager.default.createDirectory(atPath: NSTemporaryDirectory().appending(strFilePath), withIntermediateDirectories: true, attributes: nil)
        let strLocalFilePath = strFilePath.appending(fileName)
        strPath =  NSTemporaryDirectory().appending(strLocalFilePath)
        let aws1 = AWSCommonController()
        aws1.aWSControllerDelegate = self as? AWSControllerDelegate
        aws1.downloadImage(filePath: strFilePath, fileName: fileName, strFolder: strFolder)
    }
    
    //Mark:- Function for calling after file downloading is finished
    func didFinishDownloadingFile(strMessage:String? , filePath : URL!)  {
     print("downloading file is finished")
    }
    
    //Mark:- Function for calling when downloading file is failed
    func didFailedDownloadingFile(strMessage: String?)  {
          print("failed to download file")
    }
}





No comments:

Post a Comment

Dependency Manager in iOS

What is Dependency Management? When working in iOS, there are plenty of amazing third party libraries that we can use to save us time an...