I'm currently looking into the ReactiveCocoa and though I'm already a bit familiar with it's classes, I guess that I still have some problems thinking reactively.
I'd appreciate if someone more experienced could take a look at my code (which already works correctly) and point me to any things which are currently not reactive enough but implemented in a classic way just using the RAC classes.
Of course any examples from your side are highly preferable.
Briefly about the functionality of my code:
I send several asynchronous requests to an image search API, get the result as an array of links, and then use them for asynchronous downloading the images and finally draw them at a large UIImageView by replacing it's image with a new one generated every time when a new image is downloaded.
Thanks!
- (void)fillImageView:(UIImageView *)imageView withQuery:(NSString *)query {
self.timestamp = [NSDate timeIntervalSinceReferenceDate];
[[[self searchAllSignalWithQuery:query]
map:^id(NSArray *images) {
NSLog(#"Processing images. Count: %ld", images.count);
return [self processSignalWithImages:images imageView:imageView];
}]
subscribeCompleted:^{
}];
}
- (RACSignal *)searchAllSignalWithQuery:(NSString *)query {
NSMutableArray *searchSignals = [NSMutableArray array];
for (NSInteger pageIndex = 0; pageIndex < kSearchPageCount; pageIndex++) {
[searchSignals addObject:[self searchSignalWithQuery:query top:kSearchPageSize skip:(pageIndex * kSearchPageSize)]];
}
return [[RACSignal zip:searchSignals] flattenMap:^id(NSArray *results) {
NSMutableArray *images = [NSMutableArray array];
for (NSArray *result in results) {
[images addObjectsFromArray:result];
}
return [RACSignal return:images];
}];
}
- (RACSignal *)searchSignalWithQuery:(NSString *)query top:(NSInteger)top skip:(NSInteger)skip {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSURLSessionDataTask *task = [Common searchTaskWithQuery:query top:top skip:skip completionBlock:^(NSArray *images) {
NSLog(#"Adding images. Count: %ld", images.count);
[subscriber sendNext:images];
[subscriber sendCompleted];
}];
[task resume];
return nil;
}];
}
- (RACSignal *)processSignalWithImages:(NSArray *)images imageView:(UIImageView *)imageView {
NSInteger imageIndex = 0;
NSMutableArray *downloadSignals = [NSMutableArray array];
RACScheduler *drawScheduler = [RACScheduler schedulerWithPriority:RACSchedulerPriorityDefault];
for (NSString *url in images) {
RACSignal *downloadSignal = [self downloadSignalWithURL:url];
[[downloadSignal deliverOn:[RACScheduler schedulerWithPriority:RACSchedulerPriorityBackground]] subscribeNext:^(UIImage *image) {
NSLog(#"Drawing image. Index: %ld", imageIndex);
if (!image) {
image = [UIImage imageNamed:#"error"];
}
[drawScheduler schedule:^{
UIImage *newImage = [Common imageByDrawingImage:image onImageView:imageView atIndex:imageIndex];
[[RACScheduler mainThreadScheduler] schedule:^{
imageView.image = newImage;
}];
}];
}];
[downloadSignals addObject:downloadSignal];
imageIndex++;
}
RACSignal *processSignal = [RACSignal combineLatest:downloadSignals];
[processSignal subscribeCompleted:^{
NSLog(#"Completed. Duration: %.2f seconds", ([NSDate timeIntervalSinceReferenceDate] - self.timestamp));
}];
return processSignal;
}
- (RACSignal *)downloadSignalWithURL:(NSString *)url {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSURLSessionDataTask *task = [Common downloadTaskWithURL:url completionBlock:^(UIImage *image) {
[subscriber sendNext:image];
[subscriber sendCompleted];
}];
[task resume];
return nil;
}];
}
Related
I don't have any idea how to merge a lot of signals and get results from a RACTuple, its seems to be like easy answer but I can't found that.
What we have for exmaple:
NSArray *a = #[#{#"k1":#"v1"},
#{#"k2":#"v2"},
#{#"k3":#"v3"},
#{#"k4":#"v4"},
#{#"k5":#"v5"},
#{#"k6":#"v6"},
#{#"k7":#"v7"}];
NSArray *b = #[#{#"kk1":#"vv1"},
#{#"kk2":#"vv2"},
#{#"kk3":#"vv3"},
#{#"kk4":#"vv4"},
#{#"kk5":#"vv5"},
#{#"kk6":#"vv6"},
#{#"kk7":#"vv7"}];
and
RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
RACSignal *s1 = [self adaptObjects:a];
RACSignal *s2 = [self adaptObjects:b];
return [[RACSignal merge:#[s1,s2]] map:^id(id value) {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:value];
return nil;
}];
}];
}];
[[command execute:nil] subscribeNext:^(RACTuple *x) {
NSLog(#"%#",x);
}];
this operator map is wrong I know that, but this is for example
- (RACSignal *)adaptObjects:(NSArray *)objects {
return [objects.rac_sequence.signal flattenMap:^RACStream *(id x) {
return [self adaptObject:x];
}];
}
- (RACSignal*)adaptObject:(NSDictionary*) x {
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
// some operations with data here
[subscriber sendNext:x];
return nil;
}];
}
In NSLog I want to see tuple result with two arrays first - s1, second - s2
Thx
I've written a small example, hope it helps you.
NSArray *a = #[#{#"k1":#"v1"},
#{#"k2":#"v2"},
#{#"k3":#"v3"},
#{#"k4":#"v4"},
#{#"k5":#"v5"},
#{#"k6":#"v6"},
#{#"k7":#"v7"}];
NSArray *b = #[#{#"kk1":#"vv1"},
#{#"kk2":#"vv2"},
#{#"kk3":#"vv3"},
#{#"kk4":#"vv4"},
#{#"kk5":#"vv5"},
#{#"kk6":#"vv6"},
#{#"kk7":#"vv7"}];
- (NSArray<RACSignal *> *)rac_signalsFromArray:(NSArray *)array {
NSMutableArray<RACSignal *> *signals = [NSMutableArray array];
for (NSDictionary *dict in array) {
[signals addObject:[RACSignal return:dict]];
}
return signals;
}
NSArray *Asignals = [self rac_signalsFromArray:a];
NSArray *Bsignals = [self rac_signalsFromArray:b];
NSArray *signals = [[NSArray arrayWithArray:Asignals] arrayByAddingObjectsFromArray:Bsignals];
[[RACSignal zip:signals] subscribeNext:^(RACTuple *tuple) {
// tuple here
}];
I am playing with code from some book regarding Reactive Cocoa and I am stuck with this one:
Here is my photo importer code :
+ (RACSignal *)importPhotos {
NSURLRequest *request = [self popularURLRequest];
return [[[[[[NSURLConnection rac_sendAsynchronousRequest:request] map:^id(RACTuple *value) {
return [value second];
}] deliverOn:[RACScheduler mainThreadScheduler]]
map:^id(NSData *value) {
id result = [NSJSONSerialization JSONObjectWithData:value
options:0
error:nil];
return [[[result[#"photos"] rac_sequence] map:^id(NSDictionary *photoDictionary) {
FRPPhotoModel *photoModel = [FRPPhotoModel new];
[self configurePhotoModel:photoModel withDict:photoDictionary];
[self downloadThumbnailForPhotoModel:photoModel];
return photoModel;
}] array];
}] publish] autoconnect];
}
+ (void)configurePhotoModel:(FRPPhotoModel *)photoModel withDict:(NSDictionary *)dict {
photoModel.photoName = dict[#"name"];
photoModel.identifier = dict[#"id"];
photoModel.photographerName = dict[#"user"][#"username"];
photoModel.rating = dict[#"rating"];
[[self urlForImageSize:3 inArray:dict[#"images"]] subscribeNext:^(id x) {
photoModel.thumbnailURL = x;
}];
}
+ (RACSignal *)urlForImageSize:(NSInteger)size inArray:(NSArray *)array {
return [[[[array rac_sequence] filter:^BOOL(NSDictionary *value) {
return [value[#"size"] integerValue] == size;
}] map:^id(NSDictionary *value) {
return value[#"url"];
}] signal];
}
+ (void)downloadThumbnailForPhotoModel:(FRPPhotoModel *)photoModel {
[[RACObserve(photoModel, thumbnailURL) flattenMap:^RACStream *(id value) {
return [self download:value];
}] subscribeNext:^(id x) {
photoModel.thumbnailData = x;
}];
}
+ (RACSignal *)download:(NSString *)urlString {
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]];
return [[NSURLConnection rac_sendAsynchronousRequest:request] map:^id(RACTuple *value) {
return [value second];
}];
}
and UI is updated like this:
RAC(self.imageView, image) = [[RACObserve(self, photoModel.thumbnailData) filter:^BOOL(id value) {
return value != nil;
}] map:^id(id value) {
return [UIImage imageWithData:value];
}];
Can you please explain why my UI is not updated or updated wrongly with new UIImages which I get from that NSData objects.
So the first problem was that flattenMap delivers on some background RACScheduler. Changed to this:
[[[[RACObserve(photoModel, thumbnailURL) ignore:nil] flattenMap:^RACStream *(id value) {
return [self download:value];
}] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(id x) {
photoModel.thumbnailData = x;
}];
Also another problem was that download:nil was throwing an error, which was not caught by subscriber and thus terminated signal which provided values of observing. Adding ignore:nil fixed issue.
So I have successfully turned a button into an off and on switch that changes the label.
I was also able to have it start a timed processed set off when that is to occur, and it have the ability to shut off the timed process.
Anyways I need to way to shut down the timed process I was wondering if there was a way to stop it without using the disposable. With a second takeUntil signal.
Edit I think what I was trying to do was slightly misleading let me show my current solution that works.
-(RACSignal*) startTimer {
return [[RACSignal interval:1.0
onScheduler:[RACScheduler mainThreadScheduler]]
startWith:[NSDate date]];
}
-(void) viewWillAppear:(BOOL)animated {}
-(void) viewDidLoad {
self.tableView.delegate = self;
self.tableView.dataSource = self;
RACSignal* pressedStart = [self.start rac_signalForControlEvents:UIControlEventTouchUpInside];
#weakify(self);
RACSignal* textChangeSignal = [pressedStart map:^id(id value) {
#strongify(self);
return [self.start.titleLabel.text isEqualToString:#"Start"] ? #"Stop" : #"Start";
}];
// Changes the title
[textChangeSignal subscribeNext:^(NSString* text) {
#strongify(self);
[self.start setTitle:text forState:UIControlStateNormal];
}];
RACSignal* switchSignal = [[textChangeSignal map:^id(NSString* string) {
return [string isEqualToString:#"Stop"] ? #0 : #1;
}] filter:^BOOL(id value) {
NSLog(#"Switch %#",value);
return [value boolValue];
}];
[[self rac_signalForSelector:#selector(viewWillAppear:)]
subscribeNext:^(id x) {
}];
static NSInteger t = 0;
// Remake's it self once it is on finished.
self.disposable = [[[textChangeSignal filter:^BOOL(NSString* text) {
return [text isEqualToString:#"Stop"] ? [#1 boolValue] : [#0 boolValue];
}]
flattenMap:^RACStream *(id value) {
NSLog(#"Made new Sheduler");
#strongify(self);
return [[self startTimer] takeUntil:switchSignal];
}] subscribeNext:^(id x) {
NSLog(#"%#",x);
#strongify(self);
t = t + 1;
NSLog(#"%zd",t);
[self updateTable];
}];
[[self rac_signalForSelector:#selector(viewWillDisappear:)] subscribeNext:^(id x) {
NSLog(#"viewWillAppear Dispose");
[self.disposable dispose];
}];
}
-(BOOL) isGroupedExcercisesLeft {
BOOL isGroupedLeft = NO;
for (int i =0;i < [self.excercises count]; i++) {
Excercise* ex = [self.excercises objectAtIndex:i];
if(ex.complete == NO && ex.grouped == YES) {
isGroupedLeft = YES;
break;
}
}
return isGroupedLeft;
}
-(void) updateTable {
// Find the
NSInteger nextRow;
if (([self.excercises count] > 0 || self.excercises !=nil) && [self isGroupedExcercisesLeft]) {
for (int i =0;i < [self.excercises count]; i++) {
Excercise* ex = [self.excercises objectAtIndex:i];
if(ex.complete == NO && ex.grouped == YES) {
nextRow = i;
break;
}
}
NSIndexPath* path = [NSIndexPath indexPathForItem:nextRow inSection:0];
NSArray* indexPath = #[path];
// update //
Excercise* ex = [self.excercises objectAtIndex:nextRow];
[self.tableView scrollToRowAtIndexPath:path atScrollPosition:UITableViewScrollPositionTop animated:YES];
if (ex.seconds <= 0) {
RLMRealm* db = [RLMRealm defaultRealm];
[db beginWriteTransaction];
ex.complete = YES;
[db commitWriteTransaction];
}
else {
// Update Seconds
RLMRealm* db = [RLMRealm defaultRealm];
[db beginWriteTransaction];
ex.seconds = ex.seconds - 1000;
NSLog(#"Seconds: %zd",ex.seconds);
[db commitWriteTransaction];
// Update table
[self.tableView reloadRowsAtIndexPaths:indexPath withRowAnimation:UITableViewRowAnimationNone];
}
} else {
NSLog(#"Done");
SIAlertView *alertView = [[SIAlertView alloc] initWithTitle:#"Deskercise" andMessage:#"Excercises Complete"];
[alertView addButtonWithTitle:#"Ok"
type:SIAlertViewButtonTypeDefault
handler:^(SIAlertView *alert) {
}];
alertView.transitionStyle = SIAlertViewTransitionStyleBounce;
[alertView show];
NSLog(#"Dispose");
[self.disposable dispose];
}
}
The issue with using takeUntil:self.completeSignal is that when you change completeSignal to another value, it isn't passed to any function that was already waiting for the variable that completeSignal was previously holding.
- (RACSignal*) startTimer {
#weakify(self)
return [[[RACSignal interval:1.0
onScheduler:[RACScheduler mainThreadScheduler]]
startWith:[NSDate date]]
takeUntil:[[self.start rac_signalForControlEvents:UIControlEventTouchUpInside]
merge:[[RACObserve(self, completeSignal) skip:1] flattenMap:
^RACStream *(RACSignal * signal) {
#strongify(self)
return self.completeSignal;
}]]
];
}
The signal is now observing and flattening completeSignal, which will give the desired effect. Signals that complete without sending next events are ignored by takeUntil:, so use self.completedSignal = [RACSignal return:nil], which sends a single next event and then completes.
However, this code is anything but ideal, let's look at a better solution.
#property (nonatomic, readwrite) RACSubject * completeSignal;
- (RACSignal*) startTimer {
return [[[RACSignal interval:1.0
onScheduler:[RACScheduler mainThreadScheduler]]
startWith:[NSDate date]]
takeUntil:[[self.start rac_signalForControlEvents:UIControlEventTouchUpInside]
merge:self.completeSignal]
];
}
- (void) viewDidLoad {
[super viewDidLoad];
self.completeSignal = [RACSubject subject];
self.tableView.delegate = self;
self.tableView.dataSource = self;
RACSignal * pressedStart = [self.start rac_signalForControlEvents:UIControlEventTouchUpInside];
#weakify(self);
RACSignal* textChangeSignal = [[pressedStart startWith:nil] scanWithStart:#"Stop" reduce:^id(id running, id next) {
return #{#"Start":#"Stop", #"Stop":#"Start"}[running];
}];
[self.start
rac_liftSelector:#selector(setTitle:forState:)
withSignals:textChangeSignal, [RACSignal return:#(UIControlStateNormal)], nil];
[[[pressedStart flattenMap:^RACStream *(id value) { //Using take:1 so that it doesn't get into a feedback loop
#strongify(self);
return [self startTimer];
}] scanWithStart:#0 reduce:^id(NSNumber * running, NSNumber * next) {
return #(running.unsignedIntegerValue + 1);
}] subscribeNext:^(id x) {
#strongify(self);
[self updateTable];
NSLog(#"%#", x);
}];
}
- (void) updateTable {
//If you uncomment these then it'll cause a feedback loop for the signal that calls updateTable
//[self.start sendActionsForControlEvents:UIControlEventTouchUpInside];
//[self.completeSignal sendNext:nil];
if ([self.excercises count] > 0 || self.excercises !=nil) {
} else {
}
}
Let's run through the list of changes:
completeSignal is now a RACSubject (a manually controlled RACSignal).
For purity and to get rid of the #weakify directive, textChangeSignal now uses the handy scanWithStart:reduce: method, which lets you access an accumulator (this works well for methods that work with an incrementing or decrementing number).
start's text is now being changed by the rac_liftSelector function, which takes RACSignals and unwraps them when all have fired.
Your flattenMap: to replace pressedStart with [self startTimer] now uses scanWithStart:reduce, which is a much more functional way to keep count.
I'm not sure if you were testing by having updateTable contain completion signals but it definitely causes a logic issue with your flattenMap: of pressedButton, the resulting feedback loop eventually crashes the program when the stack overflows.
I'm new to iOS development and in my app I'm seeing some strange memory usage behavior.
I'm getting objects from server in such setupDataForPage method:
- (void)setupDataForPage:(int)page actionType:(NSString *)type success:(void (^)())callback
{
__weak MyTableViewController *weakSelf = self;
// clearing image cache because feed contains a lot of images
[[SDImageCache sharedImageCache] clearMemory];
[[SDImageCache sharedImageCache] clearDisk];
MyHTTPClient *API = [MyHTTPClient new];
[API feedFor:page success:^(AFHTTPRequestOperation *operation, id data) {
NSArray *data = [data objectForKey:#"data"];
if ([data count] > 0) {
// remove all objects to refresh with new ones
if ([type isEqualToString:#"pullToRefresh"]) {
[weakSelf.models removeAllObjects];
}
// populate data
NSMutableArray *result = [NSMutableArray new];
for (NSDictionary *modelData in data) {
MyModel *model = [[MyModel alloc] initWithDictionary:modelData];
[result addObject:model];
}
[weakSelf.models addObjectsFromArray:result];
[weakSelf.tableView reloadData];
}
callback();
} failure:nil];
}
it is used in viewDidLoad while getting initial request and also for pull refresh and infinite scrolling:
- (void)viewDidLoad {
[super viewDidLoad];
__block int page = 1;
__weak MyTableViewController *weakSelf = self;
// initial load
[self setupDataForPage:page actionType:#"initial" success:^{ page += 1; }];
// pull to refresh
[self.tableView addPullToRefreshWithActionHandler:^{
[weakSelf setupDataForPage:1 actionType:#"pullToRefresh" success:^{
[weakSelf.tableView.pullToRefreshView stopAnimating];
}];
}];
// infinite scrolling
[self.tableView addInfiniteScrollingWithActionHandler:^{
[weakSelf setupItemsForPage:page actionType:#"infiniteScroll" success:^{
page += 1;
[weakSelf.tableView.infiniteScrollingView stopAnimating];
}];
}];
}
I noticed that even after pull to refresh action which returns the same data (and I'm just removing all models and add them once more) my app's memory usage grows from nearly 19mb to 24mb..
I would like someone more experienced to look at this piece of code to determine whether it contains some possible memory leaks.. Should I somehow delete NSMutableArray *result variable after assigning it to models array?
Thanks!
First of all, use #autoreleasepool here:
#autoreleasepool {
NSArray *data = [data objectForKey:#"data"];
if ([data count] > 0) {
// remove all objects to refresh with new ones
if ([type isEqualToString:#"pullToRefresh"]) {
[weakSelf.models removeAllObjects];
}
// populate data
NSMutableArray *result = [NSMutableArray new];
for (NSDictionary *modelData in data) {
MyModel *model = [[MyModel alloc] initWithDictionary:modelData];
[result addObject:model];
}
[weakSelf.models addObjectsFromArray:result];
[weakSelf.tableView reloadData];
}
}
#autoreleasepool allows you to release every object allocated in that scope IMMEDIATELY.
This is perfect situation where use it ;)
In the below code, self.theFiles prints the null. Because after calling [self.restClient loadMetadata:#"/"], getFiles method keeps working.
I want getFiles method to wait until [self.restClient loadMetadata:#"/"] finishes to get file list and put the data to self.theFiles object. But after [self.restClient loadMetadata:#"/"], getFiles keeps working.
#synthesize theFiles;
- (void)getFiles:(CDVInvokedUrlCommand*)command
{
if (![[DBSession sharedSession] isLinked]) {
[[DBSession sharedSession] linkFromController:self];
}
else
{
[self.restClient loadMetadata:#"/"];
}
NSLog(self.theFiles); //Prints null
NSLog(#"Finished");
}
#pragma mark DBRestClientDelegate methods
- (void)restClient:(DBRestClient*)client loadedMetadata:(DBMetadata*)metadata {
NSArray* validExtensions = [NSArray arrayWithObjects:#"txt", #"text", nil];
NSMutableArray* newPhotoPaths = [NSMutableArray new];
for (DBMetadata* child in metadata.contents) {
self.theFiles=child.path;
NSString* extension = [[child.path pathExtension] lowercaseString];
if (!child.isDirectory && [validExtensions indexOfObject:extension] != NSNotFound) {
[newPhotoPaths addObject:child.path];
}
}
NSLog(self.theFiles); //Prints the file list
}
Output:
null
Finished
/file1.txt
/file2.txt
..... the file list in my dropbox
Is it possible to work my getFiles method with delegate synchronously?
UPDATE
I updated my code. I want to lock the thread using NSLock but doesn't work. The same result.
#synthesize theFiles;
- (void)getFiles:(CDVInvokedUrlCommand*)command
{
if (![[DBSession sharedSession] isLinked]) {
[[DBSession sharedSession] linkFromController:self];
}
else
{
if ([self.theLock tryLock]) {
[self.restClient loadMetadata:#"/"];
NSLog(self.theFiles); //Prints null
NSLog(#"Finished");
}
}
}
#pragma mark DBRestClientDelegate methods
- (void)restClient:(DBRestClient*)client loadedMetadata:(DBMetadata*)metadata {
NSArray* validExtensions = [NSArray arrayWithObjects:#"txt", #"text", nil];
NSMutableArray* newPhotoPaths = [NSMutableArray new];
for (DBMetadata* child in metadata.contents) {
self.theFiles=child.path;
NSString* extension = [[child.path pathExtension] lowercaseString];
if (!child.isDirectory && [validExtensions indexOfObject:extension] != NSNotFound) {
[newPhotoPaths addObject:child.path];
}
}
NSLog(self.theFiles); //Prints the file list
[self.theLock unlock];
}