plist 中的每一页 utteranceSting 我们都创建了一个RWTPage.displayText。因此,每页的文本会一次性地显示出来。
由于
You’ve constructedeach RWTPage.displayTextfrom the combined utteranceStringsfor the page in the plist. So, your page view displays the entire page’s text.
However, remember that RWTPageViewController.speakNextUtterancecreates a single AVSpeechUtterancefor the entire RWTPage.displayText.The result is that it overlooks your carefully parsed utterance properties.
In order to modifyhow each utterance is spoken, you need to synthesize each page’s text asindividual utterances. If only there were some way to observe and control howand when AVSpeechSynthesizerspeaks. Hmmm…
实现委托
AVSpeechSynthesizer 有一个委托AVSpeechSynthesizerDelegate ,在合成器生命周期中它负责通知各种重要的事件和动作。通过实现这些委托方法,我们可以让声音更自然。
打开 RWTPage.h,加入下列代码:
@property (nonatomic, strong, readonly) NSArray *utterances; |
打开 RWTPage.m ,加入下列代码:
@property (nonatomic, strong, readwrite) NSArray *utterances; |
注意这个技巧:将一个属性在头文件中声明为只读,而在实现文件中声明为可读可写。这相当于只有对象自己能够写这个属性。
将 pageWithAttribute:方法修改为:
+ (instancetype)pageWithAttributes:(NSDictionary*)attributes { RWTPage *page = [[RWTPage alloc] init]; if ([[attributes objectForKey:RWTPageAttributesKeyUtterances] isKindOfClass:[NSString class]]) { page.displayText = [attributes objectForKey:RWTPageAttributesKeyUtterances]; page.backgroundImage = [attributes objectForKey:RWTPageAttributesKeyBackgroundImage]; // 1 page.utterances = @[[[AVSpeechUtterance alloc] initWithString:page.displayText]]; } else if ([[attributes objectForKey:RWTPageAttributesKeyUtterances] isKindOfClass:[NSArray class]]) { NSMutableArray *utterances = [NSMutableArray arrayWithCapacity:31]; NSMutableString *displayText = [NSMutableString stringWithCapacity:101]; for (NSDictionary *utteranceAttributes in [attributes objectForKey:RWTPageAttributesKeyUtterances]) { NSString *utteranceString = [utteranceAttributes objectForKey:RWTUtteranceAttributesKeyUtteranceString]; NSDictionary *utteranceProperties = [utteranceAttributes objectForKey:RWTUtteranceAttributesKeyUtteranceProperties]; AVSpeechUtterance *utterance = [[AVSpeechUtterance alloc] initWithString:utteranceString]; [utterance setValuesForKeysWithDictionary:utteranceProperties]; if (utterance) { [utterances addObject:utterance]; [displayText appendString:utteranceString]; } } page.displayText = displayText; page.backgroundImage = [UIImage imageNamed:[attributes objectForKey:RWTPageAttributesKeyBackgroundImage]]; // 2 page.utterances = [utterances copy]; } return page; } |
新加的代码位于注释 1 和注释 2 处。它们根据NSString 或NSArray的情况来设置 page.utterances 属性。
打开 RWTPageViewController.h ,修改为如下内容:
#import <UIKit/UIKit.h> @import AVFoundation; // 1 @interface RWTPageViewController : UIViewController<AVSpeechSynthesizerDelegate> @property (nonatomic, weak) IBOutlet UILabel *pageTextLabel; @property (nonatomic, weak) IBOutlet UIImageView *pageImageView; @end |
在注释 1 处,声明了RWTPageViewController 实现了AVSpeechSynthesizerDelegate 协议。
打开 RWTPageViewController.m 加入:
@property (nonatomic, assign) NSUInteger nextSpeechIndex; |
我们用一个属性来记录下一个要读到的 RWTPage.utterances 索引。
修改 setupForCurrentPage方法:
- (void)setupForCurrentPage { self.pageTextLabel.text = [self currentPage].displayText; self.pageImageView.image = [self currentPage].backgroundImage; self.nextSpeechIndex = 0; } |
修改 speakNextUtterance方法:
- (void)speakNextUtterance { // 1 if (self.nextSpeechIndex < [[self currentPage].utterances count]) { // 2 AVSpeechUtterance *utterance = [[self currentPage].utterances objectAtIndex:self.nextSpeechIndex]; self.nextSpeechIndex += 1; // 3 [self.synthesizer speakUtterance:utterance]; } } |
- 在注释为 1 的地方,我们先确保 nextSpeechUtterance 不会出现下标越界。
- 在注释为 2 的地方,我们获得当前要读到的 utterance,然后 nextSpeechUtterance 加1。
- 最终,注释为 3 的地方,朗读 utterance。
编译运行,听到了吗?在念第一页的时候,你只听见一个词“Whisky”。因为我们还没有实现其他的委托方法,因此当一个utterance 念完后,并不会接着念下一个 utterance。
修改 startSpeaking方法:
- (void)startSpeaking { if (!self.synthesizer) { self.synthesizer = [[AVSpeechSynthesizer alloc] init]; // 1 self.synthesizer.delegate = self; } [self speakNextUtterance]; } |
注释“1”的地方,我们将 ViewController 作为合成器的委托。
在 RWTPageViewController.m 最后加入一个方法:
#pragma mark - AVSpeechSynthesizerDelegate Protocol - (void)speechSynthesizer:(AVSpeechSynthesizer*)synthesizer didFinishSpeechUtterance:(AVSpeechUtterance*)utterance { NSUInteger indexOfUtterance = [[self currentPage].utterances indexOfObject:utterance]; if (indexOfUtterance == NSNotFound) { return; } [self speakNextUtterance]; } |
这段代码在当前 utterance (文本)念完后,开始念下一段。
编译运行,现在的效果是:
- 一旦当前文本念完,自动跳到下一文本。直至这一页都念完。
- 无论向前还是向后翻页时,这一页的内容都不再继续念了。
- 发声更自然,这归功于 WhirlySquirrelly.plist文件中的 utteranceProperties 字段。为此,作者不得不手工调整每一个词句。
如何使用 iOS 7 的 AVSpeechSynthesizer 制作有声书(3)