技术开发 频道

iPhone企业应用实例分析系列教程

  【IT168技术】本文给大家分享的是iPhone企业应用实例分析系列教程,首先给大家介绍一个通过手机进行企业工作流和文档管理的项目,要求基于Web技术、多层架构、业务层使用Web Service提供服务,客户端需要支持iPhone、Android、Blackberry和Windows Mobile平台,系统框架如图5-1所示。


▲图5-1 系统架构

  系统需求及主要用例

  项目名称:WebDoc Mobile

  系统需求:

  (1)用户认证:用户必须登录才可以使用本应用。

  (2)搜索功能:通过文档主题、代码等进行搜索以及通过我的文档、部门文档进行归类和搜索。

  (3)查看文档细节:文档作者/创建日期等详细资料;文档相关附件;文档变更历史。

  (4)工作流处理:允许用户对文档进行工作流处理。

  (5)性能:内存优化管理并进行缓存处理以提高响应时间,并最小化客户端对服务器的请求数量。

  (6)通信:服务器和客户端采用Web Service进行通信。

  (7)加密:网络数据必须进行加密。

  (8)网络:网络连接状态侦测,并采用异步网络请求。

  (9)错误处理:能够捕捉程序异常,没有宕机情况。

  (10)其他:易于维护和扩展。

  主要用例如图5-2、图5-3所示。

  说明:

  (1)查看我的文档:列出所有我要处理的文档。

  (2)查看部门文档:列出所有部门处理中的文档。

  (3)搜索文档:根据文档处理状态、部门、用户等条件搜索文档。

  (4)文档统计:根据时间段、处理部门、责任人等进行统计,并以图表形式显示。

  (5)上传文档和下载文档附件:用户通过手机上传、下载和查看文档。

  (6)工作流处理:设置文档在当前工作流中可以设置的状态、添加注释等。


▲图5-2 主要用例1


▲图5-3 主要用例2

  iPhone企业应用实例分析之二:程序处理流程

  程序处理流程总体框图如图5-4所示。


▲图5-4 程序处理流程图

  (1)用户启动程序时,显示闪屏。

  (2)显示系统主菜单,主要有“我的文档”、“部门文档”、“文档搜索”和“统计图查询”。

  (3)用户选择“我的文档”以后显示需要我处理的文档列表。

  (4)用户选择“部门文档”以后显示部门列表。

  (5)用户选择“文档搜索”以后显示搜索条件设置界面。

  (6)用户选择部门列表中的部门以后显示部门处理中的文档列表。

  (7)用户设置搜索条件,单击搜索以后显示搜索结果文档列表。

  (8)在文档列表界面显示文档名称、文档标识码,并可以前后翻页。

  (9)用户选择文档列表中的文档时,显示该文档的详情,详情分为4个页面显示,第1页显示文档名称、作者、日期、状态等详细资料;第2页显示文档的附件,用户单击附件时可以将附件下载到手机;第3页显示文档处理历史记录;第4页显示文档工程流处理界面。

  (10)用户选择“统计图查询”以后显示统计图列表。

  (11)用户选择统计图列表中的记录时显示统计图。

  (12)用户单击Info按钮时显示程序版本等信息。

  iPhone企业应用实例分析之三:程序框架分析

  WebDoc Mobile项目是典型的多层流程型系统,所以系统主要使用UINavigation Controller进行用户界面的导航,这样当用户从上一层界面进入下一层界面,在下一层界面的事情处理完以后,又可以方便地返回到上一层界面,在用户登录系统以后,系统显示主菜单,如图5-6所示。

  主菜单分为4个选项,即“我的文档”、“部门文档”、“高级搜索”和“统计图”, 主菜单在MainViewController类中实现,该类使用UITableView来对菜单项进行管理, UITableView的数据源使用一个NSMutableArray来提供表格数据,表格绘制时,使用以下命令。

