Как написать «скорочиталку» для iOS за полчаса

от автора

Прочитав на хабре посты про скорочтение QuisyReader и 500 слов в минуту без подготовки, захотелось реализовать данную идею для смартфонов Apple своими силами. Для этого я разработал API, исходные коды, которого опубликованы на github.

О принципе функционирования API и о том, как создать программу для скорочтения на его основе, я расскажу под катом

Как это работает?

За основу созданного API был взят принцип работы Spritz и его метод окрашивания и позиционирования слов.
Сначала API запускает повторяющийся таймер, срабатывающий несколько раз в секунду. Таймер вызывает метод, который запрашивает очередное слово для отрисовки у своего источника данных. После получения искомого слова нам нужно узнать позицию буквы, которую будем раскрашивать. С помощью эмпирических изысканий удалось установить, что буквы раскрашиваются следующим образом:

  • Длина слова 1. Раскрашиваемая буква 1.
  • Длина слова 2-5. Раскрашиваемая буква 2.
  • Длина слова 6-9. Раскрашиваемая буква 3.
  • и т.д.

Соответственно позиция нужной буквы вычисляется по простейшей формуле:

(([word length] + 6) / 4) - 1; 

Зная позицию, используем NSAttributedString для окрашивания букв в разные цвета, черный для основного слова и красный для акцентируемой буквы.
Теперь необходимо рассчитать координаты центра нужной буквы. Для этого подсчитываем ширину символов с помощью метода sizeWithFont: или sizeWithAttributes: в зависимости от версии iOS.
Дело за малым создаем UILabel, помещаем в него NSAttributedString и устанавливаем фрейм с шириной рассчитанной для выбранного шрифта, с помощью всё тех же методов. Полученный UILabel и позицию центра акцентируемой буквы передаем классу RRTargetView, который занимается отрисовкой мишени, помогающей сконцентрировать внимание на нужной букве, и позиционированием UILabel в пределах этой мишени.

Пишем свой Reader

Теперь чуть подробнее об API, опубликованном на github, который будем использовать для реализации нашей задачи.
Класс RRViewController отвечает за формирование строки, которая будет отрисовываться. У класса есть следующие публичные свойства и методы:

Запуск/приостановка чтения.

- (void)startReading; - (void)pauseReading; 

Изменение скорости чтения, путем передачи отрицательного или положительного значения. Скорость измеряется в словах в минуту.

- (void)changeSpeed:(int)speedModification; 

Изменение размера шрифта, также задается отрицательным или положительным значением, относительно текущего размера. Заданы пороговые значения от 16 до 100.

- (void)changeFont:(int)fontModification; 

А также два свойства хранящие ссылку на объекты, которые реализуют Delegate и DataSource протоколы класса RRViewController.

@property (nonatomic, weak) id <RRViewControllerDataSource> dataSource; @property (nonatomic, weak) id <RRViewControllerDelegate> delegate; 

Отрисовка мишени и позиционирование текста выполняется наследником UIView классом RRTargetView. У класса один публичный метод и одно свойство.
Устанавливает точку в диапазоне от 0.0 до 1.0 в которой отрисовывается вертикальная засечка у мишени, текст позиционируется относительно этой точки. Значение по умолчанию равно 1/3.

@property (nonatomic) CGFloat horizontalAccentPosition; 

Метод принимает UILabel с текстом. AccentPoint — это точка которая будет совмещена с вертикальной засечкой.

- (void)positionLabel:(UILabel *)label withAccentPoint:(CGPoint)point; 

Пример на изображении

Также имеется два протокола.
Протокол RRViewControllerDelegate

Оба метода являются необязательными, они нужны для того, чтобы получить оповещение об изменении размера шрифта и скорости чтения.

- (void)reportFontSize:(CGFloat)size; - (void)reportReadingSpeed:(NSUInteger)speed; 

Протокол RRViewControllerDataSource
Опциональные методы:

- (NSString *)longestWordWithFont:(UIFont *)font; 

Метод возвращает RRViewController’у самое длинное слово в тексте, оно необходимо для работы системы автоматического подбора размера шрифта. Также для работы системы автоматического подбора текста, нужно в [NSUserDefaults standardUserDefaults] установить булево значение YES для ключа auto_text_size.

- (NSString *)previousWord; 

Метод для получения предыдущего слова, на случай если в тексте встретилось незнакомое слово и вы хотите вручную вернуться к нему.

Два обязательных метода:

- (NSString *)nextWord; 

RRViewController запрашивает у модели данных следующее слово для отображения.

- (NSString *)currentWord; 

Запрашивается текущее слово, например, на случай изменения шрифта для того, чтобы произвести повторную отрисовку текста.


Ну а теперь напишем свою читалку.
Для начала создаем проект (Single View Application), даём ему благозвучное имя, например, Super Fast Reader.

Теперь экспортируем в проект API, который скачиваем с github, он будет заниматься отрисовкой текста.

Открываем Storyboard, где присутствует UIViewController, созданный по умолчанию. Добавляем в него контейнер, в качестве класс контроллера, помещенного в контейнер, указываем RRViewController. Также нам понадобятся элементы для управления и ввода текста. Добавим кнопки «Старт», «Пауза», поле для ввода текста UITextView, UIView, в который будут помещены все элементы управления для их группировки, а также UIScrollView в который мы поместим контейнер и UIView с элементами управления. Теперь связываем эти элементы с кодом приложения. Создаем IBOutlet для UItextView и UIScrollView и два IBAction для обработки нажатий кнопок «Старт» и «Пауза».

