14

コンテンツを作成して既存のバックエンドに送信するアプリケーションに取り組んでいます。内容は、タイトル、写真、場所です。派手なものはありません。

バックエンドは少し複雑なので、私がしなければならないことは次のとおりです。

  • ユーザーが写真を撮り、タイトルを入力し、マップがその場所を使用することを許可します
  • 投稿の一意の識別子を生成します
  • バックエンドで投稿を作成する
  • 写真をアップロードする
  • UI を更新する

これを機能させるためにいくつかの NSOperation サブクラスを使用しましたが、自分のコードを誇りに思っているわけではありません。ここにサンプルがあります。

NSOperation *process = [NSBlockOperation blockOperationWithBlock:^{
    // Process image before upload
}];

NSOperation *filename = [[NSInvocationOperation alloc] initWithTarget: self selector: @selector(generateFilename) object: nil];

NSOperation *generateEntry = [[NSInvocationOperation alloc] initWithTarget: self selector: @selector(createEntry) object: nil];

NSOperation *uploadImage = [[NSInvocationOperation alloc] initWithTarget: self selector: @selector(uploadImageToCreatedEntry) object: nil];

NSOperation *refresh = [NSBlockOperation blockOperationWithBlock:^{
    // Update UI
    [SVProgressHUD showSuccessWithStatus: NSLocalizedString(@"Success!", @"Success HUD message")];
}];

[refresh addDependency: uploadImage];

[uploadImage addDependency: generateEntry];
[generateEntry addDependency: filename];
[generateEntry addDependency: process];

[[NSOperationQueue mainQueue] addOperation: refresh];
[_queue addOperations: @[uploadImage, generateEntry, filename, process] waitUntilFinished: NO];

ここに私が好きではないものがあります:

  • 私のcreateEntry:たとえば、生成されたファイル名をプロパティに保存しています。これは、クラスのグローバルスコープと一致しています
  • uploadImageToCreatedEntry: メソッドでは、dispatch_async + dispatch_get_main_queue() を使用して、HUD のメッセージを更新しています。

このようなワークフローをどのように管理しますか? 複数の完了ブロックを埋め込むことは避けたいと思います.NSOperationが本当に進むべき道だと感じていますが、どこかにもっと良い実装があると感じています.

ありがとう!

4

4 に答える 4

16

ReactiveCocoaを使用すると、これを非常に簡単に実現できます。その大きな目標の 1 つは、この種の構成を簡単にすることです。

ReactiveCocoa について聞いたことがない場合、またはよく知らない場合は 、簡単な説明の概要を確認してください。

ここでフレームワーク全体の概要を複製することは避けますが、RAC は実際に約束/未来のスーパーセットを提供すると言えば十分です。まったく異なるオリジン (UI、ネットワーク、データベース、KVO、通知など) のイベントを構成および変換でき、非常に強力です。

このコードの RAC 化を開始するには、最初にできる最も簡単な方法は、これらの個別の操作をメソッドに入れ、それぞれがRACSignal. これは厳密に必要というわけではありませんが (それらはすべて 1 つのスコープ内で定義できます)、コードがよりモジュール化され、読みやすくなります。

たとえば、processと に対応するいくつかの信号を作成してみましょうgenerateFilename

- (RACSignal *)processImage:(UIImage *)image {
    return [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id<RACSubscriber> subscriber) {
        // Process image before upload

        UIImage *processedImage = …;
        [subscriber sendNext:processedImage];
        [subscriber sendCompleted];
    }];
}

- (RACSignal *)generateFilename {
    return [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id<RACSubscriber> subscriber) {
        NSString *filename = [self generateFilename];
        [subscriber sendNext:filename];
        [subscriber sendCompleted];
    }];
}

他の操作 (createEntryおよびuploadImageToCreatedEntry) は非常に似ています。

これらを配置したら、それらを構成して依存関係を表現するのは非常に簡単です (ただし、コメントが少し密集しているように見えます)。

