Question

I have a ViewModel for a UITableView that holds an array of data elements. The UITableView implements a pull-to-refresh and infinite-scroll behavior. Data elements are requested via RestKit from a server in a paginated fashion which means I have to track the current page somehow. I created 2 separate RACCommands to differentiate between refreshs and infinite-scroll-loads where the refresh always loads page 0. The whole thing works but I don't like it and want to know if there is a better way to do this. Plus right now the 2 commands may be executed concurrently which is not intended.

@interface TableDataViewModel ()

@property(nonatomic, readonly) RestApiConnector *rest;
@property(nonatomic) int page;
@property(nonatomic) NSMutableArray *data;
@property(nonatomic) RACCommand *loadCommand;
@property(nonatomic) RACCommand *reloadCommand;

@end

@implementation TableDataViewModel

objection_requires_sel(@selector(rest))

- (id)init {
    self = [super init];
    if (self) {
        [self configureLoadCommands];
        [self configureActiveSignal];
    }
    return self;
}

- (void)configureActiveSignal {
    [self.didBecomeActiveSignal subscribeNext:^(id x) {
        if (!self.data) {
            [self.reloadCommand execute:self];
        }
    }];
}

- (void)configureLoadCommands {
    self.loadCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
        return [self.rest dataSignalWithPage:self.page];
    }];
    self.reloadCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
        return [self.rest dataSignalWithPage:self.page];
    }];
    self.loadCommand.allowsConcurrentExecution = FALSE;
    self.reloadCommand.allowsConcurrentExecution = FALSE;

    [self.reloadCommand.executionSignals subscribeNext:^(id x) {
        RAC(self, data) = [x map:^id(id value) {
            self.page = 0;
            return [value mutableCopy];
        }];
    }];

    [self.loadCommand.executionSignals subscribeNext:^(id x) {
        RAC(self, data) = [x map:^id(id value) {
            self.page++;
            if (self.data) {
                NSMutableArray *array = [self.data mutableCopy];
                [array addObjectsFromArray:value];
                return array;
            } else {
                return [value mutableCopy];
            }
        }];
    }];
}
@end

I just started using ReactiveCocoa, so I'd appreciate any other tips for this.

Thanks!

Was it helpful?

Solution

I just started using ReactiveCocoa, so I'd appreciate any other tips for this.

If you only want one command or the other to execute at a time, one way to achieve this is to just use a single command (and disable concurrent execution, as you have already done):

1  const NSUInteger kLoad   = 0;
2  const NSUInteger kReload = 1;
3
4  - (void)configureActiveSignal {
5      @weakify(self);
6      RACSignal *dba = [[self.didBecomeActiveSignal filter:^(id _) {
7              @strongify(self);
8              return self.data;
9          }]
10          mapReplace:@( kReload )];
11
12      RACSignal *ls = [self.command rac_liftSelector:@selector(execute:) withSignals:dba];
13      [[ls publish] connect];
14  }
15  
16  - (void)configureCommand {
17      @weakify(self);
18      self.command = [[RACCommand alloc] initWithSignalBlock:^(NSNumber *loadOrReload) {
19              @strongify(self);
20              return RACTuplePack(loadOrReload, [self.rest dataSignalWithPage:self.page]);
21          }];
22
23      RAC(self, page) = [self.command.executionSignals reduceEach:^(NSNumber *loadOrReload, id _) {
24              @strongify(self);
25              return kReload == loadOrReload.integerValue ? @0 : @( ++self.page );
26          }];
27      RAC(self, data) = self.command.executionSignals reduceEach:^(NSNumber *loadOrReload, NSArray *data) {
28              @strongify(self);
29              if (kReload == loadOrReload.integerValue)
30              {
31                  return [data mutableCopy];
32              }
33              else
34              {
35                  NSMutableArray *ma = [self.data mutableCopy];
36                  [ma addObjectsFromArray:data]
37                  return ma;
38              }
39          }];
40
41      self.command.allowsConcurrentExecution = NO;

Nota bene, this is totally untested. But I hope it communicates the basic idea.

  • You can infer from the code that there is only one command now, and its instance variable is named command. Instead of getting called with the array of data, it is now called with a constant to indicate whether it should load orreload, which you can see on line 18.
  • Line 5: Your code had a lot of retain cycles. You can't strongly reference self from within a block that is strongly referenced by self (either directly, or via an indirect chain of object ownership). ReactiveCocoa comes with the @strongify/weakify macros to help make this less arduous. As you can see, any time you need to reference self within a block that is owned by a command (which is itself owned by self, thus the cycle), you need to create a weak reference to self and then use that inside the block. You use @strongify inside of the block to avoid a data race with ARC's release mechanism.
  • Line 6: Transform the didBecomeActive notifications into executions of self.command with a parameter of kReload (this replaces your old reloadCommand), but only if self.data != nil, using -filter:.
  • Line 12: Here's where you actually call [self.command execute:], but you do this by lifting the selector onto the signal just created above. Presumably you have some other unshown code that used to execute self.loadCommand, you will need to adjust that to pass @( kLoad ) instead.
  • Line 13: The -publish/connect idiom is conventionally used when you want to subscribe to a signal without actually doing anything with its value. This isn't strictly necessary in RAC 2.x.
  • Line 18: You can see that the command will call its signal block with a NSNumber now, that is, the constant that you have lifted up onto the signal created on line 6. However, where before the command returned just the data, it now returns a tuple containing both the input to -execute: and the data. This is because as you'll see below, downstream operations need access to both pieces of information.
  • Line 23: Instead of using the RAC() macro inside of a signal subscription block (which is almost always an antipattern), you now use it directly in the -configureCommand method. Here we are saying that any time the command is executed, set self.page to either 0 or ++self.page, depending on whether the command was executed with kLoad or kReload. The -reduceEach: operation is just a convenience to destructure the tuple being sent through the signal, and here we only care about the constant. Note the side-effect of incrementing self.page in this -reduceEach: operation. It's generally bad form to have side effects in your signal operations. The danger is that if something other than RAC(self, page) subscribes to the returned signal, the instance variable can be incremented more times than you would expect. It would be ideal if you could obtain the value of self.page from the source of data somehow, rather than tracking it in a global instance variable.
  • Line 27: This is just a variation on what you just did on line 23, but instead of setting self.page, you're setting self.data. This time the -reduceEach: operation does need to use the second value in the tuple (the data), which is transformed appropriately for either kLoad or kReload, and then ultimately set on the ivar.
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top