📜 ⬆️ ⬇️

How to write a "skorochitku" for iOS in half an hour

After reading posts about speed reading QuisyReader and 500 words per minute without preparation , I wanted to implement this idea for Apple smartphones on my own. For this, I developed an API, source code, which is published on github .



About the principle of functioning of the API and how to create a program for fast reading based on it, I will tell you under the cat
')


How it works?


The basis of the created API was taken the principle of Spritz and its method of staining and positioning words.
First, the API starts a repeating timer that fires several times a second. The timer calls a method that asks for the next word to draw from its data source. After receiving the desired word, we need to know the position of the letter, which we will color. With the help of empirical research it was possible to establish that the letters are colored as follows:

Accordingly, the position of the desired letter is calculated by the simplest formula:
(([word length] + 6) / 4) - 1; 

Knowing the position, we use the NSAttributedString to color the letters in different colors, black for the main word and red for the accented letter.
Now you need to calculate the coordinates of the center of the desired letter. To do this, we calculate the width of characters using the sizeWithFont: or sizeWithAttributes: method, depending on the iOS version.
Things are easy, we create a UILabel, put an NSAttributedString in it and set a frame with the width calculated for the selected font, using all the same methods. The received UILabel and the position of the center of the accented letter are passed to the RRTargetView class, which is engaged in drawing the target, which helps to focus attention on the desired letter, and positioning the UILabel within this target.

We write the Reader


Now a little more about the API, published on github , which we will use to implement our task.
The RRViewController class is responsible for forming the string that will be drawn. The class has the following public properties and methods:

Start / pause reading.
 - (void)startReading; - (void)pauseReading; 

Change the read speed by passing a negative or positive value. Speed ​​is measured in words per minute.
 - (void)changeSpeed:(int)speedModification; 

Changing the font size is also set to a negative or positive value, relative to the current size. Thresholds are set from 16 to 100.
 - (void)changeFont:(int)fontModification; 

As well as two properties that store a reference to objects that implement the Delegate and DataSource protocols of the RRViewController class.
 @property (nonatomic, weak) id <RRViewControllerDataSource> dataSource; @property (nonatomic, weak) id <RRViewControllerDelegate> delegate; 

The target is drawn and the text is positioned by the UIView successor of the RRTargetView class. The class has one public method and one property.
Sets a point in the range from 0.0 to 1.0 where the vertical notch at the target is drawn, the text is positioned relative to this point. The default value is 1/3.
 @property (nonatomic) CGFloat horizontalAccentPosition; 

Method accepts UILabel with text. AccentPoint is the point to be aligned with the vertical serif.
 - (void)positionLabel:(UILabel *)label withAccentPoint:(CGPoint)point; 

Example in the image


There are also two protocols.
RRViewControllerDelegate protocol

Both methods are optional, they are needed in order to receive an alert about changing the font size and speed of reading.
 - (void)reportFontSize:(CGFloat)size; - (void)reportReadingSpeed:(NSUInteger)speed; 

RRViewControllerDataSource protocol
Optional methods:
 - (NSString *)longestWordWithFont:(UIFont *)font; 

The method returns to RRViewController the longest word in the text, it is necessary for the system to automatically select the font size. Also, for the automatic text matching system to work, you need to set [NSUserDefaults standardUserDefaults] to the Boolean value YES for the auto_text_size key.
 - (NSString *)previousWord; 

Method to get the previous word, just in case there is an unfamiliar word in the text and you want to manually return to it.

Two required methods:
 - (NSString *)nextWord; 

RRViewController prompts the data model for the next word to display.
 - (NSString *)currentWord; 

The current word is requested, for example, in case of changing the font in order to re-draw the text.




Well, now we will write our reader.
To begin, create a project (Single View Application), give it an euphonic name, for example, Super Fast Reader.



Now we export to the API project, which we download from github , it will be engaged in drawing the text.

Open Storyboard, where there is a default UIViewController. Add a container to it, as the controller class placed in the container, specify RRViewController. We also need elements to control and enter text. Add the “Start”, “Pause” buttons, the UITextView, UIView text input box, into which all the controls for grouping them will be placed, and the UIScrollView into which we will place the container and the UIView with the controls. Now we associate these elements with the application code. Create an IBOutlet for UItextView and UIScrollView and two IBActions for handling the “Start” and “Pause” buttons.
 @property (weak, nonatomic) IBOutlet UITextView *textView; @property (weak, nonatomic) IBOutlet UIScrollView *scrollView; - (IBAction)startReading:(UIButton *)sender; - (IBAction)pauseReading:(UIButton *)sender; 


