Frage

I'm using ReactiveCocoa and am trying to apply MVVM. I have a fairly typical UITableView scenario with a refresh control for reloading data.

I've omitted the the UITableDataSource/Delegate methods as these are straight forward. The code below illustrates how I've designed the ViewModel and the ViewController to fit together.

ViewModel.h

@property (strong, nonatomic, readonly) RACCommand *getItemsCommand;
@property (strong, nonatomic, readonly) NSArray *items;

ViewModel.m

- (instancetype)init {
    self = [super init];
    if (!self) return nil;

    @weakify(self);
    self.getItemsCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
        return [[ItemsDataSource getItems]
                doNext:^(NSArray *items) {
                    @strongify(self);
                    // I actually do a little extra work here such as sorting
                    // the items appropriately for the view.
                    self.items = items;
                }];
    }];

    return self;
}

ViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];

    [self.tableView addSubview:self.refreshControl];

    RACSignal *refreshSignals = [RACSignal merge:@[
        [self.refreshControl rac_signalForControlEvents:UIControlEventValueChanged],
        [RACSignal return:nil]
    ]];

    [refreshSignals
     subscribeNext:^(id x) {
         [self.viewModel.getItemsCommand execute:nil];
     }];

    [RACObserve(self.viewModel, items)
     subscribeNext:^(NSArray *items) {
         [self.tableView reloadData];
     } completed:^{
         [self.refreshControl endRefreshing];
     }];
}

Questions/Problems

  1. The completed block where I call endRefreshing never gets executed and for the life of me I can't figure out why.
  2. Would it be better to use a public method - (RACSignal *)getItems instead of the getItems RACCommand?
  3. Is my usage of doNext: in the ViewModel correct in order to apply side effects (i.e. the sorting of the items array) without causing an additional subscription?
War es hilfreich?

Lösung 2

1) Well, let's look at the signal:

RACObserve(self.viewModel, items)

When will that complete? Only when self.viewModel or self is deallocated, just like any other RACObserve. As long as those objects are around, it'll keep on nexting any time you set self.items.

It appears that you want it to endRefreshing once the getItemsCommand finishes executing, and you have this sort of implicit expectation that, since you know that command sets self.viewModel.items, that completion will somehow propagate -- but this isn't the case. To see when the command completes, you have to subscribe to the command's returned signal.

2) The advantage of RACCommand is the auto enabling/disabling behavior, which you aren't really taking advantage of here. The more canonical thing to do would be self.refreshControl.rac_command = self.viewModel.getItemsCommand;. That'll handle the endRefreshing stuff for you, saving you the headache from part 1.

3) ...sort of. The do* family of methods injects side effects for every subscription to the signal. So if you subscribe to a signal twice, and it sends a next, any doNext block it has will be invoked twice. An explicit subscription is more clear, since you want to execute this exactly once per next, regardless of how many times it's subscribed to.

    @weakify(self);
    self.getItemsCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
        RACSignal *itemsSignal = [ItemsDataSource getItems];
        [itemsSignal subscribeNext:^(NSArray *items) {
            @strongify(self);
            // Do stuff...
            self.items = items;
        }];
        return itemsSignal;
    }];

Andere Tipps

I suggest making getItemsCommand use -map: to sort and process the items array. Leave any other side effect work to be done in a separate -doNext:. Once you have your command following this pattern (which is more compositional in RAC), then you can use the RAC() macro to assign the command's finished product, the sorted array, to the items property.

RAC(self, items) = [self.getItemsCommand.executionSignals concat];

RAC has a built-in command support for UIRefreshControl that will start/stop the refresh control along with the start/stop of the command. You should find that you can reduce your UIRefreshControl code to:

self.refreshControl.rac_command = self.getItemsCommand;

For table reloading, you can do:

[RACObserve(self, items) subscribeNext:^(id _) {
    @strongify(self);
    [self.tableView reloadData];
}];

Hope that helps.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top