preface

FRP is a technology with a steep learning curve. If I recall my previous learning process, it will go back and forth several times, and I always have a strong sense of frustration. But fortunately insisted on down, now is also used more smoothly.

One of the easiest things to tease about FRP is the lack of good learning materials and documentation. I felt the same way at first, but after a lot of trial and error, I realized it wasn’t really the documentation. My conclusion — don’t expect to be able to explain how FRP works without code. This is a significant difference between FRP and other programming techniques, just as it is difficult to describe a mathematical formula in a single paragraph. Moreover, even when you start looking at the various types of code written with FRP, it still feels too abstract, and you still need a lot of time to experience the code, or to “understand” some of the basics.

There is no shortcut to learning about entry. The best way is to learn by code. Here are some information about entry that I think is better

Say FRP learning curve is very steep, not only refers to the introduction to study it more time-consuming fee brain, when into the door or find some feeling after a little, then will face the second question: FRP inside there is a function of some abstract operation, how to use the basic function to solve the problem of all kinds of business? Especially those very abstract operation, how can use?

This series of articles is mainly aimed at the second question, do some demos.

FRP can be regarded as a more advanced Pipeline programming paradigm. One of the essence of Pipeline is that it can be flexibly combined. Although there are only dozens of commonly used operations in FRP, once they are assembled like building blocks, the strength of FRP suddenly shows.

FRP is usually provided to users in the form of libraries or frameworks, and has been implemented in many common programming languages. In this series of articles, you’ll be writing in RAC 2, the Objective-C version of ReactiveCocoa. But FRP is essentially a programming paradigm. From a Pipeline perspective, it focuses on how to assemble pipelines of different shapes, rather than the specific materials (programming languages) from which pipelines are made. From a framework perspective, although there are different language versions implemented, But the basic operations such as map, map, and reduce that were presented in each release were the same in concept and pattern of behavior. Therefore, FRP is also a “Learn once, write anywhere” technology.

FRP has several obvious benefits, such as the ability to reduce the use of intermediate state variables, the ability to write compact code, and the ability to write code that runs asynchronously in a synchronous style, which will be explored throughout this series.

Handle keyboard pop-up and hide

In a UIViewController, when the keyboard pops up, in order to prevent the keyboard from blocking a UIView, we need to relayout the view according to the height of the keyboard. The code written in RAC looks like the following:

//1 - (void)initPipeline { @weakify(self); RACSignal *keyboardWillShowNotification = [[NSNotificationCenter.defaultCenter rac_addObserverForName:UIKeyboardWillShowNotification object:nil] map:^id(NSNotification *notification) { //2 NSDictionary* userInfo = [notification userInfo];  NSValue* aValue = [userInfo objectForKey:UIKeyboardFrameEndUserInfoKey]; return aValue; }]; [[[[[NSNotificationCenter.defaultCenter rac_addObserverForName:UIKeyboardWillHideNotification object:nil] map:^id(NSNotification *notification) { //3 return [NSValue valueWithCGRect:CGRectZero];  }] merge:keyboardWillShowNotification] //4 takeUntil:self.rac_willDeallocSignal] //6 subscribeNext:^(NSValue *value) { NSLog(@"Keyboard size is: %@", value); //5 @strongify(self); Self. MessageEditViewContainerViewBottomConstraint. Constant = 5.0 + value CGRectValue [] size. The height; [UIView animateWithDuration:0.6 animations:^{@strongify(self);  [self.view layoutIfNeeded]; }];  } completed:^{ //6 NSLog(@"%s, Keyboard Notification signal completed", __PRETTY_FUNCTION__); }]; }Copy the code