实例分析之三:程序框架分析
▲图5-6 系统主菜单界面

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection: (NSInteger)section{
    return menuList.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath{
   NSDictionary
*dataDictionary = [menuList    objectAtIndex:indexPath.row];
    cell.textLabel.text
= [dataDictionary valueForKey:kTitleKey];
    return cell;
}
 

  返回表格行数和每行的具体内容,每行表格数据包含下一层ViewController界面的标题和类名称,当用户单击主菜单的菜单项时,程序先使用该类名称调用NSClassFromString()创建类对象,然后再创建ViewController对象,并将ViewController加入UINavigation Controller中进行界面显示。

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath: (NSIndexPath *)indexPath{
targetViewController
= [[NSClassFromString(viewControllerName) alloc] initWithNibName:viewControllerName bundle:nil];
[self.navigationController pushViewController:targetViewController animated:YES];
}

  在用户选择“我的文档”或者选择“部门文档”界面的具体部门后,以及使用“文档搜索”功能搜索文档后,程序就显示文档列表,如图5-7所示。

实例分析之三:程序框架分析
▲图5-7 文档列表界面

  文档列表显示使用DocListViewController类实现,该类包含文档记录分页,前后导航功能,当记录超过每页记录显示的最大行数时,用户可以使用前向和后向箭头进行翻页,另外在用户进行搜索以后,程序也使用这个类来显示搜索结果。

  DocListViewController类包含一个UITableView成员变量,程序使用这个表格来显示文档列表,表格的数据源使用一个NSMutableArray来存储和提供数据,这个数组的每个元素都是一个DocumentDetailViewController类实例,在文档列表显示前使用一个遍历来创建这些DocumentDetailViewController类实例。

for(i = 0; i < nResult; i++){
        DocumentDetailViewController
*presidentsViewController =
        [[NSClassFromString(viewControllerName) alloc]
         initWithNibName:viewControllerName bundle:nil];            
        Document
*doc = [objects objectAtIndex:i];
        presidentsViewController.title
= doc.documentName;
        [presidentsViewController setDocument:doc];
        [controllers addObject:presidentsViewController];
        [presidentsViewController release];                  
}

  当用户单击文档列表中的某个文档时,程序就从数组中取出对应的元素,然后显示DocumentDetailViewController对象。

- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath
*)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];    
    NSUInteger row
= [indexPath row];    
    
if(self.controllers != nil){        
        DocumentDetailViewController
*nextController = [self.controllers
                                                     objectAtIndex:row];    
        
//preload detail, attachment and history.
        [nextController preLoadData];
                    
        
//clear cache
        [self handleWithCache];    
    
        
//push document detail view...
        [self.navigationController pushViewController:nextController animated:YES];
    }    
}

  因为DocumentDetailViewController类需要包含的内容较多,所以程序使用分页显示的方式来组织这些内容,DocumentDetailViewController类使用一个UISegmentedControl类来管理文档详情、文档附件、文档历史和工作流4个具体页面,如图5-8所示。

  这样,当用户进入文档详情的时候,就可以方便地在文档附件、文档历史等页面之间进行切换,在DocumentDetailViewController类的viewDidLoad()方法中通过addTarget()函数设置UISegmentedControl页面切换时的响应函数,这个响应函数在UISegmentedControl对象创建时进行设置,这里设置的是didChangeSegmentControl函数。

self.segmentedControl = [[UISegmentedControl alloc] initWithItems:segmentTitles];
[self.segmentedControl addTarget:self
     action:@selector(didChangeSegmentControl:)
     forControlEvents:UIControlEventValueChanged];

  当用户在页面之间切换时,这个函数就得到调用,函数首先将当前活动页面移除,然后再激活并显示用户选择的页面。

实例分析之三:程序框架分析
▲图5-8 文档详情界面

