subviewを階層構造で管理できるUIViewControllerを設計する

| コメント(0) | トラックバック(0)

bringSubviewToFrontなどのメソッドでsubviewの表示順を操作することは可能ですが、subviewの数や配置の複雑度が増してくると処理が煩雑になりがちです。


例えば

 ・背景(2views)

 ・情報表示(5views)

 UIボタン群(5views~10views)

 ・広告を上下に1つずつ配置(2views?)

という構造を考えてみましょう。


背景は常に一番下に配置されますが、状況によりviewの表示順を入れ替える必要があります。

情報表示画面は常に最重要な画面を前面に表示する必要があり、頻繁に表示順の入れ替えが行われます。

UIボタン群は最前面に表示している情報表示画面を操作するためのもの、また情報表示画面を切り替えるためのものがあり、viewの数や表示順は状況により変わります。

UIボタンは常に情報表示画面より上に配置し、隠れないことが求められます。

広告はUIボタンを隠さない位置に表示され、常に最前面に配置します。サードパーティ製の広告SDKを用いるため、どのような実装がされているかは不明です。


このような複雑度を持つアプリの場合、もはやbringSubviewToFrontでなんとかしようとするのは現実的ではありません。

insertSubviewexchangeSubviewAtIndexを使うことで多少コード量は減らすことができますが、それでも煩雑であるという印象は解消できないでしょう。

ある情報表示画面を前面に配置したい場合、次の条件でsubviewsから挿入箇所を検索する必要があります。

 ・情報表示画面群の最前面

 ・UIボタン群の1つ背面である

UIボタン群の中で常に最背面にあるボタンが固定なら楽かもしれませんんが、UIボタンは状況により変わります。

もしかすると検索対象としたUIボタンインスタンスそのものがsubviewsに含まれていないかもしれません。


背景の入れ替えについても安易にsendSubviewToBackを用いるのは危険です。

なぜなら今後の仕様追加により背景viewが追加された場合に不具合の原因となる可能性が高くなるからです。


UIボタンの入れ替えも大変です。

広告のview構成が不明であるため、広告viewの1つ背面にinsertするのは危険です。(広告が読み込まれたタイミングでview構成が変わり、もしかしたらviewのインスタンスすら変わっているかもしれません)

そのため現在前面に表示されている情報表示画面を検索する必要が出てきます。


仕様変更も視野に入れて設計することを考えただけでウンザリしてきたのではないでしょうか。


今回はこのような複雑なsubview構成に対応するため、階層構造でsubviewを管理できるUIViewControllerを設計します。

階層は好きな数を自由に追加でき、階層に追加したsubviewはその階層内でのみ表示順の入れ替えが可能です。

たとえbringSubviewToFrontを使ったとしても、その階層より上の階層に存在するsubviewを上書きすることはありません。


先ほどの例を階層構造を用いて実装した場合のイメージを下図に示します。


Hierarchy.png


それぞれのsubviewは対応した階層にaddされます。

先ほどと同じくある情報表示画面を前面に配置したい場合の処理を考えてみましょう。

情報表示階層にaddされている情報表示画面はその上階層であるUIボタン階層を上書きすることはできません。

つまり情報表示画面を前面に表示したいのであれば、単純にbringSubviewToFrontを使うだけで実装することができます。


背景も前面に出したいsubviewbringSubviewToFrontするだけです。

bringSubviewToFrontによる実装であれば、今後背景viewが増えたとしても不具合は発生しません。


UIボタンの入れ変えはどうでしょうか。

UIボタン階層に存在するsubviewを一掃し、新しいUIボタンをUIボタン階層にaddするだけで完了です。


広告がどのようなview構造で実装されていたとしても、それは広告階層の中の問題です。

他の階層が気にする必要はなくなり、実装におけるリスクも随分軽減されます。


階層はUIViewで実装しますが、そのままUIViewを使ってしまうとタッチイベントがブロックされてしまいます。