The numbers are the key points:

  1. A lot of times, pipelines only need to be built once. If it’s for UIViewController, it’s usually done by calling [self initPipeline] in viewDidLoad. If it’s for UIView, The awakeFromNib method is most likely to be called. One of the following strategies is to construct all pipelines as soon as the module is “alive”. If the module is model or service, it is likely to be constructed after init. InitPipeline is called, but for UI modules, because there are iOS platform-specific view loading strategies, and pipelines are usually associated with UI interaction, it is usually necessary to construct pipelines in view life-dependent methods.
  2. Through the map operation, converts UIKeyboardWillShowNotification into a CGRect (packaging in NSValue). Map operation is one of the core basic operations in FRP, and also one of the operations that most embodies the philosophy of functional programming (FP). The so-called philosophy is described in popular words, that is, “divide complex business into small tasks one by one, and each small task requires an input value. It gives an output value (as well as an error message) and focuses on one thing for each small task. If the output value of the first small task is the input value of the second small task, then the map operation can be used to concatenate the two small tasks. When to receive notifications for a UIKeyboardWillShowNotification, this small task is NSNotification input values, the output value is the corresponding CGRect size keyboard, small task itself do, Fetch the NSValue that wraps this CGRect from the NSNotification.
  3. When to receive notifications for a UIKeyboardWillHideNotification, this little task to do, and the inside of the 2 small task is similar, only this time, NSNotification does not include the size of the keyboard, So we can build one ourselves with CGRectZero.
  4. And finally, to get to the point of this code,mergeThe effect of operation here is equivalent to taking the output values of the two small tasks in 2 and 3 as their own input values, arranging them in chronological order, and then returning them to the next link in Pipeline as the output values of their own small tasks.It's still very abstract, isn't it? That's okay. I told you it's hard to describe in words. Use NSLog(@"Keyboard size is: %@", value) to see how merge works.
  5. This is where the business really wants to be. Calculate the size of the layout based on the output value of the previous small task (the keyboard size CGRect).
  6. TakeUntil is a difficult point. If you do not call it, you will find that the business in the previous 5 is still normal. However, when self is dealloc (such as after pop UIViewController), NSLog (@ “the rid_device_info_keyboard size is: %@ “, value) will still be executed (since the retain cycle has been handled, self is nil) because the Pipeline is not released when self is dealloc, Pipeline still has data in the flow. This topic is important as it relates to memory management strategies in the RAC framework and will be covered later. All you need to know for now is that one line of takeUntil: self.rac_willdeallocsignal is handy.

Next, Complete, error on Singal

In the process of learning, I found that there was a problem that was easily ignored, that is, when the three kinds of data of Signal next, complete and error would be sent out. I made a summary of this problem and put it in this document. The main goal is to present Signal’s key messages in a straightforward format, which is briefly summarized here.

Basic format

HotSignal   // or ColdSignal
Completion: ...
Error: ...
Scheduler: ...
Multicast: ...
Copy the code

Keyword Description

  • HotSignal And ColdSignal:
    • HotSignalSignal is activated.
    • ColdSignalSubscribed: Signal needs to subscribe to activate;
  • T: Indicates the type of Signal sendNext.
    • T: Indicates that the next event will be sent only once. The content is typeTAn instance of the;
    • T?: Indicates that the next event will be sent only once. The content is typeTOrnil;
    • [T]: indicates that 0 to n next events are sent. The content is typeTAn instance of the;
    • [T?]: indicates that 0 to n next events are sent. The content is typeTOrnil;
    • NoneSaid:Don’tSend the next event;
  • E: Indicates the type of Signal sendErrorNSErrorNoError; NoErrorSignal does not sendError;
  • Completion: describes the situation sendCompleted;
    • If the number of times the next event is sent isInfinitely many timesThe consumer will never receive a Completed event, so this line can be left out.
  • Error: describes what case sendError; If Signal does not know sendError, this line can be omitted;
  • Scheduler: The thread on which the Signal is basedmain specified currentThe default iscurrent
    • Pipelines within the main module switch schedulers, so it is the responsibility of the module to ensure that the final signal is always on the Main schedular
    • Specified a task queue that was customized internally by the module to ensure that the signal eventually returned was in that particular schedular (or using the global default background schedular)
    • The internal pipeline for the current module does not do any scheduler switching and does not specify a specific schedular, so the signal returned is consistent with the external caller’s thread
  • Multicast: Whether to broadcast, usuallyYES NOThe default isNO

All the non-nested signals that make sense are possible

* HotSignal * HotSignal * HotSignal<[T], NoError=""> * HotSignal<[T?] , NoError=""> * HotSignal * HotSignal * HotSignal * HotSignal<[T], NSError=""> * HotSignal<[T?] , NSError=""> * HotSignal * ColdSignal * ColdSignal * ColdSignal<[T], NoError=""> * ColdSignal<[T?] , NoError=""> * ColdSignal * ColdSignal * ColdSignal * ColdSignal<[T], NSError=""> * ColdSignal<[T?] , NSError=""> * ColdSignalCopy the code