[[[[[[self
    generateFilename]
    flattenMap:^(NSString *filename) {
        // Returns a signal representing the entry creation.
        // We assume that this will eventually send an `Entry` object.
        return [self createEntryWithFilename:filename];
    }]
    // Combine the value with that returned by `-processImage:`.
    zipWith:[self processImage:startingImage]]
    flattenMap:^(RACTuple *entryAndImage) {
        // Here, we unpack the zipped values then return a single object,
        // which is just a signal representing the upload.
        return [self uploadImage:entryAndImage[1] toCreatedEntry:entryAndImage[0]];
    }]
    // Make sure that the next code runs on the main thread.
    deliverOn:RACScheduler.mainThreadScheduler]
    subscribeError:^(NSError *error) {
        // Any errors will trickle down into this block, where we can
        // display them.
        [self presentError:error];
    } completed:^{
        // Update UI
        [SVProgressHUD showSuccessWithStatus: NSLocalizedString(@"Success!", @"Success HUD message")];
    }];

一部のメソッドの名前を変更して、依存関係からの入力を受け入れることができるようにしたことに注意してください。これにより、ある操作から次の操作に値をフィードするより自然な方法が得られます。

ここには大きな利点があります。

  • トップダウンで読むことができるので、物事が起こる順序や依存関係がどこにあるのかを理解するのは非常に簡単です.
  • の使用によって証明されるように、異なるスレッド間で作業を移動するのは非常に簡単です-deliverOn:
  • これらのメソッドのいずれかによって送信されたエラーは、残りのすべての作業を自動的にキャンセルし、最終的にsubscribeError:簡単に処理できるようにブロックに到達します。
  • これを他のイベント ストリーム (つまり、操作だけでなく) と組み合わせることもできます。たとえば、UI シグナル (ボタンのクリックなど) が発生した場合にのみトリガーするように設定できます。

ReactiveCocoa は巨大なフレームワークであり、残念ながらその利点を小さなコード サンプルに要約するのは困難です。どのような場合に ReactiveCocoa を使用するかの例を確認して、ReactiveCocoa がどのように役立つかについて詳しく知ることを強くお勧めします。

于 2013-09-19T17:43:09.343 に答える
7

