6

多くの場合、GUIでは、基本的にシーケンシャルなプログラムフローに直面しますが、非同期操作(NSURLConnectionダウンロードなど)とUIアクション(ユーザーがでオプションを選択するのを待つ)が含まれますUIActionSheet

たとえば、説明のためだけに

  1. を表示しUIActionSheet、ユーザーに色の選択を促します。
  2. 色が青の場合は、サーバーからファイルをダウンロードします。
    1. ダウンロードが失敗した場合は、ユーザーに通知します(UIAlertView、[OK]をタップするのを待ちます)
    2. ユーザーがで[はい]を選択した場合はUIAlertView、ダウンロードを再試行してください。
  3. 色が黒の場合は、別のことをしてください。
  4. などなど。

フローはシーケンシャルです。1の前に2を実行しません。ただし、非同期操作とUI要素のため、デリゲート(またはブロック)を使用すると、これはすぐにスパゲッティコードになります。

そのようなコードを書くための一般的なアプローチはありますか?

4

1 に答える 1

9

Reactive Cocoaというライブラリがあります。これは素晴らしいですが、慣れるのは難しいです。

目標を達成するためのより簡単な方法ですが、それほど素晴らしいものではありませんが、UIAlertViewとUIActionSheetの周りにブロックラッパーを使用することです。また、これは、ネットワークコードにコールバックブロックがあることを前提としています。

例:

- (void)showActionSheet
{
    BlockActionSheet *sheet = [BlockActionSheet sheetWithTitle:@"Choose one"];
    __weak BlockActionSheet *weakSheet = sheet;

    [sheet addButtonWithTitle:@"Blue" atIndex:0 block:^{
        [self downloadFileFromServerSuccessBlock:^{
            //YAY
        } failureBlock:^{
            BlockAlertView *alert = [BlockAlertView alertWithTitle:@"Failure" message:@"Something Went Wrong"];
            [alert addButtonWithTitle:@"Try Again" block:^{
                [weakSheet showInView:self.view];
            }];
            [alert setCancelButtonWithTitle:@"Cancel" block:nil];
        }];
    }];

    [sheet addButtonWithTitle:@"Black" atIndex:1 block:^{
        //something else
    }];

    [sheet setCancelButtonWithTitle:@"Cancel" block:nil];

    [sheet showInView:self.view];
}

したがって、最後の行「[sheet showInView:self.view]」はすべてを開始します。青を選択すると、そのブロックが呼び出されます。ネットワークコードはブロックもサポートしているため、そこから成功と失敗のブロックコールバックを取得します。失敗した場合は、ブロックベースのアラートビューをポップアップ表示して、ActionSheetが再び表示されるようにします。

これが少なくとも少しは役に立ったと思います。また、「self.view」呼び出しで発生している強い参照がある可能性があるため、ビューへの参照も弱くします。

これがReactiveCocoaの例です。私はこのフレームワークに非常に慣れていないので、正しく使用していることを願っています。

/*
 *  This function is getting called from a login view controller.
 */

- (void)sendAuthentication
{
    /*
     *  A RACSubscribable will invoke the where^ block when it calls [sendNext:];
     *  That call is coming from the ThaweQBRequestController.
     *  If the where^ block return YES then the subscribeNext^ block will get called.
     *  asMaybes wraps the object I send back in a RACMaybe object which has a convenience method [hasObject];
     *  The subscribeNext^ block has an object coming into it, that object is an array in this case that I sent
     *  from the ThaweQBRequestController.
     */

    RACSubscribable *sub = [[ThaweQBRequestController logInWithUsername:self.thaweusername password:self.thawepassword] asMaybes];

    [[sub where:^(id x) {
        return [x hasObject];
    }]
     subscribeNext:^(id _) {

         NSArray *array = [_ object];

         NSString *errcode = [array objectAtIndex:0];

         if (errcode.boolValue == YES)
             //YAY SUCCESS!!!

         } else {
             //UH OH
         }
         // Update UI, the array has the username and password in it if I want to display or whatever is clever.
     }];
}

次に、私が持っているネットワーク要求コントローラーに入ります。

