'Hello World, Goodbye .XIB!' -- 一个无 .XIB 的 Hello World 应用程序
本文将介绍如何创建一个简单的 iPhone 应用程序,该程序显示一个图形,然后移除 .XIB 文件,仅通过代码完成所有功能。
第一部分:Hello world!
我将创建一个简单的 iPhone 应用程序来显示一个图形。然后,我将移除 .XIB 文件,并通过代码实现所有功能。
我以一种适合初学者的方式撰写了这篇文章,但其中一些内容可能有些高级。但生活就是挑战……
好的,我们开始吧。启动 Xcode,创建新项目 -> 基于视图的应用程序,命名为“XIB”。
构建并运行 [cmd + enter],您应该会看到一个空白屏幕,顶部有状态栏。
在“群组和文件”窗格中右键单击“类”-> 添加新文件 -> iPhone OS Objective-C 类,将其命名为“myView”。它会自动创建一个头文件。
将这些文件中的文本替换为:
// MyView.h #import <UIKit/UIKit.h> @interface MyView : UIView { } @end // MyView.m #import "MyView.h" @implementation MyView /* - (id)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { } return self; } */ - (void)drawRect:(CGRect)rect { NSLog(@"MyView:drawRect..."); CGContextRef X = UIGraphicsGetCurrentContext(); CGRect bounds = CGContextGetClipBoundingBox(X); CGPoint center = CGPointMake((bounds.size.width / 2), (bounds.size.height / 2)); NSLog(@"--> (drawRect) bounds:'%@'", NSStringFromCGRect(bounds)); // fill background rect dark blue CGContextSetRGBFillColor(X, 0,0,0.3, 1.0); CGContextFillRect(X, bounds); // circle CGContextSetRGBFillColor(X, 0,0,0.6, 1.0); CGContextFillEllipseInRect(X, bounds); // fat rounded-cap line from origin to center of view CGContextSetRGBStrokeColor(X, 0,0,1, 1.0); CGContextSetLineWidth(X, 30); CGContextSetLineCap(X, kCGLineCapRound); CGContextBeginPath(X); CGContextMoveToPoint(X, 0,0); CGContextAddLineToPoint(X, center.x, center.y); CGContextStrokePath(X); // Draw the text NoXIB in red char* text = "Hello World!"; CGContextSelectFont(X, "Helvetica Bold", 24.0f, kCGEncodingMacRoman); CGContextSetTextDrawingMode(X, kCGTextFill); CGContextSetRGBFillColor(X, 0.8f, 0.3f, 0.1f, 1.0f); CGAffineTransform xform = CGAffineTransformMake( 1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f ); CGContextSetTextMatrix(X, xform); CGContextShowTextAtPoint(X, center.x, center.y, text, strlen(text)); } - (void)dealloc { [super dealloc]; } @end
现在双击 XIBViewController.xib 在 Interface Builder 中打开它。将视图的类设置为 myView -- 您无需输入 -- 它在下拉菜单中。
[Cmd + s] [Cmd + q] [Cmd + enter] (保存,退出 IB 并构建+运行),您应该会看到这个:
这并非随意的艺术尝试。它实际上是一个非常好的视觉测试工具。
- 矩形包含了整个可视区域。
- 内部的椭圆可以让我们立即看到是否有部分可视区域被裁剪了 -- 如果它在所有四边都刚好触碰到边框,那么一切正常。
- 这条线从 (0, 0) 连接到视图的中心。关于坐标系统有很多遗留的混淆(有些从顶部开始,有些从底部开始),所以您真的需要知道 (0, 0) 在哪里。
- 我在线的两端都做了标记。有些视图会裁剪到边界,有些则不会。这是找出这一点的良好方法。
简直是魔法!是的,但这是件坏事。那么,我们如何刮掉所有的“魔法”并用老式的代码替换它,以便我们可以掌握正在发生的事情呢?
第二部分:告别 .XIB
如果您对 .XIB 一无所知,在我们废弃它们之前应该稍作考虑。
想象一下使用 IB -- 拖放和点击,来制作按钮、菜单等。所以 XIB 存储了所有这些东西,有点像这样:
{ UIButton: Name = Bob, Color = red, rect = { 10,10, 50, 50 }, ... } { ... } etc.
回到您的代码中,您会做类似这样的事情:
@property (bla) IBOutlet UIButton *Bob;
……给您一个句柄,您可以用来操作您在 IB 中创建的对象。
当您运行代码时,Xcode 会负责处理 .XIB,创建并初始化一个 `UIButton`,设置 `Color = red, rect = {10, 10, 50, 50}, etc`。然后将您代码中的 `*Bob IBOutlet` 指向它,这样您的代码就可以直接操作它。
当然,这是一种过度简化。我实际上还不了解更多。希望这个项目会有帮助!让我们开始吧!
第一步:删除两个 .XIB 文件
实际上,在您执行此操作之前,请启动一个名为 NoXIB 的新项目并将其更新到此状态。(注意:重命名项目可能会很麻烦)。我还将 `myView.m` 中的颜色从蓝色改为红色,文本从“Hello World!”改为“Goodbye XIB!”。
尝试构建和运行,体验一下 WTF 的时刻。它仍然可以工作!!!是的 -- 这是一个 bug。当您删除一个 .xib 时,模拟器没有删除它。
- 清空模拟器:[iPhone 模拟器 -> 重置内容和设置]
- [构建 -> 清理所有目标] (实际上是双重 bug -- 因为也需要这一步)
再试一次 -- 这次应用程序会如预期一样崩溃,将您带回主屏幕。很好。
接下来,加载 `info.plist` 并删除 `[key:value]` 对 [Main nib file base name: MainWindow]。再次运行,这次不会崩溃。只会黑屏。意料之中……
接下来是一堆代码的调整,所以我建议您下载项目来玩。为了完整起见,我将在此列出 -- 它不长。我已自行将 main.m 移动到 classes 文件夹,这完全是错误的,但是 -- 它展示了类之间调用顺序的自上而下顺序。
在大多数情况下,我将让代码自行说明 -- 我已经为它添加了大量的注释。如果您对此类事物不熟悉,建议您 [Alt + 双击] 获取任何关键字的帮助,并在每个方法的开头设置一个断点,然后单步执行 [Shift + Cmd + O] 整个过程。仅单步执行本身是行不通的 -- 您可能会轻易地单步执行跳过这一行代码……
CGRect X = self.viewController.view.bounds;
而不意识到仅仅访问视图控制器的视图对象就可能触发它的 `loadView` 和 `viewDidLoad` 方法(这只会在视图是 null 指针的情况下发生)。
也许有一天 IDE 会发展到单步执行会考虑回调的程度。在此期间,断点是您的朋友。
您还可以使用 [Shift+Cmd+R] 打开控制台 -- 我在弄清楚各种视图/窗口/视图控制器/边界等矩形时非常需要它。这并不简单。
在 `settings.h` 文件中有一个选项:
#define SHOW_SB NO
此模板提供了显示状态栏的选项。由于我喜欢零混乱,所以我已经去掉了它。但是,如果有人想要它,只需将其切换为 YES 即可。
如果您是这些事的新手并想立即上手,我建议您将代码放在 `myView.m` 中。您在上面不需要触碰任何东西很长一段时间。
// // Settings.h // NoXIB // // Created by pi on 27/08/2010. // /* This template lets you choose whether you wish to show the status bar. On iOS 3+ there is a third option: to show it transparent, while allowing the window to draw full-screen behind it. If you want this, you may need to fiddle with self.viewController.wantsFullScreenLayout */ #define SHOW_SB NO // // main.m // NoXIB // // Created by pi on 13/08/2010. // #import <UIKit/UIKit.h> int main(int argc, char *argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; int retVal = UIApplicationMain(argc, argv, nil, @"NoXIBAppDelegate"); [pool release]; return retVal; } // // NoXIBAppDelegate.h // NoXIB // // Created by pi on 13/08/2010. // #import <UIKit/UIKit.h> @class NoXIBViewController; @interface NoXIBAppDelegate : NSObject <UIApplicationDelegate> { UIWindow *window; NoXIBViewController *viewController; } @property (nonatomic, retain) /*IBOutlet*/ UIWindow *window; @property (nonatomic, retain) /*IBOutlet*/ NoXIBViewController *viewController; @end // // NoXIBAppDelegate.h // NoXIB // // Created by pi on 13/08/2010. // #import <UIKit/UIKit.h> @class NoXIBViewController; @interface NoXIBAppDelegate : NSObject <UIApplicationDelegate> { UIWindow *window; NoXIBViewController *viewController; } @property (nonatomic, retain) /*IBOutlet*/ UIWindow *window; @property (nonatomic, retain) /*IBOutlet*/ NoXIBViewController *viewController; @end // // NoXIBAppDelegate.m // NoXIB // // Created by pi on 13/08/2010. // #import "NoXIBAppDelegate.h" #import "NoXIBViewController.h" #import "Settings.h" @implementation NoXIBAppDelegate @synthesize window; @synthesize viewController; - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [[UIApplication sharedApplication] setStatusBarHidden:(SHOW_SB ? NO : YES)]; CGRect appFrame = [UIScreen mainScreen].applicationFrame; // Create the window that will host the viewController // windowRect must start at 0, 0 // if (SHOW_SB == YES), appFrame will be '{{0, 20}, {320, 460}}' CGRect windowRect = CGRectMake(0, 0, appFrame.size.width, appFrame.size.height); self.window = [[[UIWindow alloc] initWithFrame:windowRect] autorelease]; /* NOTE: init_withFrame lets the view controller know the dimensions of the view it must create. it will create the view when loadView is triggered. This will occur automatically the first time viewController.view is accessed. ie in [window addSubview:viewController.view]; below */ self.viewController = [[[NoXIBViewController alloc] init_withFrame : appFrame] autorelease]; /* viewController.view: If you access this property and its value is currently nil, the view controller automatically calls the loadView method and returns the resulting view. The default loadView method attempts to load the view from the nib file associated with the view controller (if any). If your view controller does not have an associated nib file, you should override the loadView method and use it to create the root view and all of its subviews.*/ // so can't do this: //NSLog(@" * viewController.view.bounds:'%@'", // NSStringFromCGRect(viewController.view.bounds) ); // or LoadView then viewDidLoad would get triggered // accessing a nil viewController.view forces loadView then viewDidLoad to be called [window addSubview:viewController.view]; [window makeKeyAndVisible]; return YES; } - (void)dealloc { [viewController release]; [window release]; [super dealloc]; } @end // // NoXIBViewController.h // NoXIB // // Created by pi on 13/08/2010. // #import <UIKit/UIKit.h> @interface NoXIBViewController : UIViewController { CGRect frame; } @property (nonatomic) CGRect frame; - (id) init_withFrame : (CGRect) _frame; @end // // NoXIBViewController.m // NoXIB // // Created by pi on 13/08/2010. // #import "NoXIBViewController.h" #import "MyView.h" @implementation NoXIBViewController @synthesize frame; - (id) init_withFrame : (CGRect) _frame { frame = _frame; return [self init]; } // Implement loadView to create a view hierarchy programmatically, without using a nib. /* The view controller calls this method when the view property is requested but is currently nil. If you create your views manually, you must override this method and use it to create your views. If you use Interface Builder to create your views and initialize the view controller—that is, you initialize the view using the initWithNibName:bundle: method, set the nibName and nibBundle properties directly, or create both your views and view controller in Interface Builder—then you must not override this method. */ - (void)loadView { UIView* myV = [MyView alloc]; [myV initWithFrame: frame]; [self setView: myV]; [myV release]; } /* Discussion This method is called after the view controller has loaded its associated views into memory. This method is called regardless of whether the views were stored in a nib file or created programmatically in the loadView method. This method is most commonly used to perform additional initialization steps on views that are loaded from nib files. */ /* - (void)viewDidLoad { [super viewDidLoad]; } */ - (void)didReceiveMemoryWarning { // Releases the view if it doesn't have a superview. [super didReceiveMemoryWarning]; // Release any cached data, images, etc that aren't in use. } - (void)viewDidUnload { // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; } - (void)dealloc { [super dealloc]; } @end // // MyView.h // NoXIB // // Created by pi on 13/08/2010. // #import <UIKit/UIKit.h> @interface MyView : UIView { } @end // // MyView.m // NoXIB // // Created by pi on 13/08/2010. // #import "MyView.h" @implementation MyView /* - (id)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { } return self; } */ - (void)drawRect:(CGRect)rect { CGContextRef X = UIGraphicsGetCurrentContext(); CGRect bounds = CGContextGetClipBoundingBox(X); CGPoint center = CGPointMake((bounds.size.width / 2), (bounds.size.height / 2)); NSLog(@"--> (drawRect) bounds:'%@'", NSStringFromCGRect(bounds)); // fill background rect dark red CGContextSetRGBFillColor(X, 0.3,0,0, 1.0); CGContextFillRect(X, bounds); // circle CGContextSetRGBFillColor(X, 0.6,0,0, 1.0); CGContextFillEllipseInRect(X, bounds); // fat rounded-cap line from origin to center of view CGContextSetRGBStrokeColor(X, 1,0,0, 1.0); CGContextSetLineWidth(X, 30); CGContextSetLineCap(X, kCGLineCapRound); CGContextBeginPath(X); CGContextMoveToPoint(X, 0,0); CGContextAddLineToPoint(X, center.x, center.y); CGContextStrokePath(X); // Draw the text NoXIB in light blue char* text = "Goodbye XIB!"; CGContextSelectFont(X, "Helvetica Bold", 24.0f, kCGEncodingMacRoman); CGContextSetTextDrawingMode(X, kCGTextFill); CGContextSetRGBFillColor(X, 0.1f, 0.3f, 0.8f, 1.0f); CGAffineTransform xform = CGAffineTransformMake( 1.0f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f ); CGContextSetTextMatrix(X, xform); CGContextShowTextAtPoint(X, center.x, center.y, text, strlen(text)); } - (void)dealloc { [super dealloc]; } @end
……或者直接下载 zip 文件
PS:在良好的配色方案下,代码更容易理解。我将用户定义的对象的颜色归类到红色、橙色、黄色光谱中,而标准对象则放在蓝色、紫色端,而不是仅仅尝试让东西看起来漂亮。这是我正在使用的截图:
请随意将其放入 `~/Library/Application Support/Xcode/Color Themes` (您可能需要创建一个文件夹),然后在 Xcode -> preferences -> fonts and colours -> colour scheme 中应该能找到它。在这里查看不同颜色代表的含义很有价值。