Storyboard view


Species hierarchy


It's time to work with RRViewController.
The first thing to do is get a link to it. When the container was created, the “Embed segue” was automatically created; we will assign a name to it through InterfaceBuilder, for example, RVCBecomesChild. At the moment of program execution, when adding RRViewController to the container, the prepareForSegue: sender: method will be called, which we will use.

First, create a property in which the object reference will be stored:
 @property (weak, nonatomic) RRViewController *readingVC; 

Now we implement the method:
 - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { if ([segue.identifier isEqualToString:@"RVCBecomesChild"]) { self.readingVC = segue.destinationViewController; } } 

Link to the controller is. What's next?
And then it is necessary to implement the required RRViewControllerDataSource protocol methods for the RRViewController class:
 - (NSString *)nextWord; - (NSString *)currentWord; 

The first thing we need is to inform RRViewController that we are its data source.
 - (void)viewDidLoad { [super viewDidLoad]; self.readingVC.dataSource = self; } 


Do not forget to inform the compiler that we support the protocol:
 @interface SFRViewController () <RRViewControllerDataSource> 

This is necessary so that the compiler does not issue a warning when assigning self.readingVC.dataSource = self, as well as to give us a warning in case all of the methods marked as @required are not implemented by us.

The text for reading will be taken from the UITextView field, in addition, we need a variable that will display the current position in the text.
 @property (nonatomic) NSUInteger currentWord; - (NSString *)nextWord { self.textPosition++; NSArray *words = [self.textView.text componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; if (self.textPosition >= [words count]) { self.textPosition = 0; } return [words objectAtIndex:self.textPosition]; } - (NSString *)currentWord { NSArray *words = [self.textView.text componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; if (self.textPosition >= [words count]) { self.textPosition = 0; } return [words objectAtIndex:self.textPosition]; } 


Not the most reasonable implementation, because when you query each word, we parse the entire text, it’s best to do it once when the text changes, and store the result in a variable. In what method it can be implemented I will write a little later.
Now we will implement actions for the buttons; for this, we will call the methods of the RRViewController class.
 - (IBAction)startReading:(UIButton *)sender { [self.readingVC startReading]; } - (IBAction)pauseReading:(UIButton *)sender { [self.readingVC pauseReading]; } 

Is done. You can run the program and enjoy the result.

It's not a bug


Immediately you can see that the speed is quite slow, and the font is a bit small. This problem is solved very easily. Create buttons that will call RRViewController methods. As an argument, negative or positive integers are passed to the method, which indicate how much these parameters should change.
 - (void)changeSpeed:(int)speedModification; - (void)changeFont:(int)fontModification; 

I will leave the implementation of these methods at your discretion, and we will deal with another problem.

In our program, when you try to add text, the keyboard overlaps the input field, the keyboard also does not retract when you press the Enter key (which is generally correct, because UITextView is used to enter multi-line text and pressing the Enter key inserts a line break character into the text, but we can change this behavior).
To solve this problem, we need to receive a response from the text field. To do this, let him know that our ViewController class is a delegate of the UITextView class and implements its protocol.
 @interface SFRViewController () <RRViewControllerDataSource, UITextViewDelegate> 

We also need to subscribe to receive messages from the keyboard, along with a message about its appearance, we will receive a dictionary containing its current dimensions, which we will need to calculate the UIScrollView offset.
 - (void)viewDidLoad { [super viewDidLoad]; self.readingVC.dataSource = self; self.textView.delegate = self; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWasShown:) name:UIKeyboardWillShowNotification object:nil]; } 

Next, we implement two methods that we need.
First, the method that will be called when the keyboard appears:
 - (void)keyboardWasShown:(NSNotification *)notification { CGSize keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size; [self.scrollView setContentInset:UIEdgeInsetsMake(-keyboardSize.height, 0, 0, 0)]; } 

Now we process the text change. If the hyphenation of the line "\ n" is inserted, remove the keyboard and remove the vertical offset for the UIScrollView. By the way, in the same method, you can implement the string parsing so that it is executed once when the text is changed.
 - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { if ([text isEqualToString:@"\n"]) { [textView resignFirstResponder]; } return YES; } 


Run again. Works! You can enjoy the result and implement your own ideas.
Naturally, this article showed the easiest way to get data, in the case of a more competent implementation, it is worth creating a separate class that will be engaged in parsing and providing data.

Afterword


The API is at the stage of development / revision, so I’m happy to accept any comments and suggestions for changing it, or fork the repository and make the changes in your own way.

Have a nice reading!

Source: https://habr.com/ru/post/218121/


All Articles