@property (weak, nonatomic) IBOutlet UITextView *textView; @property (weak, nonatomic) IBOutlet UIScrollView *scrollView;  - (IBAction)startReading:(UIButton *)sender; - (IBAction)pauseReading:(UIButton *)sender; 

Представление в Storyboard

Иерархия видов

Настало время для работы с RRViewController.
Первое, что необходимо сделать, это получить на него ссылку. При создании контейнера был автоматически создан «Embed segue», назначим ему имя через InterfaceBuilder, например, RVCBecomesChild. В момент исполнения программы, при добавлении RRViewController в контейнер будет вызван метод prepareForSegue:sender:, чем мы и воспользуемся.

Сначала создаем свойство, в котором будет хранится ссылка на объект:

@property (weak, nonatomic) RRViewController *readingVC; 

Теперь реализуем метод:

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

Ссылка на контроллер есть. Что дальше?
А дальше необходимо реализовать обязательные методы протокола RRViewControllerDataSource для класса RRViewController:

- (NSString *)nextWord; - (NSString *)currentWord; 

Первое, что нам необходимо, сообщить RRViewController’у о том, что мы являемся его источником данных.

- (void)viewDidLoad {     [super viewDidLoad];     self.readingVC.dataSource = self; } 

Не забываем сообщить компилятору о том, что мы поддерживаем протокол:

@interface SFRViewController () <RRViewControllerDataSource> 

Это нужно для того, чтобы компилятор не выдавал предупреждения при присвоении self.readingVC.dataSource = self, а также для того, чтобы нам выдавалось предупреждение в случае, если не все методы помеченные, как @required были нами реализованы.

Текст для чтения будем брать из поля UITextView, дополнительно понадобится переменная, которая будет отображать текущую позицию в тексте.

@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]; } 

Не самая разумная реализация, ведь при запросе каждого слова мы производим парсинг всего текста, лучше выполнять это один раз при изменении текста, а результат хранить в переменной. В каком методе это можно реализовать напишу немного позже.
Теперь реализуем действия для кнопок, для этого будет вызывать методы класса RRViewController.

- (IBAction)startReading:(UIButton *)sender {     [self.readingVC startReading]; }  - (IBAction)pauseReading:(UIButton *)sender {     [self.readingVC pauseReading]; } 

Готово. Можно запускать программу и наслаждаться результатом.

It’s not a bug, it’s a feature

Сразу можно заметить, что скорость достаточно медленная, а шрифт мелковат. Решается эта проблема очень легко. Создаем кнопки, которые будут вызывать методы RRViewController. В качестве аргумента методу передаются отрицательные, либо положительные целые числа, которые сообщают на сколько должны измениться эти параметры.

- (void)changeSpeed:(int)speedModification; - (void)changeFont:(int)fontModification; 

Реализацию этих методов оставлю на ваше усмотрение, а мы займемся решением другой проблемы.

В нашей программе при попытке добавления текста клавиатура перекрывает поле для ввода, также клавиатура не убирается при нажатии клавиши Enter, (что в общем то является правильным, ведь UITextView используется для ввода многострочного текста и нажатие клавиши «Enter» вставляет символ переноса строки в текст, но мы можем поменять это поведение).
Чтобы решить эту задачу, нам нужно получать отклик от текстового поля. Для этого сообщим ему, что наш класс ViewController является делеагатом класса UITextView и реализует его протокол.

@interface SFRViewController () <RRViewControllerDataSource, UITextViewDelegate> 

Также нам нужно подписаться на получение сообщений от клавиатуры, вместе с сообщением о её появлении мы будем получать словарь содержащий её текущие размеры, которые понадобятся нам для расчета смещения UIScrollView.

- (void)viewDidLoad {     [super viewDidLoad];     self.readingVC.dataSource = self;     self.textView.delegate = self;          [[NSNotificationCenter defaultCenter] addObserver:self                                              selector:@selector(keyboardWasShown:)                                                  name:UIKeyboardWillShowNotification                                                object:nil]; } 

Далее реализуем два метода, которые нам необходимы.
Сначала метод, который будет вызываться при появлении клавиатуры:

- (void)keyboardWasShown:(NSNotification *)notification {     CGSize keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;     [self.scrollView setContentInset:UIEdgeInsetsMake(-keyboardSize.height, 0, 0, 0)]; } 

Теперь обрабатываем изменение текста. Если происходит вставка символа переноса строки "\n", убираем клавиатуру и убираем вертикальное смещение для UIScrollView. Кстати в этом же методе вы можете реализовать парсинг строки для того, чтобы он выполнялся единожды при изменении текста.

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {     if ([text isEqualToString:@"\n"])     {         [textView resignFirstResponder];     }     return YES; } 

Запускаем еще раз. Работает! Можно наслаждаться результатом и заниматься реализацией собственных идей.
Естественно в данной статье был показан самый простой способ получения данных, в случае более грамотной реализации, стоит создать отдельный класс, который будет заниматься парсингом и предоставлением данных.

Послесловие

API находится на стадии разработки/доработки, поэтому с удовольствием приму любые замечания и предложения по его изменению, либо форкайте репозиторий и выполняйте изменения на свой лад.

Приятного скорочтения!

ссылка на оригинал статьи http://habrahabr.ru/post/218121/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *