如何编写iOS5杂志程序(2)

原文:http://ios-blog.co.uk/tutorials/how-to-make-a-magazine-app-in-ios-part-ii/

改变

教程第一部分介绍了许多东西。 抱歉我又迟到了,在写本文的同时我关注了iOS5的新特性,但由于NDA的缘故我不得泄露任何关于新SDK的内容。最终的例子同时提供了对iOS4和iOS5 的支持。

我不会过多讲解Newsstand,归根结底我们将创建一个杂志应用程序,newsstand的实现细节与此无关。在我的博客我写了俩个教程,你可以阅读它们(这里以及这里),它们已经包括了newsstand的方方面面。简单而言,newsstand在ipad和iPhone上采用全新的方式来展现杂志,原来的图标代以杂志(或报刊)的封面,然后所有的newsstand图标被放在了一起。对于开发者,newsstand包含了一个Newsstand  Kit框架,包括内容的下载、安装及组织方式。

示例程序

下图显示了程序的部分截图。9个杂志、9个水果味的封面。你可以下载、通过进度条查看下载进度、然后阅读杂志。另外一张图显示了Newsstand。在Nesstand组中,原本的应用程序图标被杂志封面图标所替代。但在iOS4的iPad中,程序仍然显示的是应用程序图标。

如何编写iOS5杂志程序(2)_第1张图片

程序代码放在了这里:  GitHub。不要将这些代码用于生产,除非你经过大量测试。但在真正的开发中可以用它来作为一个不错的起点。实际上,在第1部分我们已经解释过这些代码的主要结构;我建议你在阅读本章前先阅读第一部分内容,以更好地理解程序的主要组成。接下来讨论的过程中,要始终将期刊管理(控制器,而不是视图控制器)与UI尽可能分离。理论上,“书店管理器”和“期刊模型”也能在Mac下重用,因为它们与UI是非常“松耦合”的。

我用“单窗口模板”创建这个程序。在application:didFinishLaunchingWithOptions方法中,加入2个主要组件:

-(BOOL)application:(UIApplication *)applicationdidFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{    
    // 创建 "Store" 实例
    _store = [[Store alloc] init];
    [_store startup];

    self.shelf = [[[ShelfViewController alloc] initWithNibName:nilbundle:nil] autorelease];
    _shelf.store=_store;

    self.window = [[[UIWindow alloc] initWithFrame:[[UIScreenmainScreen] bounds]] autorelease];
    self.window.rootViewController = _shelf;
    [self.window makeKeyAndVisible];
    return YES;
}

 

这两个组件分别是:

  • Store类,即框图中的“书店管理器”;继承自NSObject,与UI没有任何关系。
  • ShelfViewController类,代表了应用程序界面。它有一个Store类型的属性。它不直接访问Store类的属性,而是通过简单API来从Store获取所需信息。我们也可以使用委托,但这基本上只是在特定程序中的特定的两个类间使用,没有必要为它们的交互专门定义一套协议。这个控制器可以分为两部分,一个用于UI,即书架,一个用于后台,即书店。书架严格依赖于书店,反之则不然。书店到书架的通讯使用懒惰模式,即通知。

程序通过info.plist集成到newsstand。具体请阅读苹果文档或者我的教程。

模型和控制器

程序中有1个模型,即“Issue”模型,代表了位于书店或用户已购期刊中的一本期刊。还有一个控制器,即“书店”。虽然这个控制器并不是UI组件,但对于本程序的“后台”,有这两个组件就足够了。理论上,我们可以不使用任何用户界面即可检索书店状态并下载杂志。这是杂志应用程序中的基本概念,有许多事件是在后台发生而无需用户干预:也就是说,程序在UI尚未加载的情况下就能执行任务。

Issue类表示所有杂志的特性,唯一的id,名称、发行日期、封面图片的url、内容的url(杂志的内容可以是pdf文件、epub文件或者zip压缩包)。特别是id,在整个杂志的生命周期中都必须存在(比如,名称可以由于地区不同而改变,但id明显不行)。此外,id还用于让Newsstand识别一本唯一在刊物(通过NKIssue的name字段),也用于将产品和AppStore关联(如果要实现应用内购买的话)。

