Прочитав на хабре посты про скорочтение 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 находится на стадии разработки/доработки, поэтому с удовольствием приму любые замечания и предложения по его изменению, либо форкайте репозиторий и выполняйте изменения на свой лад.
Приятного скорочтения!
Автор: GxocT