I would like to make multiple requests to server to fetch posts and comments one after another. So, I created this example with dispatch_group which fetches all the POSTS serially one after another and then after it finished with the POSTS, it fetches comments one after another.
Here is a rough schema about how this works.
Fetch post 1
Fetch post 2
Fetch post 3
....
Fetch post 50
Fetch comment 1
Fetch comment 2
...
Fetch comment 50
So, all these should work serially as shown, like it fetches post 1, finishes with that and then fetches post 2 finish and so on.
The following example works fine for the purpose. But, now I would want to have a call back to actually know when syncing of 50 posts were finished and when 50 comments were finished. I tried that by adding dispatch_group_notify after for loop in requestOne and requestTwo. But, the notify method seems to be called when all the tasks have been completed. How can that be achieved ? I am not native English speaker so, please write down if I need to improve the post, I can still try :)
#interface GroupTest ()
#property (nonatomic, readonly) dispatch_group_t group;
#property (nonatomic, readonly) dispatch_queue_t serialQueue;
#end
#implementation GroupTest
- (instancetype)init
{
if (self = [super init]) {
_group = dispatch_group_create();
_serialQueue = dispatch_queue_create("com.test.serial.queue",
DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (void)start
{
dispatch_async(self.serialQueue, ^{
[self requestOneCompletion:^{
NSLog(#"Request 1 completed");
}];
[self requestTwoCompletion:^{
NSLog(#"Request 2 completed");
}];
});
}
- (void)requestTwoCompletion:(void(^)(void))completion
{
for (NSUInteger i = 1; i <= 50; i++) {
dispatch_group_enter(self.group);
[self requestComment:i
completion:^(id response){
NSLog(#"%#", response);
dispatch_group_leave(self.group);
}];
dispatch_group_wait(self.group, DISPATCH_TIME_FOREVER);
}
}
- (void)requestOneCompletion:(void(^)(void))completion
{
for (NSUInteger i = 1; i <= 50; i++) {
dispatch_group_enter(self.group);
[self requestPost:i
completion:^(id response){
NSLog(#"%#", response);
dispatch_group_leave(self.group);
}];
dispatch_group_wait(self.group, DISPATCH_TIME_FOREVER);
}
}
- (void)requestComment:(NSUInteger)comment
completion:(void(^)(id))completion
{
NSString *urlString = [NSString stringWithFormat:#"https://jsonplaceholder.typicode.com/comments/%lu", (unsigned long)comment];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:[NSURL URLWithString:urlString]
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
id object = [NSJSONSerialization JSONObjectWithData:data
options:0
error:nil];
completion(object);
}];
[dataTask resume];
}
- (void)requestPost:(NSUInteger)post
completion:(void(^)(id))completion
{
NSString *urlString = [NSString stringWithFormat:#"https://jsonplaceholder.typicode.com/posts/%lu", (unsigned long)post];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:[NSURL URLWithString:urlString]
completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
id object = [NSJSONSerialization JSONObjectWithData:data
options:0
error:nil];
completion(object);
}];
[dataTask resume];
}
#end
I think what you want to do is the following. Note that this is made so each completion block for requestTwoCompletion and requestOneCompletion will be called after all 50 calls are done. The order of the 50 calls are not guaranteed.
The main changes I made were the dispatch_group_t is local to each method and I moved dispatch_group_wait outside of the for loop. In this case it takes away the benefit of completion since the wait will block unit it is done. If you are highly insistent on completion being used and it not blocking, you can wrap this all in a dispatch_async.
- (void)requestTwoCompletion:(void(^)(void))completion
{
dispatch_group_t group = dispatch_group_create();
for (NSUInteger i = 1; i <= 50; i++) {
dispatch_group_enter(group);
[self requestComment:i
completion:^(id response){
NSLog(#"%#", response);
dispatch_group_leave(group);
}];
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
completion();
}
- (void)requestOneCompletion:(void(^)(void))completion
{
dispatch_group_t group = dispatch_group_create();
for (NSUInteger i = 1; i <= 50; i++) {
dispatch_group_enter(group);
[self requestPost:i
completion:^(id response){
NSLog(#"%#", response);
dispatch_group_leave(group);
}];
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
}
As this is in a serial queue, the way this will work is requestOneCompletion will finish all 50, and then requestTwoCompletion will run all 50 next.
Related
I have a function that calls an API with NSURLSessionDataTask you can see it here:
- (void)getExplorerUrl:(void (^)(NSString *))measurement_url {
NSString *path = [NSString stringWithFormat:#"https://api.ooni.io/api/v1/measurements?report_id=%#&input=%#", self.report_id, self.url_id.url];
NSURL *url = [NSURL URLWithString:path];
NSURLSessionDataTask *downloadTask = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error) {
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
NSArray *resultsArray = [dic objectForKey:#"results"];
if ([resultsArray count] == 0)
measurement_url(nil);
measurement_url([[resultsArray objectAtIndex:0] objectForKey:#"measurement_url"]);
}
else {
// Fail
measurement_url(nil);
NSLog(#"error : %#", error.description);
}
}];
[downloadTask resume];
}
This function uses a completion handler to return a value when the async call is finished.
Now I want a for cycle to loop many objects and call this API for every object:
for (Measurement *measurement in [Measurement measurementsWithJson]){
[measurement getExplorerUrl:^(NSString *measurement_url) {
if (measurement_url != nil){
//Do something
NSLog(#"%# measurement_url %#",measurement.Id, measurement_url);
}
else {
NSLog(#"%# measurement_url null", measurement.Id);
}
}];
}
Is there a way to set a max concurrent async calls to 10? And then execute the next call as soon as one call finishes.
I agree with #Rob that he can create his own configuration for URLSession. However, if this sharedSession is used across different jobs and he wanted this job to run with max concurrent async calls to 10, I would suggest to use either NSOperationQueue or dispatch_semaphore to solve this problem. Please refer to the example below to have draftily understand on these approaches
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 10;
for (int i = 1; i <= 30; i++) {
[queue addOperationWithBlock:^{
NSLog(#"[Q] %d", i);
sleep(1);
}];
}
or
dispatch_queue_t q = dispatch_queue_create("q.q", DISPATCH_QUEUE_CONCURRENT);
dispatch_semaphore_t s = dispatch_semaphore_create(10);
for (int i = 1; i <= 30; i++) {
dispatch_async(q, ^{
NSLog(#"[Q] %d", i);
sleep(1);
dispatch_semaphore_signal(s);
});
}
You can observe from the console to see the results. Basically the 2 approaches will perform maximum 10 calls at the same time, and as long as one finished, others will be enter to the execution queue.
I hope this will help you to address your problem. Can have any discussion as needed.!!!
How to cancel all operations in NSOperationQueue? I used cancelAllOperations method, but it didn't work, the NSOperationQueue is still calling server to upload photo.
I put every single connection on NSOperationQueue with loop.
- (void)sendingImage:(NSArray *)imgArray compression:(CGFloat)compression
{
hud = [MBProgressHUD showHUDAddedTo: self.view animated: YES];
hud.label.text = [NSString stringWithFormat: #"Waiting for Loading"];
[hud.button setTitle: #"Cancel" forState: UIControlStateNormal];
[hud.button addTarget: self action: #selector(cancelWork:) forControlEvents: UIControlEventTouchUpInside];
__block int photoFinished = 0;
self.queue = [[NSOperationQueue alloc] init];
self.queue.maxConcurrentOperationCount = 5;
[self.queue addObserver: self forKeyPath: #"operations" options: 0 context: NULL];
NSBlockOperation *operation = [[NSBlockOperation alloc] init];
__weak NSBlockOperation *weakOperation = operation;
__block NSString *response = #"";
for (int i = 0; i < imgArray.count; i++) {
operation = [NSBlockOperation blockOperationWithBlock:^{
[self uploadingPhoto];
}];
[operation setCompletionBlock:^{
NSLog(#"Operation 1-%d Completed", i);
photoFinished++;
dispatch_async(dispatch_get_main_queue(), ^{
hud.label.text = [NSString stringWithFormat: #"%d photo complete uploading", photoFinished];
});
}];
[self.queue addOperation: operation];
}
}
I want to press cancel button on MBProgressHUD to first canceled all the NSURLSessionDataTask and then cancel all operations, but didn't work.
- (void)cancelWork:(id)sender {
NSLog(#"cancelWork");
NSLog(#"self.queue.operationCount: %lu", (unsigned long)self.queue.operationCount);
[session getTasksWithCompletionHandler:^(NSArray<NSURLSessionDataTask *> * _Nonnull dataTasks, NSArray<NSURLSessionUploadTask *> * _Nonnull uploadTasks, NSArray<NSURLSessionDownloadTask *> * _Nonnull downloadTasks) {
if (!dataTasks || !dataTasks.count) {
return;
}
for (NSURLSessionDataTask *task in dataTasks) {
[task cancel];
if ([self.queue operationCount] > 0) {
[self.queue cancelAllOperations];
}
}
}];
}
I used semaphore to let NSURLSession become Synchronous connection.
- (void)uploadingPhoto {
request setting above
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.timeoutIntervalForRequest = 1200;
session = [NSURLSession sessionWithConfiguration: config];
dataTask = [session dataTaskWithRequest: request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error == nil) {
str = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
NSLog(#"str: %#", str);
}
dispatch_semaphore_signal(semaphore);
}];
NSLog(#"task resume");
[dataTask resume];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
return str;
}
Any comments or solutions will be greatly appreciated.
An NSOperation does not by default have support for cancellation. See the class documentation. One extract is:
Canceling an operation does not immediately force it to stop what it is doing. Although respecting the value in the cancelled property is expected of all operations, your code must explicitly check the value in this property and abort as needed.
It also seems hard to implement cancellation using NSBlockOperation.
Need some help and explanation, because i'm really stuck in my question. i need to make this:
1) I make one request to the server, get some response and then i want to make another request every 7 seconds(example). also get some response. if it satisfy several conditions -> stop timer and do some stuff.
Main problem is that timer never stops, despite the fact that all in all i get response right. i assume that i use GCD incorrectly. because in debug this code behaves really strange.
What i have done:
This is my request function(it became like this after i read about 50 links how to do similar things)
-(void)makeRequestWithURL:(NSString*)urlString andParams:(NSString*)params andCompletionHandler:(void(^)(NSDictionary *responseData, NSError *error))completionHnadler{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
NSURL *url = [NSURL URLWithString:urlString];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url
cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0];
request.HTTPMethod = #"POST";
request.HTTPBody = [params dataUsingEncoding:NSUTF8StringEncoding];
NSURLSessionDataTask *postDataTask = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (completionHnadler) {
if (error) {
dispatch_async(dispatch_get_main_queue(), ^{
completionHnadler(nil, error);
});
} else {
NSError *parseError;
json = [NSJSONSerialization JSONObjectWithData:data
options:NSJSONReadingMutableContainers
error:&parseError];
dispatch_async(dispatch_get_main_queue(), ^{
completionHnadler(json, parseError);
});
}
}
}];
[postDataTask resume]; }
I create my timer like this:
dispatch_source_t CreateDispatchTimer(uint64_t interval,
uint64_t leeway,
dispatch_queue_t queue ,
dispatch_block_t block) {
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
if (timer) {
// Use dispatch_time instead of dispatch_walltime if the interval is small
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), interval, leeway);
dispatch_source_set_event_handler(timer, block);
dispatch_resume(timer);
}
return timer; }
and called it like this:
-(void)checkForPassenger {
timerSource = CreateDispatchTimer(7ull * NSEC_PER_SEC, 1ull * NSEC_PER_SEC, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if([self getNotificationsRequest] == YES) {
dispatch_source_cancel(timerSource);
} else {
NSLog(#"go on timer");
}
NSLog(#"Driver checked for passenger!");
}); }
this is the code of periodic response:
-(BOOL)getNotificationsRequest {
NSString *urlString = #"http://primetime.by/temproad/do";
NSString *params = [NSString stringWithFormat:#"event={\"type\": \"in.getNotifications\"}&session_id=%#",session_id];
[self makeRequestWithURL:urlString andParams:params andCompletionHandler:^(NSDictionary *responseData, NSError *error) {
if ([[responseData objectForKey:#"rc"] intValue] == 0) {
NSArray *temp_notifications = [responseData objectForKey:#"notifications"];
if (temp_notifications.count != 0) {
notification = [[Notification alloc] initWithNotification:[[responseData objectForKey:#"notifications"] objectAtIndex:0]];
}
}
}];
if (notification) {
return YES;
} else {
return NO;
} }
and this is what i do in main request:
[self makeRequestWithURL:urlString andParams:params andCompletionHandler:^(NSDictionary *responseData, NSError *error) {
if ([[responseData objectForKey:#"rc"] intValue] == 0) {
route = [[Route alloc] initWithData:responseData];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self checkForPassenger];
});
}
}];
NSLog(#"bye");
maybe explanation is bad so i can answer any question.
thx
I have a queue of blocks which perform a webservice call. The problem is that the downloaded data is not freed after the block's end. I read a lot about retains but I can't make ARC dealloc the memory.
Here's the code:
Create the queue of blocks which download the data
- (void)syncData
{
dispatch_semaphore_t sema = dispatch_semaphore_create(0);
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
dispatch_async(queue, ^{
[Model syncAziende:^(id response, NSError *error) {
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
[Model syncContatti:^(id response, NSError *error) {
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
[Model syncDestinazioni:^(id response, NSError *error) {
dispatch_semaphore_signal(sema);
}];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
and so on...
});
}
On Model.m
+ (void)syncAziende:(RequestFinishBlock)completation
{
__weak typeof(self)selfObject = self;
[selfObject syncData:^(id response, NSError *error) {
completation(response,error);
} wsEndPoint:kCDCEndPointGetAziende tableName:kCDCDBAziendeTableName];
}
+ (void)syncContatti:(RequestFinishBlock)completation
{
__weak typeof(self)selfObject = self;
[selfObject syncData:^(id response, NSError *error) {
completation(response,error);
} wsEndPoint:kCDCEndPointGetContatti tableName:kCDCDBContattiTableName];
}
// and so on...
Where syncData is:
+ (void)syncData:(RequestFinishBlock)completation wsEndPoint:(NSString*) url tableName:(NSString *)table
{
__weak typeof(self)selfObject = self;
[selfObject getDataFromWS:^(id WSresponse, NSError* WSError)
{
completation(nil,nil);
}WSUrl:url];
}
Where getDataFromWS is:
+ (void)getDataFromWS:(RequestFinishBlock)completation WSUrl:(NSString *)svcUrl
{
__weak typeof(self)selfObject = self;
[selfObject getJsonDataFromURL:^(id response, NSError *error)
{
completation(response,error);
}url:svcUrl];
}
Where getJsonDataFromURL is:
+(void)getJsonDataFromURL:(RequestFinishBlock)completation url:(NSString*)url
{
__weak typeof(self)selfObject = self;
__weak AFHTTPRequestOperationManager *manager = [selfObject getAuthorizedRequestionOperationManager];
[manager.requestSerializer setValue:#"application/json" forHTTPHeaderField:#"Accept"];
[manager.requestSerializer setValue:#"application/json" forHTTPHeaderField:#"Content-Type"];
[manager.requestSerializer setValue:#"gzip" forHTTPHeaderField:#"Accept-Encoding"];
[manager GET:url parameters:nil success:^(AFHTTPRequestOperation *operation, __weak id responseObject) {
completation([responseObject objectForKey:#"d"],nil);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
completation(nil,error);
}];
}
These __weak references don't really make sense within the context of a class method. It only makes sense to use the weak-self pattern when dealing with instance methods.
So, this begs the question as to which object you're concerned about not getting released. I don't see anything here that would cause the object that has syncData as an instance method to be retained.
Having said that, this routine with the series of network requests that you have made synchronous through the use of the semaphores would (a) retain any autorelease objects that were instantiated throughout the process (notably, the operations themselves); (b) not allow you to cancel the process once it starts; and (c) run the requests serially (for which you'll pay a serious performance penalty and should be avoided unless you absolutely have to).
I'd suggest a simplification of the code. I'd eliminate the dispatch code with the semaphores. I'd also get rid of all of those weak references to self in class methods.
You should simply issue your GET requests. If you really need them to run serially (as implied by your use of the semaphore), then just set the maxConcurrentOperationCount to 1 for the operationQueue of the AFHTTPRequestOperationManager instance. But don't make them run serially unless you absolutely have to, because you pay a significant performance penalty by doing that. But avail yourself of the NSOperationQueue that AFNetworking provides, rather than also doing your own GCD code (and worse, using semaphores within that GCD code).
But this way, if you want to cancel the requests, you can just call cancelAllOperations for the operationQueue of the AFHTTPRequestOperationManager. You can also control the degree of concurrency with maxConcurrentOperationCount.
Given your description and based on the comments, a naive - but still insufficient -
solution may look as follows:
// A generic completion handler:
typedef void (^completion_t)(id result, NSError* error);
- (void) fetchJSONFromURL:(NSURL*)url completion:(completion_t)completion;
- (void) syncAziendeWithInput:(id)input completion:(completion_t)completion;
- (void) syncContattiWithInput:(id)input completion:(completion_t)completion;
- (void) syncDestinazioniWithInput:(id)input completion:(completion_t)completion;
- (void) syncData
{
dispatch_queue_t sync_queue = dispatch_queue_create("sync_queue", 0); // serial queue
NSURL* url = ...;
[self fetchJSONFromURL:url completion:^(id result, NSError*error){
dispatch_async(sync_queue, ^{
[self syncAziendeWithInput:result completion:^(id result, NSError* error){
...
}];
});
}];
url = ...;
[self fetchJSONFromURL:url completion:^(id result, NSError*error){
dispatch_async(sync_queue, ^{
[self syncContattiWithInput:result completion:^(id result, NSError* error){
...
}];
});
}];
url = ...;
[self fetchJSONFromURL:url completion:^(id result, NSError*error){
dispatch_async(sync_queue, ^{
[self syncDestinazioniWithInput:result completion:^(id result, NSError* error){
...
}];
});
}];
...
}
Caveats:
Oversimplification. A practical solution becomes far more elaborated:
No error handling yet.
No way to cancel the asynchronous tasks.
syncData is asynchronous, but has no completion handler. That is, we don't know when it's complete. We could utilize dispatch_group in order to implement an approach to signal the completion of a number of asynchronous methods.
With the help of a third party library we could implement a solution which does all that above, and looks basically even simpler.
I am using a NSURLSession to get the values to populate a TableView. I am updating the TableView in the completion handler, but using [[NSThread currentThread] isMainThread] has shown me that the completion handler isn't running in the main thread. Since I should only updating the UI from the main thread, I know this isn't correct. Is there a way to trigger an action on the main thread from the completion handler? Or is using a NSURLSession the wrong way to go about this?
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:#"http://myurl"]
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSError *jsonError = nil;
NSArray* jsonUsers = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
if (jsonError) {
NSLog(#"error is %#", [jsonError localizedDescription]);
// Handle Error and return
return;
}
self.userArray = jsonUsers;
[self.userTableView reloadData];
if ([[NSThread currentThread] isMainThread]){
NSLog(#"In main thread--completion handler");
}
else{
NSLog(#"Not in main thread--completion handler");
}
}] resume];
Yes, just dispatch your main thread stuff using GCD:
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:#"http://myurl"]
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSError *jsonError = nil;
NSArray* jsonUsers = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
if (jsonError) {
NSLog(#"error is %#", [jsonError localizedDescription]);
// Handle Error and return
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
self.userArray = jsonUsers;
[self.userTableView reloadData];
if ([[NSThread currentThread] isMainThread]){
NSLog(#"In main thread--completion handler");
}
else{
NSLog(#"Not in main thread--completion handler");
}
});
}] resume];
#graver's answer is good. Here's another way you can do it:
NSURLSession* session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]
delegate:nil
delegateQueue:[NSOperationQueue mainQueue]];
[[session dataTaskWithURL:[NSURL URLWithString:#"http://myurl"]
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSError *jsonError = nil;
NSArray* jsonUsers = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
if (jsonError) {
NSLog(#"error is %#", [jsonError localizedDescription]);
// Handle Error and return
return;
}
self.userArray = jsonUsers;
[self.userTableView reloadData];
if ([[NSThread currentThread] isMainThread]){
NSLog(#"In main thread--completion handler");
}
else{
NSLog(#"Not in main thread--completion handler");
}
}] resume];
This way you create a session that calls the completion block and any delegate methods on the main thread. You may find this more aesthetically pleasing, but you do lose the advantage of running the "hard work" in the background.
Here is the best way to update UI from blocks and completion handler, and also when you not confrim which thread running your code.
static void runOnMainThread(void (^block)(void))
{
if (!block) return;
if ( [[NSThread currentThread] isMainThread] ) {
block();
} else {
dispatch_async(dispatch_get_main_queue(), block);
}
}
This is static method which will have a block, and will run on main thread, it will act like a
runOnMainThread(^{
// do things here, it will run on main thread, like updating UI
});
You can try this:
[self.userTableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO];
send a notification on completion that will be observed by the table view controller which will then do a reloadData;
this has the advantage that if you later move this download code off to a separate class, e.g. a model object, then no changes are required and also the code can be reused in other projects without making changes
Swift 3.1
DispatchQueue.main.async {
tableView.reloadData()
}