2013-05-27 12 views
5

Also habe ich gerade erst mit ReactiveCocoa angefangen, und ich dachte, der beste Weg zu lernen wäre, einfach hineinzuspringen und den bestehenden Code, den ich habe, umzubauen. Ich wollte etwas Kritik bekommen und sicherstellen, dass ich in die richtige Richtung gehe.Refactoring in ReactiveCocoa

So in der App ich Refactoring bin, habe ich eine Menge Code, der so geht:

[self.ff getArrayFromUri:@"/States?sort=name asc" onComplete:^(NSError *theErr, id theObj, NSHTTPURLResponse *theResponse) { 
    if(!theErr) { 
     //do something with theObj 
    } 
    else { 
     //handle the error 
    } 
}]; 

ich zur Zeit habe dies wie so in ReactiveCocoa Refactoring:

-(void)viewDidLoad { 
//ReactiveCocoa 
RACCommand *command = [RACCommand command]; 
RACSubject *subject = [RACSubject subject]; 
[[[command addSignalBlock:^RACSignal *(id value) { 
    NSError *err; 
    NSArray *array = [self.ff getArrayFromUri:@"/States" error:&err]; 
    err ? [subject sendError:err] : [subject sendNext:array]; 
    return [RACSignal empty]; 
}]switchToLatest]deliverOn:[RACScheduler mainThreadScheduler]]; 

[subject subscribeNext:^(NSArray *x) { 
    [self performSegueWithIdentifier:kSomeSegue sender:x]; 
} error:^(NSError *error) { 
    NSLog(@"the error = %@", error.localizedDescription); 
}]; 

self.doNotLocation = [UIButton buttonWithType:UIButtonTypeCustom]; 
[self.doNotLocation setBackgroundImage:[UIImage imageNamed:@"BlackButton_small.png"] forState:UIControlStateNormal]; 
[[self.doNotLocation rac_signalForControlEvents:UIControlEventTouchUpInside] executeCommand:command]; 
RAC(self.doNotLocation.enabled) = RACAbleWithStart(command, canExecute); 
RAC([UIApplication sharedApplication],networkActivityIndicatorVisible) = RACAbleWithStart(command, executing); } 

Ist das etwa Wie sollte ich mit dem RAC Subject verfahren oder gibt es einen besseren Weg? Dieses ganze Konzept ist neu für mich, da meine einzigen Programmiersprachen bisher Java und Objective-C waren, so dass mich diese funktionale, reaktive Denkweise ein wenig aus dem Konzept wirft.

Antwort

11

Leider gibt es ein paar Probleme mit dem Codebeispiel Ihnen präsentiert:

  1. Der Block zu -addSignalBlock: geben wird ein leeres Signal zurück. Das sollte eine Warnflagge sein, da es fast nie Junk-Return-Werte gibt. In diesem Fall bedeutet dies, dass der Block seine Arbeit synchron ausführt. Um das Blockieren des Hauptthreads zu vermeiden, sollten Sie ein Signal erstellen, das asynchron funktioniert, und es zurückgeben.
  2. Die -switchToLatest und -deliverOn: machen nichts. Die meisten Signaloperatoren funktionieren nur, wenn das resultierende Signal abonniert ist. In diesem Fall verschwindet es einfach im Äther.

Wir können beide Probleme auf einmal lösen. -addSignalBlock: gibt ein Signal von die Signale zurück, die im Block zurückgegeben werden. Wenn wir etwas Bedeutungsvolles zurückgeben, kann es außerhalb dieser Methode behandelt werden.

Zunächst einmal muss diese nach oben hinzugefügt werden:

@weakify(self); 

Wenn @strongify(self) unten verwendet wird, wird es a retain cycle verhindern. Dies ist notwendig, weil die RACCommand so lange wie self lebt.

nun die Schaffung der inneren Signale:

RACSignal *requestSignals = [command addSignalBlock:^(id value) { 
    return [RACSignal start:^(BOOL *success, NSError **err) { 
     @strongify(self); 

     NSArray *array = [self.ff getArrayFromUri:@"/States" error:err]; 
     *success = (array != nil); 

     return array; 
    }]; 
}]; 

Innerhalb des Blocks erzeugt dies einfach ein Signal, das seine Ergebnisse -getArrayFromUri:error: und übergeben Sie zurück oder einen Fehler aufrufen wird, wenn man aufgetreten. +start: wird dafür sorgen, dass die Arbeit im Hintergrund abläuft.

Aus all dem erhalten wir , die ein Signal dieser erstellten Signale ist. Dies kann vollständig ersetzt die verwendeten RACSubject ursprünglich:

RACSignal *arrays = [[[requestSignals 
    map:^(RACSignal *request) { 
     return [request catch:^(NSError *error) { 
      NSLog(@"the error = %@", error); 
      return [RACSignal empty]; 
     }]; 
    }] 
    flatten] 
    deliverOn:RACScheduler.mainThreadScheduler]; 

Zuerst wir verwandeln jedes inneres Signal zu protokollieren, dann ignorieren, Fehler. (Es ist ein wenig kompliziert, aber ein RAC-Operator might be added, es in der Zukunft zu tun.)

Dann wir flatten das Signal von Signalen. Das Ergebnis arrays ist ein Signal, das die Werte der inneren Signale durchläuft. Deshalb mussten wir Fehler ignorieren - wenn einer von ihnen diesen Punkt erreichte, würden wir für immer alle Werte aus den inneren Signalen verlieren.

Schließlich haben wir „Lift“ der Wähler aufzurufen:

[self rac_liftSelector:@selector(performSegueWithIdentifier:sender:) withObjects:kSomeSegue, arrays]; 

