ソースコードリーディング MGTileMenu

目的

本を読んだりアプリを作るだけでは分からない「真似すべきスタイル」を得るため。

方法

  1. 動かしてみる
  2. 時系列で処理を追っていく
  3. 静的・動的な構造を絵に描く
  4. コードの良かったところをメモする

MGTileMenu

@psychs に教えてもらった https://github.com/mattgemmell/MGTileMenu を読む。

動かしてみる。ダブルタップすると開く。

初期化

MLGViewController.m。
UITapGestureRecognizer で起動アクションを登録。

- (void)viewDidLoad
{
    [super viewDidLoad];
	
	// Set up recognizers.
	UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
	doubleTapRecognizer.numberOfTapsRequired = 2;
	doubleTapRecognizer.delegate = self;
	[self.view addGestureRecognizer:doubleTapRecognizer];
	
	UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
	tapRecognizer.delegate = self;
	[self.view addGestureRecognizer:tapRecognizer];
}

ダブルタップ

@selector(handleGesture:) で指定しているのでそこを見る。ダブルタップなら開く、それ以外の場合にもし開いていたら閉じる。
初期化の遅延。タップされた場所に表示。inView でどの view に表示すべきかを指定。なるほど。

if (!tileController || tileController.isVisible == NO) {
	if (!tileController) {
		// Create a tileController.
		tileController = [[MGTileMenuController alloc] initWithDelegate:self];
		tileController.dismissAfterTileActivated = NO; // to make it easier to play with in the demo app.
	}
	// Display the TileMenu.
	[tileController displayMenuCenteredOnPoint:loc inView:self.view];
}

MGTileMenuController

まずはヘッダを見ておおまかに把握。細かいけど delegate の変数名は theDelegate か。たしかにこっちのほうがいいかも。
TileMenu のコンフィグレーションのためのプロパティがいくつか。

MGTileMenuController の初期化

if (theDelegate && [theDelegate conformsToProtocol:@protocol(MGTileMenuDelegate)]) {
    _delegate = theDelegate;
    return (self = [self initWithNibName:nil bundle:nil]);
}

return nil;

initWithNibName でインスタンス変数の初期化。

delegate のチェックをしてだめなら nil を返してる。このスタイル初めて見た。まねしよう。そうかこれは外部に公開している部品なのでユーザーがただしい delegate を渡すとは限らないのかな。いや待てよ。id してるから静的にチェックされるはず?
それとどちらかというと契約による設計的には assert すべきな気がする。

MGTileMenuController loadView

view を組み立てていく。MGTileMenuView を見ないと。そういえば UIView を独自に定義するコードを見るのも初めてかも。楽しみ。


ここって self.view に対してあれこれプロパティ設定してるけど MGTileMenuView のコンストラクタでやったらダメなのかな。あとで確認。

    self.view = [[MGTileMenuView alloc] initWithFrame:CGRectMake(0, 0, bezelSize, bezelSize)];
	((MGTileMenuView *)(self.view)).controller = self;
	
	self.view.opaque = NO;
	self.view.backgroundColor = [UIColor clearColor];
	self.view.layer.opaque = NO;
	
	self.view.layer.shadowRadius = 5.0;
	self.view.layer.shadowOpacity = 0.75;
	self.view.layer.shadowOffset = CGSizeMake(0, 5);
	if (!_shadowsEnabled) {
		self.view.layer.shadowRadius = 0.0;
		self.view.layer.shadowOffset = CGSizeZero;
	}


View を組み立てていく時にこの辺りでスタイルに迷う。

  • この場合のようにベタっと書いてしまうのが分かりやすい。
  • Extract method くらいはしようぜ。
  • 完全にクラスにしてしまう