- (void)didChangeSegmentControl:(UISegmentedControl *)control {
    
if (self.activeViewController) {
        [self.activeViewController viewWillDisappear:NO];
        [self.activeViewController.view removeFromSuperview];
        [self.activeViewController viewDidDisappear:NO];
    }
            
    self.activeViewController
= [self.segmentedViewControllers objectAtIndex: control.selectedSegmentIndex];
    [self.activeViewController viewWillAppear:YES];
    [self.view addSubview:self.activeViewController.view];
    [self.activeViewController viewDidAppear:NO];    
    
}
 

  这样就实现了在4个页面之间进行切换、隐藏和显示的功能。DocumentDetailView Controller类通过UISegmentedControl类来组织页面内容是程序的主体所在,也是编程的重点,该类通过另外4个类来组织和管理页面内容,另外4个类分别是DocDetailView Controller类,用来显示文档明细;DocFilesViewController类,用来管理文档附件;DocHistoryViewController类,用来管理文档变更历史;DocWorkflowViewController类,实现工作流管理。

  当用户单击“部门文档”主菜单时,程序显示部门列表界面,如图5-9所示。

  当用户选择具体部门时,程序通过Web Service查询服务器端的后台数据库,然后将服务器返回数据进行显示,即显示用户所选部门当前处理中的文档列表。

  当用户选择“统计图”主菜单时,程序列出系统现有的统计图列表,如图5-10所示。

  在用户单击统计图明细图标 时,程序通过Web Service查询服务器端后台数据库,并根据返回数据将统计图显示在手机上,如图5-11所示。

实例分析之三:程序框架分析
▲图5-9 部门列表界面 图5-10 统计图列表 图5-11 统计图界面

  iPhone企业应用实例分析之四:技术要点分析(1)

  1.异步网络通信

  在WebDoc Mobile项目中,系统的异步网络通信功能在AsyncNet类中实现,系统使用AsyncNet类来封装对NSURLConnection的操作,在iOS开发中通常使用NSOperation来处理多任务的并发问题,因为NSURLConnection本身已经支持异步操作,所以没有必要再使用NSOperation来对每个请求进行包装,而是使用一个NSMutableArray来存取请求队列,并使用一个NSMutableDictionary来将请求对象和响应数据进行关联,NSURLConnection对象作为Key,请求对象作为值,增加关联的代码:

NSMutableDictionary *requests;
AsyncNetRequest
*request = [[AsyncNetRequest alloc] init];
[requests setObject:request forKey:
  [NSValue valueWithNonretainedObject:con]];
[request release];

  2.Core Data缓存数据

  在进行iPhone软件开发时,使用Core Data进行数据缓存或者管理持久数据是一项必须掌握的基本技术。前面我们已经做过介绍,和前面使用Core Data管理“动物园”项目持久数据不同的是,程序在这里没有使用表之间的关联关系,而只是定义文档、文档附件、文档历史以及统计图4种对象对应的数据库模型,用来缓存服务器返回的数据,并不作为关系数据和持久数据使用,在每次程序启动时都会清空数据库,重新使用服务器返回的最新数据,Core Data只起到数据缓存的作用,当数据从服务器返回以后,在第二次使用时程序从缓存取数据而不是从服务器取数据,这样可以提高反应速度,文档对象对应的数据库模型包含的字段定义如下。

//  Document.h
#import
<CoreData/CoreData.h>

@interface Document: NSManagedObject{    
}


  3.RSA算法加解密

  在WebDoc Mobile项目中,iPhone客户端和服务器端(使用Microsoft .NET技术)采用Web Service进行相互通信,通信双方需要将数据进行加密处理,以保证网络通信的安全性,未经认证的客户端Web Service调用将不能在服务器端执行,系统采用业界目前广泛采用的PKI(公钥基础设施)技术进行用户认证管理,使用RSA算法进行加解密,有关RSA算法加解密的具体内容在本书前面的章节已经做了介绍,实现的细节请参考本书附带的光盘中的DocMobile工程,这里就不再详述。

  4.自定义控件制作

  在WebDoc Mobile项目中,由于用户界面的需要,系统制作了一些自定义的界面控件,其中包括与桌面软件类似的ComboBox界面控件,如图5-12所示。

实例分析之四:技术要点分析(一)
▲图5-12 自定义ComboBox控件

  该控件用于用户登录和高级搜索时,提供下拉多项目选择,程序使用UITextField类、UIPickerView类和UIToolbar类三个主要类实现,具体的实现方法在前面的章节已经做了具体介绍。

  自定义搜索控件,控件外观如图5-13所示。

实例分析之四:技术要点分析(一)
▲图5-13 自定义搜索控件

  该控件提供在许多数据记录中进行选择过滤的功能,当用户输入一个字符或者单词,程序实时从数据记录中找出开头字符或者记录中包含该字符或者单词的记录,把记录进行实时过滤。这样,用户就不用在很长的列表里面查找,而是从过滤后的少量记录里面挑选,该控件在需要用户从非常多的选项中做选择的时候,可以作为界面设计元素。控件使用SearchViewController类实现,SearchViewController类内部使用UITableView和UISearchBar联合实现记录过滤功能,当用户输入字符时,程序使用NSString类的rangeOfString方法对记录进行过滤,并使用过滤后的记录刷新UITableView的内容,代码如下。