除此之外,Issue类在杂志下载过程中也很重要。Store类负责启动下载,但Issue类要负责监控进度,当下载完成时进行安装。

最后,Issue类还能表示一本杂志是否已经下载以及是否存在于用户的图书库中。Issue类有个isIssueAvailableForRead方法,用于通知视图是否允许对期刊进行某些操作(阅读或下载),并显示相应内容。

Store类是app中的控制器类。它在应用程序启动就初始化,而且一直不释放。这个类一初始化就开始从出版商的服务器获取商品(杂志)列表。这里我们用一个简单的plist文件实现了杂志列表,对它进行解码(反序列化)并创建Issue对象,下载它们的封面图片。所有这些都使用GCD来异步执行,当杂志列表就绪,向所有相关对象(尤其是ViewController)发送消息通知,以便UI更新。注意,status属性用于表示书店状态。我们重写了它的setter方法,在这个方法中张贴状态更新通知。为简单起见,我们将状态值限定为“未初始化”、“正在下载”、“就绪”及“错误”。根据需要你可以增加额外的状态。最后,当连接不可用时,我们从本地的plist文件加载书店数据(哪怕用户不连接互联网,也能够访问他下载过的内容)。

downloadStoreIssues方法是类的核心代码,我们列出如下:

-(void)downloadStoreIssues{
    self.status=StoreStatusDownloading;
   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0), ^{
        NSArray *_list = [[NSArray alloc]initWithContentsOfURL:[NSURLURLWithString:@"http://www.viggiosoft.com/media/data/iosblog/magazine/store.plist"]];
        if(!_list) {
            // let's try to retrieve it locally
            _list = [[NSArray alloc]initWithContentsOfURL:[self fileURLOfCachedStoreFile]];
        }
        if(_list) {
            // now creating all issues andstoring in the storeIssues array
            [_listenumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
                NSDictionary*issueDictionary = (NSDictionary *)obj;
                Issue *anIssue =[[Issue alloc] init];
                anIssue.issueID=[issueDictionaryobjectForKey:@"ID"];
               anIssue.title=[issueDictionary objectForKey:@"Title"];
               anIssue.releaseDate=[issueDictionary objectForKey:@"Release date"];
               anIssue.coverURL=[issueDictionary objectForKey:@"Cover URL"];
               anIssue.downloadURL=[issueDictionary objectForKey:@"Download URL"];
                anIssue.free=[(NSNumber*)[issueDictionary objectForKey:@"Free"] boolValue];
                [anIssueaddInNewsstand];
                [storeIssuesaddObject:anIssue];
                [anIssue release];
                // dispatch coverloading
                if(![anIssuecoverImage]) {
                   dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                       NSData *imgData = [NSData dataWithContentsOfURL:[NSURLURLWithString:anIssue.coverURL]];
                       if(imgData) {
                           [imgData writeToURL:[anIssue.contentURLURLByAppendingPathComponent:@"cover.png"] atomically:YES];
                       }
                    });
                }
            }];
            // let's save the file locally
            [_list writeToURL:[selffileURLOfCachedStoreFile] atomically:YES];
            [_list release];
            self.status=StoreStatusReady;
        } else {
            ELog(@"Store downloadfailed.");
            storeIssues = nil;
            self.status=StoreStatusError;
        }
    });
}

在这段代码中,我在plist下载之后立即开始封面图片的下载。这样做并不好,因为在刷新app 状态之前,如果网络状况较差,这个额外的网络通信会导致延迟出现。体验更好的做法是:在一个真实的app中,由于期刊的数目总是有限的,我们可以在后台下载封面图片,每当下完一个图片就通知UI进行更新。

 

视图控制器

UI将很快出现,它根据Store类发送的通知进行更新。当通知中心将Store类的“就绪”信号发送到UI时,所有的UI对象将被加载(在这里即CoverView类,一个UIView,仅包含最少的应用程序逻辑),然后向用户显示书架。

这是,app停止后台处理,等待用户输入。有两种可能:

  • 如果杂志已经被下载,用户将看到“READ”按钮,点击该按钮可以阅读该杂志。本例中所有杂志都是pdf文件,我们利用iOS自带的Quick Look框架就足以显示pdf文件。
  • 如果杂志尚未下载,用户将看到“DOWNLOAD”按钮,点击该按钮开始下载并显示进度条。下载完成,我们替换按钮是的文字,并隐藏进度条。

