ホームページ >バックエンド開発 >PHPチュートリアル >iOS 5 ストーリーボードを使ってみる (2)
テンプレートセル
tableViewControllerを追加するときにXcodeが警告を発行することに気づきましたか?
「サポートされていない構成: プロトタイプ可能なセルには再利用識別子が必要です」、TableViewController をストーリーボードに追加する場合、Xcode はデフォルトでプロトタイプ セル (テンプレート セル) を使用します。しかし、それを構成していないため、この警告が表示されます。
テンプレート セルは、優れたストーリーボード機能です。元の nib ファイルよりもはるかに優れています。以前は、テーブル ビューのセルをカスタマイズしたい場合は、コードでセル オブジェクトに独自のサブビューを追加するか、新しいペン先を作成してペン先から独自のセルをロードしていました。しかし、テンプレート セルの登場により、これらすべてが簡素化され、ストーリーボード エディタで直接独自のテーブル ビュー セルをデザインできるようになりました。
TableViewController には空のテンプレート セルが付属しています。このセルをクリックすると、プロパティ パネルでスタイルを [字幕] に設定できます。これにより、セルが 2 つのラベルを持つセルに変わります。 TableViewCell を自分で手動で作成したことがある場合は、これが UITableViewCellStyleSubtitle スタイルであることを知っているはずです。テンプレート セルを使用すると、組み込みスタイルを使用してセルを作成したり、完全なカスタム セルを作成したりできます (これについては後ほど説明します)。
アクセサリのプロパティを開示インジケータに変更し、再利用識別子(再利用ID)を「PlayerCell」に設定します。こうすることで、Xcode は警告を即座に抑制します。すべてのテンプレート セルは依然として通常の UITableViewCell オブジェクトであり、再利用 ID が残ります。Xcode は、それを設定することを忘れないようにプロンプトを表示するだけです (少なくとも、この警告は表示されます)。
プログラムを実行しても何も変わりませんでした。驚かないでください。まだデータ ソースを提供していないため、テーブル ビューには行が表示されません。
新しいファイルをプロジェクトに追加します。 UIViewControllerサブクラス テンプレートを選択します。クラスに PlayersViewController という名前を付け、UITableViewController から継承していることを確認します。ストーリーボードでこのクラスの UI がすでに設計されているため、「WithXib...」オプションを選択しないでください。ペン先はもう必要ありません!
ストーリーボード エディターに戻り、TableViewController を選択します。 [Identity] パネルで、そのクラスを PlayersViewController に設定します。このステップは、ストーリーボードにあるシーンを独自の ViewController サブクラスに関連付けるため、重要です。この手順を必ず覚えておいてください。そうしないと、作成したクラスがまったく役に立たなくなります。
今後、プログラムの実行後、ストーリーボード内の tableViewController は PlayersViewController クラスのインスタンスになります。
PlayersViewController.h ファイルに変数配列属性を追加します。
#import <UIKit/UIKit.h> @interface PlayersViewController : UITableViewController @property ( nonatomic, strong ) NSMutableArray * players; @end
この配列は、アプリケーション内のモデル データ、つまり Player オブジェクトを保存します。次に、Player クラスを作成します。 Objective-C クラス テンプレートを使用して、新しいファイルを作成します。 Player という名前が付けられ、NSObject を継承します。
Player.h ファイル:
@interface Player : NSObject @property ( nonatomic, copy ) NSString * name; @property ( nonatomic, copy ) NSString * game; @property ( nonatomic, assign ) int rating; @end
Player.m ファイル
#import "Player.h" @implementation Player @synthesize name; @synthesize game; @synthesize rating; @end
これらについては何も驚くべきことではありません。 Player は、プレーヤーの名前、プレーヤーがプレイしているゲーム、レベル (1 ~ 5 つ星) の 3 つのプロパティを持つ単純なオブジェクトです。
AppDelegate に配列を配置し、テストのためにいくつかの Player オブジェクトを配列に配置します。この配列は PlayerViewController の player プロパティに割り当てられます。
AppDelegate.m で、Player クラスと PlayersViewController クラスの import ステートメントを追加し、players というインスタンス変数を追加します:
#import "AppDelegate.h" #import "Player.h" #import "PlayersViewController.h" @implementation AppDelegate { NSMutableArray * players; } ......
DidFinishLaunchingWithOptions メソッドを変更します:
- ( BOOL ) application :( UIApplication *) application didFinishLaunchingWithOptions :( NSDictionary *) launchOptions { players = [ NSMutableArray arrayWithCapacity : 20 ] ; Player * player = [[ Player alloc ] init ] ; player.name = @ "Bill Evans" ; player.game = @ "Tic-Tac-Toe" ; player.rating = 4 ; [ players addObject : player ] ; player = [[ Player alloc ] init ] ; player.name = @ "Oscar Peterson" ; player.game = @ "Spin the Bottle" ; player.rating = 5 ; [ players addObject : player ] ; player = [[ Player alloc ] init ] ; player.name = @ "Dave Brubeck" ; player.game = @ "Texas Hold’em Poker" ; player.rating = 2 ; [ players addObject : player ] ; UITabBarController * tabBarController = ( UITabBarController *) self.window.rootViewController; UINavigationController * navigationController = [[ tabBarController viewControllers ] objectAtIndex : 0 ] ; PlayersViewController * playersViewController = [[ navigationController viewControllers ] objectAtIndex : 0 ] ; playersViewController.players = players; return YES ; }
First作成するいくつかの Player オブジェクトが player 配列に追加されます。それから:
UITabBarController * tabBarController = ( UITabBarController *) self.window.rootViewController; UINavigationController * navigationController = [[ tabBarController viewControllers ] objectAtIndex : 0 ] ; PlayersViewController * playersViewController = [[ navigationController viewControllers ] objectAtIndex : 0 ] ; playersViewController.players = players;
はい、これは何ですか? Players 配列を PlayersViewController の player プロパティに割り当てて、TabeViewController のデータ ソースとして機能させたいと考えています。ただし、アプリケーション デリゲートは PlayersViewController がどこにあるかわからないため、ストーリーボードから見つける必要があります。これは、私を悩ませるストーリーボード使用の欠点の 1 つです。 IB を使用している場合は、MainWindow.xib にアプリケーション デリゲートへの参照があり、最上位の ViewController をアプリケーション デリゲートの IBOutlet プロパティに接続できます。しかし、今ではストーリーボードを使用するとそれが不可能になります。アプリケーション デリゲートは、最上位の ViewController で参照できなくなります。コードを通じてのみ参照を取得できるのは本当に残念です。
UITabBarController * tabBarController = ( UITabBarController *) self.window.rootViewController;
ストーリーボードの初期ビュー コントローラーが TabBarController であることがわかっているので、ウィンドウ オブジェクトの rootViewController からそれへの参照を取得し、型変換を実行できます。
PlayersViewController は最初のタブの NavigationController コンテナにあるため、最初に UINavigationController オブジェクトを取得します:
UINavigationController * navigationController = [[ tabBarController viewControllers ] objectAtIndex : 0 ] ;
次に、NavigationController の rootViewController で PlayersViewController を取得できます:
PlayersViewController * playersViewController = [[ navigationController viewControllers ] objectAtIndex : 0 ] ;
但是,UINavigationController 没有 rootViewController属性。因此我们必须从 viewControllers 数组中检索。(它有一个 topViewController 属性,但那个是位于viewControllers栈顶的 view controller。而我们要的是栈低的 view controller。虽然在程序刚启动的时候,栈顶和栈底实际上是一个,你也可以使用topViewController,但这不是那么安全)
现在我们有了 Player 数组,可以回到PlayersViewController 中创建我们的数据源了。
打开PlayersViewController.m,修改 table view 的数据源方法:
- ( NSInteger ) numberOfSectionsInTableView :( UITableView *) tableView { return 1 ; } - ( NSInteger ) tableView :( UITableView *) tableView numberOfRowsInSection :( NSInteger ) section { return [ self.players count ] ; }
重要的是cellForRowAtIndexPath方法。 Xcode 创建的模板代码是这样的:
- ( UITableViewCell *) tableView :( UITableView *) tableView cellForRowAtIndexPath :( NSIndexPath *) indexPath { static NSString * CellIdentifier = @ "Cell" ; UITableViewCell * cell = [ tableView dequeueReusableCellWithIdentifier : CellIdentifier ] ; if ( cell == nil ) { cell = [[ UITableViewCell alloc ] initWithStyle : UITableViewCellStyleDefault reuseIdentifier : CellIdentifier ] ; } // Configure the cell... return cell; }
毫无疑问,你曾经无数次地在这个地方编写自己的 table view 代码。但现在不同了。将代码修改为:
- ( UITableViewCell *) tableView :( UITableView *) tableView cellForRowAtIndexPath :( NSIndexPath *) indexPath { UITableViewCell * cell = [ tableView dequeueReusableCellWithIdentifier : @ "PlayerCell" ] ; Player * player = [ self.players objectAtIndex : indexPath.row ] ; cell.textLabel.text = player.name; cell.detailTextLabel.text = player.game; return cell; }
代码变得更简单了! 其实你只需要从这里获得新的 cell :
UITableViewCell * cell = [ tableView dequeueReusableCellWithIdentifier :@ "PlayerCell" ] ;
不再需要复用单元格了,它会自动从模板 cell 获得一份拷贝给你使用!你只需要提供复用的ID(你曾经在故事版编辑器中为模板cell 设置过的,在本例中,即“PlayerCell”)。记得设置这个ID,否则模板 cell 不会生效。
由于 PlayersViewController 不认识 Player 类,你还需要导入Player 类的头文件:
#import "Player.h"
此外还要合成 players 属性:
@synthesize players;
运行程序,如下图所示:
注意:在本例中,我们只用了一种模板 cell,如果你需要显示多种 cell,你可以加入更多的模板 cell。你可以复制已有的模板cell为新的cell,也可以增加TableView 的 Prototype Cells 属性值。注意,确保每个模板 cell 都有自己的复用ID。
使用神奇的模板cell只需一行代码,这是件了不起的事情!
设计完全自定义的模板cell
对于大部分 app,使用标准的 cell 样式就足矣。但我想在单元格右边加一张图片以显示玩家级别(以星级的形式)。UITableViewCell的标准样式中不包含可以在单元格中放入一个 ImageView,因此我只能选择定制设计。
回到 MainStoryboard.storyboard,选择模板cell,将Style 属性设置为 Custom。默认的 label 将消失。
首先增加 cell 的高度为 55 像素。拖拽它下端的拉柄可以改变它的高度,也可以修改Size 面板中的 Row height 值。
拖两个 Label 到 Cell 中,将它们放置到大致等于原先所在的位置。随意修改它们的字体和颜色。将两个label 的高亮色为白色。这样当用户点击 cell 时看起来会好一些,因为此时 cell的背景为蓝色。
拖一个 ImageView 到 cell 右端,紧靠着右箭头。调整它宽度为81,高度无所谓。设置它的 Mode 为 Center(在属性面板的 View 下面)以便当我们将图片放入时它不会被拉伸。
我将俩个 label 的宽度设置为210,这样不会遮住 ImageView。最终设计完成是这个样子:
由于是定制单元格,我们不再使用 cell 的 textLabel 和detailTextLabel 属性来显示文本。这两个标签的属性在我们的 cell 中也不再存在。
我们将通过 tag 检索我们想要的 Label。
对于 Name 标签,tag 设置为100,对于 Game 标签,tag 设置为102。你可以在属性面板中设置 tag。
打开PlayersViewController.m ,将cellForRowAtIndexPath 方法修改为:
- ( UITableViewCell *) tableView :( UITableView *) tableView cellForRowAtIndexPath :( NSIndexPath *) indexPath { UITableViewCell * cell = [ tableView dequeueReusableCellWithIdentifier : @ "PlayerCell" ] ; Player * player = [ self.players objectAtIndex : indexPath.row ] ; UILabel * nameLabel = ( UILabel *)[ cell viewWithTag : 100 ] ; nameLabel.text= player.name; UILabel * gameLabel = ( UILabel *)[ cell viewWithTag : 101 ] ; gameLabel.text = player.name; UIImageView * ratingImageView = ( UIImageView *) [ cell viewWithTag : 102 ] ; ratingImageView.image = [ self imageForRating : player.rating ] ; return cell; }
这里调用了一个新方法imageForRating,这个方法实现如下:
- ( UIImage *) imageForRating :( int ) rating { switch ( rating ) { case 1 : return [ UIImage imageNamed : @ "1StarSmall.png" ] ; case 2 : return [ UIImage imageNamed : @ "2StarsSmall.png" ] ; case 3 : return [ UIImage imageNamed : @ "3StarsSmall.png" ] ; case 4 : return [ UIImage imageNamed : @ "4StarsSmall.png" ] ; case 5 : return [ UIImage imageNamed : @ "5StarsSmall.png" ] ; } return nil ; }
再次运行程序。
啊哈,看起来有点不太对劲。我们修改了模板 cell 的高度,但tableView 并不知道。有两个办法:改变 table view 的 Row Height 属性,或者修改 heightForRowAtIndexPath 方法。前者更为简单,因此我使用了前者。
注意:如果你事先无法确定 cell 高度,或者你有不同高度的几种 cell,你应该使用 heightForRowAtIndexPath 。
返回MainStoryboard.storyboard,在TableView 的 Size 面板中,将 Row Height 设置为55
如果你用拖拽而不是直接键入的方式改变 cell 的高度,tableview 的 Row Height 属性也会自动随之改变。
再次运行程序,这次看起来就好多了。
子类化模板 Cell
我们的 Table View 看起来不错吧!但我并不喜欢用 tag 去访问 UILabel 和其他 cell 的 subview。如果这些Label 能连接到 IBOutlet 属性岂不是更好?
在项目中添加新的 File,使用 Objective-C class 模板。类名为PlayerCell ,继承自 UITableViewCell。
修改 PlayerCell.h 为:
@interface PlayerCell : UITableViewCell @property ( nonatomic, strong ) IBOutlet UILabel * nameLabel; @property ( nonatomic, strong ) IBOutlet UILabel * gameLabel; @property ( nonatomic, strong ) IBOutlet UIImageView * ratingImageView; @end
修改 PlayerCell.m 为:
#import "PlayerCell.h" @implementation PlayerCell @synthesize nameLabel; @synthesize gameLabel; @synthesize ratingImageView; @end
内容不多,仅仅是加了几个属性,nameLabel, gameLabel 以及 ratingImageView。
回到MainStoryboard.storyboard,选择模板 cell ,在 Identity 面板改变其 Class 为“PlayerCell”。这样当你用 dequeueReusableCellWithIdentifier 方法获得一个 cell时,它实际上返回一个PlayerCell 给你。
注意,我将类的名字和重用 ID 取成了一样——都叫做 PlayerCell——这仅仅是因为我喜欢这样。其实二者毫无干系,你完全让它们不一样。
选择,你可以将 label 和 ImageView 连接到IBOutlet。选中 Label 然后从它的连接面板拖一条线到 TableViewCell,或者用 Ctrl+左键从 TableViewCell 拖到 Label 上。
重点:你可以在控件和 TableViewCell 间建立连接,而不仅仅是在控件和 ViewController 间建立连接!如你所见,当你的数据源用 dequeueReusableCellWithIdentifier向 Table View 请求新的单元格时,TableView 并不真正把模板 cell 给你,它只是给你一份模板 cell 的拷贝(也可能是一个已经存在的cell——在复用的情况下)。也就是说任何时候都存在多个 PlayerCell 实例。如果你连接 cell 上的一个 Label 到ViewController 的 IBOutlet上,那么会有多个 Label 在试图使用相同的 IBOutlet。那就麻烦了。(顺便说一句,如果在你的Cell 上有一个 Custom Button 或者其他控件,你可以将模板 cell 连接到 ViewController 的 action 上。
现在,我们已经连接好这些属性。我们的代码可以变得更加简洁:
- ( UITableViewCell *) tableView :( UITableView *) tableView cellForRowAtIndexPath :( NSIndexPath *) indexPath { PlayerCell * cell = ( PlayerCell *)[ tableView dequeueReusableCellWithIdentifier : @ "PlayerCell" ] ; Player * player = [ self.players objectAtIndex : indexPath.row ] ; cell.nameLabel.text = player.name; cell.gameLabel.text = player.game; cell.ratingImageView.image = [ self imageForRating : player.rating ] ; return cell; }
这样还差不多。我们将dequeueReusableCellWithIdentifier 返回的结果转换为PlayerCell,然后用它的属性去访问 Label 和 UIImageView。我真的喜欢使用模板 cell,它使我的TableView 代码看起来整洁多了。
当然,你仍然需要导入 PlayerCell 类:
#import "PlayerCell.h"
运行程序,跟前面一模一样,但在表格中使用的是我们自己的TableViewCell 子类。
还有一些设计技巧。在设计自己的 TableViewCell 时,你需要注意一些地方。首先,你应当设置Label 的 Highlighted Color(高亮色) ,以便用户在点击表格行时感觉更好。
其次,你应当确保添加的内容能自动适应单元格尺寸的变化。例如,当你需要表格行能够被删除或移动时 ,Cell 尺寸会发生改变。
添加下列方法到 PlayerViewController.m:
- ( void ) tableView :( UITableView *) tableView commitEditingStyle :( UITableViewCellEditingStyle ) editingStyle forRowAtIndexPath :( NSIndexPath *) indexPath { if ( editingStyle == UITableViewCellEditingStyleDelete ) { [ self.players removeObjectAtIndex : indexPath.row ] ; [ tableView deleteRowsAtIndexPaths :[ NSArray arrayWithObject : indexPath ] withRowAnimation : UITableViewRowAnimationFade ] ; } }
实现这个方法后,表格的“轻扫以删除”功能被开启。运行程序,在某行上进行轻扫手势,看看会发生什么。
删除按钮出现在 cell 上,但它同时也遮住了等级图片。实际上是因为删除按钮占据了部分cell 空间,而 cell 大小随之改变,ImageView 却没有改变。
要解决这个问题,打开 MainStoryBoard.storyboard,选择 ImageView ,在 Size 面板中修改 Autosizing 以便它始终位于 superview 的右端:
Label 的 Autosizing 设置如下,因此当 cell 尺寸改变时,Label 的尺寸也随之变化:
经过这些调整,删除按钮的出现会将星级图标挤到左边:
読者に任せて、削除ボタンが表示されたときに星を消すこともできます。重要なことは、TableViewCell を設計するときにこれらの詳細を明確にする必要があるということです。
次のステップ
このチュートリアルの後半では、セグエ、静的 tableViewCell、プレーヤー ウィンドウの追加、ゲーム選択ウィンドウ、およびこのチュートリアルのサンプル コードについて説明しました。
上記は iOS 5 ストーリーボード紹介 (2) の内容です。その他の関連コンテンツについては、PHP 中国語 Web サイト (www.php.cn) をご覧ください。