- (void) searchTableView {
    
    NSString
*searchText = search.text;
            
    
for (NSString *sTemp in tempArray)
    {
        NSRange titleResultsRange
= [sTemp rangeOfString:searchText options: NSCaseInsensitiveSearch];
        
        
if (titleResultsRange.length > 0)
            [searchArray addObject:sTemp];
    }
    
}

  iPhone企业应用实例分析之四:技术要点分析(2)

  1.表格控件定制

  在iOS开发中,UITableView是使用频率最高的控件之一,为了实现各种用户界面的需要,经常需要对表格的每一行进行定制,如图5-14所示是文档历史的显示界面。

实例分析之四:技术要点分析(二)
▲图5-14 文档历史界面

  程序通过设置cell.textLabel.text和cell.detailTextLabel.text来达到如图5-14所示的显示效果,代码如下。

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath
*)indexPath {
    
    static NSString
*AttachmentsCell= @"HistoryCell";
    
    UITableViewCell
*cell = [tableView dequeueReusableCellWithIdentifier:
                             AttachmentsCell];
    
if (cell == nil) {
        cell
= [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyle Value2
                                      reuseIdentifier: AttachmentsCell] autorelease];
    }

    
if(records != nil){    
            NSUInteger row
= [indexPath row];
            DocumentHistory
*history = [records objectAtIndex:row];        
            cell.textLabel.text
= history.historyText;  
            cell.detailTextLabel.text
= history.historyTitle;
            cell.detailTextLabel.numberOfLines
= 2;
            cell.detailTextLabel.lineBreakMode
= UILineBreakModeWordWrap;
            
    }    
    return cell;
}

  通常使用子类化UITableViewCell类的方法来定制表格控件,然后在UITableView进行表格绘制调用cellForRowAtIndexPath()时,使用该定制的子类呈现用户界面。在统计图列表的实现类StatisticsViewController中就使用了一个自定义的IndicatorSubviewCell类来定制每一行的显示,该类是UITableViewCell类的子类,如图5-15所示是统计图表格控件制作出来的效果图。

实例分析之四:技术要点分析(二)
▲图5-15 统计图界面

  IndicatorSubviewCell类使用.xib文件来创建界面显示内容,这样可以使用Interface Builder可视化地创建各种界面元素,Interface Builder提供了一个“Table View Cell”的设计控件,专门用来创建各种自定义表格控件,你只需要像使用普通View一样将需要的界面元素拖放进视图,并将其连接到相应的类成员即可,如图5-16所示是统计图表格控件在Interface Builder中的设计图。

实例分析之四:技术要点分析(二)
▲图5-16 统计图控件设计界面

  我们看到上面的设计视图包括两个UILabel和一个UIImageView,和上面的表格控件效果图并不一致,少了最右边的那个放大镜 图标,这是因为最后这个图标是一个可单击区域,用户单击这个区域以后程序显示具体的统计图,这个区域是由程序进行创建而不是通过Interface Builder可视化创建的,设计视图对应的类定义如下。

//IndicatorSubviewCell.h
#import
<Foundation/Foundation.h>
#import
"IndicatorCell.h"

@interface IndicatorSubviewCell : UITableViewCell
{
    IBOutlet UIImageView
*iconView;
    IBOutlet UILabel
*nameLabel;
    IBOutlet UILabel
*priceLabel;    
}

@
end

  2.自定义UIToolbar

  在WebDoc Mobile项目中,程序使用自定义的UIToolbar来实现工作流设置的输入确认,虽然UIActionSheet类和UIToolbar类都可以提供带按钮的用户输入界面,但前者属于弹出式的模式窗口,而后者则是非弹出式的界面输入元素,工作方式不一样,因为在DocumentDetailViewController类中包含文档详情、文档附件、文档历史和工作流4个UISegmentedControl页面,“文档附件”页面需要提供UIToolbar按钮来引导用户浏览手机本地目录,以便选择需要上传的文件,“工作流”页面需要提供UIToolbar按钮来确认工作流设置,而其他两个页面不需要UIToolbar按钮,为了保持界面的一致性,程序通过用户当前选择的页面来判断是否该隐藏还是显示UIToolbar按钮,并根据不同页面做出不同的响应,如图5-17所示是带UIToolbar的工作流设置界面。