Dies wird -performSegueWithIdentifier:sender: erneut senden, wenn arrays einen neuen Wert sendet (der ein NSArray vom Netz zurückgegeben werden). Sie können daran denken, wie eine Methode im Laufe der Zeit aufrufen. Dies ist better than a subscription, weil es Nebenwirkungen und Speicherverwaltung vereinfacht.

+0

Danke für die Antwort, ich weiß es zu schätzen. Ich erhalte ein Problem mit Ihrem bereitgestellten Code, in der vorletzten Probe. '[request catch:]' sagt, dass es ein 'RACSignal' zurückgeben muss und nicht ungültig ist. Fehle ich da gerade etwas? –

+0

Ah, tut mir leid, du hast vollkommen recht. Sie können einfach "[RACSignal empty]" zurückgeben (um anzuzeigen, dass das Fehlersignal durch ein sofort erfolgreiches ersetzt werden soll). Ich werde meine Antwort aktualisieren. –

+0

Ok, also ich habe alles aktualisiert, und es funktioniert, außer ich bekomme eine erhebliche Verzögerung (ca. 3 Sekunden) und auch es scheint den Haupt-Thread blockiert (alle Benutzerinteraktion reagiert nicht) von dem Zeitpunkt an, als ich auf die Schaltfläche tippen wenn 'prepareForSegue: sender:' aufgerufen wird. Wenn ich jedoch "RACSignal starWithScheduler: block" mache und "[RACScheduler mainThreadScheduler]" für den Scheduler übergebe, wird es wie erwartet ausgeführt. Also was fehlt mir hier? Ehrlich gesagt, fühle ich mich durch dieses Zeug wie ein Idiot, vielleicht sollte ich zurück zu "Haskell for pre-K" gehen, bevor ich mich mit diesem Zeug beschäftige. –

1

In meiner Erfahrung mit dem Framework habe ich festgestellt, dass es sehr wenig Grund gibt, RACSubject direkt zu verwenden, besonders für einmalige Signale wie diese. RACSubjects stellen veränderbare Signale dar, die Sie in diesem Fall nicht benötigen und die Komplexität Ihres Codes erhöhen können. Es wäre viel besser, wenn Sie ein Vanille-Signal zurückzukehren (durch +[RACSignal createSignal:]) innerhalb dieses Befehlsblock, dann haben die resultierenden Anforderungscode, den Körper des Signals bilden:

[[[command addSignalBlock:^RACSignal *(id value) { 
    // 
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { 
     //Request code here 
      return nil; 
    }]; 
}]switchToLatest]deliverOn:[RACScheduler mainThreadScheduler]]; 

Oder noch besser, können Sie Refactoring getArrayFromUri:error: ein Signal zurück und dieser ternäre Anweisung loszuwerden:

[[[command addSignalBlock:^RACSignal *(id value) { 
    return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { 
     //... 
     [[self getArrayFromUri:@"/States"]subscribeError:^(NSError *error) { 
      [subscriber sendError:error]; 
     } completed:^{ 
      [subscriber sendNext:array]; 
     }]; 
      return nil; 
     }]; 
    }]switchToLatest]deliverOn:RACScheduler.mainThreadScheduler]; 

Was das Problem des Abonnements in der nächsten Zeile diejenigen Nebenwirkungen des Signals ist, können wir sie in Betracht gezogen werden können, so stellen explizit so mit die entsprechenden Varianten von do: gelten für das Signal für den Befehl:

[[[command addSignalBlock:^RACSignal *(id value) { 
     return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { 
      [[[[self getArrayFromUri:@"/States"]doError:^(NSError *error) { 
       NSLog(@"the error = %@", error.localizedDescription); 
       [subscriber sendError:err]; 
      }] doNext:^(NSArray *array) { 
       [subscriber sendNext:array]; 
       [self performSegueWithIdentifier:kSomeSegue sender:array]; 
      }] subscribeCompleted:^{ 
       [subscriber sendCompleted]; 
      }]; 
      return [RACDisposable disposableWithBlock:^{ 
       // Cleanup 
      }]; 
     }]; 
    }]switchToLatest]deliverOn:[RACScheduler mainThreadScheduler]]; 

Schließlich weil Befehle ein anders aus Signalen arbeiten, werden die äußersten Betreiber nicht ausgewertet (danke, @jspahrsummers), so dass Sie sie entfernen können.

[command addSignalBlock:^RACSignal *(id value) { 
      return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { 
       [[[[self getArrayFromUri:@"/States"]doError:^(NSError *error) { 
        NSLog(@"the error = %@", error.localizedDescription); 
        [subscriber sendError:err]; 
       }] doNext:^(NSArray *array) { 
        [subscriber sendNext:array]; 
        [self performSegueWithIdentifier:kSomeSegue sender:array]; 
       }] subscribeCompleted:^{ 
        [subscriber sendCompleted]; 
       }]; 
       return [RACDisposable disposableWithBlock:^{ 
        // Cleanup 
       }]; 
      }]; 
     }]; 
+0

Leider kann ich "getArrayFromUri: error" nicht umgestalten, da es ein Framework von Drittanbietern ist und ich nur Zugriff auf die Header habe. Ich habe jedoch alles so verändert, wie Sie es haben, und es funktioniert genauso. Das zu lernen fühlt sich an, als würde man von vorne beginnen. –

+0

Ja, das Framework hat sicherlich eine schmerzhaft steile Lernkurve, aber wenn man erst einmal gelernt hat, wie es funktioniert, ist es wirklich mächtig. – CodaFi

+0

@CodaFi 'switchToLatest] deliverOn: [RACScheduler mainThreadScheduler]]' ist ein No-Op in allen von Ihnen bereitgestellten Samples, da danach keine Subscription stattfindet. Gleiches mit deinen '-do ...' Methoden. –