原文: http://www.raywenderlich.com/64623/make-narrated-book-using-avspeechsynthesizer-ios-7
随着 PageViewController 的引入,苹果让开发人员们制作图书类app 更加轻松。
不幸的是,对于生活在朝九晚五繁忙节奏中的人们来说,阅读也是一件奢侈的事情。为什么你不能在读一本小说的同一时候做其它事情呢?
在 Siri 刚開始出现的时候,苹果以前用复杂的动态文本阅读将开发人员拒之门外,但当iOS7 公布的时候,苹果最终放开了这扇大门。
在本教程中,你将制作一本故事书。这本书的每一页都会在显示文字的同一时候朗读文字中的内容。
有声的阅读将让你的 app 在 iTunes 中显得与众不同,同一时候还保护了视力。
有声书尤其受广播听众的喜爱。由于它同意人们在锻炼、烹饪或工作的同一时候进行“阅读”。
当你制作自己的有声书时, 你将学习到:
- 怎样使用 AVSpeechSynthesizer 和 AVSpeechUtterance 让 iOS 设备朗读文本
- How to make this synthesized speech sound more natural by modifying AVSpeechUtterance properties like pitch and rate.
- 怎样改动 AVSpeechUtterance 属性比如 pitch 和 rate,使合成的语音更自然
AVSpeechSynthesizer当然比不上真人语音。但它对于你将要开发的 app 来说,相对easy一些。
注意:关于怎样用 Sprite Kit 开发iPad儿童书籍,请參考Tammy Coron 的教程: How to Create an Interactive Children’s Book for the iPad
開始:AVSpeechSynthesizer
首先,请下载 初始项目。进入NarratedBookUsingAVSpeechStarter 文件夹。双击 NarratedBookUsingAVSpeech.xcodeproj 以打开初始项目。
Build & run 。你将在模拟器中看到:
书的内容是关于松鼠的童谣。尽管不是亚马逊买得最火的读物,但对于本教程来说足够了。
向左滑动进行向后翻页,向右滑动则返回前一页。
噢。它已经拥有了主要的“书”的功能,真是不错的開始。
理解机制
注意:教程的最后,会留给你几个习题。
接下来一节将包含演示样例项目的一些内容,以便你能独立完毕这些习题。假设你对这部分内容不感兴趣。请跳到下一节。
初始项目包含两个类:
1. Models: 用于存放书籍的内容,它是page 的集合。
2. Presentation: 将 models 展现到屏幕并响应用户动作(比如滑动手势)。
在你制作自己的图书时,理解这两个类的工作机制是非常有必要的。
打开RWTBook.h:
@interface RWTBook : NSObject //1 @property (nonatomic, copy, readonly) NSArray *pages; //2 + (instancetype)bookWithPages:(NSArray*)pages; //3 + (instancetype)testBook; @end |
- pages 属性存放了 Page 对象的数组,每一个 Page对象代表图书中的每一页。
- bookWithPages: 方法是一个初始化 Book 的方法,它用指定的 page 对象数组为參数。返回一个 book 对象。
- testBook 创建 Book 对象,用于測试。
在開始增加和读取你自己的图书内容之前,就先使用 testBook 创建一个简单的 Book 吧。
RWTPage.h声明例如以下:
//1 extern NSString* const RWTPageAttributesKeyUtterances; extern NSString* const RWTPageAttributesKeyBackgroundImage; @interface RWTPage : NSObject //2 @property (nonatomic, strong, readonly) NSString *displayText; @property (nonatomic, strong, readonly) UIImage *backgroundImage; //3 + (instancetype)pageWithAttributes:(NSDictionary*)attributes; @end |
- 常量用于从字典中检索页。RWTPageAttributesKeyUtterances常量能够检索出page 对象中的文本,RWTPageAttributesKeyBackgroundImage则用于检索 page 对象所用的背景图片。
- displayText 属性用于存储 page 的文本,backgroundImage 属性用于存储 page 的背景图片。
- pageWithAttributes:用指定的 NSDictionary 创建一个 page 实例。
RWTPageViewController.m声明例如以下:
#pragma mark - Class Extension // 1 @interface RWTPageViewController () @property (nonatomic, strong) RWTBook *book; @property (nonatomic, assign) NSUInteger currentPageIndex; @end @implementation RWTPageViewController #pragma mark - Lifecycle // 2 - (void)viewDidLoad { [super viewDidLoad]; [self setupBook:[RWTBook testBook]]; UISwipeGestureRecognizer *swipeNext = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(gotoNextPage)]; swipeNext.direction = UISwipeGestureRecognizerDirectionLeft; [self.view addGestureRecognizer:swipeNext]; UISwipeGestureRecognizer *swipePrevious = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(gotoPreviousPage)]; swipePrevious.direction = UISwipeGestureRecognizerDirectionRight; [self.view addGestureRecognizer:swipePrevious]; } #pragma mark - Private // 3 - (RWTPage*)currentPage { return [self.book.pages objectAtIndex:self.currentPageIndex]; } // 4 - (void)setupBook:(RWTBook*)newBook { self.book = newBook; self.currentPageIndex = 0; [self setupForCurrentPage]; } // 5 - (void)setupForCurrentPage { self.pageTextLabel.text = [self currentPage].displayText; self.pageImageView.image = [self currentPage].backgroundImage; } // 6 - (void)gotoNextPage { if ([self.book.pages count] == 0 || self.currentPageIndex == [self.book.pages count] - 1) { return; } self.currentPageIndex += 1; [self setupForCurrentPage]; } // 7 - (void)gotoPreviousPage { if (self.currentPageIndex == 0) { return; } self.currentPageIndex -= 1; [self setupForCurrentPage]; } @end |
以上代码说明例如以下:
- book 属性保存了当前的 RWTBook 对象,currentPageIndex属性保存了 RWTBook 对象的当前页索引。
- 当视图载入完毕,设置要显示的 page,并增加滑动手势的识别器以便用户能通过手势进行翻页。
- 返回当前页的 RWTPage 对象。
- 设置 book 属性并将当前页置为第一页。
- 设置当前页的显示内容。
- 查找下一页。假设该页存在。则将下一页设置为当前页。
该方法由 swipeNext 手势识别器调用。
- 查找上一页。假设该页存在。则将上一页设置为当前页。该方法由 swipePrevious 手势识别器调用。
播放和停止!
这是一个非常要命的问题。
打开RWTPageViewController.m,在#import "RWTPage.h" 以下增加:
@import AVFoundation; |
iOS 语音功能由 AVFoundation 框架提供,你必须导入这个框架。
提示: @import会导入并连接 AVFoundation 框架。关于 iOS7 中 @import 及相关的 O-C 语言新特性,请參考这篇文章What’sNew in Objective-C and Foundation in iOS 7。
在 currentPageIndex 属性声明之下增加:
@property (nonatomic, strong) AVSpeechSynthesizer *synthesizer; |
synthesizer 对象将用于朗读每一页中的文字。
能够将 ViewController 中定义的AVSpeechSynthesizer 对象想象成一个会说话的人。而 AVSpeechUtterance 则能够想象成一张小纸条。把纸条递给这个人,则他就会念出纸条上的字。
注意:一个 AVSpeechUtterance 可能是一个单词,比方“Whisky”,或者是一个完整的语句,比方“Whisky,frisky,hippidityhop”。
在 RWTPageViewController.m 的最后增加以下方法:
#pragma mark - Speech Management - (void)speakNextUtterance { AVSpeechUtterance *nextUtterance = [[AVSpeechUtterance alloc] initWithString:[self currentPage].displayText]; [self.synthesizer speakUtterance:nextUtterance]; } |
创建了一个 utterance 对象,然后告诉 synthesizer 去念出它。
然后实现这种方法:
- (void)startSpeaking { if (!self.synthesizer) { self.synthesizer = [[AVSpeechSynthesizer alloc] init]; } [self speakNextUtterance]; } |
这种方法负责初始化 synthesizer 属性(假设它未初始化的话)。
然后调用speakNextUtterance 方法,開始朗读。
在 viewDidLoad 、gotoNextPage 和 gotoPreviousPage 方法的最后加上这行:
[self startSpeaking]; |
这样。当书一打开,或者用户前后翻页的时候。朗读就会開始。
Build & run。你会听到AVSpeechSynthesizer 发出的天籁之音。
注意:假设你什么也没听到,请检查 Mac 或者 iOS 设备的音量设置(看你是在什么地方执行这个 app 的)。你能够尝试着进行翻页看是不是能播放语音。
提示:假设你是在模拟器上执行程序。 可能控制台会输出一堆莫名其妙的错误信息。
这仅仅会在模拟器上出现。使用设备时则不会打印这些错误。
假设你听到了语音播放,请再次 Build & Run。这次,在第一页内容播放完之前,尝试向左滑动(向后翻页)。发现了什么?
synthesizer 仅仅会在第一页念完之后才開始念下一页。这不是用户想要的结果。他们会想让第一页停止播放而第二页马上開始。这点小瑕疵对于一页内容比較短的童谣来说不成问题。但试想一下,假设每页的内容都非常长的话会是什么效果……