Countdown button to send the verification code

As shown in the figure above, the requirement here is that after clicking the button in the upper right corner, the button cannot be used, and a countdown time is displayed on the button. When the countdown time is reached, the button will be available again. This requirement is not difficult, I believe we can write out, but everyone to write the code, style will certainly vary, and inevitably will need some state variables to record some information, such as timer objects and countdown time and so on. If you switch to RAC, all requirements can be met in one contiguous piece of code, which looks like this:

//1 /* ColdSignal >), NoError> Completion: the 1-minute countdown is over; Error: none; Scheduler: main; Multicast: NO; */ - (RACSignal *)retryButtonTitleAndEnable { static const NSInteger n = 60; RACSignal *timer = [[[RACSignal interval:1 onScheduler:[RACScheduler mainThreadScheduler]] //7 map:^id(id value) { return nil; //8 }] startWith:nil]; //9 //10 NSMutableArray *numbers = [[NSMutableArray alloc] init]; for (NSInteger i = n; i >= 0; i--) { [numbers addObject:[NSNumber numberWithInteger:i]]; } return [[[[[numbers.rac_sequence.signal zipWith:timer] //11 map:^id(RACTuple *tuple) { //12 NSNumber *number = tuple.first; NSInteger count = number.integerValue; If (count == 0) {return RACTuplePack(@" retry ", [NSNumber numberWithBool:YES]); } else {NSString *title = [NSString stringWithFormat:@" Retry (% LDS)", (long)count];  return RACTuplePack(title, [NSNumber numberWithBool:NO]);  } }] takeUntil:[self rac_willDeallocSignal]] //13 setNameWithFormat:@"%s, retryButtonTitleAndEnable signal", __PRETTY_FUNCTION__] logCompleted]; //14 } - (void)initPipeline { @weakify(self); [[[[[[self.retryButtton rac_signalForControlEvents:UIControlEventTouchUpInside] map:^id(id value) { //2 @strongify(self); return [self retryButtonTitleAndEnable];  }] startWith:[self retryButtonTitleAndEnable]] //3 switchToLatest] //4 takeUntil:[self rac_willDeallocSignal]] //5 subscribeNext:^(RACTuple *tuple) { //6 @strongify(self); NSString *title = tuple.first;  [self.retryButtton setTitle:title forState:UIControlStateNormal];  self.retryButtton.enabled = ((NSNumber *)tuple.second).boolValue;  } completed:^{ //5 NSLog(@"%s, pipeline completed", __PRETTY_FUNCTION__); }]; // This omits the business logic to do after clicking retryButtton, and also omits the processing logic for the validation button and the verification code input box.Copy the code

The key code is described as follows:

  1. Design a RACSignal that sends Next data each time containing the text to be displayed on the button and the available status of the button. From a module perspective, external users do not need to know the internal details of the Signal (the countdown logic), so we will look at the implementation code of the outer Pipeline and then back up to look at the internal logic of the Signal.
  2. Whenever retryButtton clicks, to restart a timer, so in this map operations, call [the self retryButtonTitleAndEnable] get a Signal, the Signal as the output value of this small task. Note that since the map operation returns a Signal, it is expected that the embedded Pipeline flatten will be needed in subsequent operations of the outer Pipeline.
  3. In the business requirement, after clicking the retryButtton, ask the server to send a verification code (this part of the code is omitted and is easier to implement in RAC), and every time you enter the UI page, The retryButtton button does not require the user to actively click the retryButtton button. First, it automatically requests the server to send a verification code. In this case, retryButtton is also required to enter the countdown mode. To insert the first Next in the outer Pipeline data, because it is the same countdown logic, so this is also called [self retryButtonTitleAndEnable] get embedded Pipeline.
  4. As mentioned above, since the nesting of Pipeline is formed, it is necessary to solve this nesting, here using switchToLatest is more appropriate. Watch out for the difference with a flattenMap.
  5. Pipeline lifetime control, this technique has been covered in previous examples, however, this is just a double insurance. The complex part is that the outer Pipeline has a switchToLatest operation. When will the Signal after the switchToLatest be Completed? Please continue to see the explanation in 13.
  6. Here is the update of retryButtton’s title and status.
  7. Now it’s time to go back to the inner Pipeline logic. Using Pipeline to achieve a timer, with RAC provided by the interval operation on the line. Next is sent on the main thread every second.
  8. The Next data on the timer in 7 is the current system time value, which is not needed in our requirements, so it is directly map to nil.
  9. The RACSignal interval will not send its first timer until one second later, and will send one immediately with startWith, which represents the initial value of the countdown.
  10. Put the countdown numbers into an array and convert it to a signal using numbers. Rac_sequence. signal.
  11. Combine the Signal from 10 and the Timer Signal from 9 with zipWith. Note that the Signal assembled by zipWith will be Completed when numbers. Rac_sequence. Signal Completed.
  12. According to the value of the countdown, calculate the title information to be displayed on the button and the status of the button.
  13. The zipWith operation in previous 11 ensures that Completed is triggered when the countdown ends, but in case the user leaves the current page during the countdown, you need to trigger Completed by taking until. The reason I focus on Completed here is because the switchToLatest operation in previous 5 willsends completed when both the receiver and the last sent signal complete.
  14. Print some log information using setNameWithFormat and logCompleted to facilitate debugging. Note the Completed Signal.