いくつかの考え:

  1. おそらく、前の操作が成功した場合にのみ次の操作を開始したいので、完了ブロックを利用する傾向があります。エラーを適切に処理し、失敗した場合に一連の操作を簡単に中断できるようにする必要があります。

  2. 操作から別の操作にデータを渡したいが、呼び出し元のクラスのプロパティを使用したくない場合は、必要なフィールドを含むパラメーターを持つカスタム操作のプロパティとして、独自の完了ブロックを定義する可能性があります。ある操作から別の操作に渡す。NSOperationただし、これはサブクラス化を行っていることを前提としています。

    たとえばFilenameOperation.h、操作サブクラスのインターフェイスを定義するがあるとします。

    #import <Foundation/Foundation.h>
    
    typedef void (^FilenameOperationSuccessFailureBlock)(NSString *filename, NSError *error);
    
    @interface FilenameOperation : NSOperation
    
    @property (nonatomic, copy) FilenameOperationSuccessFailureBlock successFailureBlock;
    
    @end
    

    同時操作でない場合、実装は次のようになります。

    #import "FilenameOperation.h"
    
    @implementation FilenameOperation
    
    - (void)main
    {
        if (self.isCancelled)
            return;
    
        NSString *filename = ...;
        BOOL failure = ...
    
        if (failure)
        {
            NSError *error = [NSError errorWithDomain:... code:... userInfo:...];
            if (self.successFailureBlock)
                self.successFailureBlock(nil, error);                                                    
        }
        else
        {
            if (self.successFailureBlock)
                self.successFailureBlock(filename, nil);
        }
    }
    
    @end
    

    明らかに、同時操作がある場合は、すべての標準isConcurrentの 、isFinishedおよびisExecutingロジックを実装しますが、考え方は同じです。余談ですが、成功または失敗をメイン キューにディスパッチする人もいるので、必要に応じてそれを行うこともできます。

    とにかく、これは、適切なデータを渡す独自の完了ブロックを持つカスタム プロパティのアイデアを示しています。関連する操作の種類ごとにこのプロセスを繰り返すことができます。次に、次のように、それらをすべて連鎖させることができます。

    FilenameOperation *filenameOperation = [[FilenameOperation alloc] init];
    GenerateOperation *generateOperation = [[GenerateOperation alloc] init];
    UploadOperation   *uploadOperation   = [[UploadOperation alloc] init];
    
    filenameOperation.successFailureBlock = ^(NSString *filename, NSError *error) {
        if (error)
        {
            // handle error
            NSLog(@"%s: error: %@", __FUNCTION__, error);
        }
        else
        {
            generateOperation.filename = filename;
            [queue addOperation:generateOperation];
        }
    };
    
    generateOperation.successFailureBlock = ^(NSString *filename, NSData *data, NSError *error) {
        if (error)
        {
            // handle error
            NSLog(@"%s: error: %@", __FUNCTION__, error);
        }
        else
        {
            uploadOperation.filename = filename;
            uploadOperation.data     = data;
            [queue addOperation:uploadOperation];
        }
    };
    
    uploadOperation.successFailureBlock = ^(NSString *result, NSError *error) {
        if (error)
        {
            // handle error
            NSLog(@"%s: error: %@", __FUNCTION__, error);
        }
        else
        {
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                // update UI here
                NSLog(@"%@", result);
            }];
        }
    };
    
    [queue addOperation:filenameOperation];
    
  3. より複雑なシナリオでのもう 1 つのアプローチは、サブクラスに標準メソッドの動作にNSOperation類似した手法を採用させることです。この手法では、他の操作でKVO に基づいて状態を設定します。これにより、操作間のより複雑な依存関係を確立できるだけでなく、それらの間でデータベースを渡すこともできます。これはおそらくこの質問の範囲を超えています (そして、私はすでに tl:dr に苦しんでいます) が、ここでさらに必要な場合はお知らせください。addDependencyNSOperationisReadyisFinished

  4. uploadImageToCreatedEntryメインスレッドにディスパッチされていることはあまり心配しません。複雑な設計では、特定の種類の操作専用のさまざまなキューがあり、UI の更新がメイン キューに追加されるという事実は、このモードと完全に一致しています。しかし、代わりに、同等のものdispatch_asyncを使用する傾向があるかもしれません:NSOperationQueue

    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        // do my UI update here
    }];
    
  5. これらすべての操作が必要なのだろうか。たとえば、filename独自の操作を正当化するのに十分に複雑であると想像するのに苦労しています(ただし、リモートソースからファイル名を取得している場合は、別の操作が完全に理にかなっています)。あなたはそれを正当化するのに十分複雑なことをしていると思いますが、それらの操作の名前は不思議に思います。

  6. 必要に応じて、promise を使用して (a) 個別の操作間の論理関係を制御する、couchdeveloperRXPromiseのクラスを見てみることをお勧めします。(b) あるデータから次のデータへの受け渡しを簡素化します。Mike Ash には、同じことを行う古いクラスがあります。MAFuture

    どちらも自分のコードで使用することを考えるほど十分に成熟しているかどうかはわかりませんが、興味深いアイデアです。

于 2013-09-18T16:41:50.663 に答える
3

私はおそらく完全に偏見があります-しかし、特定の理由で-@Robのアプローチ#6が好きです;)

完了ブロックで完了を通知する代わりに、Promise を返す非同期メソッドと操作用の適切なラッパーを作成したと仮定すると、ソリューションは次のようになります。

RXPromise* finalResult = [RXPromise all:@[[self filename], [self process]]]
.then(^id(id filenameAndProcessResult){
    return [self generateEntry];
}, nil)
.then(^id(id generateEntryResult){
    return [self uploadImage];
}, nil)
.thenOn(dispatch_get_main_queue() , ^id(id uploadImageResult){
    [self refreshWithResult:uploadImageResult];
    return nil;
}, nil)
.then(nil, ^id(NSError*error){
    // Something went wrong in any of the operations. Log the error:
    NSLog(@"Error: %@", error);
});

そして、非同期シーケンス全体をいつでもどこでもキャンセルしたい場合は、それがどれだけ進んでも:

[finalResult.root cancel];

(小さな注意: プロパティrootは RXPromise の現在のバージョンではまだ使用できませんが、基本的に実装は非常に簡単です)。

于 2013-09-18T22:21:13.547 に答える