视图控制器依赖于Store类。为了获得刊物信息(期刊数目、每一期的细节),视图控制器通过简单API,而不是直接访问Store类的属性。

/* "numberOfIssues"is used to retrieve the number of issues in the store */
-(NSInteger)numberOfStoreIssues;

/* "issueAtIndex:" retrieves the issue at the given index */
-(Issue *)issueAtIndex:(NSInteger)index;

/* "issueWithID:" retrieves the issue with the given ID */
-(Issue *)issueWithID:(NSString *)issueID;

 

根据这个简单API以及期刊的属性,视图控制器将创建期刊视图(即CoverView)并放到屏幕上。

下载杂志

在一个杂志类应用程序中,有3个重要的问题:检索和显示书架的内容,下载并阅读杂志(在一个杂志类app中,一个好的PDF或epub阅读器是必须的;但本文的主题,是介绍杂志app的结构和相关技术,而阅读器更多地是与用户体验有关)。

依照我的想法,在下载杂志的过程中,用户必须完全不加干预,用户的动作必须完全不会影响下载的结果。现在有许多app都有这样一个缺点:他们放一个转轮在屏幕中央,用于让用户等待,并阻塞用户与UI对象交互。这种做法很简单,但不是一种良好的体验。在等待的过程中,用户可以阅读其他期刊,可以返回书店并决定暂时切换到别的app,或者最终关闭网络。Newsstand Kit提供了系统级别的方法,简化开发者的工作,同时提供了良好的用户体验。

一旦用户开始下载,视图控制器会发送一个现在请求给Store类。在下面的代码(scheduleDownloadOfIssue:方法)中,会生成网络请求并发送到后台。注意,我们根据iOS的本将代码分为两个部分。如果是iOS5,我们必须使用Newsstand——下载将被放入Newsstand队列中由系统进行管理;如果是iOS4,我们采用常规的基于NSOperation的方法:这种情况下,我们无法简单地获取下载内容的长度,因此在iOS4中进度条不可见。而Newsstand在下载完将在进度条上显示“forfree”。

-(void)scheduleDownloadOfIssue:(Issue*)issueToDownload {
    NSString *downloadURL = [issueToDownload downloadURL];
    NSURLRequest *downloadRequest = [NSURLRequestrequestWithURL:[NSURL URLWithString:downloadURL]];
    if(isOS5()) {
        // iOS5 : use Newsstand
        NKIssue *nkIssue = [issueToDownloadnewsstandIssue];
        NKAssetDownload *assetDownload = [nkIssueaddAssetWithRequest:downloadRequest];
        [assetDownloaddownloadWithDelegate:issueToDownload];
    } else {
        // iOS4 : use NSOperation
        NSURLConnection *conn = [NSURLConnectionconnectionWithRequest:downloadRequest delegate:issueToDownload];
        NSInvocationOperation *op = [[NSInvocationOperationalloc] initWithTarget:self selector:@selector(startDownload:) object:conn];
        if(!downloadQueue) {
            downloadQueue = [[NSOperationQueuealloc] init];
           downloadQueue.maxConcurrentOperationCount=1;
        }
        [downloadQueue addOperation:op];
        [downloadQueue setSuspended:NO];
    }
}

// iOS4 only
-(void)startDownload:(id)obj {
    NSURLConnection *conn = (NSURLConnection *)obj;
    [conn start];
}

在这两种情况中,我要强调一个事实:Store启动下载线程(operation),但它将此后的工作委托给其他类来实现,比如正在被下载的Issue对象。因此由Issue类来跟中下载进度直至结束。

Issue类将扮演Store类创建的下载线程的委托对象。使用和不使用Newsstand,委托协议是不相同的。使用Newsstand,你需要使用NSURLConnectionDownloadDelegate 协议。如果不使用Newsstand,则使用 NSURLConnectionDataDelegate协议——它派生自NSURLConnectionDelegate协议。二个协议的不同在于,前者下载到文件系统,后者仅是内存数据。 在第二种情况下,我们将整个下载内容存放在内存,只有下载结束才将它保存到磁盘——不要在最终产品中这样做,因为如果你下载的内容达到上百M时,这将导致程序崩溃。