UIボタン階層の上に広告階層が存在するため、すべてのタッチイベントは広告階層にブロックされUIボタンを押すことはできません。

この問題を解決するため、一切のタッチイベントをスルーするUIView(TouchTransmissionView)を実装します。


TouchTransmissionView.h

@interface TouchTransmissionView : UIView


@property (nonatomic, readonly) NSUInteger hierarchy;


- (id)initWithFrame:(CGRect)frame hierarchy:(NSUInteger)hierarchy;

- (NSComparisonResult)compareHierarchyAscending:(TouchTransmissionView*)view;


@end


TouchTransmissionView.m

@interface TouchTransmissionView ()

{

NSUInteger _hierarchy;

}


@end


@implementation TouchTransmissionView


@synthesize hierarchy = _hierarchy;


- (id)initWithFrame:(CGRect)frame hierarchy:(NSUInteger)hierarchy

{

    self = [super initWithFrame:frame];

    if (self) {

self->_hierarchy = hierarchy;

    }

    return self;

}


- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event

{

UIView* eventView = nil;

UIView *hitView = [super hitTest:point withEvent:event];

if (hitView != self) eventView = hitView;

return eventView;

}


- (NSComparisonResult)compareHierarchyAscending:(TouchTransmissionView*)view

{

if (self.hierarchy > view.hierarchy) return NSOrderedDescending;

else if (self.hierarchy < view.hierarchy) return NSOrderedAscending;

else return NSOrderedSame;

}


@end


subviewを階層構造で管理するためのUIViewController(HierarchyViewController)を実装します。


HierarchyViewController.h

@interface HierarchyViewController : UIViewController


- (void)addSubview:(UIView*)view hierarchy:(NSUInteger)hierarchy;


- (void)bringSubviewToFront:(UIView*)view;


- (NSArray*)subviews;

- (NSArray*)subviewsOfHierarchy:(NSUInteger)hierarchy;


@end


HierarchyViewController.m

#import "TouchTransmissionView.h"


@interface HierarchyViewController ()


- (TouchTransmissionView*)addLayerViewWithHierarchy:(NSUInteger)hierarchy;


@property (nonatomic, retain) NSMutableArray* layerViewList;


@end


@implementation HierarchyViewController


@synthesize layerViewList;


- (TouchTransmissionView*)addLayerViewWithHierarchy:(NSUInteger)hierarchy

{

TouchTransmissionView* newLayerView = [[[TouchTransmissionView alloc] initWithFrame:self.view.bounds hierarchy:hierarchy] autorelease];

newLayerView.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin |

UIViewAutoresizingFlexibleLeftMargin |

UIViewAutoresizingFlexibleRightMargin |

UIViewAutoresizingFlexibleTopMargin |

UIViewAutoresizingFlexibleHeight |

UIViewAutoresizingFlexibleWidth ;

[self.view addSubview:newLayerView];

[self.layerViewList addObject:newLayerView];

NSArray* sortedLayerViewList = [self.layerViewList sortedArrayUsingSelector:@selector(compareHierarchyAscending:)];

for (UIView* view in sortedLayerViewList) {

[self.view bringSubviewToFront:view];

}

self.layerViewList = [[sortedLayerViewList mutableCopy] autorelease];

return newLayerView;

}


#pragma mark - public

- (void)addSubview:(UIView*)view hierarchy:(NSUInteger)hierarchy

{

if (self.layerViewList == nil) {

self.layerViewList = [NSMutableArray array];

}

UIView* targetLayerView = nil;

for (TouchTransmissionView* layerView in self.layerViewList) {

if (layerView.hierarchy == hierarchy) {

targetLayerView = layerView;

break;

}

}

if (targetLayerView == nil) {

targetLayerView = [self addLayerViewWithHierarchy:hierarchy];

}

[targetLayerView addSubview:view];

}


- (NSArray*)subviewsOfHierarchy:(NSUInteger)hierarchy

