2.2 通过示例将Object-C与MonoTouch进行对比
下面通过示例来说明上面讲到的一些概念。前面提到的UIActionSheet将会在示例中使用。通过Objective-C与C#的对比,将有助于清楚地了解如何使用MonoTouch开发应用程序。
注意 通常,使用MonoTouch开发应用程序不需要Xcode或Objective-C,这里这样做的目的是作为基础知识辅助说明MonoTouch的设计。如果有兴趣想了解更多的相关技术,推荐阅读Stephen G. Kochan写的《Programming in Objective-C 2.0》。
示例将会如图2-2所示,允许改变应用程序的背景图片。
2.2.1 从Xcode开始编写应用程序
首先使用Xcode创建示例应用程序。在开发时,可以借鉴之前使用MonoDevelop开发应用程序时的步骤,并对比它们之间的不同。打开Xcode并创建一个名为LMT2-1的基于窗口的应用程序。在IB中双击MainWindow.xib文件打开它。如图2-3所示,在IB中将Library窗口中的UIImageView拖到该窗口。UIImageView将用来显示背景图片,因为要在代码中改变背景图片,所以需要为它创建一个outlet。
IB中要连接的outlet在Objective-C的头文件中定义,稍后要在Xcode中进行添加。不过,在添加之前,先要在IB中完成界面设计,将一个按钮拖放到图片视图的顶部。
注意 按钮通常与UIBarButtonItem一起放在UIToolbar内。
按钮的作用是打开UIActionSheet,然后在UIActionSheet中切换图片视图中的图片。要将响应按钮触碰动作的处理代码与按钮连接起来,需要在头文件定义一个操作。在Xcode定义好这些之后,返回IB中进行连接。现在,回到Xcode。
在Xcode中,选择AppDelegate的头文件(LMT2_1AppDelegate.h),然后在文件里添加代码清单2-1中的代码。
代码清单2-1 Objective-C头文件中的Outlet和Action
#import <UIKit/UIKit.h>
@interface LMT2_1AppDelegate : NSObject <UIApplicationDelegate> {
UIWindow *window;
UIImageView *imageView;
}
@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UIImageView *imageView;
-(IBAction) changePicture: (id) sender;
@end
代码中为UIImageView增加了一个称为imageView的实例变量,并使用@property指令声明它是IBOutlet,这样在xib文件中,IB就可以使用这个实例变量来连接图片视图。
注意 IBOutlet只是一个空的宏,不需要为它编写代码,它的主要作用是与IB集成。同样,IBAction也不会有返回值,它只是作为IB的一个提示。
还需要在头文件中定义动作,以便在IB中连接按钮的触碰事件。此外,在括号中定义的可选参数(nonatomic和retain),会让编译器根据内存管理器和线程来生成实际的属性。可以看到,这里确实没有属性,它只是围绕getter和setter方法的“糖衣语法”(syntactic sugar)。完成Xcode的代码后,转到实现文件(LMT2_1AppDelegate.m)添加属性和动作方法的代码。
另一半的Objective-C属性需要使用@synthesize指令,在实现文件添加该指令后,编译器会在头文件中生成属性的定义。稍后在使用MonoTouch实现此过程的时候,会发现MonoTouch更简单、更便捷。现在开始添加属性和方法。
在LMT2_1AppDelegate.m文件中,在Xcode模板已有的窗口语句下添加imageView的Synthesize语句。此外,添加changePicture:方法,并在方法内添加如代码清单2-2中所列出的记录字符串的存根实现。要记住,对编译器来说,IBAction是没有返回值的。在介绍UIActionSheet的时候,还要回到这里的实现操作。现在,保存Xcode中的文件后,切换回IB中的MainWindow.xib。
代码清单2-2 LMT2_AppDelegate.m
#import "LMT2_1AppDelegate.h"
@implementation
@synthesize window;
@synthesize imageView;
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch
[window makeKeyAndVisible];
return YES;
}
-(IBAction) changePicture: (id) sender{
NSLog(@"placeholder for UIActionSheet code");
}
- (void)dealloc {
[window release];
[imageView release];
[super dealloc];
}
在IB中,现在要将outlet连接到imageView,这样才可以在AppDelegate类中通过代码改变背景图片。还要设置Target-Action,这样才能在触碰按钮的时候调用changePicture:方法。现在开始完成这些步骤,要注意区分Xcode和Objective-C与MonoTouch之间的不同。
首先,将AppDelegate的imageView的outlet连接到xib中的UIImageView。如图2-4所示,在MainWindow.xib和Connections Inspector中选择AppDelegate,然后将imageView outlet拖到窗口的UIImageView中。
接着,需要连接的是AppDelegate代码中创建的changePicture:方法。这是前面讲过的Target-Action模式的一个例子。当在MonoTouch中实现这些功能时,会看到在C#中是如何使用动作及如何将代码连接到C#风格的事件的。要将Objective-C的处理代码连接到按钮上的事件,先在MainWindow.xib中选择该按钮,然后在Connections Inspector中将TouchUpInside事件拖到AppDelegate。当松开鼠标按钮,IB会弹出一个包含所有使用IBAction声明的方法菜单。此时这个菜单中只有一个changePicture:方法。选择changePicture,注意在Connections Inspector中,按钮的TouchUpInside事件与目标类AppDelegate中的changePicture:动作建立了连接。保存后,在Xcode中运行应用程序。
应用程序运行后,从Xcode运行调试器控制台(Run→Console)。在触碰应用程序的按钮后,会在控制台看到如图2-5所示的日志信息。
现在已经通过Objective-C的Target-Action模式连接了事件,下一步要做的是使用MonoDevelop实现同样的功能。
2.2.2 在MonoTouch中实现相同的功能
现在,留意一下MonoTouch是如何使用C#的方式实现事件响应的Target-Action模式的。打开MonoDevelop并创建一个名为LMT2-2的基于iPhone窗口的新工程。在IB中打开MainWindow.xib,添加一个UIImageView和一个按钮,修改按钮上的文本为“Change Image”。现在要为图片视图添加一个outlet以便代码可以访问到它,因此在此立即创建这个outlet。因为是由AppDelegate的代码访问这个outlet,所以要在AppDelegate中添加这个outlet。在Library窗口和IB的标签页内再次选择AppDelegate,在Outlets区域内添加一个名为imageView的outlet,修改它的类型为UIImageView。
注意 MonoTouch中的UIKit版本和基础架构分别包含在命名空间MonoTouch.UIKit和MonoTouch.Foundation中。
在Objective-C中,是在AppDelegate的头文件中添加outlet的,而在MonoTouch中,一旦xib文件中的对象(如UIImageView实例)和outlet之间建立连接,就会通过在局部类中生成的属性将outlet连接到代码。属性用ConnectAttribute进行声明,它的作用与Objective-C中的IBOutlet一样,即与IB集成。
现在,在MainWindow.xib窗口中选择AppDelegate,如之前做过的一样,将imageView的outlet连接到在窗口中的UIImageView。如果在IB中保存文件后,切换到MonoDevelop中查看MainWindow.xib.designer.cs文件,就会看到局部类和刚才提及的属性。现在,连接按钮。
第1章演示了如何使用C#风格的事件来连接UIButton事件。现在可以试着使用与刚才在Objective-C中使用的Target-Aciton模式来达到相同的效果。要记住,目标是AppDelegate,要在AppDelegate中创建动作方法来响应按钮的TouchUpInside事件。利用MonoDevelop与IB集成,可以达到相同的效果。在IB的Library窗口中,选择AppDelegate的Actions标签(在Outlets标签页的右边第一个),用“changePicture:”选择器添加一个动作。然后,选择窗口中的按钮,将Connections Inspector中的Touch Up Inside事件拖到MainWindow.xib中的AppDelegate上进行连接。松开鼠标,弹出一个之前在Xcode和IB中出现过的changePicture:选择器菜单。选择changePicture:选择器,并在IB中保存所有修改。如果回头看看在MainWindow.xib.designer.cs文件中的局部类,就会看到新代码已添加,ExportAttribute声明的作用是将IB中添加的动作处理代码挂接到changePicture选择器。现在添加实现代码(类似占位符的代码,后面将会替换为UIActionSheet)。在Main.cs文件中,添加以下代码到AppDelegate类:
partial void changePicture (MonoTouch.UIKit.UIButton sender){
Console.WriteLine("changePicture called in MonoTouch");
}
运行应用程序并触碰按钮,将会看到写在MonoDevelop中的Application Output标签页中的文本信息(如图2-6所示)。
提示 使用C#事件可以避免不必要的动作。就如第1章所看到的,这样做的好处是,可以使用C#风格的事件来实现,从而避免使用动作方法来实现。只需要为按钮创建一个outlet就可通过程序访问注册的事件。MonoTouch支持创建C#事件委托的显式回调方法,以及匿名方法和lambda表达式。
正如所看到的,MonoTouch可以使用与Objective-C相同的模式实现事件处理。此外,MonoTouch提供了更灵活的C#风格的事件以供选择。
现在要做的是为UIActionSheet编码以改变图片。还是先通过Xcode实现,然后再通过MonoTouch实现,以比较两者在实现Objective-C委托模式之间的不同。不过,在实现之前,要先花点时间了解MonoTouch是如何实现AppDelegate和如何处理它的。
2.2.3 AppDelegate实现的比较
在本书前几章的简单示例中,还不能对应用程序进行结构化设计,这将在第3章进行讲述。然而,在开发应用程序中总会用到AppDelegate。在目前的示例中,所有的应用程序代码都是在它内部添加的,实际上它的作用是通过其他类来处理应用程序逻辑。AppDelegate是一个在应用程序的生命周期内用来响应UIApplication各类操作并进行处理的类,这些处理包括应用程序终止、应用程序启动完成、接收系统发出的内存警告等。在Objective-C中,AppDelegate定义为一个名为UIApplicationDelegate的协议。记住Objective-C协议基本类似于C#接口,除了一些方法指定为可选的外。如果打开Xcode中的LMT2_1AppDelegate.h文件,会看到以下代码:
@interface LMT2_1AppDelegate : NSObject
这是Objective-C在相关LMT2_1AppDelegate.m文件中实现LMT2_1AppDelegate类定义的方式,它派生于NSObject(CocoaTouch中其他类的基类,类似于.NET中的Object类)并遵循UIApplicationDelegate协议。这意味着实现将包含协议的所有请求方法以及可选方法。这是在LMT2_AppDelegate.m文件中实现的方法application:didFinishLaunchingWithOptions:,应用程序完成启动后会调用该方法。与MonoTouch进行比较,就会发现AppDelegate是作为一个类实现的。如果作为接口实现,那会工作不了,因为它不能给可选方法附加协议类型,因而它必须实现为一个如代码清单2-3所示的包含各种虚函数的类。这就是在MonoTouch中看到的模式,将Objective-C协议转换为派生于基类的类,并通过重写虚函数插入实现代码。
代码清单2-3 MonoTouch AppDelegate类
[Register("UIApplicationDelegate")]
[Model()]
public class UIApplicationDelegate : NSObject
{
// Constructors
public UIApplicationDelegate();
public UIApplicationDelegate(NSCoder coder);
public UIApplicationDelegate(NSObjectFlag t);
public UIApplicationDelegate(IntPtr handle);
// Methods
public virtual void FinishedLaunching(UIApplication application);
public virtual bool FinishedLaunching(UIApplication application,
NSDictionary launchOptions);
public virtual void OnActivated(UIApplication application);
public virtual void OnResignActivation(
UIApplication application);
public virtual void HandleOpenURL(UIApplication application,
NSUrl url);
public virtual void ReceiveMemoryWarning(
UIApplication application);
public virtual void WillTerminate(UIApplication application);
public virtual void ApplicationSignificantTimeChange(
UIApplication application);
public virtual void WillChangeStatusBarOrientation(
UIApplication application,
UIInterfaceOrientation newStatusBarOrientation,
double duration);
public virtual void DidChangeStatusBarOrientation(
UIApplication application,
UIInterfaceOrientation oldStatusBarOrientation);
public virtual void WillChangeStatusBarFrame(
UIApplication application, RectangleF newStatusBarFrame);
public virtual void ChangedStatusBarFrame(
UIApplication application, RectangleF oldStatusBarFrame);
public virtual void RegisteredForRemoteNotifications(
UIApplication application, NSData deviceToken);
public virtual void FailedToRegisterForRemoteNotifications(
UIApplication application, NSError error);
public virtual void ReceivedRemoteNotification(
UIApplication application, NSDictionary userInfo);
...
}
注意 所有有ExportAttribute定义的方法,在重写虚函数时,不需要知道实际的选择器或提供属性。
正如前面所看到的,在MonoTouch的实现中,MonoDevelop和IB之间相结合产生的所有代码都结束在一个局部类定义中,这为实现应用程序代码提供了很大的自由度,无须使用工具接口。继承的层次结构可由开发人员在局部类定义中自行设置。在AppDelegate的情况下,MonoDevelop代码模板从UIApplicationDelegate派生出AppDelegate,在Main.cs文件中,代码如下:
public partial class AppDelegate : UIApplicationDelegate
模板还重写了FinishedLaunching方法,这里可以实现之前在Objective-C实现的(就像前面在Xcode应用程序中看到的)application:didFinishLaunchin-gWithOptions:。
注意 在tirania.org/tmp/rosetta.html中,可以找到一个非常有用的名为“MonoTouch Rosetta Stone”的文档,它列出了MonoTouch中与Objective-C选择器对应的C#实现。
AppDelegate是CocoaTouch中使用的Delegation模式的一个示例,MonoTouch完全支持。现在要做的是在应用程序中使用UIActionSheet,这是iOS SDK中另一种Delegation例子。
2.2.4 通过Xcode实现UIActionSheet
现在回到Xcode(听起来像电影)创建UIActionSheet并设置不同按钮以便改变imageView中的图片。目前,示例一直是在设计窗口和AppDelegate中完成的,不算复杂。如前所述,这不是太规范的设计,具体会在第3章中讲述。现在需要一个视图来作为UI的容器,一直以来,为了简单起见都直接跳过了,没有讨论。这个视图就是早已存在的imageView,将用来显示ActionSheet。在LMT2_1AppDelegate.m文件中,添加如代码清单2-4所示的代码到changPicture:方法来创建ActionSheet。
代码清单2-4 创建UIActionSheet的代码
-(IBAction) changePicture: (id) sender{
UIActionSheet *changeImageSheet = [[UIActionSheet alloc]
initWithTitle:@"Change Image"
delegate:self
cancelButtonTitle:@"Cancel"
destructiveButtonTitle:NULL
otherButtonTitles:@"Image 1", @"Image 2", NULL];
[changeImageSheet showInView:imageView];
[changeImageSheet release];
}
代码将创建并显示一个ActionSheet,它内部除了包含"Image 1"和"Image 2"两个按钮外,还包含一个"Cancel"(取消)按钮。使用otherButtonTitles定义的按钮的索引分别是0和1,而取消按钮的索引是2。按钮的单击操作将使用索引进行区分,这样的处理方式是Objective-C Delegation(委托)的又一个例子。注意代码中创建ActionSheet时设置的Delegation(委托)属性为self,说明它将引用当前对象,与C#中的this一样。也就是说,传递给ActionSheet的对象就是LMT2_1AppDelegate实例,因为当前类就是这个。因此,LMT2_1AppDelegate需要遵循UIActionSheetDelegate协议。现在设置UIActionSheetDelegate协议,打开对应的头文件并在尖括号内UIApplicationDelegate协议定义的后面加入协议。现在要实现UIActionSheetDelegate协议的带有选择器的actionSheet: clickedButtonAtIndex:方法以便接收UIActionSheet的按钮单击后的回调处理。要完成这个,在@end:前的头文件添加以下代码:
-(void)actionSheet:(UIActionSheet *)actionSheet
clickedButtonAtIndex:(NSInteger)buttonIndex;
当UIActionSheet的按钮单击后,实现方法将执行并返回单击按钮的索引。处理代码可根据索引改变图片。
在LMT2_1AppDelegate.m文件中添加代码清单2-5所示的代码完成实现。
代码清单2-5 actionSheet:clickedButtonAtIndex:方法的实现代码
- (void)actionSheet:(UIActionSheet *)actionSheet
clickedButtonAtIndex:(NSInteger)buttonIndex{
switch (buttonIndex) {
case 0:
imageView.image = [UIImage imageNamed: @"image1.jpg"];
break;
case 1:
imageView.image = [UIImage imageNamed: @"image2.jpg"];
break;
case 2:
NSLog(@"cancel");
break;
default:
break;
}
}
代码清单2-5引用了image1.jpg和iamge2.jpg这两个图片。
将图片从Finder拖放到Group & files下的LMT2-1工程的根目录下,Xcode自动将它们打包到应用程序发布包。松开鼠标后,选择Copy Items into Destination Group抯 Folder以便将它们复制到与Xcode工程相同的位置。
在Xcode中生成并运行应用程序,然后单击按钮打开ActionSheet并从中选择一个按钮来改变图片。当选择ActionSheet的按钮时,ActionSheet的委托被调用,从而处理按钮单击的代码也会调用。可以看到,Objective-C中的委托设计模式非常类似于其他语言的回调接口,譬如C#。不过,由于可选方法需要支持协议,所以MonoTouch通过类来实现。如以上看到的,在Objective-C中,AppDelegate的单一实现在示例中要遵循名为UIApplicationDelegate和UIActionSheetDelegate的协议。在MonoTouch中,要处理这个可能会不知所措,无从下手,因而需要使用类来实现,但不允许多重继承。现在回到MonoDevelop并完成实现。
2.2.5 在MonoTouch中实现UIActionSheet
在MonoTouch中,会在构造函数中映射代码清单2-4中Objective-C的initWithTitle: delegate:cancelButtonTitle:destructiveButtonTitle:otherB-uttonTitles:方法。此外,在C#中显示UIActionSheet仍然可以调用ShowInView方法。不过,这时候委托指针指向的不是当前实例,因为它已经是UIApplicationDelegate的子类,而是从UIActionSheetDelegate派生的嵌套类,这样就可以保证委托实现封装在类(AppDelegate)内,以便创建的类实例可以进行委托处理(UIActionSheet)。其实,也不必非得这样嵌入委托类,还可以在这个类的外部创建它。不过,像嵌入委托这样的处理方式在MonoTouch中是经常用到的,而且它表现得很好。根据以上说明,就可实现如代码清单2-6所示的changePicture方法。
代码清单2-6 在MonoTouch中显示UIActionSheet
UIActionSheet _changePictureSheet;
...
partial void changePicture (MonoTouch.UIKit.UIButton sender){
_changePictureSheet = new UIActionSheet(
"Change Picture", new ChangePictureActionSheetDelegate(this),
"Cancel", null, "Image 1", "Image 2");
_changePictureSheet.ShowInView(imageView);
}
ChangePictureActionSheetDelegate就是刚才讲到的嵌套类。在Objective-C中,使用actionSheet:clickedButtonAtIndex:方法来处理UIActionSheet按钮的单击操作。在MonoTouch中,前面已经提过,要通过重写虚函数来实现。实现方法的定义代码如下:
public virtual void Clicked (UIActionSheet actionSheet, int buttonIndex)
与Objective-C示例一样,实现将使用单击按钮的索引来切换图片视图的图片。在当前版本的MonoTouch中要注意,Cancel(取消)按钮的索引是最后一个索引,而不是Objective-C中的第一个索引。如果需要,也可以通过UIActionSheet的CancelButtonIndex属性来修改Cancel(取消)按钮的索引值。为了简单起见,这里将使用默认值,ChangePictureActionSheetDelegate类的实现代码如代码清单2-7所示。
注意 下一版本的MonoTouch会固定Cancel(取消)按钮的位置。
代码清单2-7 在ChangePictureActionSheetDelegate类实现单击操作
class ChangePictureActionSheetDelegate : UIActionSheetDelegate
{
AppDelegate _appDel;
public ChangePictureActionSheetDelegate (AppDelegate appDel)
{
_appDel = appDel;
}
public override void Clicked (UIActionSheet actionSheet,
int buttonIndex)
{
switch (buttonIndex) {
case 1:
appDel.imageView.Image = UIImage.FromFile("image1.jpg");
break;
case 2:
_appDel.imageView.Image = UIImage.FromFile("image2.jpg");
break;
}
}
}
现在重复在Xcode中的操作,将图片复制到工程目录下。在Finder中将图片拖到MonoDevelop解决方案树下,然后单击如图2-8所示的对话框中的Copy按钮。在解决方案树中的图片上右击(如果使用的单按钮鼠标,请按下Ctrl键再单击),然后设置为Build Action to Content,这样就可以在MonoDevelop生成应用程序时把图片打包到应用程序。
注意 如果在MonoDevelop中创建文件夹,等同于在磁盘上创建文件夹,这与在Xcode中在磁盘根目录下默认为虚拟目录不同。
最后完成如代码清单2-8所示的Main.cs文件。如果现在生成并运行应用程序,切换图片,那么效果与在Objective-C中看到的一样。
代码清单2-8 Main.cs的最后版本
using System;
using System.Collections.Generic;
using System.Linq;
using MonoTouch.Foundation;
using MonoTouch.UIKit;
namespace LMT22
{
public class Application
{
static void Main (string[] args)
{
UIApplication.Main (args);
}
}
// The name AppDelegate is referenced in the MainWindow.xib file.
public partial class AppDelegate : UIApplicationDelegate
{
UIActionSheet _changePictureSheet;
// This method is invoked when the application has
// loaded its UI and its ready to run
public override bool FinishedLaunching (UIApplication app,
NSDictionary options)
{
window.MakeKeyAndVisible ();
return true;
}
class ChangePictureActionSheetDelegate : UIActionSheetDelegate
{
AppDelegate _appDel;
public ChangePictureActionSheetDelegate (
AppDelegate appDel)
{
_appDel = appDel;
}
public override void Clicked (UIActionSheet actionSheet,
int buttonIndex)
{
switch (buttonIndex) {
case 1:
_appDel.imageView.Image =
UIImage.FromFile ("image1.jpg");
break;
case 2:
_appDel.imageView.Image =
UIImage.FromFile ("image2.jpg");
break;
}
}
}
partial void changePicture (MonoTouch.UIKit.UIButton sender)
{
_changePictureSheet = new UIActionSheet (
"Change Picture",
new ChangePictureActionSheetDelegate (this), "Cancel",
null, "Image 1", "Image 2");
_changePictureSheet.ShowInView (imageView);
}
// This method is required in iPhoneOS 3.0
public override void OnActivated (UIApplication application)
{
}
}
}
现在,已经了解了如何在MonoTouch中使用C#开发应用程序,并与在Xcode中使用Objective-C开发iPhone应用程序的核心设计模式做了比较。下面要了解的是MonoTouch如何创建这些并让它们工作。