实例分析之四:技术要点分析(二)
▲图5-17 自定义UIToolbar用户界面

  在DocumentDetailViewController类的实现中,程序定义一个UIToolbar类成员变量,并在viewWillAppear函数中创建UIToobar对象,代码如下。

- (void)viewWillAppear:(BOOL)animated
{
    toolbar
= [[UIToolbar alloc] init];
    [self setNavigatinBarStyle:DEFAULT_STATUS_BAR_STYLE];
    [toolbar sizeToFit];
    CGFloat toolbarHeight
= [toolbar frame].size.height;
    CGRect rootViewBounds
= self.parentViewController.view.bounds;
    CGFloat rootViewHeight
= CGRectGetHeight(rootViewBounds);
    CGFloat rootViewWidth
= CGRectGetWidth(rootViewBounds);
    CGRect rectArea
= CGRectMake(0, rootViewHeight - toolbarHeight, rootView Width, toolbarHeight);
    [toolbar setFrame:rectArea];

    [self.navigationController.view addSubview:toolbar];
    
    
if(self.segmentedControl != nil){
        
if(self.segmentedControl.selectedSegmentIndex == 1||
            self.segmentedControl.selectedSegmentIndex
== 3){
            NSMutableArray
* toolbarItems = [[NSMutableArray arrayWithArray: toolbar.items] retain];
            
int a = [toolbarItems count];
            
if (a == 0 ) {                
                infoButton
= [[UIBarButtonItem alloc]
                              initWithTitle:@
"AttachFile"
                              style:UIBarButtonItemStyleDone target:self action:@selector(browseFileSystem:)];
                [toolbar setItems:[NSArray arrayWithObjects:infoButton,nil]];
            }
            self.navigationController.toolbarHidden
= YES;
            
if(self.segmentedControl.selectedSegmentIndex == 3)
                infoButton.title
= @"Done";
            
else
                infoButton.title
= @"AttachFile";
        }
else{
            self.navigationController.toolbarHidden
= NO;
        }
    }
else{
       self.navigationController.toolbarHidden
= NO;
    }    
}

  程序在创建UIToolbar对象后,通过计算界面的宽度和高度,调用UIView的setFrame()方法将Toolbar放置在屏幕的最下方,并根据用户当前选择的UISegmentedControl页面,隐藏或者显示Toolbar,并在Toolbar上添加相应的按钮。

  iPhone企业应用实例分析之四:技术要点分析(3)

  1.目录浏览器制作

  在WebDoc Mobile项目中,用户需要将手机本地的文件上传到服务器端,iOS并没有提供目录浏览控件供开发者使用,所以只有自行开发实现目录浏览的功能,如图5-18所示是实现的界面。

实例分析之四:技术要点分析(三)
▲图5-18 目录浏览控件用户界面

  程序递归根目录下的所有目录和文件,目录以 图标显示,文件以 图标显示,当用户单击目录图标时,程序列出该目录下的所有文件和目录,用户单击返回按钮又可以返回上一层目录,当用户单击文件图标时,程序显示文件的详情,如修改日期、文件大小等明细信息,如图5-19所示。

  手机目录浏览器的实现,在DirectoryViewController类中,该类继承自UITableView Controller,并实现UINavigationControllerDelegate接口,以便使用UINavigationController的导航功能,从文件目录的下一层方便地返回到目录的上一层,在DirectoryViewController类中使用一个NSArray类型的成员变量directoryContents来存储文件和目录名,并作为表格控件的数据源,在创建DirectoryViewController对象以后,程序使用NSFileManager类的directoryContentsAtPath方法列出指定目录下所有的文件和目录,并赋值给directory Contents成员变量。

实例分析之四:技术要点分析(三)
▲图5-19 文件详情用户界面