为了使下载进度可视化并实时根据进度更新UI,我们决定让Store 控制器和任何UI组件分离。因此我们使用了KVO模型和通知。当视图控制器开始一个下载进程时,它将自己以及杂志视图(coverView)设置为download对象(Issue)的观察者,以便当下载结束(成功或失败)便可更新UI状态。

-(void)downloadIssue:(Issue*)issue updateCover:(CoverView *)cover {
    cover.progress.alpha=1.0;
    cover.button.alpha=0.0;
    [issue addObserver:cover forKeyPath:@"downloadProgress"options:NSKeyValueObservingOptionNew context:NULL];
    [[NSNotificationCenter defaultCenter] addObserver:selfselector:@selector(issueDidEndDownload:)name:ISSUE_END_OF_DOWNLOAD_NOTIFICATION object:issue];
    [[NSNotificationCenter defaultCenter] addObserver:selfselector:@selector(issueDidFailDownload:)name:ISSUE_FAILED_DOWNLOAD_NOTIFICATION object:issue];
    [[NSNotificationCenter defaultCenter] addObserver:coverselector:@selector(issueDidEndDownload:)name:ISSUE_END_OF_DOWNLOAD_NOTIFICATION object:issue];
    [[NSNotificationCenter defaultCenter] addObserver:coverselector:@selector(issueDidFailDownload:)name:ISSUE_FAILED_DOWNLOAD_NOTIFICATION object:issue];
    [_store scheduleDownloadOfIssue:issue];
}

当下载线程终止,cover view和视图控制器都需要从通知中心注销。 但是允许cover view继续监听进程状态,因此下载开始时,cover view就注册为Issue的downloadProgress属性的观察者。也就是说,每当该下载进度变化,coverview将收到 downloadPregress属性变化通知,有此更新progress bar状态(因此我们的UIProgressBar就有了一个根据后台状态变化的属性,即“下载进度”)。coverview在下载结束时注销它自己。

当下载结束,Issue对象将下载内容拷贝到最终的目标文件夹。使用Newsstand框架,这个目标文件夹由系统指定。而在iOS4中,目标文件夹为caches目录以便和iCloud兼容(iOS4不支持iCloud,但我们的app是同时运行在两种iOS版本中的,我们必须同时考虑到两种情况)。在使用Newsstand的情况下,我们也要更新Newsstand图标为该封面图片。下面的下载完成后的处理代码中,我们简单地使用一句代码实现了这一点(同时在最后也张贴了下载完成通知)。

-(void)connectionDidFinishDownloading:(NSURLConnection*)connection destinationURL:(NSURL *)destinationURL {
    // copy the file to the destination directory
    NSURL *finalURL = [[self contentURL]URLByAppendingPathComponent:@"magazine.pdf"];
    ELog(@"Copying item from %@ to%@",destinationURL,finalURL);
    [[NSFileManager defaultManager] copyItemAtURL:destinationURLtoURL:finalURL error:NULL];
    [[NSFileManager defaultManager] removeItemAtURL:destinationURLerror:NULL];
    // update Newsstand icon
    [[UIApplication sharedApplication] setNewsstandIconImage:[selfcoverImage]];
    // post notification
    [self sendEndOfDownloadNotification];
}

 

结论

本文即将结束。我们花了很大的决心去让app兼容iOS4和iOS5。在代码里面你还会发现一些有趣的东西,例如,一个“Store Kit”的钩子:这是典型属于开发者的一个不成熟的想法,他们的杂志可能会用于上架销售。如果是这样,则将期刊价格存放到出版商服务器上没有丝毫意义,我们只能从一个地方上检索价格信息,那就是苹果商店。因此我们的app必须异步查询iTunes商店上所有出版物的价格,然后显示。代码中没有加入这部分,只是预留了一个钩子以便今后的扩展,如果读者需要,我会另外写一个扩展的教程。欢迎任何建议,以及对本项目的参与: GitHubhosted code。欢迎将示例代码作为框架应用于任何app。如果你不喜欢这篇文章,至少你要喜欢里面的pdf文件:一些文学经典著作以及一个Django(一个web开源框架)手册。

你可能感兴趣的:(如何编写iOS5杂志程序(2))