Update 12-December-2016: TABResourceLoader was created using this pattern and is production ready.
TL;DR - Define the resources in your application by conforming to protocols that define where and how to get them using a generic service type. Finally make your operations conform to a protocol to use them to retrieve your resources
With the addition of protocol extensions in Swift 2.0 it's now possible to focus less on subclassing and define your interfaces with default implementations on a protocol extension. This concept is used extensibly in the approach I currently use to a load resources into an iOS app. The sample code on this post can be downloaded from Github.
Defining a Resource
A resource can be defined as something that you application will consume. For example, it could be a JSON or XML file, an image, a video, etc. In Swift this can be represented using a protocol with an associated type:
public protocol ResourceType {
associatedtype Model
}
The
Model would be the strong type used to represent that data in your code. Specifically, we can then define a JSON resource. It conforms ResourceType and contains two parsing functions, a way of retrieving the generic Model from a JSON dictionary and a way of parsing it from a JSON array.public protocol JSONResourceType: ResourceType {
func modelFrom(jsonDictionary jsonDictionary: [String : AnyObject]) -> Model?
func modelFrom(jsonArray jsonArray: [AnyObject]) -> Model?
}
Naturally for most cases a resource will come from either a dictionary or an array so I use a protocol extension to add default implementations of this functions to return nil. When you create a new type that conforms to
JSONResourceTypeyou should provide an implementation to either of these functions.extension JSONResourceType {
public func modelFrom(jsonDictionary jsonDictionary: [String : AnyObject]) -> Model? { return nil }
public func modelFrom(jsonArray jsonArray: [AnyObject]) -> Model? { return nil }
}
The following step is to transform some
NSData into either a JSON dictionary (i.e. [String: AnyObject]) or a JSON array (i.e. [AnyObject]). This is done using the commonly known NSJSONSerialization.JSONObjectWithData. As a side note, I prefer to use a Result type as the return type since this works very well for operations where the return type is different depending whether the function succeeds or fails:public enum Result<T> {
case Success(T)
case Failure(ErrorType)
}
enum ParsingError: ErrorType {
case InvalidJSONData
case CannotParseJSONDictionary
case CannotParseJSONArray
case UnsupportedType
}
extension JSONResourceType {
func resultFrom(data data: NSData) -> Result<Model> {
guard let jsonObject = try? NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers) else {
return .Failure(ParsingError.InvalidJSONData)
}
if let jsonDictionary = jsonObject as? [String: AnyObject] {
return resultFrom(jsonDictionary: jsonDictionary)
}
if let jsonArray = jsonObject as? [AnyObject] {
return resultFrom(jsonArray: jsonArray)
}
// This is likely an impossible case since `JSONObjectWithData` likely only returns [String: AnyObject] or [AnyObject] but still needed to appease the compiler
return .Failure(ParsingError.UnsupportedType)
}
private func resultFrom(jsonDictionary jsonDictionary: [String: AnyObject]) -> Result<Model> {
if let parsedResults = modelFrom(jsonDictionary: jsonDictionary) {
return .Success(parsedResults)
} else {
return .Failure(ParsingError.CannotParseJSONDictionary)
}
}
private func resultFrom(jsonArray jsonArray: [AnyObject]) -> Result<Model> {
if let parsedResults = modelFrom(jsonArray: jsonArray) {
return .Success(parsedResults)
} else {
return .Failure(ParsingError.CannotParseJSONArray)
}
}
}
At this point we can load any
JSONResourceType from some NSData, so the next step is to get the data from somewhere. For the sake of keeping an already long post short, I'm only going to show how to retrieve it from a web service.
Before diving into the networking logic I'll introduce another protocol to define the necessary values to retrieve a
JSONResourceType from the network. In most scenarios the following 4 parameters will be sufficient to download the resource.public protocol NetworkResourceType {
var url: NSURL { get }
var HTTPMethod: String { get }
var allHTTPHeaderFields: [String: String]? { get }
var JSONBody: AnyObject? { get }
}
In a lot of cases only the URL is necessary to know where the JSON file is so we can add default values for the other three values:
public extension NetworkResourceType {
var HTTPMethod: String { return "GET" }
var allHTTPHeaderFields: [String: String]? { return nil }
var JSONBody: AnyObject? { return nil }
}
The final step is to add the ability to generate a
NSURLRequest from these parameters:extension NetworkResourceType {
func urlRequest() -> NSURLRequest {
let request = NSMutableURLRequest(URL: url)
request.allHTTPHeaderFields = allHTTPHeaderFields
request.HTTPMethod = HTTPMethod
if let body = JSONBody {
request.HTTPBody = try? NSJSONSerialization.dataWithJSONObject(body, options: NSJSONWritingOptions.PrettyPrinted)
}
return request
}
}
At this point we can define a
NetworkJSONResource by combining NetworkResource and JSONResource:public protocol NetworkJSONResourceType: NetworkResourceType, JSONResourceType {}
By conforming to
NetworkJSONResourceType we know where the location of the JSON response is and how to map it into an app-specific data type.Downloading the resource from the network
Now that we know how a
NetworkJSONResourceType looks it's time to download it! This is done using the NSURLSession.sharedSession. The first thing to do is to declare a protocol that defines how a resource service looks like. It has 2 functions, one for initialization and another for fetching the resource with a completion handler:public protocol ResourceServiceType {
associatedtype Resource: ResourceType
init()
func fetch(resource resource: Resource, completion: (Result<Resource.Model>) -> Void)
}
Then we can create a
NetworkJSONService that conforms to this ResourceServiceType to download the resource.
As an aside, before diving into the
NetworkJSONService I define a URLSessionType protocol that wraps NSURLSession's dataTaskWithRequest method, so that I can then use dependency injection to test the service by passing in a URLSessionType type.protocol URLSessionType {
func perform(request request: NSURLRequest, completion: (NSData?, NSURLResponse?, NSError?) -> Void)
}
extension NSURLSession: URLSessionType {
public func perform(request request: NSURLRequest, completion: (NSData?, NSURLResponse?, NSError?) -> Void) {
dataTaskWithRequest(request, completionHandler: completion).resume()
}
}
The implementation of the
NetworkJSONService service is very simple. In the fetch(resource:completion:)method from ResourceServiceType the URLSessionType performs a request and calls the completion handler with the Result. The handling of the response is done in the private resultFrom(resource data:URLResponse:error:) method. At this point the usual checking for errors is carried out (error checking could be more exhaustive). Finally the JSONResourceType method defined previously to convert the NSData onto the needed model is invoked.enum NetworkJSONServiceError: ErrorType {
case NetworkingError(error: NSError)
case NoData
}
public struct NetworkJSONService<Resource: NetworkJSONResourceType> {
private let session: URLSessionType
init(session: URLSessionType) {
self.session = session
}
private func resultFrom(resource resource: Resource, data: NSData?, URLResponse: NSURLResponse?, error: NSError?) -> Result<Resource.Model> {
if let error = error {
return .Failure(NetworkJSONServiceError.NetworkingError(error: error))
}
guard let data = data else {
return .Failure(NetworkJSONServiceError.NoData)
}
return resource.resultFrom(data: data)
}
}
// MARK: - ResourceServiceType
extension NetworkJSONService: ResourceServiceType {
public init() {
self.init(session: NSURLSession.sharedSession())
}
public func fetch(resource resource: Resource, completion: (Result<Resource.Model>) -> Void) {
let urlRequest = resource.urlRequest()
session.perform(request: urlRequest) { (data, _, error) in
completion(self.resultFrom(resource: resource, data: data, URLResponse: nil, error: error))
}
}
}
That's pretty much all that is needed to have a very flexible and testable networking stack where all the endpoints called are defined as resources.
Using an operation to retrieve the data from the network
If you use
NSOperations, this pattern fits very well with them. If you don't know much about them I would highly encourage watching the Advanced Operations WWDC 2016 video and checking out the sample code.
From this point I'll assume that you have a basic understanding of how to use operations. The first thing to do is define two protocols,
Cancellable to know whether the operation has been cancelled and Finishable to be called when the work in the operation has been completed. An NSOperation will conform to the first protocol, so no extra work is needed to conform to it, the second one is inspired by the Operation class in the Advanced Operations sample code where the subclass of the operation must call finish with some errors if any exist when the work is complete.public protocol Cancellable: class {
var cancelled: Bool { get }
}
public protocol Finishable: class {
func finish(errors: [NSError])
}
The next protocol defines what a
ResourceOperationType should do. This conforms to both Cancellable and Finishable, and it include an associated type that conforms to the ResourceType protocol defined previously. The other requirements are a method to fetch the associatedtype Resource using a ResourceServiceType and a method that will be called when the operation completes providing the Result of it.public protocol ResourceOperationType: Cancellable, Finishable {
associatedtype Resource: ResourceType
func fetch<Service: ResourceServiceType where Service.Resource == Resource>(resource resource: Resource, usingService service: Service)
func didFinishFetchingResource(result result: Result<Resource.Model>)
}
Noticeably, the
ResourceOperationType protocol does not provide a definition of where the Resource needs to be retrieved from, it could come form somewhere in the disk, the cloud, etc. Only it defines that it can be retrieved using a ResourceServiceType. To avoid having to write the code that fetches the resource using a service on each operation a function can be implemented on an extension on ResourceOperationType to be called in the method where the operation does it's work. (For NSOperations this can be called on the main() function).public extension ResourceOperationType {
public func fetch<Service: ResourceServiceType where Service.Resource == Resource>(resource resource:Resource, usingService service: Service) {
if cancelled { return }
service.fetch(resource: resource) { [weak self] (result) in
NSThread.executeOnMain { [weak self] in
guard let strongSelf = self else { return }
if strongSelf.cancelled { return }
strongSelf.finish([])
strongSelf.didFinishFetchingResource(result: result)
}
}
}
}
That's it! A few protocol definitions, some enums and one struct later we can create our operations that conform to
ResourceOperationType and use a NetworkJSONService to download our JSON in a concurrent way.A concrete example
After writing a lot of code and explaining the various patterns of this architecture it's time to share a concrete example to see how this works in the real world. Let's say we are downloading a list of cities from some web service where the JSON response looks like this:
{
"cities": [{
"name": "Paris"
}, {
"name": "London"
}]
}
The model in our application can be represented by the following struct:
struct City {
let name: String
}
The parsing can be done in an extension where the
City can be initialised from a JSON dictionary. (Note there are simpler ways to write the parsing logic, check out JSONUtilities for an example)extension City {
init?(jsonDictionary: [String : AnyObject]) {
guard let parsedName = jsonDictionary["name"] as? String else {
return nil
}
name = parsedName
}
}
After creating the model we need to create the
CitiesResource where we define how to decode the 'cities' key into an array of JSON dictionary cities.// This base endpoint would probably be defined somewhere globally
let baseURL = NSURL(string: "http://localhost:8000/")!
struct CitiesResource: NetworkJSONResourceType {
typealias Model = [City]
let url: NSURL
init(continent: String) {
url = baseURL.URLByAppendingPathComponent("\(continent).json")
}
//MARK: JSONResource
func modelFrom(jsonDictionary jsonDictionary: [String: AnyObject]) -> [City]? {
guard let
citiesJSONArray: [[String: AnyObject]] = jsonDictionary["cities"] as? [[String: AnyObject]]
else {
return []
}
return citiesJSONArray.flatMap(City.init)
}
}
Once all the information is defined about how to parse the
cities endpoint into an array of cities (i.e. [City]) it's time to download them. To do this I have BaseOperation which is a subclass of NSOperation where I have all the finished, executing and canceled KVO boilerplate (see the implementation on Github). Nothing fancy, the only thing needed to know is that the work is carried out on the execute() function. Again I highly encourage to watch the Advanced Operations WWDC video as this BaseOperation can be replaced by the Operation Apple provides in the sample code.
The operation is initialised with a continent (maybe something that the user selected from a table) to be able to create the
CitiesResource. To notify about the completion of the operation I use a completion handler containing the generic operation and result. final class ResourceOperation<ResourceService: ResourceServiceType>: BaseOperation, ResourceOperationType {
typealias Resource = ResourceService.Resource
typealias DidFinishFetchingResourceCallback = (ResourceOperation<ResourceService>, Result<Resource.Model>) -> Void
private let resource: Resource
private let service: ResourceService
private let didFinishFetchingResourceCallback: DidFinishFetchingResourceCallback
init(resource: ResourceService.Resource, service: ResourceService = ResourceService(), didFinishFetchingResourceCallback: DidFinishFetchingResourceCallback) {
self.resource = resource
self.service = service
self.didFinishFetchingResourceCallback = didFinishFetchingResourceCallback
super.init()
}
override func execute() {
fetch(resource: resource, usingService: service)
}
func didFinishFetchingResource(result result: Result<Resource.Model>) {
didFinishFetchingResourceCallback(self, result)
}
}
The definition of the operation for the
CitiesResource that uses a NetworkJSONService can be declared in a typealiastypealias CitiesNetworkResourceOperation = ResourceOperation<NetworkJSONService<CitiesResource>>
At this point the networking stack is complete and can be used wherever it makes sense in your application. For the sake of simplicity we'll put in a view controller following the commonly used MVC pattern (i.e. Massive View Controller):
class ViewController: UIViewController {
private let operationQueue = NSOperationQueue()
override func viewDidLoad() {
super.viewDidLoad()
let americaResource = CitiesResource(continent: "america")
let citiesNetworkResourceOperation = CitiesNetworkResourceOperation(resource: americaResource) { [weak self] operation, result in
if operation.cancelled { return }
// Handle result if operation not cancelled
}
operationQueue.addOperation(citiesNetworkResourceOperation)
}
}
Conclusions
There is very little repetition in the components built with this architecture. It's very easy to test, everything in this post apart from the
ViewController can be easily unit tested (mainly through dependency injection). The components are small and easy to reason about, meaning onboarding of new developers is simpler. The ownership of each object is simple, so once the operation is torn down, the whole stack is removed from memory. Finally, adding this pattern to a current architecture is straight forward because each resource has it's own networking stack independent of the others.
To see how this pattern can be use to load a JSON file from disk see the Github repository where the code for
LoadItcan be found.
hmmmm
ReplyDeleteWould love to see a rewrite of this using Firebase and the Codable protocol
ReplyDeleteNice and informative article.Thanks for sharing such nice article, keep on updating.
ReplyDeletePython Training in Chennai
Python Training
This comment has been removed by the author.
ReplyDeleteI really like the dear information you offer in your articles. I’m able to bookmark your site and show the kids check out up here generally. Im fairly positive theyre likely to be informed a great deal of new stuff here than anyone
ReplyDeleteClick here:
Microsoft azure training in annanagar
Click here:
Microsoft azure training in velarchery
I found your blog while searching for the updates, I am happy to be here. Very useful content and also easily understandable providing.. Believe me I did wrote an post about tutorials for beginners with reference of your blog.
ReplyDeleteClick here:
angularjs Training in online
Click here:
angularjs training in annanagar
Click here:
angularjs training in bangalore
Click here:
angularjs training in chennai
This is a nice article here with some useful tips for those who are not used-to comment that frequently. Thanks for this helpful information I agree with all points you have given to us. I will follow all of them.
ReplyDeleteBlueprism training in Chennai
Blueprism training in Bangalore
Blueprism training in Pune
Blueprism online training
Blueprism training in tambaram
We are a group of volunteers and starting a new initiative in a community. Your blog provided us valuable information to work on.You have done a marvellous job!
ReplyDeleteData Science course in rajaji nagar | Data Science with Python course in chenni
Data Science course in electronic city | Data Science course in USA
Data science course in pune | Data science course in kalyan nagar
Hmm, it seems like your site ate my first comment (it was extremely long) so I guess I’ll just sum it up what I had written and say, I’m thoroughly enjoying your blog. I as well as an aspiring blog writer, but I’m still new to the whole thing. Do you have any recommendations for newbie blog writers? I’d appreciate it.
ReplyDeleteAWS Interview Questions And Answers
AWS Training in Pune | Best Amazon Web Services Training in Pune
Amazon Web Services Training in Pune | Best AWS Training in Pune
AWS Online Training | Online AWS Certification Course - Gangboard
It is amazing and wonderful to visit your site.Thanks for sharing this information,this is useful to me...
ReplyDeleteangularjs Training in chennai
angularjs-Training in pune
angularjs-Training in chennai
angularjs Training in chennai
angularjs-Training in tambaram
angularjs-Training in sholinganallur
cattle feed bags supplier
ReplyDeleteGoyal packers and movers in Panchkula is highly known for their professional and genuine packing and moving services. We are top leading and certified relocation services providers in Chandigarh deals all over India. To get more information, call us.
ReplyDeletePackers and movers in Chandigarh
Packers and movers in Panchkula
Packers and movers in Mohali
Packers and movers in Zirakpur
Packers and movers in Patiala
Packers and movers in Ambala
Packers and movers in Ambala cantt
Packers and movers in Pathankot
Packers and movers in Jalandhar
Packers and movers in Ludhiana
I think things like this are really interesting. I absolutely love to find unique places like this. It really looks super creepy though!!
ReplyDeletemachine learning training in Chennai
machine learning training center in chennai
top institutes for machine learning in chennai
Thank you for such a wonderful blog. It's very great concept and I learn more details to your blog. I want more details from your blog.
ReplyDeleteBlue Prism Training Centers in Bangalore
Blue Prism Institute in Bangalore
Blue Prism Training Institute in Bangalore
Blue Prism Course in Adyar
Blue Prism Training in Ambattur
Blue Prism Course in Perambur
Given so much info in it, The list of your blogs are very helpful for those who want to learn more interesting facts. Keeps the users interest in the website, and keep on sharing more
ReplyDeleteOur Credo Systemz Which is designed to offer you OpenStack Training skills required to kick-start your journey as an OpenStack Cloud Administrator.
Please free to call us @ +91 9884412301 / 9600112302
Openstack course training in Chennai | best Openstack course in Chennai | best Openstack certification training in Chennai | Openstack certification course in Chennai | openstack training in chennai omr | openstack training in chennai velachery | openstack training in Chennai | openstack course fees in Chennai | openstack certification training in Chennai | best openstack training in Chennai | openstack certification in Chennai
Wow!! Really a nice Article. Thank you so much for your efforts. Definitely, it will be helpful for others. I would like to follow your blog. Share more like this. Thanks Again.
ReplyDeleteReact js training in Chennai | Best React js training institute in Chennai | Best React js training near me | React js training online
Really great blog... Thanks for your information
ReplyDeleteSelenium Course in Bangalore
selenium course in coimbatore
Java Training in Bangalore
Python Training in Bangalore
IELTS Coaching in Madurai
IELTS Coaching in Coimbatore
Java Training in Coimbatore
WE ARE PROVIDING BEST JAVA TRAINING IN COIMBATORE..PLEASE VISIT THE LINK
Deletehttps://www.appincoimbatore.com/java-training-in-coimbatore
Very useful information, if you are Looking for software courses?
ReplyDeleteLoadrunner Training in Chennai
JAVA Training in Chennai
Hadoop Training in Chennai
Selenium Training in Chennai
German Classes in chennai
web designing Training in chennai
java training in T Nagar
Advanced java training in chennai
Alot of blogs I see these days don't really provide anything that I'm interested in, but I'm most definitely interested in this one. Just thought that I would post and let you know. Nice! thank you so much! Thank you for sharing.
ReplyDeleteThanks for such a great article here. I was searching for something like this for quite a long time and at last, I’ve found it on your blog. It was definitely interesting for me to read about their market situation nowadays.angularjs best training center in chennai | angularjs training in velachery | angularjs training in chennai | angularjs training in omr
ReplyDeleteGood Post! Thank you so much for sharing this pretty post, it was so good to read and useful to improve my knowledge as updated one, keep blogging.
ReplyDeletelenovo service center in velachery
lenovo service center in porur
lenovo service center in vadapalani
Thanks for your post which gather more knowledge about this topic. I read your blog everytime which is helpful and effective.
ReplyDeleteDigital marketing training in chennai
Digital marketing course in chennai
SEO training in chennai
Thanks for your post which gather more knowledge about the topic. I read your blog everything is helpful and effective.
ReplyDeleteGerman Classes in Chennai
German Language Course in Chennai
German Language Classes in Chennai
German Courses in Chennai
German classes in Anna Nagar
German classes in Velachery
German classes in Tambaram
German classes in Adyar
I am definitely enjoying your website. You definitely have some great insight and great stories.
ReplyDeleteMicrosoft Azure online training
Selenium online training
Java online training
Python online training
uipath online training
You wrote a such a nice blog!! It is very useful for my future reference. All the info you shared with us are truly tremendous.
ReplyDeleteAngularjs Training in Chennai
Angularjs course in Chennai
Selenium Training in Chennai
Software Testing Training in Chennai
Java Training in Chennai
AngularJS Training in Anna Nagar
AngularJS Training in T Nagar
Outstanding blog!!! Thanks for sharing with us...
ReplyDeleteIELTS Coaching in Madurai
IELTS Coaching Center in Madurai
IELTS Coaching in Coimbatore
ielts coaching center in coimbatore
RPA training in bangalore
Selenium Training in Bangalore
Java Training in Madurai
Oracle Training in Coimbatore
PHP Training in Coimbatore
Blog presentation is really awesome... Thanks for your blog...
ReplyDeletebest java training in coimbatore
java classes in coimbatore
Java Course in Bangalore
Software Testing Course in Coimbatore
Spoken English Class in Coimbatore
Web Designing Course in Coimbatore
Tally Course in Coimbatore
Get The Best Repairing Service of office chairs shockers of your sofa or billiards and more other furniturea at best price.For more info visit our site given below :-
ReplyDeleteChair Repairing in nawada
Shocker Repairing in dwarka
Sofa Repairing in delhi ncr
Furniture Repairing in janakpuri
Billiards Repairing in nehru place
chair repair in dwarka
sofa repair in south delhi
shocker repair in nawada
furniture repair in nawada
norton product key activation
ReplyDeletemcafee activate enter code
activate norton with product key
AVG Antivirus phone number
Microsoft Edge Phone number
kaspersky phone number
Outlook Customer service number
comcast customer support number
Nice Blog
ReplyDeleteCheck out the best entertainment unit
thank you so much for sharing this useful message to us
ReplyDeletebest java training in chennai
best python training in chennai
selenium training in chennai
selenium training in omr
selenium training in sholinganallur
The post is written in very a good manner and it entails many useful information for me. I am happy to find your distinguished way of writing the post. Now you make it easy for me to understand and implement the concept.
ReplyDeleteReactJS Online Training
Thanks for sharing this information with us. The content was helpful to many of the readers.
ReplyDeleteEducation Franchise India
Spoken English Franchise
Franchise For Spoken English Classes
Top Education Franchise In India
Best Education Franchise In India
Computer Education Franchise
Education Franchise India
Computer Center Franchise
Education Franchise Opportunities In India
Nice blog thanks for sharing with us
ReplyDeleteforgot yahoo password
forgot aol password
forgot outlook password
at&t phone password reset
forgot roadrunner password
Nice Article!!!!
ReplyDeleteweb designing course in chennai with placement
php mysql course in chennai
magento training in chennai
Nice post..Thanks for sharing.
ReplyDeletePython training in Chennai/Python training in OMR/Python training in Velachery/Python certification training in Chennai/Python training fees in Chennai/Python training with placement in Chennai/Python training in Chennai with Placement/Python course in Chennai/Python Certification course in Chennai/Python online training in Chennai/Python training in Chennai Quora/Best Python Training in Chennai/Best Python training in OMR/Best Python training in Velachery/Best Python course in Chennai/<a
ReplyDeleteIt is very beneficial information for me, I really impressed. This is very nice article and continues sharing with us...
Unix Training in Chennai
Unix Shell Scripting Training in Chennai
Embedded System Course Chennai
Linux Training in Chennai
Corporate Training in Chennai
Job Openings in Chennai
Placement Training in Chennai
Soft Skills Training in Chennai
JMeter Training in Chennai
Appium Training in Chennai
Thanks for sharing an informative blog keep rocking bring more details.I like the helpful info you provide in your articles. I’ll bookmark your weblog and check again here regularly. I am quite sure I will learn much new stuff right here! Good luck for the next!
ReplyDeleteweb designing classes in chennai | web designing training institute in chennai
web designing and development course in chennai | web designing courses in Chennai
best institute for web designing in chennai | web designing course with placement in chennai
ReplyDeleteGet inspired by your blog. Keep doing like this....
Tally Course in Chennai
Tally training in coimbatore
Tally course in madurai
Tally Classes in Chennai
tally classes in coimbatore
tally coaching centre in coimbatore
tally training institute in coimbatore
Software Testing Training in Chennai
asdd
ReplyDeleteThanks for your valuable post... The data which you have shared is more informative for us...
ReplyDeleteaws Training in Bangalore
python Training in Bangalore
hadoop Training in Bangalore
angular js Training in Bangalore
bigdata analytics Training in Bangalore
ReplyDeleteI am obliged to you for sharing this piece of statistics
here and updating us together with your inventive steerage.
sd movies point
ReplyDeleteThanks for this wonderful blog it is really informative to all.keep update more information about this
Selenium Training in Chennai
Selenium Training in Bangalore
Selenium Training in Coimbatore
Selenium course in Chennai
Selenium Course in Bangalore
Selenium Course in Coimbatore
Software Testing Course in Chennai
Hacking Course in Bangalore
Read my post here
ReplyDeleteJava programmer
Java programmer
Java programmer
This is good information and really helpful for the people who need information about this.
ReplyDeleteGerman Classes in Chennai
German Language Course in Chennai
IELTS Coaching in Chennai
Japanese Classes in Chennai
Spoken English Classes in Chennai
TOEFL Coaching in Chennai
spanish classes in chennai
content writing course in chennai
German Classes in Velachery
German Classes in Adyar
This blog gives more attractive information.i am really impressed with this blog.
ReplyDeletePHP Training in Chennai
PHP Training in Bangalore
PHP Training in Coimbatore
PHP Course in Madurai
PHP Course in Bangalore
PHP Training Institute in Bangalore
PHP Classes in Bangalore
Best PHP Training Institute in Bangalore
DevOps Training in Bangalore
DOT NET Training in Bangalore
Thanks for sharing such a great information..Its really nice and informative..
ReplyDeletewindows azure training
Thanks for sharing this informations.
ReplyDeletehadoop training in coimbatore
C and C++ training in coimbatore
embedded training institute in coimbatore
RPA Course in coimbatore
Robotic process automation training in coimbatore
Blue prism training in coimbatore
Ui Path training in coimbatore
your blog is very nice.Devops is very important for any organization, So Devops training is must to improve yourself business.
ReplyDeletedevops training in chennai | devops training in annanagar | devops training in omr | devops training in porur | devops training in tambaram | devops training in velachery
Attend The Artificial Intelligence course From ACTE. Practical Artificial Intelligence course Sessions With Assured Placement Support From Experienced Faculty. ACTE Offers The Artificial Intelligence course
ReplyDeleteartificial-intelligence training in chennai | artificial-intelligence training in annanagar | artificial-intelligence training in omr | artificial-intelligence training in porur | artificial-intelligenceartificial-intelligence training in tambaram | artificial-intelligence training in velachery
Really nice post. Thank you for sharing Apache Spark
ReplyDeleteapache-spark training in chennai | apache-spark training in annanagar | apache-spark training in omr | apache-spark training in porur | apache-spark training in tambaram | apache-spark training in velachery
Thanks for this wonderful blog it is really informative to all.keep update more information about Amazon Web Services
ReplyDeleteamazon-web-services training in chennai | amazon-web-services training in annanagar | amazon-web-services training in omr | amazon-web-services training in porur | amazon-web-services training in tambaram | amazon-web-services training in velachery
This is really a very good article about Java. Java training in chennai Thanks for taking the time to discuss with us, I feel happy about learning this topic. keep sharing your information regularly for my future reference.
ReplyDeleteJava training in chennai | Java training in annanagar | Java training in omr | Java training in porur | Java training in tambaram | Java training in velachery