{

TouchTransmissionView* targetLayerView = nil;

for (TouchTransmissionView* layerView in self.layerViewList) {

if (layerView.hierarchy == hierarchy) {

targetLayerView = layerView;

break;

}

}

return targetLayerView.subviews;

}


- (NSArray*)subviews

{

NSMutableArray* subViewList = [NSMutableArray array];

for (UIView* layerView in self.layerViewList) {

[subViewList addObjectsFromArray:layerView.subviews];

}

return subViewList;

}


- (void)bringSubviewToFront:(UIView*)view

{

for (UIView* layerView in self.layerViewList) {

for (UIView* subView in layerView.subviews) {

if (subView == view) {

[layerView bringSubviewToFront:subView];

break;

}

}

}

}


@end



最後にサンプルとしてHierarchyViewControllerを継承したSampleViewControllerを実装し、動作を確認してみましょう。


SampleViewController.h

@interface SampleViewController : HierarchyViewController

@end


SampleViewController.m

typedef enum {

ViewHierarchy_BackGround = 0,

ViewHierarchy_Back,

ViewHierarchy_Middle,

ViewHierarchy_Front,

}eViewHierarchy;


@interface SampleViewController ()


- (void)handleBackgroundView_Tap:(UITapGestureRecognizer*)sender;


@end


@implementation SampleViewController


- (void)viewDidLoad {

    [super viewDidLoad];

UIView* view2 = [[[UIView alloc] initWithFrame:CGRectMake(30, 30, 200, 200)] autorelease];

view2.backgroundColor = [UIColor blueColor];

[self addSubview:view2 hierarchy:ViewHierarchy_Front];

UIView* view1 = [[[UIView alloc] initWithFrame:CGRectMake(90, 90, 200, 200)] autorelease];

view1.backgroundColor = [UIColor redColor];

[self addSubview:view1 hierarchy:ViewHierarchy_Back];

UIView* backgroundView = [[[UIView alloc] initWithFrame:self.view.bounds] autorelease];

backgroundView.backgroundColor = [UIColor greenColor];

[self addSubview:backgroundView hierarchy:ViewHierarchy_BackGround];

UIView* middleView1 = [[[UIView alloc] initWithFrame:CGRectMake(60, 130, 250, 40)] autorelease];

middleView1.backgroundColor = [UIColor yellowColor];

[self addSubview:middleView1 hierarchy:ViewHierarchy_Middle];

UIView* middleView2 = [[[UIView alloc] initWithFrame:CGRectMake(70, 150, 250, 40)] autorelease];

middleView2.backgroundColor = [UIColor grayColor];

[self addSubview:middleView2 hierarchy:ViewHierarchy_Middle];

[backgroundView addGestureRecognizer:[[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleBackgroundView_Tap:)] autorelease]];

}


- (void)handleBackgroundView_Tap:(UITapGestureRecognizer*)sender

{

NSArray* subViews = [self subviewsOfHierarchy:ViewHierarchy_Middle];

UIView* backView = [subViews firstObject];

[self bringSubviewToFront:backView];

}


@end


2014-12-02 04.57.22.png

各subviewを配置するための階層はeViewHierarchyで管理しています。

subviewは最背面から順にaddSubviewをしているわけではなく、まったくのチグハグです。

しかし階層により表示順が管理されているため、正しい表示順が保持されます。

背景view(緑色のview)をタップすると、Middle階層に存在するviewの表示順が入れ替わります。

表示順の入れ替えにbringSubviewToFrontを用いていますが、階層内の最前面に移動するだけで決してFront階層を上書きすることはありません。


あまり多くの階層を作ってしまうとパフォーマンスの悪影響が考えられますので、その点には注意してください。

トラックバック(0)

トラックバックURL: http://www.ict-fractal.com/MovableType/mt/mt-tb.cgi/78

コメントする

Twitterボタン
Twitterブログパーツ