+ (RACAsyncSubject *)logInWithUsername:(NSString *)username password:(NSString *)password
{
    /*
     *  First we have our loginCommand. It will invoke the block we give it on a background thread.
     *  This block returns a Subscribable. We can subscribe other blocks to be invoked when the aync call is done.
     *  In this case we are creating loginResult to retrieve the async call.
     */

    RACAsyncCommand *loginCommand = [RACAsyncCommand command];

    /*
     *  Now we have our AsyncSubject. We are instantiated it here and are going to pass it back to the login view controller;
     *  When our async call finishes and we filter through the results we will call [subject sendNext:] to invoke blocks we created in
     *  the login view controller;
     *  We will filter the results uses the loginResult blocks.
     */
    RACAsyncSubject *subject = [RACAsyncSubject subject];

    /*
     *  loginResult is a "Subscribable". Every time it gets a [sendNext:] call it runs the blocks assosiated with it.
     *  These [sendNext:] calls are coming from our network code.
     *  'repeat' means that even after the async block is invoked it keeps a reference to it incase we want to use it again.
     *  'asMaybes' wraps a RACMaybe object around the object you send to the loginResult blocks. The benefit of a RACMaybe is
     *  that it has some convienence methods like 'hasError' and 'hasObject'.
     */

    RACSubscribable *loginResult = [[[loginCommand
                                      addAsyncBlock:^(id _) {
                                          return [self authenticateUser:username password:password];
                                      }]
                                     repeat]
                                    asMaybes];

    /*
     *  The first block, where^, get called every time loginResult calls [sendNext:].
     *  If it returns YES then the select^ block gets called. The select^ block invokes the subscribeNext^ block and sends it the
     *  error.
     */

    [[[loginResult
       where:^(id x) {
           return [x hasError];
       }]
      select:^(id x) {
          return [x error];
      }]
     subscribeNext:^(id x) {
         NSLog(@"network error omg: %@", x);
     }];

    /*
     *  Same as above except this time we are looking for instances when we have an object instead of an NSError.
     *  The object we are getting is being returned from our network code. In this case it is an integer, 0 means we had a successfull login.
     *  Now we can call [subject sencNext:] to inform our login view controller of the pass or fail of the authentication.
     *  [sendCompleted] is a cleanup call, allowing ReactiveCocoa to dispose of our blocks and free up memory.
     */

    [loginResult
      where:^(id x) {
          return [x hasObject];
      }]
     subscribeNext:^(id _) {
         NSNumber *number;
         NSString *errcode = [_ object];
         if (errcode.intValue == 0) number = [NSNumber numberWithBool:YES] ?: [NSNumber numberWithBool:NO];
         [subject sendNext:[NSArray arrayWithObjects:number, username, password, nil]];
         [subject sendCompleted];
     }];

    /*
     *  [execute:] starts the whole thing off. This call invokes the loginCommand block that returns the async call to the loginResult subscribable.
     */

    [loginCommand execute:@"This value gets transfered to the addAsyncBlock:^(id _) block above."];

    return subject;
}

/*
 *  This function uses a QuickBase wrapper I made to make server request and whatnot.
 *  This function is called when the [loginCommand execute:] call in the previous function gets called
 *  and invokes the loginCommand block which returns the async request.
 *  That request is what this function is returning.
 */

+ (RACAsyncSubject *)authenticateUser:(NSString *)username password:(NSString *)password
{
    QBRequest *request = [[QBRequest alloc] init];

    [request setQuickBaseAction:QBActionTypeAuthenticate];

    [request setURLString:URLstring withDatabaseID:nil];

    [request setApplicationToken:appToken];

    return [request sendAndPersist:NO username:username password:password];
}

そして今、私たちは実際にネットワークラッパーにいて、リクエストがいつ完了したか失敗したかなどを認識しています。

- (RACAsyncSubject *)sendAndPersist:(BOOL)persist username:(NSString *)username password:(NSString *)password
{
    self.subject = [RACAsyncSubject subject];

    /*
     *  These next two blocks are called when my network request is done.
     *  My AsyncSubject is a property so that I can reference it later when I parse
     *  throught the response and figure out whether I logged in correctly or not.
     *  In the case that the network call itself fails, then the AysncSubject calls
     *  [sendError:] which will invoke those NSError capturing blocks in the ThaweQBRequestController.
     */

    [anOp onCompletion:^(MKNetworkOperation *completedOperation) {

        dispatch_async(background_parseSave_queue(), ^{

            [self updateDatabase];
        });

    } onError:^(NSError *error) {
        [subject sendError:error];
    }];

    [engine enqueueOperation:anOp];

    return subject;
}

そして最後に、[sendNext:]という件名がパーサーにあるときのアイデアをお伝えします。self.currentParsedCharacterDataは、整数値を持つNSStringです。

else if ([elementName isEqualToString:@"errcode"])
    {
        if ([self.action isEqualToString:@"API_Authenticate"]) {
            [subject sendNext:[self.currentParsedCharacterData copy]];
            [subject sendCompleted];
        }
    }

これは長いことは知っていますが、実際のコード例をいくつか挙げたいと思いました。

于 2012-08-22T00:39:23.563 に答える