のどれが文化的に良いスタイルなんだろう。

	// Close button
	_closeButton = [UIButton buttonWithType:UIButtonTypeCustom];
	UIImage *closeImage;
	if (_closeButtonImage != nil) {
		closeImage = _closeButtonImage;
	} else {
		closeImage = [UIImage imageNamed:@"CloseButton"];
	}
	_closeButton.accessibilityLabel = NSLocalizedString(@"Close", @"Accessibility label for Close button");
	_closeButton.accessibilityHint = NSLocalizedString(@"Closes the menu", @"Accessibility hint for Close button");
	CGRect closeFrame = CGRectZero;
	closeFrame.size = closeImage.size;
	_closeButton.frame = closeFrame;
	[_closeButton setBackgroundImage:closeImage forState:UIControlStateNormal];
	if (_selectedCloseButtonImage != nil) {
		[_closeButton setBackgroundImage:_selectedCloseButtonImage forState:UIControlStateHighlighted];
	} else {
		[_closeButton setBackgroundImage:nil forState:UIControlStateHighlighted];
	}
	[_closeButton addTarget:self action:@selector(dismissMenu) forControlEvents:UIControlEventTouchUpInside];
	[self.view addSubview:_closeButton];
タイルの生成

タイルは Custom button なんだね。

	for (int i = 0; i < (numTiles + 1); i++) {
		tileButton = [UIButton buttonWithType:UIButtonTypeCustom];
		tileButton.userInteractionEnabled = NO;
		tileButton.tag = i;

やはりここもベタ書き。layer.zPosition 知らなかった。メモ。

MGTileMenuView

drawRect を override して controller の _bezelPath を draw してる。controller に密結合。_bezelPath とは

- (UIBezierPath *)_bezelPath
{
	CGRect bezelRect = self.view.bounds;
	CGFloat halfTile = (CGFloat)(self.tileSide) / 2.0;
	bezelRect.origin.x += halfTile;
	bezelRect.origin.y += halfTile;
	bezelRect.size.width -= self.tileSide;
	bezelRect.size.height -= self.tileSide;
	UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:bezelRect 
													cornerRadius:_cornerRadius];
	return path;
}

これ。ふーむ。self.views.bounds 等の controller しか知り得ないものが欲しかったのか。

displayMenuPage:

loadView で self.view に準備してあった tile menu を parentView 内に表示。
表示する直前に各種パラメータが変更されていることを想定して調整。なるほど。複数枚のタイルの位置もこのタイミングで調整。


アニメーション。おー。こういうの出来るんだ。

// Add appearance animations.
NSArray *animations = [self _animationsForAppearing:YES];
int i = 0;
for (CAAnimation *animation in animations) {
	[self.view.layer addAnimation:animation forKey:[NSString stringWithFormat:@"%d", i]];
	i++;
}

アニメーション

アニメーションを深追いしてみよう。ざっくりいうと効果を指定して、from, to へ移動。

- (NSArray *)_animationsForAppearing:(BOOL)appearing
{
	NSMutableArray *animations = [NSMutableArray arrayWithCapacity:0];
	
	if (appearing) {
		CABasicAnimation *expandAnimation;
		expandAnimation = [CABasicAnimation animationWithKeyPath:@"transform"];
		[expandAnimation setValue:MG_ANIMATION_APPEAR forKey:@"name"];
		[expandAnimation setRemovedOnCompletion:NO];
		[expandAnimation setDuration:MG_ANIMATION_DURATION];
		[expandAnimation setFillMode:kCAFillModeForwards];
		[expandAnimation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
		[expandAnimation setDelegate:self];
		CGFloat factor = 0.6;
		CATransform3D transform = CATransform3DMakeScale(factor, factor, factor);
		expandAnimation.fromValue = [NSValue valueWithCATransform3D:transform];
		expandAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
		
		[animations addObject:expandAnimation];
		.

その他

  • (void)didReceiveMemoryWarning 知らなかった。
  • テストはない
  • @protocol で @required/@optional

感想

コードが色々勉強になるのはもちろんだが、コンポーネントとしての手触り感をどこで実現しているのかが分かって良かった。これは最初の UI としての設計とその後の iteration で磨き上げているんだろうなあ。そこが素晴らしい。