NSURLConnection does not call complete across multiple Views

前端 未结 5 526
感动是毒
感动是毒 2021-01-29 03:07

Earlier today I asked the following question: iOS block being stoped when view pushed

The operation I mentioned (OP1) is actually a \"http get\" to my server, using NSUR

5条回答
  •  轮回少年
    2021-01-29 03:43

    You might be surprised, but there are a couple of solutions - some of which are very common and can be implemented very easily ;) Even though, this answer is ridiculous elaborate, the actual solution to your problem will not exceed a few lines of code. :)

    You ran into a typical "async problem" - well, it's less than a problem, rather a typical programming task nowadays.

    What you have is an asynchronous task, OP1. This will be started from within ViewController 1 (VC1), and at some indeterminate time later, it will eventually produce either a result or an error.

    The eventual result of OP1 should be handled later in VC2.

    There are a few approaches how a client can obtain the eventual result, for example: via KVO, delegate method, completion block, callback function, future or promise and per notification.

    These approaches above have one property in common: the call-site gets notified by the asynchronous result provider (and not vice versa).

    Polling for the result until it is available, is a bad approach. Likewse, hanging in a semaphore and blocking the current thread until the result is "signaled" is equally suboptimal.

    You are probably familiar with completion blocks. A typical asynchronous method which notifies the call-site when the result is available looks like this:

    typedef void (^completion_block_t)(id result);
    
    - (void) doSomethingAsyncWithCompletion:(completion_block_t)completionHandler;
    

    Note: the call-site provides the completion handler, while the async tasks calls the block when it is finished, and passes its result (or error) to the result parameter of the block. Unless otherwise stated, the execution context - that is the thread or dispatch queue or NSOperationQueue - of where the block will be executed is not known.

    But when thinking about your problem, a simple async function and a completion handler doesn't yield a viable solution. You cannot pass that "method" easily from VC1 to VC2 and then later "attach" somehow a completion block in VC2.

    Luckily, any asynchronous task can be encapsulated into an NSOperation. An NSOperation has a completion block as a property which can be set by the call-site or elsewhere. And an NSOperation object can be easily passed from VC1 to VC2. VC2 simply adds a completion block to the operation, and eventually gets notified when its finished and the result is available.

    However, while this would be a viable solution for your problem - there are in fact a few issues with this approach - which I don't want to elaborate, but instead propose an even better one: "Promises".

    A "Promise" represents the eventual result of an asynchronous task. That is, a promise will exist even though the result of the asynchronous task is not yet evaluated. A Promise is an ordinary object which you can send messages. Thus, Promises can be passed around much like NSOperations. A Promise is the return value of an asynchronous method/function:

    -(Promise*) doSomethingAsync;
    

    Don't mismatch a Promise with the asynchronous function/method/task/operation - the promise is just a representation of the eventual result of the task.

    A Promise MUST be eventually resolved by the asynchronous task - that is, the task MUST send the promise a "fulfill" message along with the result value, or it MUST send the promise the "reject" message along with an error. The promise keeps a reference of that result value passed from the task.

    A Promise can be resolved only once!

    In order to obtain the eventual result a client can "register" a success handler and an error handler . The success handler will be called when the task fulfills the promise (that is, it was successful), and the error handler will be called when the task rejected the promise passing along the reason as an error object.

    Assuming a particular implementation of a promise, resolving a promise may look like this:

    - (Promise*) task {
        Promise* promise = [Promise new];
        dispatch_async(private_queue, ^{
            ...
            if (success) {
                [promise fulfillWithValue:result];
            }
            else {
                NSError* error = ...; 
                [promise rejectWithReason:error];
            }
        });     
        return promise;
    }
    

    A client "registers" handlers for obtaining the eventual result as follows:

    Promise* promise = [self fetchUsers];
    
    promise.then( ,  );
    

    The success handler and error handler block are declared as follows:

    typedef  id (^success_handler_block)(id result);
    typedef  id (^error_handler_block)(NSError* error);
    

    In order to just "register" a success handler (for the case, the async tasks "returns" successfully) one would write:

    promise.then(^id(id users) {
        NSLog(@"Users:", users);
        return nil;
    }, nil);
    

    If the task succeeds, the handler will be called - which prints the users to the console. When the task fails, the success handler will not be called.

    In order to just "register" an error handler (for the case, the async tasks fails) one would write:

    promise.then(nil, ^id(NSError* error) {
        NSLog(@"ERROR:", error);
        return nil;
    }, nil);
    

    If the task succeeds, the error handler will not be called. Only if the task fails (or any children tasks), this error handler will be invoked.

    When the result of the async task is eventually available, the code within the handlers will be executed "in some unspecified execution context". That means, it may execute on any thread. (Note: there are ways to specify the execution context, say the main thread).

    A promise can register more than one handler pair. You can add as many handlers as you want, and where and when you want. Now, you should understand the connection with your actual problem:

    You can start an asynchronous task in VC1, and get a promise. Then pass this promise to VC2. In VC2 you can add your handler, which will get invoked when the result is eventually available.

    Don't worry when the result is actually already available when passing the promise to VC2, that is, when the promise has been resolved already. You can still add handlers and they get fired properly (immediately).

    You can also "chain" multiple tasks - that is, invoke task2 once when task1 is finished. A "chain" or "continuation" of four async tasks looks as follows:

    Promise* task4Promise = 
    [self task1]
    .then(^id(id result1){
        return [task2WithInput:result1];
    }, nil)
    .then(^id(id result2){
        return [task3WithInput:result2];
    }, nil)
    .then(^id(id result3){
        return [task4WithInput:result3];
    }, nil);
    

    task4Promise represents the eventual result of task4WithInput:.

    One can also execute tasks in parallel, like taskB and taskC which will get started in parallel when taskA has been finished successfully:

    Promise* root = [self taskA];
    root.then(^id(id result){
        return [self taskB];
    }, nil);
    root.then(^id(id result){
        return [self taskC];
    }, nil);
    

    With this scheme, one can define an acyclic graph of tasks, where each is dependent on the successful execution of its successor ("parent"). "Errors" will be passed through to the root, and handled by the last error handler (if any).

    There are a few implementations for Objective-C. I've written one myself: "RXPromise" (available on GitHub). One of the strongest feature is "Cancellation" - which is NOT a standard feature of promises, but implemented in RXPromise. With this, you can selectively cancel a tree of asynchronous tasks.

    There is a lot more about promises. You may search the web, especially in the JavaScript community.

提交回复
热议问题