Memory management, automatic release Pipeline

As you can see from the previous code, there are several places that emphasize the need to trigger Completed. This is entirely for the purpose of correct memory management, avoiding memory leaks and manually calling the disposal. TakeUntil: self.rac_willdeallocSignal is a common method.

In another typical scenario, Completed could also be triggered by a takeUntil operation as follows:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { WWKPhoto *photo = self.photos[indexPath.row]; XMCollectionImageViewCell *cell = [self.imageCollectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([XMCollectionImageViewCell class]) forIndexPath:indexPath]; cell.imageView.image = photo.thumbnail; @weakify(self); [[[cell.longPressSignal map:^id(XMCollectionImageViewCell *viewCell) { @strongify(self);  return [self.imageCollectionView indexPathForCell:viewCell];  }] takeUntil:[cell rac_prepareForReuseSignal]] subscribeNext:^(NSIndexPath *longPressIndexPath) { @strongify(self); UIAlertController * alert = [UIAlertController alertControllerWithTitle: @ "sure to delete the picture" message: nil preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction * ok = [UIAlertAction actionWithTitle: @ "sure" style: UIAlertActionStyleDefault handler: ^ (UIAlertAction * action){ @strongify(self);  [[self mutableArrayValueForKey:@keypath(self, photos)] removeObjectAtIndex:longPressIndexPath.row];  [self.imageCollectionView deleteItemsAtIndexPaths:@[longPressIndexPath]]; }]; UIAlertAction * cancel = [UIAlertAction actionWithTitle: @ "cancel" style: UIAlertActionStyleDefault handler: ^ (UIAlertAction * action) { [alert dismissViewControllerAnimated:YES completion:nil]; }]; [alert addAction:ok]; [alert addAction:cancel];  [self.containerViewController presentViewController:alert animated:YES completion:nil]; } completed:^{ }]; return cell; }Copy the code

This code is also very simple, the only thing that needs special attention is the takeUntil:[cell rac_prepareForReuseSignal] sentence, because UICollectionViewCell itself has a reuse mechanism, The lifetime of the Pipeline on each cell is not the same as the lifetime of the cell itself, so you cannot rely on cell.rac_willdeallocSignal, Instead, use [cell rac_prepareForReuseSignal], a more accurate Signal.

Design a Signal so that it sends Completed events to take advantage of Pipeline auto-release and keep the code simple. Some of the most commonly used signals in the RAC framework have internal implementations that do this with takeUntil operations, such as the following signals:

@interface UIControl (RACSignalSupport) - (RACSignal *)rac_signalForControlEvents:(UIControlEvents)controlEvents; @end @interface UIGestureRecognizer (RACSignalSupport) - (RACSignal *)rac_gestureSignal; @end RACObserve macro definitionCopy the code


The following Signal, which has no Completed event, requires its consumer to decide when to release the corresponding Pipeline:

@interface NSNotificationCenter (RACSupport)
- (RACSignal *)rac_addObserverForName:(NSString *)notificationName object:(id)object;
@endCopy the code