- (void) loadDirectoryContents {
    [directoryContents release];
    directoryContents
= [[NSMutableArray alloc] init];
    directoryContents
= [[NSFileManager defaultManager]
        directoryContentsAtPath: directoryPath];    
    [directoryContents retain];
}
 

  在表格绘制时,查询directoryContents成员变量的元素个数就得到表格的行数,查询表格行对应的数组元素就得到文件或者目录的名称,这样就可以正确显示每一行的内容。

  …

  2.文件上传和下载

  5.7.7节通过UITableView和UINavigationController两个类实现目录浏览器,在用户浏览到想要上传的文件后,选择该文件就可以将其上传到服务器,在文件的上传过程中,需要显示上传进度,以便用户了解当前的进度,如图5-20所示。

  下面来看具体如何使用UIAlertView实现文件上传时的进度条显示功能。

实例分析之四:技术要点分析(三)
▲图5-20 文件上传用户界面

- (void) createProgressionAlertWithMessage:(NSString *)message
                              withActivity:(BOOL)activity{
        
    
if(progressAlert != nil){
        [progressAlert release];
        progressAlert
= nil;
    }

    
// This timer takes the place of a real task
    amt
= 0.0;
    
timer = [NSTimer scheduledTimerWithTimeInterval: 0.5
                                            target: self
                                                 selector: @selector (handleTimer:)
                                           userInfo: nil
                                            repeats: YES];
    
    
if(!progressAlert)
    {
        progressAlert
= [[UIAlertView alloc] initWithTitle: message
                                                  message: @
"Please wait..."
                                                 delegate: self
                                         cancelButtonTitle: nil
                                         otherButtonTitles: nil];
    
      
// Create the progress bar and add it to the alert
      
if (activity) {
           UIActivityIndicatorView
*activityView = [[UIActivityIndicatorView alloc]
initWithActivityIndicatorStyle:UIActivityIndicator ViewStyleWhite];
           activityView.frame
= CGRectMake(139.0f-18.0f, 80.0f, 37.0f, 37.0f);
           [progressAlert addSubview:activityView];
           [activityView startAnimating];
           [activityView release];                  
          
       }
else {
           progressView
= [[UIProgressView alloc] initWithFrame:CGRectMake (30.0f, 80.0f, 225.0f, 90.0f)];
           [progressAlert addSubview:progressView];
           [progressView setProgressViewStyle: UIProgressViewStyleBar];
           [progressView release];
       }
        
        
// Add a label to display download/upload size.
        UILabel
*label = [[UILabel alloc] initWithFrame:CGRectMake(90.0f, 90.0f, 225.0f, 40.0f)];
        label.backgroundColor
= [UIColor clearColor];
        label.textColor
= [UIColor whiteColor];
        label.font
= [UIFont systemFontOfSize:12.0f];
        label.text
= @"";
        label.tag
= 1;
        [progressAlert addSubview:label];        
    }
    
    [progressAlert show];        
    
}
 

  程序使用一个NSTimer来处理上传超时,若超时则终止上传操作。

- (void) handleTimer: (id) atimer
{
      amt
+= 1;
    
if(progressView != nil)
        [progressView setProgress: (amt
/ DOWNLOAD_TIMEOUT)];
    
if (amt > DOWNLOAD_TIMEOUT) {
         UILabel
*label = (UILabel *)[progressAlert viewWithTag:1];
         label.text
= @"Sorry, Time Out...";          
         [atimer invalidate];
         atimer
= nil;
         [progressAlert dismissWithClickedButtonIndex:
0 animated:TRUE];    
         [progressAlert release];
         progressAlert
= nil;
     }
}

  函数调用:

[self createProgressionAlertWithMessage:@"Upload Document ..." withActivity:NO];

  文档附件上传界面如图5-21所示,用户单击“AttachFile”按钮,程序显示目录列表供用户浏览和选择文件,用户选择具体的文件后即可将文件上传至服务器。

实例分析之四:技术要点分析(三)
▲图5-21 文档附件上传界面

  本文节选自《iOS软件开发揭密:iPhone&iPad企业应用和游戏开发》一书,由电子工业出版社正式出版,本书由虞斌著。

iPhone SDK开发基础之自定义仪表控件
▲图书

0
相关文章